From 3082cf733d092ad440488d8e70741130eee24ec6 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 16 Sep 2024 13:36:29 +0900 Subject: [PATCH 001/133] feat: Adapt parachain-template-node to ziggurat --- Cargo.toml | 2 + ziggurat/node/Cargo.toml | 90 ++++ ziggurat/node/build.rs | 7 + ziggurat/node/src/chain_spec.rs | 202 +++++++++ ziggurat/node/src/cli.rs | 111 +++++ ziggurat/node/src/command.rs | 384 +++++++++++++++++ ziggurat/node/src/main.rs | 13 + ziggurat/node/src/rpc.rs | 56 +++ ziggurat/node/src/service.rs | 408 ++++++++++++++++++ ziggurat/runtime/Cargo.toml | 184 ++++++++ ziggurat/runtime/build.rs | 17 + ziggurat/runtime/src/apis.rs | 305 +++++++++++++ ziggurat/runtime/src/benchmarks.rs | 36 ++ ziggurat/runtime/src/configs/mod.rs | 304 +++++++++++++ ziggurat/runtime/src/configs/xcm_config.rs | 193 +++++++++ ziggurat/runtime/src/lib.rs | 304 +++++++++++++ ziggurat/runtime/src/weights/block_weights.rs | 53 +++ .../runtime/src/weights/extrinsic_weights.rs | 53 +++ ziggurat/runtime/src/weights/mod.rs | 27 ++ .../runtime/src/weights/paritydb_weights.rs | 63 +++ .../runtime/src/weights/rocksdb_weights.rs | 63 +++ ziggurat/zombienet.toml | 17 + 22 files changed, 2892 insertions(+) create mode 100644 ziggurat/node/Cargo.toml create mode 100644 ziggurat/node/build.rs create mode 100644 ziggurat/node/src/chain_spec.rs create mode 100644 ziggurat/node/src/cli.rs create mode 100644 ziggurat/node/src/command.rs create mode 100644 ziggurat/node/src/main.rs create mode 100644 ziggurat/node/src/rpc.rs create mode 100644 ziggurat/node/src/service.rs create mode 100644 ziggurat/runtime/Cargo.toml create mode 100644 ziggurat/runtime/build.rs create mode 100644 ziggurat/runtime/src/apis.rs create mode 100644 ziggurat/runtime/src/benchmarks.rs create mode 100644 ziggurat/runtime/src/configs/mod.rs create mode 100644 ziggurat/runtime/src/configs/xcm_config.rs create mode 100644 ziggurat/runtime/src/lib.rs create mode 100644 ziggurat/runtime/src/weights/block_weights.rs create mode 100644 ziggurat/runtime/src/weights/extrinsic_weights.rs create mode 100644 ziggurat/runtime/src/weights/mod.rs create mode 100644 ziggurat/runtime/src/weights/paritydb_weights.rs create mode 100644 ziggurat/runtime/src/weights/rocksdb_weights.rs create mode 100644 ziggurat/zombienet.toml diff --git a/Cargo.toml b/Cargo.toml index a5057456..b0511770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ members = [ "primitives/ethereum", "primitives/runtime", "runtime/common", + "ziggurat/node", + "ziggurat/runtime", ] [workspace.dependencies] diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml new file mode 100644 index 00000000..e01b4440 --- /dev/null +++ b/ziggurat/node/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "ziggurat-node" +version = "0.1.0" +description = "Ziggurat node (Babel testnet)" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "GPL-3.0-or-later" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[[bin]] +name = "ziggurat" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +codec = { package = "parity-scale-codec", version = "3.6" } +color-print = "0.3" +docify = "0.2" +futures = "0.3" +jsonrpsee = { version = "0.23", features = ["server"] } +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +ziggurat-runtime = { path = "../runtime", default-features = true } + +frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-chain-spec = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-offchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-network = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-network-sync = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-sysinfo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-telemetry = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-tracing = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +prometheus-endpoint = { package = "substrate-prometheus-endpoint", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +polkadot-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +polkadot-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-collator = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-consensus-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-consensus-proposer = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-client-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-relay-chain-interface = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } + +[build-dependencies] +substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } + +[features] +default = [] +runtime-benchmarks = [ + "cumulus-primitives-core/runtime-benchmarks", + "frame-benchmarking-cli/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "polkadot-cli/runtime-benchmarks", + "polkadot-primitives/runtime-benchmarks", + "sc-service/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "ziggurat-runtime/runtime-benchmarks", +] +try-runtime = [ + "polkadot-cli/try-runtime", + "sp-runtime/try-runtime", + "ziggurat-runtime/try-runtime", +] diff --git a/ziggurat/node/build.rs b/ziggurat/node/build.rs new file mode 100644 index 00000000..e3bfe311 --- /dev/null +++ b/ziggurat/node/build.rs @@ -0,0 +1,7 @@ +use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; + +fn main() { + generate_cargo_keys(); + + rerun_if_git_head_changed(); +} diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs new file mode 100644 index 00000000..0fb4a7d7 --- /dev/null +++ b/ziggurat/node/src/chain_spec.rs @@ -0,0 +1,202 @@ +use cumulus_primitives_core::ParaId; +use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT}; +use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; +use sc_service::ChainType; +use serde::{Deserialize, Serialize}; +use sp_core::{sr25519, Pair, Public}; +use sp_runtime::traits::{IdentifyAccount, Verify}; +use ziggurat_runtime as runtime; + +/// Specialized `ChainSpec` for the normal parachain runtime. +pub type ChainSpec = sc_service::GenericChainSpec; + +/// The default XCM version to set in genesis config. +const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; + +/// Helper function to generate a crypto pair from seed +pub fn get_from_seed(seed: &str) -> ::Public { + TPublic::Pair::from_string(&format!("//{}", seed), None) + .expect("static values are valid; qed") + .public() +} + +/// The extensions for the [`ChainSpec`]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChainSpecGroup, ChainSpecExtension)] +pub struct Extensions { + /// The relay chain of the Parachain. + #[serde(alias = "relayChain", alias = "RelayChain")] + pub relay_chain: String, + /// The id of the Parachain. + #[serde(alias = "paraId", alias = "ParaId")] + pub para_id: u32, +} + +impl Extensions { + /// Try to get the extension from the given `ChainSpec`. + pub fn try_get(chain_spec: &dyn sc_service::ChainSpec) -> Option<&Self> { + sc_chain_spec::get_extension(chain_spec.extensions()) + } +} + +type AccountPublic = ::Signer; + +/// Generate collator keys from seed. +/// +/// This function's return type must always match the session keys of the chain in tuple format. +pub fn get_collator_keys_from_seed(seed: &str) -> AuraId { + get_from_seed::(seed) +} + +/// Helper function to generate an account ID from seed +pub fn get_account_id_from_seed(seed: &str) -> AccountId +where + AccountPublic: From<::Public>, +{ + AccountPublic::from(get_from_seed::(seed)).into_account() +} + +/// Generate the session keys from individual elements. +/// +/// The input must be a tuple of individual keys (a single arg for now since we have just one key). +pub fn template_session_keys(keys: AuraId) -> runtime::SessionKeys { + runtime::SessionKeys { aura: keys } +} + +pub fn development_config() -> ChainSpec { + // Give your base currency a unit name and decimal places + let mut properties = sc_chain_spec::Properties::new(); + properties.insert("tokenSymbol".into(), "UNIT".into()); + properties.insert("tokenDecimals".into(), 12.into()); + properties.insert("ss58Format".into(), 42.into()); + + ChainSpec::builder( + runtime::WASM_BINARY.expect("WASM binary was not built, please build it!"), + Extensions { + relay_chain: "paseo-local".into(), + // You MUST set this to the correct network! + para_id: 1000, + }, + ) + .with_name("Development") + .with_id("dev") + .with_chain_type(ChainType::Development) + .with_genesis_config_patch(testnet_genesis( + // initial collators. + vec![ + ( + get_account_id_from_seed::("Alice"), + get_collator_keys_from_seed("Alice"), + ), + ( + get_account_id_from_seed::("Bob"), + get_collator_keys_from_seed("Bob"), + ), + ], + vec![ + get_account_id_from_seed::("Alice"), + get_account_id_from_seed::("Bob"), + get_account_id_from_seed::("Charlie"), + get_account_id_from_seed::("Dave"), + get_account_id_from_seed::("Eve"), + get_account_id_from_seed::("Ferdie"), + get_account_id_from_seed::("Alice//stash"), + get_account_id_from_seed::("Bob//stash"), + get_account_id_from_seed::("Charlie//stash"), + get_account_id_from_seed::("Dave//stash"), + get_account_id_from_seed::("Eve//stash"), + get_account_id_from_seed::("Ferdie//stash"), + ], + get_account_id_from_seed::("Alice"), + 1000.into(), + )) + .build() +} + +pub fn local_testnet_config() -> ChainSpec { + // Give your base currency a unit name and decimal places + let mut properties = sc_chain_spec::Properties::new(); + properties.insert("tokenSymbol".into(), "UNIT".into()); + properties.insert("tokenDecimals".into(), 12.into()); + properties.insert("ss58Format".into(), 42.into()); + + #[allow(deprecated)] + ChainSpec::builder( + runtime::WASM_BINARY.expect("WASM binary was not built, please build it!"), + Extensions { + relay_chain: "paseo-local".into(), + // You MUST set this to the correct network! + para_id: 1000, + }, + ) + .with_name("Local Testnet") + .with_id("local_testnet") + .with_chain_type(ChainType::Local) + .with_genesis_config_patch(testnet_genesis( + // initial collators. + vec![ + ( + get_account_id_from_seed::("Alice"), + get_collator_keys_from_seed("Alice"), + ), + ( + get_account_id_from_seed::("Bob"), + get_collator_keys_from_seed("Bob"), + ), + ], + vec![ + get_account_id_from_seed::("Alice"), + get_account_id_from_seed::("Bob"), + get_account_id_from_seed::("Charlie"), + get_account_id_from_seed::("Dave"), + get_account_id_from_seed::("Eve"), + get_account_id_from_seed::("Ferdie"), + get_account_id_from_seed::("Alice//stash"), + get_account_id_from_seed::("Bob//stash"), + get_account_id_from_seed::("Charlie//stash"), + get_account_id_from_seed::("Dave//stash"), + get_account_id_from_seed::("Eve//stash"), + get_account_id_from_seed::("Ferdie//stash"), + ], + get_account_id_from_seed::("Alice"), + 1000.into(), + )) + .with_protocol_id("template-local") + .with_properties(properties) + .build() +} + +fn testnet_genesis( + invulnerables: Vec<(AccountId, AuraId)>, + endowed_accounts: Vec, + root: AccountId, + id: ParaId, +) -> serde_json::Value { + serde_json::json!({ + "balances": { + "balances": endowed_accounts.iter().cloned().map(|k| (k, 1u64 << 60)).collect::>(), + }, + "parachainInfo": { + "parachainId": id, + }, + "collatorSelection": { + "invulnerables": invulnerables.iter().cloned().map(|(acc, _)| acc).collect::>(), + "candidacyBond": EXISTENTIAL_DEPOSIT * 16, + }, + "session": { + "keys": invulnerables + .into_iter() + .map(|(acc, aura)| { + ( + acc.clone(), // account id + acc, // validator id + template_session_keys(aura), // session keys + ) + }) + .collect::>(), + }, + "polkadotXcm": { + "safeXcmVersion": Some(SAFE_XCM_VERSION), + }, + "sudo": { "key": Some(root) } + }) +} diff --git a/ziggurat/node/src/cli.rs b/ziggurat/node/src/cli.rs new file mode 100644 index 00000000..16417e77 --- /dev/null +++ b/ziggurat/node/src/cli.rs @@ -0,0 +1,111 @@ +use std::path::PathBuf; + +/// Sub-commands supported by the collator. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand { + /// Build a chain specification. + BuildSpec(sc_cli::BuildSpecCmd), + + /// Validate blocks. + CheckBlock(sc_cli::CheckBlockCmd), + + /// Export blocks. + ExportBlocks(sc_cli::ExportBlocksCmd), + + /// Export the state of a given block into a chain spec. + ExportState(sc_cli::ExportStateCmd), + + /// Import blocks. + ImportBlocks(sc_cli::ImportBlocksCmd), + + /// Revert the chain to a previous state. + Revert(sc_cli::RevertCmd), + + /// Remove the whole chain. + PurgeChain(cumulus_client_cli::PurgeChainCmd), + + /// Export the genesis head data of the parachain. + /// + /// Head data is the encoded block header. + #[command(alias = "export-genesis-state")] + ExportGenesisHead(cumulus_client_cli::ExportGenesisHeadCommand), + + /// Export the genesis wasm of the parachain. + ExportGenesisWasm(cumulus_client_cli::ExportGenesisWasmCommand), + + /// Sub-commands concerned with benchmarking. + /// The pallet benchmarking moved to the `pallet` sub-command. + #[command(subcommand)] + Benchmark(frame_benchmarking_cli::BenchmarkCmd), +} + +const AFTER_HELP_EXAMPLE: &str = color_print::cstr!( + r#"Examples: + ziggurat build-spec --disable-default-bootnode > ziggurat.plain.json + Export a chainspec for a local testnet in json format. + ziggurat --chain ziggurat.plain.json --tmp -- --chain paseo-local + Launch a full node with chain specification loaded from ziggurat.plain.json. + ziggurat + Launch a full node with default parachain local-testnet and relay chain paseo-local. + ziggurat --collator + Launch a collator with default parachain local-testnet and relay chain paseo-local. + "# +); +#[derive(Debug, clap::Parser)] +#[command( + propagate_version = true, + args_conflicts_with_subcommands = true, + subcommand_negates_reqs = true +)] +#[clap(after_help = AFTER_HELP_EXAMPLE)] +pub struct Cli { + #[command(subcommand)] + pub subcommand: Option, + + #[command(flatten)] + pub run: cumulus_client_cli::RunCmd, + + /// Disable automatic hardware benchmarks. + /// + /// By default these benchmarks are automatically ran at startup and measure + /// the CPU speed, the memory bandwidth and the disk speed. + /// + /// The results are then printed out in the logs, and also sent as part of + /// telemetry, if telemetry is enabled. + #[arg(long)] + pub no_hardware_benchmarks: bool, + + /// Relay chain arguments + #[arg(raw = true)] + pub relay_chain_args: Vec, +} + +#[derive(Debug)] +pub struct RelayChainCli { + /// The actual relay chain cli object. + pub base: polkadot_cli::RunCmd, + + /// Optional chain id that should be passed to the relay chain. + pub chain_id: Option, + + /// The base path that should be used by the relay chain. + pub base_path: Option, +} + +impl RelayChainCli { + /// Parse the relay chain CLI parameters using the para chain `Configuration`. + pub fn new<'a>( + para_config: &sc_service::Configuration, + relay_chain_args: impl Iterator, + ) -> Self { + let extension = crate::chain_spec::Extensions::try_get(&*para_config.chain_spec); + let chain_id = extension.map(|e| e.relay_chain.clone()); + let base_path = para_config.base_path.path().join("polkadot"); + Self { + base_path: Some(base_path), + chain_id, + base: clap::Parser::parse_from(relay_chain_args), + } + } +} diff --git a/ziggurat/node/src/command.rs b/ziggurat/node/src/command.rs new file mode 100644 index 00000000..ffbf8340 --- /dev/null +++ b/ziggurat/node/src/command.rs @@ -0,0 +1,384 @@ +use std::net::SocketAddr; + +use cumulus_client_service::storage_proof_size::HostFunctions as ReclaimHostFunctions; +use cumulus_primitives_core::ParaId; +use frame_benchmarking_cli::{BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE}; +use log::info; +use sc_cli::{ + ChainSpec, CliConfiguration, DefaultConfigurationValues, ImportParams, KeystoreParams, + NetworkParams, Result, SharedParams, SubstrateCli, +}; +use sc_service::config::{BasePath, PrometheusConfig}; +use ziggurat_runtime::Block; + +use crate::{ + chain_spec, + cli::{Cli, RelayChainCli, Subcommand}, + service::new_partial, +}; + +fn load_spec(id: &str) -> std::result::Result, String> { + Ok(match id { + "dev" => Box::new(chain_spec::development_config()), + "" | "local" => Box::new(chain_spec::local_testnet_config()), + path => Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?), + }) +} + +impl SubstrateCli for Cli { + fn impl_name() -> String { + "Parachain Collator Template".into() + } + + fn impl_version() -> String { + env!("SUBSTRATE_CLI_IMPL_VERSION").into() + } + + fn description() -> String { + format!( + "Parachain Collator Template\n\nThe command-line arguments provided first will be \ + passed to the parachain node, while the arguments provided after -- will be passed \ + to the relay chain node.\n\n\ + {} -- ", + Self::executable_name() + ) + } + + fn author() -> String { + env!("CARGO_PKG_AUTHORS").into() + } + + fn support_url() -> String { + "https://github.com/paritytech/polkadot-sdk/issues/new".into() + } + + fn copyright_start_year() -> i32 { + 2020 + } + + fn load_spec(&self, id: &str) -> std::result::Result, String> { + load_spec(id) + } +} + +impl SubstrateCli for RelayChainCli { + fn impl_name() -> String { + "Parachain Collator Template".into() + } + + fn impl_version() -> String { + env!("SUBSTRATE_CLI_IMPL_VERSION").into() + } + + fn description() -> String { + format!( + "Parachain Collator Template\n\nThe command-line arguments provided first will be \ + passed to the parachain node, while the arguments provided after -- will be passed \ + to the relay chain node.\n\n\ + {} -- ", + Self::executable_name() + ) + } + + fn author() -> String { + env!("CARGO_PKG_AUTHORS").into() + } + + fn support_url() -> String { + "https://github.com/paritytech/polkadot-sdk/issues/new".into() + } + + fn copyright_start_year() -> i32 { + 2020 + } + + fn load_spec(&self, id: &str) -> std::result::Result, String> { + polkadot_cli::Cli::from_iter([RelayChainCli::executable_name()].iter()).load_spec(id) + } +} + +macro_rules! construct_async_run { + (|$components:ident, $cli:ident, $cmd:ident, $config:ident| $( $code:tt )* ) => {{ + let runner = $cli.create_runner($cmd)?; + runner.async_run(|$config| { + let $components = new_partial(&$config)?; + let task_manager = $components.task_manager; + { $( $code )* }.map(|v| (v, task_manager)) + }) + }} +} + +/// Parse command line arguments into service configuration. +pub fn run() -> Result<()> { + let cli = Cli::from_args(); + + match &cli.subcommand { + Some(Subcommand::BuildSpec(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) + }, + Some(Subcommand::CheckBlock(cmd)) => { + construct_async_run!(|components, cli, cmd, config| { + Ok(cmd.run(components.client, components.import_queue)) + }) + }, + Some(Subcommand::ExportBlocks(cmd)) => { + construct_async_run!(|components, cli, cmd, config| { + Ok(cmd.run(components.client, config.database)) + }) + }, + Some(Subcommand::ExportState(cmd)) => { + construct_async_run!(|components, cli, cmd, config| { + Ok(cmd.run(components.client, config.chain_spec)) + }) + }, + Some(Subcommand::ImportBlocks(cmd)) => { + construct_async_run!(|components, cli, cmd, config| { + Ok(cmd.run(components.client, components.import_queue)) + }) + }, + Some(Subcommand::Revert(cmd)) => { + construct_async_run!(|components, cli, cmd, config| { + Ok(cmd.run(components.client, components.backend, None)) + }) + }, + Some(Subcommand::PurgeChain(cmd)) => { + let runner = cli.create_runner(cmd)?; + + runner.sync_run(|config| { + let polkadot_cli = RelayChainCli::new( + &config, + [RelayChainCli::executable_name()].iter().chain(cli.relay_chain_args.iter()), + ); + + let polkadot_config = SubstrateCli::create_configuration( + &polkadot_cli, + &polkadot_cli, + config.tokio_handle.clone(), + ) + .map_err(|err| format!("Relay chain argument error: {}", err))?; + + cmd.run(config, polkadot_config) + }) + }, + Some(Subcommand::ExportGenesisHead(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| { + let partials = new_partial(&config)?; + + cmd.run(partials.client) + }) + }, + Some(Subcommand::ExportGenesisWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|_config| { + let spec = cli.load_spec(&cmd.shared_params.chain.clone().unwrap_or_default())?; + cmd.run(&*spec) + }) + }, + Some(Subcommand::Benchmark(cmd)) => { + let runner = cli.create_runner(cmd)?; + // Switch on the concrete benchmark sub-command- + match cmd { + BenchmarkCmd::Pallet(cmd) => + if cfg!(feature = "runtime-benchmarks") { + runner.sync_run(|config| cmd.run_with_spec::, ReclaimHostFunctions>(Some(config.chain_spec))) + } else { + Err("Benchmarking wasn't enabled when building the node. \ + You can enable it with `--features runtime-benchmarks`." + .into()) + }, + BenchmarkCmd::Block(cmd) => runner.sync_run(|config| { + let partials = new_partial(&config)?; + cmd.run(partials.client) + }), + #[cfg(not(feature = "runtime-benchmarks"))] + BenchmarkCmd::Storage(_) => Err(sc_cli::Error::Input( + "Compile with --features=runtime-benchmarks \ + to enable storage benchmarks." + .into(), + )), + #[cfg(feature = "runtime-benchmarks")] + BenchmarkCmd::Storage(cmd) => runner.sync_run(|config| { + let partials = new_partial(&config)?; + let db = partials.backend.expose_db(); + let storage = partials.backend.expose_storage(); + cmd.run(config, partials.client.clone(), db, storage) + }), + BenchmarkCmd::Machine(cmd) => + runner.sync_run(|config| cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone())), + // NOTE: this allows the Client to leniently implement + // new benchmark commands without requiring a companion MR. + #[allow(unreachable_patterns)] + _ => Err("Benchmarking sub-command unsupported".into()), + } + }, + None => { + let runner = cli.create_runner(&cli.run.normalize())?; + let collator_options = cli.run.collator_options(); + + runner.run_node_until_exit(|config| async move { + let hwbench = (!cli.no_hardware_benchmarks) + .then_some(config.database.path().map(|database_path| { + let _ = std::fs::create_dir_all(database_path); + sc_sysinfo::gather_hwbench(Some(database_path)) + })) + .flatten(); + + let para_id = chain_spec::Extensions::try_get(&*config.chain_spec) + .map(|e| e.para_id) + .ok_or("Could not find parachain ID in chain-spec.")?; + + let polkadot_cli = RelayChainCli::new( + &config, + [RelayChainCli::executable_name()].iter().chain(cli.relay_chain_args.iter()), + ); + + let id = ParaId::from(para_id); + + let tokio_handle = config.tokio_handle.clone(); + let polkadot_config = + SubstrateCli::create_configuration(&polkadot_cli, &polkadot_cli, tokio_handle) + .map_err(|err| format!("Relay chain argument error: {}", err))?; + + info!("Is collating: {}", if config.role.is_authority() { "yes" } else { "no" }); + + crate::service::start_parachain_node( + config, + polkadot_config, + collator_options, + id, + hwbench, + ) + .await + .map(|r| r.0) + .map_err(Into::into) + }) + }, + } +} + +impl DefaultConfigurationValues for RelayChainCli { + fn p2p_listen_port() -> u16 { + 30334 + } + + fn rpc_listen_port() -> u16 { + 9945 + } + + fn prometheus_listen_port() -> u16 { + 9616 + } +} + +impl CliConfiguration for RelayChainCli { + fn shared_params(&self) -> &SharedParams { + self.base.base.shared_params() + } + + fn import_params(&self) -> Option<&ImportParams> { + self.base.base.import_params() + } + + fn network_params(&self) -> Option<&NetworkParams> { + self.base.base.network_params() + } + + fn keystore_params(&self) -> Option<&KeystoreParams> { + self.base.base.keystore_params() + } + + fn base_path(&self) -> Result> { + Ok(self + .shared_params() + .base_path()? + .or_else(|| self.base_path.clone().map(Into::into))) + } + + fn rpc_addr(&self, default_listen_port: u16) -> Result> { + self.base.base.rpc_addr(default_listen_port) + } + + fn prometheus_config( + &self, + default_listen_port: u16, + chain_spec: &Box, + ) -> Result> { + self.base.base.prometheus_config(default_listen_port, chain_spec) + } + + fn init( + &self, + _support_url: &String, + _impl_version: &String, + _logger_hook: F, + _config: &sc_service::Configuration, + ) -> Result<()> + where + F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration), + { + unreachable!("PolkadotCli is never initialized; qed"); + } + + fn chain_id(&self, is_dev: bool) -> Result { + let chain_id = self.base.base.chain_id(is_dev)?; + + Ok(if chain_id.is_empty() { self.chain_id.clone().unwrap_or_default() } else { chain_id }) + } + + fn role(&self, is_dev: bool) -> Result { + self.base.base.role(is_dev) + } + + fn transaction_pool(&self, is_dev: bool) -> Result { + self.base.base.transaction_pool(is_dev) + } + + fn trie_cache_maximum_size(&self) -> Result> { + self.base.base.trie_cache_maximum_size() + } + + fn rpc_methods(&self) -> Result { + self.base.base.rpc_methods() + } + + fn rpc_max_connections(&self) -> Result { + self.base.base.rpc_max_connections() + } + + fn rpc_cors(&self, is_dev: bool) -> Result>> { + self.base.base.rpc_cors(is_dev) + } + + fn default_heap_pages(&self) -> Result> { + self.base.base.default_heap_pages() + } + + fn force_authoring(&self) -> Result { + self.base.base.force_authoring() + } + + fn disable_grandpa(&self) -> Result { + self.base.base.disable_grandpa() + } + + fn max_runtime_instances(&self) -> Result> { + self.base.base.max_runtime_instances() + } + + fn announce_block(&self) -> Result { + self.base.base.announce_block() + } + + fn telemetry_endpoints( + &self, + chain_spec: &Box, + ) -> Result> { + self.base.base.telemetry_endpoints(chain_spec) + } + + fn node_name(&self) -> Result { + self.base.base.node_name() + } +} diff --git a/ziggurat/node/src/main.rs b/ziggurat/node/src/main.rs new file mode 100644 index 00000000..12738a67 --- /dev/null +++ b/ziggurat/node/src/main.rs @@ -0,0 +1,13 @@ +//! Substrate Parachain Node Template CLI + +#![warn(missing_docs)] + +mod chain_spec; +mod cli; +mod command; +mod rpc; +mod service; + +fn main() -> sc_cli::Result<()> { + command::run() +} diff --git a/ziggurat/node/src/rpc.rs b/ziggurat/node/src/rpc.rs new file mode 100644 index 00000000..e1f6a148 --- /dev/null +++ b/ziggurat/node/src/rpc.rs @@ -0,0 +1,56 @@ +//! A collection of node-specific RPC methods. +//! Substrate provides the `sc-rpc` crate, which defines the core RPC layer +//! used by Substrate nodes. This file extends those RPC definitions with +//! capabilities that are specific to this project's runtime configuration. + +#![warn(missing_docs)] + +use std::sync::Arc; + +use ziggurat_runtime::{opaque::Block, AccountId, Balance, Nonce}; + +pub use sc_rpc::DenyUnsafe; +use sc_transaction_pool_api::TransactionPool; +use sp_api::ProvideRuntimeApi; +use sp_block_builder::BlockBuilder; +use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; + +/// A type representing all RPC extensions. +pub type RpcExtension = jsonrpsee::RpcModule<()>; + +/// Full client dependencies +pub struct FullDeps { + /// The client instance to use. + pub client: Arc, + /// Transaction pool instance. + pub pool: Arc

, + /// Whether to deny unsafe calls + pub deny_unsafe: DenyUnsafe, +} + +/// Instantiate all RPC extensions. +pub fn create_full( + deps: FullDeps, +) -> Result> +where + C: ProvideRuntimeApi + + HeaderBackend + + HeaderMetadata + + Send + + Sync + + 'static, + C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, + C::Api: substrate_frame_rpc_system::AccountNonceApi, + C::Api: BlockBuilder, + P: TransactionPool + Sync + Send + 'static, +{ + use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; + use substrate_frame_rpc_system::{System, SystemApiServer}; + + let mut module = RpcExtension::new(()); + let FullDeps { client, pool, deny_unsafe } = deps; + + module.merge(System::new(client.clone(), pool, deny_unsafe).into_rpc())?; + module.merge(TransactionPayment::new(client).into_rpc())?; + Ok(module) +} diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs new file mode 100644 index 00000000..6a21646e --- /dev/null +++ b/ziggurat/node/src/service.rs @@ -0,0 +1,408 @@ +//! Service and ServiceFactory implementation. Specialized wrapper over substrate service. + +// std +use std::{sync::Arc, time::Duration}; + +use cumulus_client_cli::CollatorOptions; +// Local Runtime Types +use ziggurat_runtime::{ + apis::RuntimeApi, + opaque::{Block, Hash}, +}; + +// Cumulus Imports +use cumulus_client_collator::service::CollatorService; +#[docify::export(lookahead_collator)] +use cumulus_client_consensus_aura::collators::lookahead::{self as aura, Params as AuraParams}; +use cumulus_client_consensus_common::ParachainBlockImport as TParachainBlockImport; +use cumulus_client_consensus_proposer::Proposer; +use cumulus_client_service::{ + build_network, build_relay_chain_interface, prepare_node_config, start_relay_chain_tasks, + BuildNetworkParams, CollatorSybilResistance, DARecoveryProfile, ParachainHostFunctions, + StartRelayChainTasksParams, +}; +#[docify::export(cumulus_primitives)] +use cumulus_primitives_core::{ + relay_chain::{CollatorPair, ValidationCode}, + ParaId, +}; +use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface}; + +// Substrate Imports +use frame_benchmarking_cli::SUBSTRATE_REFERENCE_HARDWARE; +use prometheus_endpoint::Registry; +use sc_client_api::Backend; +use sc_consensus::ImportQueue; +use sc_executor::{HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY}; +use sc_network::NetworkBlock; +use sc_service::{Configuration, PartialComponents, TFullBackend, TFullClient, TaskManager}; +use sc_telemetry::{Telemetry, TelemetryHandle, TelemetryWorker, TelemetryWorkerHandle}; +use sc_transaction_pool_api::OffchainTransactionPoolFactory; +use sp_keystore::KeystorePtr; + +#[docify::export(wasm_executor)] +type ParachainExecutor = WasmExecutor; + +type ParachainClient = TFullClient; + +type ParachainBackend = TFullBackend; + +type ParachainBlockImport = TParachainBlockImport, ParachainBackend>; + +/// Assembly of PartialComponents (enough to run chain ops subcommands) +pub type Service = PartialComponents< + ParachainClient, + ParachainBackend, + (), + sc_consensus::DefaultImportQueue, + sc_transaction_pool::FullPool, + (ParachainBlockImport, Option, Option), +>; + +/// Starts a `ServiceBuilder` for a full service. +/// +/// Use this macro if you don't actually need the full service, but just the builder in order to +/// be able to perform chain operations. +#[docify::export(component_instantiation)] +pub fn new_partial(config: &Configuration) -> Result { + let telemetry = config + .telemetry_endpoints + .clone() + .filter(|x| !x.is_empty()) + .map(|endpoints| -> Result<_, sc_telemetry::Error> { + let worker = TelemetryWorker::new(16)?; + let telemetry = worker.handle().new_telemetry(endpoints); + Ok((worker, telemetry)) + }) + .transpose()?; + + let heap_pages = config + .default_heap_pages + .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static { extra_pages: h as _ }); + + let executor = ParachainExecutor::builder() + .with_execution_method(config.wasm_method) + .with_onchain_heap_alloc_strategy(heap_pages) + .with_offchain_heap_alloc_strategy(heap_pages) + .with_max_runtime_instances(config.max_runtime_instances) + .with_runtime_cache_size(config.runtime_cache_size) + .build(); + + let (client, backend, keystore_container, task_manager) = + sc_service::new_full_parts_record_import::( + config, + telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), + executor, + true, + )?; + let client = Arc::new(client); + + let telemetry_worker_handle = telemetry.as_ref().map(|(worker, _)| worker.handle()); + + let telemetry = telemetry.map(|(worker, telemetry)| { + task_manager.spawn_handle().spawn("telemetry", None, worker.run()); + telemetry + }); + + let transaction_pool = sc_transaction_pool::BasicPool::new_full( + config.transaction_pool.clone(), + config.role.is_authority().into(), + config.prometheus_registry(), + task_manager.spawn_essential_handle(), + client.clone(), + ); + + let block_import = ParachainBlockImport::new(client.clone(), backend.clone()); + + let import_queue = build_import_queue( + client.clone(), + block_import.clone(), + config, + telemetry.as_ref().map(|telemetry| telemetry.handle()), + &task_manager, + ); + + Ok(PartialComponents { + backend, + client, + import_queue, + keystore_container, + task_manager, + transaction_pool, + select_chain: (), + other: (block_import, telemetry, telemetry_worker_handle), + }) +} + +/// Build the import queue for the parachain runtime. +fn build_import_queue( + client: Arc, + block_import: ParachainBlockImport, + config: &Configuration, + telemetry: Option, + task_manager: &TaskManager, +) -> sc_consensus::DefaultImportQueue { + cumulus_client_consensus_aura::equivocation_import_queue::fully_verifying_import_queue::< + sp_consensus_aura::sr25519::AuthorityPair, + _, + _, + _, + _, + >( + client, + block_import, + move |_, _| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + Ok(timestamp) + }, + &task_manager.spawn_essential_handle(), + config.prometheus_registry(), + telemetry, + ) +} + +#[allow(clippy::too_many_arguments)] +fn start_consensus( + client: Arc, + backend: Arc, + block_import: ParachainBlockImport, + prometheus_registry: Option<&Registry>, + telemetry: Option, + task_manager: &TaskManager, + relay_chain_interface: Arc, + transaction_pool: Arc>, + keystore: KeystorePtr, + relay_chain_slot_duration: Duration, + para_id: ParaId, + collator_key: CollatorPair, + overseer_handle: OverseerHandle, + announce_block: Arc>) + Send + Sync>, +) -> Result<(), sc_service::Error> { + let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( + task_manager.spawn_handle(), + client.clone(), + transaction_pool, + prometheus_registry, + telemetry.clone(), + ); + + let proposer = Proposer::new(proposer_factory); + + let collator_service = CollatorService::new( + client.clone(), + Arc::new(task_manager.spawn_handle()), + announce_block, + client.clone(), + ); + + let params = AuraParams { + create_inherent_data_providers: move |_, ()| async move { Ok(()) }, + block_import, + para_client: client.clone(), + para_backend: backend, + relay_client: relay_chain_interface, + code_hash_provider: move |block_hash| { + client.code_at(block_hash).ok().map(|c| ValidationCode::from(c).hash()) + }, + keystore, + collator_key, + para_id, + overseer_handle, + relay_chain_slot_duration, + proposer, + collator_service, + authoring_duration: Duration::from_millis(2000), + reinitialize: false, + }; + let fut = aura::run::( + params, + ); + task_manager.spawn_essential_handle().spawn("aura", None, fut); + + Ok(()) +} + +/// Start a node with the given parachain `Configuration` and relay chain `Configuration`. +#[sc_tracing::logging::prefix_logs_with("Parachain")] +pub async fn start_parachain_node( + parachain_config: Configuration, + polkadot_config: Configuration, + collator_options: CollatorOptions, + para_id: ParaId, + hwbench: Option, +) -> sc_service::error::Result<(TaskManager, Arc)> { + let parachain_config = prepare_node_config(parachain_config); + + let params = new_partial(¶chain_config)?; + let (block_import, mut telemetry, telemetry_worker_handle) = params.other; + let net_config = sc_network::config::FullNetworkConfiguration::< + _, + _, + sc_network::NetworkWorker, + >::new(¶chain_config.network); + + let client = params.client.clone(); + let backend = params.backend.clone(); + let mut task_manager = params.task_manager; + + let (relay_chain_interface, collator_key) = build_relay_chain_interface( + polkadot_config, + ¶chain_config, + telemetry_worker_handle, + &mut task_manager, + collator_options.clone(), + hwbench.clone(), + ) + .await + .map_err(|e| sc_service::Error::Application(Box::new(e) as Box<_>))?; + + let validator = parachain_config.role.is_authority(); + let prometheus_registry = parachain_config.prometheus_registry().cloned(); + let transaction_pool = params.transaction_pool.clone(); + let import_queue_service = params.import_queue.service(); + + // NOTE: because we use Aura here explicitly, we can use `CollatorSybilResistance::Resistant` + // when starting the network. + let (network, system_rpc_tx, tx_handler_controller, start_network, sync_service) = + build_network(BuildNetworkParams { + parachain_config: ¶chain_config, + net_config, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + para_id, + spawn_handle: task_manager.spawn_handle(), + relay_chain_interface: relay_chain_interface.clone(), + import_queue: params.import_queue, + sybil_resistance_level: CollatorSybilResistance::Resistant, // because of Aura + }) + .await?; + + if parachain_config.offchain_worker.enabled { + use futures::FutureExt; + + task_manager.spawn_handle().spawn( + "offchain-workers-runner", + "offchain-work", + sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { + runtime_api_provider: client.clone(), + keystore: Some(params.keystore_container.keystore()), + offchain_db: backend.offchain_storage(), + transaction_pool: Some(OffchainTransactionPoolFactory::new( + transaction_pool.clone(), + )), + network_provider: Arc::new(network.clone()), + is_validator: parachain_config.role.is_authority(), + enable_http_requests: false, + custom_extensions: move |_| vec![], + }) + .run(client.clone(), task_manager.spawn_handle()) + .boxed(), + ); + } + + let rpc_builder = { + let client = client.clone(); + let transaction_pool = transaction_pool.clone(); + + Box::new(move |deny_unsafe, _| { + let deps = crate::rpc::FullDeps { + client: client.clone(), + pool: transaction_pool.clone(), + deny_unsafe, + }; + + crate::rpc::create_full(deps).map_err(Into::into) + }) + }; + + sc_service::spawn_tasks(sc_service::SpawnTasksParams { + rpc_builder, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + task_manager: &mut task_manager, + config: parachain_config, + keystore: params.keystore_container.keystore(), + backend: backend.clone(), + network, + sync_service: sync_service.clone(), + system_rpc_tx, + tx_handler_controller, + telemetry: telemetry.as_mut(), + })?; + + if let Some(hwbench) = hwbench { + sc_sysinfo::print_hwbench(&hwbench); + // Here you can check whether the hardware meets your chains' requirements. Putting a link + // in there and swapping out the requirements for your own are probably a good idea. The + // requirements for a para-chain are dictated by its relay-chain. + match SUBSTRATE_REFERENCE_HARDWARE.check_hardware(&hwbench) { + Err(err) if validator => { + log::warn!( + "⚠️ The hardware does not meet the minimal requirements {} for role 'Authority'.", + err + ); + }, + _ => {}, + } + + if let Some(ref mut telemetry) = telemetry { + let telemetry_handle = telemetry.handle(); + task_manager.spawn_handle().spawn( + "telemetry_hwbench", + None, + sc_sysinfo::initialize_hwbench_telemetry(telemetry_handle, hwbench), + ); + } + } + + let announce_block = { + let sync_service = sync_service.clone(); + Arc::new(move |hash, data| sync_service.announce_block(hash, data)) + }; + + let relay_chain_slot_duration = Duration::from_secs(6); + + let overseer_handle = relay_chain_interface + .overseer_handle() + .map_err(|e| sc_service::Error::Application(Box::new(e)))?; + + start_relay_chain_tasks(StartRelayChainTasksParams { + client: client.clone(), + announce_block: announce_block.clone(), + para_id, + relay_chain_interface: relay_chain_interface.clone(), + task_manager: &mut task_manager, + da_recovery_profile: if validator { + DARecoveryProfile::Collator + } else { + DARecoveryProfile::FullNode + }, + import_queue: import_queue_service, + relay_chain_slot_duration, + recovery_handle: Box::new(overseer_handle.clone()), + sync_service: sync_service.clone(), + })?; + + if validator { + start_consensus( + client.clone(), + backend, + block_import, + prometheus_registry.as_ref(), + telemetry.as_ref().map(|t| t.handle()), + &task_manager, + relay_chain_interface, + transaction_pool, + params.keystore_container.keystore(), + relay_chain_slot_duration, + para_id, + collator_key.expect("Command line arguments do not allow this. qed"), + overseer_handle, + announce_block, + )?; + } + + start_network.start_network(); + + Ok((task_manager, client)) +} diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml new file mode 100644 index 00000000..93065203 --- /dev/null +++ b/ziggurat/runtime/Cargo.toml @@ -0,0 +1,184 @@ +[package] +name = "ziggurat-runtime" +version = "0.1.0" +description = "Ziggurat runtime (Babel testnet)" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "GPL-3.0-or-later" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = ["derive"] } +docify = "0.2" +hex-literal = { version = "0.4", optional = true } +log = { version = "0.4", default-features = false } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } +smallvec = "1.13" + +frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } +frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = ["experimental"] } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } +frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-try-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } +pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-message-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-version = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +polkadot-parachain-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-pallet-aura-ext = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-pallet-parachain-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-pallet-session-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-pallet-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-pallet-xcmp-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-primitives-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-primitives-utility = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cumulus-primitives-storage-weight-reclaim = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[build-dependencies] +substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", optional = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "cumulus-pallet-aura-ext/std", + "cumulus-pallet-parachain-system/std", + "cumulus-pallet-session-benchmarking/std", + "cumulus-pallet-xcm/std", + "cumulus-pallet-xcmp-queue/std", + "cumulus-primitives-aura/std", + "cumulus-primitives-core/std", + "cumulus-primitives-storage-weight-reclaim/std", + "cumulus-primitives-utility/std", + "frame-benchmarking?/std", + "frame-executive/std", + "frame-metadata-hash-extension/std", + "frame-support/std", + "frame-system-benchmarking?/std", + "frame-system-rpc-runtime-api/std", + "frame-system/std", + "frame-try-runtime?/std", + "log/std", + "pallet-aura/std", + "pallet-authorship/std", + "pallet-balances/std", + "pallet-collator-selection/std", + "pallet-message-queue/std", + "pallet-session/std", + "pallet-sudo/std", + "pallet-timestamp/std", + "pallet-transaction-payment-rpc-runtime-api/std", + "pallet-transaction-payment/std", + "pallet-xcm/std", + "parachain-info/std", + "parachains-common/std", + "polkadot-parachain-primitives/std", + "polkadot-runtime-common/std", + "scale-info/std", + "sp-api/std", + "sp-block-builder/std", + "sp-consensus-aura/std", + "sp-core/std", + "sp-genesis-builder/std", + "sp-inherents/std", + "sp-offchain/std", + "sp-runtime/std", + "sp-session/std", + "sp-transaction-pool/std", + "sp-version/std", + "substrate-wasm-builder", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] + +runtime-benchmarks = [ + "cumulus-pallet-parachain-system/runtime-benchmarks", + "cumulus-pallet-session-benchmarking/runtime-benchmarks", + "cumulus-pallet-xcmp-queue/runtime-benchmarks", + "cumulus-primitives-core/runtime-benchmarks", + "cumulus-primitives-utility/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system-benchmarking/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "hex-literal", + "pallet-balances/runtime-benchmarks", + "pallet-collator-selection/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "pallet-sudo/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-xcm/runtime-benchmarks", + "parachains-common/runtime-benchmarks", + "polkadot-parachain-primitives/runtime-benchmarks", + "polkadot-runtime-common/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] + +try-runtime = [ + "cumulus-pallet-aura-ext/try-runtime", + "cumulus-pallet-parachain-system/try-runtime", + "cumulus-pallet-xcm/try-runtime", + "cumulus-pallet-xcmp-queue/try-runtime", + "frame-executive/try-runtime", + "frame-support/try-runtime", + "frame-system/try-runtime", + "frame-try-runtime/try-runtime", + "pallet-aura/try-runtime", + "pallet-authorship/try-runtime", + "pallet-balances/try-runtime", + "pallet-collator-selection/try-runtime", + "pallet-message-queue/try-runtime", + "pallet-session/try-runtime", + "pallet-sudo/try-runtime", + "pallet-timestamp/try-runtime", + "pallet-transaction-payment/try-runtime", + "pallet-xcm/try-runtime", + "parachain-info/try-runtime", + "polkadot-runtime-common/try-runtime", + "sp-runtime/try-runtime", +] + +# Enable the metadata hash generation. +# +# This is hidden behind a feature because it increases the compile time. +# The wasm binary needs to be compiled twice, once to fetch the metadata, +# generate the metadata hash and then a second time with the +# `RUNTIME_METADATA_HASH` environment variable set for the `CheckMetadataHash` +# extension. +metadata-hash = ["substrate-wasm-builder/metadata-hash"] + +# A convenience feature for enabling things when doing a build +# for an on-chain release. +on-chain-release-build = ["metadata-hash"] diff --git a/ziggurat/runtime/build.rs b/ziggurat/runtime/build.rs new file mode 100644 index 00000000..4f33752c --- /dev/null +++ b/ziggurat/runtime/build.rs @@ -0,0 +1,17 @@ +#[cfg(all(feature = "std", feature = "metadata-hash"))] +#[docify::export(template_enable_metadata_hash)] +fn main() { + substrate_wasm_builder::WasmBuilder::init_with_defaults() + .enable_metadata_hash("UNIT", 12) + .build(); +} + +#[cfg(all(feature = "std", not(feature = "metadata-hash")))] +fn main() { + substrate_wasm_builder::WasmBuilder::build_using_defaults(); +} + +/// The wasm builder is deactivated when compiling +/// this crate for wasm to speed up the compilation. +#[cfg(not(feature = "std"))] +fn main() {} diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs new file mode 100644 index 00000000..f5d5d3e6 --- /dev/null +++ b/ziggurat/runtime/src/apis.rs @@ -0,0 +1,305 @@ +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to + +// External crates imports +use alloc::vec::Vec; +use frame_support::{ + genesis_builder_helper::{build_state, get_preset}, + weights::Weight, +}; +use pallet_aura::Authorities; +use sp_api::impl_runtime_apis; +use sp_consensus_aura::sr25519::AuthorityId as AuraId; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +use sp_runtime::{ + traits::Block as BlockT, + transaction_validity::{TransactionSource, TransactionValidity}, + ApplyExtrinsicResult, +}; +use sp_version::RuntimeVersion; + +// Local module imports +use super::{ + AccountId, Balance, Block, ConsensusHook, Executive, InherentDataExt, Nonce, ParachainSystem, + Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, + SLOT_DURATION, VERSION, +}; + +// we move some impls outside so we can easily use them with `docify`. +impl Runtime { + #[docify::export] + fn impl_slot_duration() -> sp_consensus_aura::SlotDuration { + sp_consensus_aura::SlotDuration::from_millis(SLOT_DURATION) + } + + #[docify::export] + fn impl_can_build_upon( + included_hash: ::Hash, + slot: cumulus_primitives_aura::Slot, + ) -> bool { + ConsensusHook::can_build_upon(included_hash, slot) + } +} + +impl_runtime_apis! { + impl sp_consensus_aura::AuraApi for Runtime { + fn slot_duration() -> sp_consensus_aura::SlotDuration { + Runtime::impl_slot_duration() + } + + fn authorities() -> Vec { + Authorities::::get().into_inner() + } + } + + impl cumulus_primitives_aura::AuraUnincludedSegmentApi for Runtime { + fn can_build_upon( + included_hash: ::Hash, + slot: cumulus_primitives_aura::Slot, + ) -> bool { + Runtime::impl_can_build_upon(included_hash, slot) + } + } + + impl sp_api::Core for Runtime { + fn version() -> RuntimeVersion { + VERSION + } + + fn execute_block(block: Block) { + Executive::execute_block(block) + } + + fn initialize_block(header: &::Header) -> sp_runtime::ExtrinsicInclusionMode { + Executive::initialize_block(header) + } + } + + impl sp_api::Metadata for Runtime { + fn metadata() -> OpaqueMetadata { + OpaqueMetadata::new(Runtime::metadata().into()) + } + + fn metadata_at_version(version: u32) -> Option { + Runtime::metadata_at_version(version) + } + + fn metadata_versions() -> Vec { + Runtime::metadata_versions() + } + } + + impl sp_block_builder::BlockBuilder for Runtime { + fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { + Executive::apply_extrinsic(extrinsic) + } + + fn finalize_block() -> ::Header { + Executive::finalize_block() + } + + fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { + data.create_extrinsics() + } + + fn check_inherents( + block: Block, + data: sp_inherents::InherentData, + ) -> sp_inherents::CheckInherentsResult { + data.check_extrinsics(&block) + } + } + + impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { + fn validate_transaction( + source: TransactionSource, + tx: ::Extrinsic, + block_hash: ::Hash, + ) -> TransactionValidity { + Executive::validate_transaction(source, tx, block_hash) + } + } + + impl sp_offchain::OffchainWorkerApi for Runtime { + fn offchain_worker(header: &::Header) { + Executive::offchain_worker(header) + } + } + + impl sp_session::SessionKeys for Runtime { + fn generate_session_keys(seed: Option>) -> Vec { + SessionKeys::generate(seed) + } + + fn decode_session_keys( + encoded: Vec, + ) -> Option, KeyTypeId)>> { + SessionKeys::decode_into_raw_public_keys(&encoded) + } + } + + impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { + fn account_nonce(account: AccountId) -> Nonce { + System::account_nonce(account) + } + } + + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi for Runtime { + fn query_info( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { + TransactionPayment::query_info(uxt, len) + } + fn query_fee_details( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment::FeeDetails { + TransactionPayment::query_fee_details(uxt, len) + } + fn query_weight_to_fee(weight: Weight) -> Balance { + TransactionPayment::weight_to_fee(weight) + } + fn query_length_to_fee(length: u32) -> Balance { + TransactionPayment::length_to_fee(length) + } + } + + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi + for Runtime + { + fn query_call_info( + call: RuntimeCall, + len: u32, + ) -> pallet_transaction_payment::RuntimeDispatchInfo { + TransactionPayment::query_call_info(call, len) + } + fn query_call_fee_details( + call: RuntimeCall, + len: u32, + ) -> pallet_transaction_payment::FeeDetails { + TransactionPayment::query_call_fee_details(call, len) + } + fn query_weight_to_fee(weight: Weight) -> Balance { + TransactionPayment::weight_to_fee(weight) + } + fn query_length_to_fee(length: u32) -> Balance { + TransactionPayment::length_to_fee(length) + } + } + + impl cumulus_primitives_core::CollectCollationInfo for Runtime { + fn collect_collation_info(header: &::Header) -> cumulus_primitives_core::CollationInfo { + ParachainSystem::collect_collation_info(header) + } + } + + #[cfg(feature = "try-runtime")] + impl frame_try_runtime::TryRuntime for Runtime { + fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { + use super::configs::RuntimeBlockWeights; + + let weight = Executive::try_runtime_upgrade(checks).unwrap(); + (weight, RuntimeBlockWeights::get().max_block) + } + + fn execute_block( + block: Block, + state_root_check: bool, + signature_check: bool, + select: frame_try_runtime::TryStateSelect, + ) -> Weight { + // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to + // have a backtrace here. + Executive::try_execute_block(block, state_root_check, signature_check, select).unwrap() + } + } + + #[cfg(feature = "runtime-benchmarks")] + impl frame_benchmarking::Benchmark for Runtime { + fn benchmark_metadata(extra: bool) -> ( + Vec, + Vec, + ) { + use frame_benchmarking::{Benchmarking, BenchmarkList}; + use frame_support::traits::StorageInfoTrait; + use frame_system_benchmarking::Pallet as SystemBench; + use cumulus_pallet_session_benchmarking::Pallet as SessionBench; + use super::*; + + let mut list = Vec::::new(); + list_benchmarks!(list, extra); + + let storage_info = AllPalletsWithSystem::storage_info(); + (list, storage_info) + } + + fn dispatch_benchmark( + config: frame_benchmarking::BenchmarkConfig + ) -> Result, sp_runtime::RuntimeString> { + use frame_benchmarking::{BenchmarkError, Benchmarking, BenchmarkBatch}; + use super::*; + + use frame_system_benchmarking::Pallet as SystemBench; + impl frame_system_benchmarking::Config for Runtime { + fn setup_set_code_requirements(code: &Vec) -> Result<(), BenchmarkError> { + ParachainSystem::initialize_for_set_code_benchmark(code.len() as u32); + Ok(()) + } + + fn verify_set_code() { + System::assert_last_event(cumulus_pallet_parachain_system::Event::::ValidationFunctionStored.into()); + } + } + + use cumulus_pallet_session_benchmarking::Pallet as SessionBench; + impl cumulus_pallet_session_benchmarking::Config for Runtime {} + + use frame_support::traits::WhitelistedStorageKeys; + let whitelist = AllPalletsWithSystem::whitelisted_storage_keys(); + + let mut batches = Vec::::new(); + let params = (&config, &whitelist); + add_benchmarks!(params, batches); + + if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } + Ok(batches) + } + } + + impl sp_genesis_builder::GenesisBuilder for Runtime { + fn build_state(config: Vec) -> sp_genesis_builder::Result { + build_state::(config) + } + + fn get_preset(id: &Option) -> Option> { + get_preset::(id, |_| None) + } + + fn preset_names() -> Vec { + Default::default() + } + } +} diff --git a/ziggurat/runtime/src/benchmarks.rs b/ziggurat/runtime/src/benchmarks.rs new file mode 100644 index 00000000..9fbf1ad8 --- /dev/null +++ b/ziggurat/runtime/src/benchmarks.rs @@ -0,0 +1,36 @@ +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to + +frame_benchmarking::define_benchmarks!( + [frame_system, SystemBench::] + [pallet_balances, Balances] + [pallet_session, SessionBench::] + [pallet_timestamp, Timestamp] + [pallet_message_queue, MessageQueue] + [pallet_sudo, Sudo] + [pallet_collator_selection, CollatorSelection] + [cumulus_pallet_parachain_system, ParachainSystem] + [cumulus_pallet_xcmp_queue, XcmpQueue] +); diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs new file mode 100644 index 00000000..689674cc --- /dev/null +++ b/ziggurat/runtime/src/configs/mod.rs @@ -0,0 +1,304 @@ +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to + +mod xcm_config; + +// Substrate and Polkadot dependencies +use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; +use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; +use frame_support::{ + derive_impl, + dispatch::DispatchClass, + parameter_types, + traits::{ + ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, TransformOrigin, VariantCountOf, + }, + weights::{ConstantMultiplier, Weight}, + PalletId, +}; +use frame_system::{ + limits::{BlockLength, BlockWeights}, + EnsureRoot, +}; +use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; +use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; +use polkadot_runtime_common::{ + xcm_sender::NoPriceForMessageDelivery, BlockHashCount, SlowAdjustingFeeUpdate, +}; +use sp_consensus_aura::sr25519::AuthorityId as AuraId; +use sp_runtime::Perbill; +use sp_version::RuntimeVersion; +use xcm::latest::prelude::BodyId; + +// Local module imports +use super::{ + weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, + AccountId, Aura, Balance, Balances, Block, BlockNumber, CollatorSelection, ConsensusHook, Hash, + MessageQueue, Nonce, PalletInfo, ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, + RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, + System, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, + MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, +}; +use xcm_config::{RelayLocation, XcmOriginToTransactDispatchOrigin}; + +parameter_types! { + pub const Version: RuntimeVersion = VERSION; + + // This part is copied from Substrate's `bin/node/runtime/src/lib.rs`. + // The `RuntimeBlockLength` and `RuntimeBlockWeights` exist here because the + // `DeletionWeightLimit` and `DeletionQueueDepth` depend on those to parameterize + // the lazy contract deletion. + pub RuntimeBlockLength: BlockLength = + BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); + pub RuntimeBlockWeights: BlockWeights = BlockWeights::builder() + .base_block(BlockExecutionWeight::get()) + .for_class(DispatchClass::all(), |weights| { + weights.base_extrinsic = ExtrinsicBaseWeight::get(); + }) + .for_class(DispatchClass::Normal, |weights| { + weights.max_total = Some(NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT); + }) + .for_class(DispatchClass::Operational, |weights| { + weights.max_total = Some(MAXIMUM_BLOCK_WEIGHT); + // Operational transactions have some extra reserved space, so that they + // are included even if block reached `MAXIMUM_BLOCK_WEIGHT`. + weights.reserved = Some( + MAXIMUM_BLOCK_WEIGHT - NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT + ); + }) + .avg_block_initialization(AVERAGE_ON_INITIALIZE_RATIO) + .build_or_panic(); + pub const SS58Prefix: u16 = 42; +} + +/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from +/// [`ParaChainDefaultConfig`](`struct@frame_system::config_preludes::ParaChainDefaultConfig`), +/// but overridden as needed. +#[derive_impl(frame_system::config_preludes::ParaChainDefaultConfig)] +impl frame_system::Config for Runtime { + /// The identifier used to distinguish between accounts. + type AccountId = AccountId; + /// The index type for storing how many extrinsics an account has signed. + type Nonce = Nonce; + /// The type for hashing blocks and tries. + type Hash = Hash; + /// The block type. + type Block = Block; + /// Maximum number of block number to block hash mappings to keep (oldest pruned first). + type BlockHashCount = BlockHashCount; + /// Runtime version. + type Version = Version; + /// The data to be stored in an account. + type AccountData = pallet_balances::AccountData; + /// The weight of database operations that the runtime can invoke. + type DbWeight = RocksDbWeight; + /// Block & extrinsics weights: base values and limits. + type BlockWeights = RuntimeBlockWeights; + /// The maximum length of a block (in bytes). + type BlockLength = RuntimeBlockLength; + /// This is used as an identifier of the chain. 42 is the generic substrate prefix. + type SS58Prefix = SS58Prefix; + /// The action to take on a Runtime Upgrade + type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_timestamp::Config for Runtime { + /// A timestamp: milliseconds since the unix epoch. + type Moment = u64; + type OnTimestampSet = Aura; + type MinimumPeriod = ConstU64<0>; + type WeightInfo = (); +} + +impl pallet_authorship::Config for Runtime { + type FindAuthor = pallet_session::FindAccountFromAuthorIndex; + type EventHandler = (CollatorSelection,); +} + +parameter_types! { + pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = ConstU32<50>; + /// The type for recording an account's balance. + type Balance = Balance; + /// The ubiquitous event type. + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = VariantCountOf; +} + +parameter_types! { + /// Relay Chain `TransactionByteFee` / 10 + pub const TransactionByteFee: Balance = 10 * MICROUNIT; +} + +impl pallet_transaction_payment::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = pallet_transaction_payment::FungibleAdapter; + type WeightToFee = WeightToFee; + type LengthToFee = ConstantMultiplier; + type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; + type OperationalFeeMultiplier = ConstU8<5>; +} + +impl pallet_sudo::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type WeightInfo = (); +} + +parameter_types! { + pub const ReservedXcmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); + pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); + pub const RelayOrigin: AggregateMessageOrigin = AggregateMessageOrigin::Parent; +} + +impl cumulus_pallet_parachain_system::Config for Runtime { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type OnSystemEvent = (); + type SelfParaId = parachain_info::Pallet; + type OutboundXcmpMessageSource = XcmpQueue; + type DmpQueue = frame_support::traits::EnqueueWithOrigin; + type ReservedDmpWeight = ReservedDmpWeight; + type XcmpMessageHandler = XcmpQueue; + type ReservedXcmpWeight = ReservedXcmpWeight; + type CheckAssociatedRelayNumber = RelayNumberMonotonicallyIncreases; + type ConsensusHook = ConsensusHook; +} + +impl parachain_info::Config for Runtime {} + +parameter_types! { + pub MessageQueueServiceWeight: Weight = Perbill::from_percent(35) * RuntimeBlockWeights::get().max_block; +} + +impl pallet_message_queue::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor< + cumulus_primitives_core::AggregateMessageOrigin, + >; + #[cfg(not(feature = "runtime-benchmarks"))] + type MessageProcessor = xcm_builder::ProcessXcmMessage< + AggregateMessageOrigin, + xcm_executor::XcmExecutor, + RuntimeCall, + >; + type Size = u32; + // The XCMP queue pallet is only ever able to handle the `Sibling(ParaId)` origin: + type QueueChangeHandler = NarrowOriginToSibling; + type QueuePausedQuery = NarrowOriginToSibling; + type HeapSize = sp_core::ConstU32<{ 103 * 1024 }>; + type MaxStale = sp_core::ConstU32<8>; + type ServiceWeight = MessageQueueServiceWeight; + type IdleMaxServiceWeight = (); +} + +impl cumulus_pallet_aura_ext::Config for Runtime {} + +impl cumulus_pallet_xcmp_queue::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ChannelInfo = ParachainSystem; + type VersionWrapper = (); + // Enqueue XCMP messages from siblings for later processing. + type XcmpQueue = TransformOrigin; + type MaxInboundSuspended = sp_core::ConstU32<1_000>; + type MaxActiveOutboundChannels = ConstU32<128>; + type MaxPageSize = ConstU32<{ 1 << 16 }>; + type ControllerOrigin = EnsureRoot; + type ControllerOriginConverter = XcmOriginToTransactDispatchOrigin; + type WeightInfo = (); + type PriceForSiblingDelivery = NoPriceForMessageDelivery; +} + +parameter_types! { + pub const Period: u32 = 6 * HOURS; + pub const Offset: u32 = 0; +} + +impl pallet_session::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ValidatorId = ::AccountId; + // we don't have stash and controller, thus we don't need the convert as well. + type ValidatorIdOf = pallet_collator_selection::IdentityCollator; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionManager = CollatorSelection; + // Essentially just Aura, but let's be pedantic. + type SessionHandler = ::KeyTypeIdProviders; + type Keys = SessionKeys; + type WeightInfo = (); +} + +#[docify::export(aura_config)] +impl pallet_aura::Config for Runtime { + type AuthorityId = AuraId; + type DisabledValidators = (); + type MaxAuthorities = ConstU32<100_000>; + type AllowMultipleBlocksPerSlot = ConstBool; + type SlotDuration = ConstU64; +} + +parameter_types! { + pub const PotId: PalletId = PalletId(*b"PotStake"); + pub const SessionLength: BlockNumber = 6 * HOURS; + // StakingAdmin pluralistic body. + pub const StakingAdminBodyId: BodyId = BodyId::Defense; +} + +/// We allow root and the StakingAdmin to execute privileged collator selection operations. +pub type CollatorSelectionUpdateOrigin = EitherOfDiverse< + EnsureRoot, + EnsureXcm>, +>; + +impl pallet_collator_selection::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type UpdateOrigin = CollatorSelectionUpdateOrigin; + type PotId = PotId; + type MaxCandidates = ConstU32<100>; + type MinEligibleCollators = ConstU32<4>; + type MaxInvulnerables = ConstU32<20>; + // should be a multiple of session or things will get inconsistent + type KickThreshold = Period; + type ValidatorId = ::AccountId; + type ValidatorIdOf = pallet_collator_selection::IdentityCollator; + type ValidatorRegistration = Session; + type WeightInfo = (); +} diff --git a/ziggurat/runtime/src/configs/xcm_config.rs b/ziggurat/runtime/src/configs/xcm_config.rs new file mode 100644 index 00000000..e162bcbf --- /dev/null +++ b/ziggurat/runtime/src/configs/xcm_config.rs @@ -0,0 +1,193 @@ +use crate::{ + AccountId, AllPalletsWithSystem, Balances, ParachainInfo, ParachainSystem, PolkadotXcm, + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, +}; +use frame_support::{ + parameter_types, + traits::{ConstU32, Contains, Everything, Nothing}, + weights::Weight, +}; +use frame_system::EnsureRoot; +use pallet_xcm::XcmPassthrough; +use polkadot_parachain_primitives::primitives::Sibling; +use polkadot_runtime_common::impls::ToAuthor; +use xcm::latest::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom, + DenyReserveTransferToRelayChain, DenyThenTry, EnsureXcmOrigin, FixedWeightBounds, + FrameTransactionalProcessor, FungibleAdapter, IsConcrete, NativeAsset, ParentIsPreset, + RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, + TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, +}; +use xcm_executor::XcmExecutor; + +parameter_types! { + pub const RelayLocation: Location = Location::parent(); + pub const RelayNetwork: Option = None; + pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); + // For the real deployment, it is recommended to set `RelayNetwork` according to the relay chain + // and prepend `UniversalLocation` with `GlobalConsensus(RelayNetwork::get())`. + pub UniversalLocation: InteriorLocation = Parachain(ParachainInfo::parachain_id().into()).into(); +} + +/// Type for specifying how a `Location` can be converted into an `AccountId`. This is used +/// when determining ownership of accounts for asset transacting and when attempting to use XCM +/// `Transact` in order to determine the dispatch Origin. +pub type LocationToAccountId = ( + // The parent (Relay-chain) origin converts to the parent `AccountId`. + ParentIsPreset, + // Sibling parachain origins convert to AccountId via the `ParaId::into`. + SiblingParachainConvertsVia, + // Straight up local `AccountId32` origins just alias directly to `AccountId`. + AccountId32Aliases, +); + +/// Means for transacting assets on this chain. +pub type LocalAssetTransactor = FungibleAdapter< + // Use this currency: + Balances, + // Use this currency when it is a fungible asset matching the given location or name: + IsConcrete, + // Do a simple punn to convert an AccountId32 Location into a native chain account ID: + LocationToAccountId, + // Our chain's account ID type (we can't get away without mentioning it explicitly): + AccountId, + // We don't track any teleports. + (), +>; + +/// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, +/// ready for dispatching a transaction with Xcm's `Transact`. There is an `OriginKind` which can +/// biases the kind of local `Origin` it will become. +pub type XcmOriginToTransactDispatchOrigin = ( + // Sovereign account converter; this attempts to derive an `AccountId` from the origin location + // using `LocationToAccountId` and then turn that into the usual `Signed` origin. Useful for + // foreign chains who want to have a local sovereign account on this chain which they control. + SovereignSignedViaLocation, + // Native converter for Relay-chain (Parent) location; will convert to a `Relay` origin when + // recognized. + RelayChainAsNative, + // Native converter for sibling Parachains; will convert to a `SiblingPara` origin when + // recognized. + SiblingParachainAsNative, + // Native signed account converter; this just converts an `AccountId32` origin into a normal + // `RuntimeOrigin::Signed` origin of the same 32-byte value. + SignedAccountId32AsNative, + // Xcm origins can be represented natively under the Xcm pallet's Xcm origin. + XcmPassthrough, +); + +parameter_types! { + // One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate. + pub UnitWeightCost: Weight = Weight::from_parts(1_000_000_000, 64 * 1024); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; +} + +pub struct ParentOrParentsExecutivePlurality; +impl Contains for ParentOrParentsExecutivePlurality { + fn contains(location: &Location) -> bool { + matches!(location.unpack(), (1, []) | (1, [Plurality { id: BodyId::Executive, .. }])) + } +} + +pub type Barrier = TrailingSetTopicAsId< + DenyThenTry< + DenyReserveTransferToRelayChain, + ( + TakeWeightCredit, + WithComputedOrigin< + ( + AllowTopLevelPaidExecutionFrom, + AllowExplicitUnpaidExecutionFrom, + // ^^^ Parent and its exec plurality get free execution + ), + UniversalLocation, + ConstU32<8>, + >, + ), + >, +>; + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = XcmRouter; + // How to withdraw and deposit an asset. + type AssetTransactor = LocalAssetTransactor; + type OriginConverter = XcmOriginToTransactDispatchOrigin; + type IsReserve = NativeAsset; + type IsTeleporter = (); // Teleporting is disabled. + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = + UsingComponents>; + type ResponseHandler = PolkadotXcm; + type AssetTrap = PolkadotXcm; + type AssetClaims = PolkadotXcm; + type SubscriptionService = PolkadotXcm; + type PalletInstancesInfo = AllPalletsWithSystem; + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type AssetLocker = (); + type AssetExchanger = (); + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + type TransactionalProcessor = FrameTransactionalProcessor; + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; +} + +/// No local origins on this chain are allowed to dispatch XCM sends/executions. +pub type LocalOriginToLocation = SignedToAccountId32; + +/// The means for routing XCM messages which are not for local execution into the right message +/// queues. +pub type XcmRouter = WithUniqueTopic<( + // Two routers - use UMP to communicate with the relay chain: + cumulus_primitives_utility::ParentAsUmp, + // ..and XCMP to communicate with the sibling chains. + XcmpQueue, +)>; + +impl pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = EnsureXcmOrigin; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmExecuteFilter = Nothing; + // ^ Disable dispatchable execute on the XCM pallet. + // Needs to be `Everything` for local testing. + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Nothing; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + // ^ Override for AdvertisedXcmVersion default + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; + type Currency = Balances; + type CurrencyMatcher = (); + type TrustedLockers = (); + type SovereignAccountOf = LocationToAccountId; + type MaxLockers = ConstU32<8>; + type WeightInfo = pallet_xcm::TestWeightInfo; + type AdminOrigin = EnsureRoot; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); +} + +impl cumulus_pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type XcmExecutor = XcmExecutor; +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs new file mode 100644 index 00000000..57800873 --- /dev/null +++ b/ziggurat/runtime/src/lib.rs @@ -0,0 +1,304 @@ +#![cfg_attr(not(feature = "std"), no_std)] +// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. +#![recursion_limit = "256"] + +// Make the WASM binary available. +#[cfg(feature = "std")] +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +pub mod apis; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarks; +mod configs; +mod weights; + +extern crate alloc; +use alloc::vec::Vec; +use smallvec::smallvec; +use sp_runtime::{ + create_runtime_str, generic, impl_opaque_keys, + traits::{BlakeTwo256, IdentifyAccount, Verify}, + MultiSignature, +}; + +#[cfg(feature = "std")] +use sp_version::NativeVersion; +use sp_version::RuntimeVersion; + +use frame_support::weights::{ + constants::WEIGHT_REF_TIME_PER_SECOND, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, + WeightToFeePolynomial, +}; +pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; +pub use sp_runtime::{MultiAddress, Perbill, Permill}; + +#[cfg(any(feature = "std", test))] +pub use sp_runtime::BuildStorage; + +use weights::ExtrinsicBaseWeight; + +/// Alias to 512-bit hash when used in the context of a transaction signature on the chain. +pub type Signature = MultiSignature; + +/// Some way of identifying an account on the chain. We intentionally make it equivalent +/// to the public key of our transaction signing scheme. +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +/// Balance of an account. +pub type Balance = u128; + +/// Index of a transaction in the chain. +pub type Nonce = u32; + +/// A hash of some data used by the chain. +pub type Hash = sp_core::H256; + +/// An index to a block. +pub type BlockNumber = u32; + +/// The address format for describing accounts. +pub type Address = MultiAddress; + +/// Block header type as expected by this runtime. +pub type Header = generic::Header; + +/// Block type as expected by this runtime. +pub type Block = generic::Block; + +/// A Block signed with a Justification +pub type SignedBlock = generic::SignedBlock; + +/// BlockId type as expected by this runtime. +pub type BlockId = generic::BlockId; + +/// The SignedExtension to the basic transaction logic. +#[docify::export(template_signed_extra)] +pub type SignedExtra = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + pallet_transaction_payment::ChargeTransactionPayment, + cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim, + frame_metadata_hash_extension::CheckMetadataHash, +); + +/// Unchecked extrinsic type as expected by this runtime. +pub type UncheckedExtrinsic = + generic::UncheckedExtrinsic; + +/// Executive: handles dispatch to the various modules. +pub type Executive = frame_executive::Executive< + Runtime, + Block, + frame_system::ChainContext, + Runtime, + AllPalletsWithSystem, +>; + +/// Handles converting a weight scalar to a fee value, based on the scale and granularity of the +/// node's balance type. +/// +/// This should typically create a mapping between the following ranges: +/// - `[0, MAXIMUM_BLOCK_WEIGHT]` +/// - `[Balance::min, Balance::max]` +/// +/// Yet, it can be used for any other sort of change to weight-fee. Some examples being: +/// - Setting it to `0` will essentially disable the weight fee. +/// - Setting it to `1` will cause the literal `#[weight = x]` values to be charged. +pub struct WeightToFee; +impl WeightToFeePolynomial for WeightToFee { + type Balance = Balance; + fn polynomial() -> WeightToFeeCoefficients { + // in Rococo, extrinsic base weight (smallest non-zero weight) is mapped to 1 MILLIUNIT: + // in our template, we map to 1/10 of that, or 1/10 MILLIUNIT + let p = MILLIUNIT / 10; + let q = 100 * Balance::from(ExtrinsicBaseWeight::get().ref_time()); + smallvec![WeightToFeeCoefficient { + degree: 1, + negative: false, + coeff_frac: Perbill::from_rational(p % q, q), + coeff_integer: p / q, + }] + } +} + +/// Opaque types. These are used by the CLI to instantiate machinery that don't need to know +/// the specifics of the runtime. They can then be made to be agnostic over specific formats +/// of data like extrinsics, allowing for them to continue syncing the network through upgrades +/// to even the core data structures. +pub mod opaque { + use super::*; + use sp_runtime::{ + generic, + traits::{BlakeTwo256, Hash as HashT}, + }; + + pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; + /// Opaque block header type. + pub type Header = generic::Header; + /// Opaque block type. + pub type Block = generic::Block; + /// Opaque block identifier type. + pub type BlockId = generic::BlockId; + /// Opaque block hash type. + pub type Hash = ::Output; +} + +impl_opaque_keys! { + pub struct SessionKeys { + pub aura: Aura, + } +} + +#[sp_version::runtime_version] +pub const VERSION: RuntimeVersion = RuntimeVersion { + spec_name: create_runtime_str!("ziggurat-runtime"), + impl_name: create_runtime_str!("ziggurat-runtime"), + authoring_version: 1, + spec_version: 1, + impl_version: 0, + apis: apis::RUNTIME_API_VERSIONS, + transaction_version: 1, + state_version: 1, +}; + +#[docify::export] +mod block_times { + /// This determines the average expected block time that we are targeting. Blocks will be + /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by + /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn + /// slot_duration()`. + /// + /// Change this to adjust the block time. + pub const MILLISECS_PER_BLOCK: u64 = 6000; + + // NOTE: Currently it is not possible to change the slot duration after the chain has started. + // Attempting to do so will brick block production. + pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; +} +pub use block_times::*; + +// Time is measured by number of blocks. +pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); +pub const HOURS: BlockNumber = MINUTES * 60; +pub const DAYS: BlockNumber = HOURS * 24; + +// Unit = the base number of indivisible units for balances +pub const UNIT: Balance = 1_000_000_000_000; +pub const MILLIUNIT: Balance = 1_000_000_000; +pub const MICROUNIT: Balance = 1_000_000; + +/// The existential deposit. Set to 1/10 of the Connected Relay Chain. +pub const EXISTENTIAL_DEPOSIT: Balance = MILLIUNIT; + +/// We assume that ~5% of the block weight is consumed by `on_initialize` handlers. This is +/// used to limit the maximal weight of a single extrinsic. +const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(5); + +/// We allow `Normal` extrinsics to fill up the block up to 75%, the rest can be used by +/// `Operational` extrinsics. +const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); + +#[docify::export(max_block_weight)] +/// We allow for 2 seconds of compute with a 6 second average block time. +const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( + WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), + cumulus_primitives_core::relay_chain::MAX_POV_SIZE as u64, +); + +#[docify::export] +mod async_backing_params { + /// Maximum number of blocks simultaneously accepted by the Runtime, not yet included + /// into the relay chain. + pub(crate) const UNINCLUDED_SEGMENT_CAPACITY: u32 = 3; + /// How many parachain blocks are processed by the relay chain per parent. Limits the + /// number of blocks authored per slot. + pub(crate) const BLOCK_PROCESSING_VELOCITY: u32 = 1; + /// Relay chain slot duration, in milliseconds. + pub(crate) const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; +} +pub(crate) use async_backing_params::*; + +#[docify::export] +/// Aura consensus hook +type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< + Runtime, + RELAY_CHAIN_SLOT_DURATION_MILLIS, + BLOCK_PROCESSING_VELOCITY, + UNINCLUDED_SEGMENT_CAPACITY, +>; + +/// The version information used to identify this runtime when compiled natively. +#[cfg(feature = "std")] +pub fn native_version() -> NativeVersion { + NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } +} + +// Create the runtime by composing the FRAME pallets that were previously configured. +#[frame_support::runtime] +mod runtime { + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask + )] + pub struct Runtime; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + #[runtime::pallet_index(1)] + pub type ParachainSystem = cumulus_pallet_parachain_system; + #[runtime::pallet_index(2)] + pub type Timestamp = pallet_timestamp; + #[runtime::pallet_index(3)] + pub type ParachainInfo = parachain_info; + + // Monetary stuff. + #[runtime::pallet_index(10)] + pub type Balances = pallet_balances; + #[runtime::pallet_index(11)] + pub type TransactionPayment = pallet_transaction_payment; + + // Governance + #[runtime::pallet_index(15)] + pub type Sudo = pallet_sudo; + + // Collator support. The order of these 4 are important and shall not change. + #[runtime::pallet_index(20)] + pub type Authorship = pallet_authorship; + #[runtime::pallet_index(21)] + pub type CollatorSelection = pallet_collator_selection; + #[runtime::pallet_index(22)] + pub type Session = pallet_session; + #[runtime::pallet_index(23)] + pub type Aura = pallet_aura; + #[runtime::pallet_index(24)] + pub type AuraExt = cumulus_pallet_aura_ext; + + // XCM helpers. + #[runtime::pallet_index(30)] + pub type XcmpQueue = cumulus_pallet_xcmp_queue; + #[runtime::pallet_index(31)] + pub type PolkadotXcm = pallet_xcm; + #[runtime::pallet_index(32)] + pub type CumulusXcm = cumulus_pallet_xcm; + #[runtime::pallet_index(33)] + pub type MessageQueue = pallet_message_queue; +} + +#[docify::export(register_validate_block)] +cumulus_pallet_parachain_system::register_validate_block! { + Runtime = Runtime, + BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::, +} diff --git a/ziggurat/runtime/src/weights/block_weights.rs b/ziggurat/runtime/src/weights/block_weights.rs new file mode 100644 index 00000000..e7fdb2aa --- /dev/null +++ b/ziggurat/runtime/src/weights/block_weights.rs @@ -0,0 +1,53 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod constants { + use frame_support::{ + parameter_types, + weights::{constants, Weight}, + }; + + parameter_types! { + /// Importing a block with 0 Extrinsics. + pub const BlockExecutionWeight: Weight = + Weight::from_parts(constants::WEIGHT_REF_TIME_PER_NANOS.saturating_mul(5_000_000), 0); + } + + #[cfg(test)] + mod test_weights { + use frame_support::weights::constants; + + /// Checks that the weight exists and is sane. + // NOTE: If this test fails but you are sure that the generated values are fine, + // you can delete it. + #[test] + fn sane() { + let w = super::constants::BlockExecutionWeight::get(); + + // At least 100 µs. + assert!( + w.ref_time() >= 100u64 * constants::WEIGHT_REF_TIME_PER_MICROS, + "Weight should be at least 100 µs." + ); + // At most 50 ms. + assert!( + w.ref_time() <= 50u64 * constants::WEIGHT_REF_TIME_PER_MILLIS, + "Weight should be at most 50 ms." + ); + } + } +} diff --git a/ziggurat/runtime/src/weights/extrinsic_weights.rs b/ziggurat/runtime/src/weights/extrinsic_weights.rs new file mode 100644 index 00000000..1a4adb96 --- /dev/null +++ b/ziggurat/runtime/src/weights/extrinsic_weights.rs @@ -0,0 +1,53 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod constants { + use frame_support::{ + parameter_types, + weights::{constants, Weight}, + }; + + parameter_types! { + /// Executing a NO-OP `System::remarks` Extrinsic. + pub const ExtrinsicBaseWeight: Weight = + Weight::from_parts(constants::WEIGHT_REF_TIME_PER_NANOS.saturating_mul(125_000), 0); + } + + #[cfg(test)] + mod test_weights { + use frame_support::weights::constants; + + /// Checks that the weight exists and is sane. + // NOTE: If this test fails but you are sure that the generated values are fine, + // you can delete it. + #[test] + fn sane() { + let w = super::constants::ExtrinsicBaseWeight::get(); + + // At least 10 µs. + assert!( + w.ref_time() >= 10u64 * constants::WEIGHT_REF_TIME_PER_MICROS, + "Weight should be at least 10 µs." + ); + // At most 1 ms. + assert!( + w.ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, + "Weight should be at most 1 ms." + ); + } + } +} diff --git a/ziggurat/runtime/src/weights/mod.rs b/ziggurat/runtime/src/weights/mod.rs new file mode 100644 index 00000000..b473d49e --- /dev/null +++ b/ziggurat/runtime/src/weights/mod.rs @@ -0,0 +1,27 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Expose the auto generated weight files. + +pub mod block_weights; +pub mod extrinsic_weights; +pub mod paritydb_weights; +pub mod rocksdb_weights; + +pub use block_weights::constants::BlockExecutionWeight; +pub use extrinsic_weights::constants::ExtrinsicBaseWeight; +pub use rocksdb_weights::constants::RocksDbWeight; diff --git a/ziggurat/runtime/src/weights/paritydb_weights.rs b/ziggurat/runtime/src/weights/paritydb_weights.rs new file mode 100644 index 00000000..25679703 --- /dev/null +++ b/ziggurat/runtime/src/weights/paritydb_weights.rs @@ -0,0 +1,63 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod constants { + use frame_support::{ + parameter_types, + weights::{constants, RuntimeDbWeight}, + }; + + parameter_types! { + /// `ParityDB` can be enabled with a feature flag, but is still experimental. These weights + /// are available for brave runtime engineers who may want to try this out as default. + pub const ParityDbWeight: RuntimeDbWeight = RuntimeDbWeight { + read: 8_000 * constants::WEIGHT_REF_TIME_PER_NANOS, + write: 50_000 * constants::WEIGHT_REF_TIME_PER_NANOS, + }; + } + + #[cfg(test)] + mod test_db_weights { + use super::constants::ParityDbWeight as W; + use frame_support::weights::constants; + + /// Checks that all weights exist and have sane values. + // NOTE: If this test fails but you are sure that the generated values are fine, + // you can delete it. + #[test] + fn sane() { + // At least 1 µs. + assert!( + W::get().reads(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, + "Read weight should be at least 1 µs." + ); + assert!( + W::get().writes(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, + "Write weight should be at least 1 µs." + ); + // At most 1 ms. + assert!( + W::get().reads(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, + "Read weight should be at most 1 ms." + ); + assert!( + W::get().writes(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, + "Write weight should be at most 1 ms." + ); + } + } +} diff --git a/ziggurat/runtime/src/weights/rocksdb_weights.rs b/ziggurat/runtime/src/weights/rocksdb_weights.rs new file mode 100644 index 00000000..3dd817aa --- /dev/null +++ b/ziggurat/runtime/src/weights/rocksdb_weights.rs @@ -0,0 +1,63 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod constants { + use frame_support::{ + parameter_types, + weights::{constants, RuntimeDbWeight}, + }; + + parameter_types! { + /// By default, Substrate uses `RocksDB`, so this will be the weight used throughout + /// the runtime. + pub const RocksDbWeight: RuntimeDbWeight = RuntimeDbWeight { + read: 25_000 * constants::WEIGHT_REF_TIME_PER_NANOS, + write: 100_000 * constants::WEIGHT_REF_TIME_PER_NANOS, + }; + } + + #[cfg(test)] + mod test_db_weights { + use super::constants::RocksDbWeight as W; + use frame_support::weights::constants; + + /// Checks that all weights exist and have sane values. + // NOTE: If this test fails but you are sure that the generated values are fine, + // you can delete it. + #[test] + fn sane() { + // At least 1 µs. + assert!( + W::get().reads(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, + "Read weight should be at least 1 µs." + ); + assert!( + W::get().writes(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, + "Write weight should be at least 1 µs." + ); + // At most 1 ms. + assert!( + W::get().reads(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, + "Read weight should be at most 1 ms." + ); + assert!( + W::get().writes(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, + "Write weight should be at most 1 ms." + ); + } + } +} diff --git a/ziggurat/zombienet.toml b/ziggurat/zombienet.toml new file mode 100644 index 00000000..016624a7 --- /dev/null +++ b/ziggurat/zombienet.toml @@ -0,0 +1,17 @@ +[relaychain] +chain = "paseo-local" + +[[relaychain.nodes]] +name = "alice" +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 2000 +default_command = "./target/release/ziggurat" + +[[parachains.collators]] +name = "collator-01" From 985ba6caf5d4135f1468dec3d2be745fdbdd4cd5 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 16 Sep 2024 14:47:34 +0900 Subject: [PATCH 002/133] feat: Add ecdsa account unification to ziggurat --- frame/babel/src/ethereum.rs | 7 +-- frame/babel/src/extensions/mod.rs | 19 ++++++++ .../unify_account.rs} | 45 ++++++++++++++++--- frame/babel/src/lib.rs | 8 +--- primitives/runtime/src/generic/mod.rs | 31 +++++++++++++ ziggurat/node/src/chain_spec.rs | 26 ++++++----- ziggurat/runtime/Cargo.toml | 7 +++ ziggurat/runtime/src/configs/mod.rs | 26 ++++++++--- ziggurat/runtime/src/lib.rs | 23 ++++++---- 9 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 frame/babel/src/extensions/mod.rs rename frame/babel/src/{extensions.rs => extensions/unify_account.rs} (66%) diff --git a/frame/babel/src/ethereum.rs b/frame/babel/src/ethereum.rs index 10aa819b..40d2ad7d 100644 --- a/frame/babel/src/ethereum.rs +++ b/frame/babel/src/ethereum.rs @@ -18,6 +18,7 @@ use crate::*; +use crate::extensions::unify_account; use core::marker::PhantomData; use frame_support::dispatch::RawOrigin; use np_ethereum::Address as EthereumAddress; @@ -31,7 +32,7 @@ use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; -use sp_core::{ecdsa, H160, H256}; +use sp_core::{ecdsa, H160}; use sp_runtime::traits::AccountIdConversion; pub struct EnsureAddress(PhantomData); @@ -63,9 +64,9 @@ where pub struct AddressMapping(PhantomData); -impl pallet_evm::AddressMapping for AddressMapping +impl pallet_evm::AddressMapping for AddressMapping where - T::AccountId: From, + T: unify_account::Config, { fn into_account_id(who: H160) -> T::AccountId { let address = EthereumAddress::from(who); diff --git a/frame/babel/src/extensions/mod.rs b/frame/babel/src/extensions/mod.rs new file mode 100644 index 00000000..d6ad8a4d --- /dev/null +++ b/frame/babel/src/extensions/mod.rs @@ -0,0 +1,19 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub mod unify_account; diff --git a/frame/babel/src/extensions.rs b/frame/babel/src/extensions/unify_account.rs similarity index 66% rename from frame/babel/src/extensions.rs rename to frame/babel/src/extensions/unify_account.rs index 9ab319c4..1b9632a3 100644 --- a/frame/babel/src/extensions.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -19,14 +19,24 @@ use crate::*; use core::marker::PhantomData; +use frame_support::traits::tokens::{fungible, Fortitude, Preservation}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +use sp_core::H256; use sp_runtime::{ - traits::{DispatchInfoOf, One, SignedExtension, Zero}, + traits::{AccountIdConversion, DispatchInfoOf, One, SignedExtension, Zero}, transaction_validity::{TransactionValidityError, ValidTransaction}, }; -/// Integrates the accounts associated with the same public key. +/// A configuration for UnifyAccount signed extension. +pub trait Config: frame_system::Config + TryInto> { + /// A map from account to addresses. + type AddressMap: UniqueMultimap; + /// Drain account balance when unifying accounts. + type DrainBalance: DrainBalance; +} + +/// Unifies the accounts associated with the same public key. /// /// WARN: This extension should be placed after the `CheckNonce` extension. #[derive(Encode, Decode, MaxEncodedLen, TypeInfo)] @@ -50,9 +60,11 @@ impl UnifyAccount { if let Ok(public) = who.clone().try_into() { #[cfg(feature = "ethereum")] { - let address = Address::ethereum(public); - T::AddressMap::try_insert(who.clone(), address) - .map_err(|_| "account integration failed: ethereum")?; + let address = EthereumAddress::from(public.clone()); + let interim = address.clone().into_account_truncating(); + T::DrainBalance::drain_balance(&interim, &who)?; + T::AddressMap::try_insert(who.clone(), Address::Ethereum(address)) + .map_err(|_| "account unification failed: ethereum")?; } } Ok(()) @@ -124,3 +136,26 @@ impl SignedExtension for UnifyAccount { Ok(()) } } + +pub trait DrainBalance { + type Output: Default; + + fn drain_balance(_src: &AccountId, _dest: &AccountId) -> Result { + Ok(Default::default()) + } +} + +impl DrainBalance for T +where + AccountId: Eq, + T: fungible::Inspect + fungible::Mutate, +{ + type Output = T::Balance; + + fn drain_balance(src: &AccountId, dest: &AccountId) -> Result { + let amount = T::reducible_balance(&src, Preservation::Expendable, Fortitude::Polite); + T::transfer(&src, &dest, amount, Preservation::Expendable) + .map_err(|_| "account draining failed") + .map(|_| amount) + } +} diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index df6350ed..4d93f2fd 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -22,9 +22,9 @@ extern crate alloc; #[cfg(feature = "ethereum")] pub mod ethereum; -mod extensions; +pub mod extensions; -pub use extensions::UnifyAccount; +pub use extensions::unify_account::UnifyAccount; #[cfg(feature = "cosmos")] pub use np_cosmos::Address as CosmosAddress; @@ -35,10 +35,6 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::ecdsa; -pub trait Config: frame_system::Config> { - type AddressMap: UniqueMultimap; -} - #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] pub enum Address { #[cfg(feature = "cosmos")] diff --git a/primitives/runtime/src/generic/mod.rs b/primitives/runtime/src/generic/mod.rs index 52ab0827..05ac4161 100644 --- a/primitives/runtime/src/generic/mod.rs +++ b/primitives/runtime/src/generic/mod.rs @@ -18,3 +18,34 @@ mod unchecked_extrinsic; pub use unchecked_extrinsic::*; + +use frame_support::{ + dispatch::{DispatchInfo, GetDispatchInfo}, + traits::ExtrinsicCall, +}; +use scale_info::TypeInfo; +use sp_runtime::traits::SignedExtension; + +impl GetDispatchInfo + for UncheckedExtrinsic +where + Call: GetDispatchInfo, + Extra: SignedExtension, +{ + fn get_dispatch_info(&self) -> DispatchInfo { + self.function.get_dispatch_info() + } +} + +impl ExtrinsicCall + for UncheckedExtrinsic +where + Address: TypeInfo, + Call: TypeInfo, + Signature: TypeInfo, + Extra: SignedExtension + TypeInfo, +{ + fn call(&self) -> &Self::Call { + &self.function + } +} diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs index 0fb4a7d7..8396b0c6 100644 --- a/ziggurat/node/src/chain_spec.rs +++ b/ziggurat/node/src/chain_spec.rs @@ -1,9 +1,9 @@ use cumulus_primitives_core::ParaId; -use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT}; +use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT, UNIT}; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; use serde::{Deserialize, Serialize}; -use sp_core::{sr25519, Pair, Public}; +use sp_core::{ecdsa, sr25519, Pair, Public}; use sp_runtime::traits::{IdentifyAccount, Verify}; use ziggurat_runtime as runtime; @@ -65,8 +65,8 @@ pub fn template_session_keys(keys: AuraId) -> runtime::SessionKeys { pub fn development_config() -> ChainSpec { // Give your base currency a unit name and decimal places let mut properties = sc_chain_spec::Properties::new(); - properties.insert("tokenSymbol".into(), "UNIT".into()); - properties.insert("tokenDecimals".into(), 12.into()); + properties.insert("tokenSymbol".into(), "ZIG".into()); + properties.insert("tokenDecimals".into(), 18.into()); properties.insert("ss58Format".into(), 42.into()); ChainSpec::builder( @@ -74,7 +74,7 @@ pub fn development_config() -> ChainSpec { Extensions { relay_chain: "paseo-local".into(), // You MUST set this to the correct network! - para_id: 1000, + para_id: 2000, }, ) .with_name("Development") @@ -105,9 +105,10 @@ pub fn development_config() -> ChainSpec { get_account_id_from_seed::("Dave//stash"), get_account_id_from_seed::("Eve//stash"), get_account_id_from_seed::("Ferdie//stash"), + get_account_id_from_seed::("Alice"), ], get_account_id_from_seed::("Alice"), - 1000.into(), + 2000.into(), )) .build() } @@ -115,8 +116,8 @@ pub fn development_config() -> ChainSpec { pub fn local_testnet_config() -> ChainSpec { // Give your base currency a unit name and decimal places let mut properties = sc_chain_spec::Properties::new(); - properties.insert("tokenSymbol".into(), "UNIT".into()); - properties.insert("tokenDecimals".into(), 12.into()); + properties.insert("tokenSymbol".into(), "ZIG".into()); + properties.insert("tokenDecimals".into(), 18.into()); properties.insert("ss58Format".into(), 42.into()); #[allow(deprecated)] @@ -125,7 +126,7 @@ pub fn local_testnet_config() -> ChainSpec { Extensions { relay_chain: "paseo-local".into(), // You MUST set this to the correct network! - para_id: 1000, + para_id: 2000, }, ) .with_name("Local Testnet") @@ -156,11 +157,12 @@ pub fn local_testnet_config() -> ChainSpec { get_account_id_from_seed::("Dave//stash"), get_account_id_from_seed::("Eve//stash"), get_account_id_from_seed::("Ferdie//stash"), + get_account_id_from_seed::("Alice"), ], get_account_id_from_seed::("Alice"), - 1000.into(), + 2000.into(), )) - .with_protocol_id("template-local") + .with_protocol_id("ziggurat-local") .with_properties(properties) .build() } @@ -173,7 +175,7 @@ fn testnet_genesis( ) -> serde_json::Value { serde_json::json!({ "balances": { - "balances": endowed_accounts.iter().cloned().map(|k| (k, 1u64 << 60)).collect::>(), + "balances": endowed_accounts.iter().cloned().map(|k| (k, 1_000_000 * UNIT)).collect::>(), }, "parachainInfo": { "parachainId": id, diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 93065203..06ab674b 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -16,6 +16,10 @@ log = { version = "0.4", default-features = false } scale-info = { version = "2.11", default-features = false, features = ["derive"] } smallvec = "1.13" +frame-babel = { workspace = true, features = ["ethereum"] } +np-runtime = { workspace = true, features = ["serde"] } +pallet-multimap = { workspace = true } + frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -79,6 +83,7 @@ std = [ "cumulus-primitives-core/std", "cumulus-primitives-storage-weight-reclaim/std", "cumulus-primitives-utility/std", + "frame-babel/std", "frame-benchmarking?/std", "frame-executive/std", "frame-metadata-hash-extension/std", @@ -88,10 +93,12 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "log/std", + "np-runtime/std", "pallet-aura/std", "pallet-authorship/std", "pallet-balances/std", "pallet-collator-selection/std", + "pallet-multimap/std", "pallet-message-queue/std", "pallet-session/std", "pallet-sudo/std", diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 689674cc..b133dbae 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -28,6 +28,7 @@ mod xcm_config; // Substrate and Polkadot dependencies use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; +use frame_babel::extensions::unify_account; use frame_support::{ derive_impl, dispatch::DispatchClass, @@ -55,11 +56,12 @@ use xcm::latest::prelude::BodyId; // Local module imports use super::{ weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, - AccountId, Aura, Balance, Balances, Block, BlockNumber, CollatorSelection, ConsensusHook, Hash, - MessageQueue, Nonce, PalletInfo, ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, - RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, - System, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, - MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, + AccountId, AddressMap, Aura, Balance, Balances, Block, BlockNumber, CollatorSelection, + ConsensusHook, Hash, Instance1, MessageQueue, Nonce, PalletInfo, ParachainSystem, Runtime, + RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, + Session, SessionKeys, System, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, + EXISTENTIAL_DEPOSIT, HOURS, MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, + SLOT_DURATION, VERSION, }; use xcm_config::{RelayLocation, XcmOriginToTransactDispatchOrigin}; @@ -100,6 +102,8 @@ parameter_types! { impl frame_system::Config for Runtime { /// The identifier used to distinguish between accounts. type AccountId = AccountId; + /// The lookup mechanism to get account ID from whatever is passed in dispatchers. + type Lookup = sp_runtime::traits::AccountIdLookup; /// The index type for storing how many extrinsics an account has signed. type Nonce = Nonce; /// The type for hashing blocks and tries. @@ -302,3 +306,15 @@ impl pallet_collator_selection::Config for Runtime { type ValidatorRegistration = Session; type WeightInfo = (); } + +#[derive_impl(pallet_multimap::config_preludes::TestDefaultConfig)] +impl pallet_multimap::Config for Runtime { + type Key = AccountId; + type Value = frame_babel::Address; + type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; +} + +impl unify_account::Config for Runtime { + type AddressMap = AddressMap; + type DrainBalance = Balances; +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs index 57800873..11cc02a6 100644 --- a/ziggurat/runtime/src/lib.rs +++ b/ziggurat/runtime/src/lib.rs @@ -14,20 +14,23 @@ mod weights; extern crate alloc; use alloc::vec::Vec; +use np_runtime::MultiSignature; use smallvec::smallvec; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{BlakeTwo256, IdentifyAccount, Verify}, - MultiSignature, }; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; -use frame_support::weights::{ - constants::WEIGHT_REF_TIME_PER_SECOND, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, - WeightToFeePolynomial, +use frame_support::{ + instances::Instance1, + weights::{ + constants::WEIGHT_REF_TIME_PER_SECOND, Weight, WeightToFeeCoefficient, + WeightToFeeCoefficients, WeightToFeePolynomial, + }, }; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; pub use sp_runtime::{MultiAddress, Perbill, Permill}; @@ -82,13 +85,14 @@ pub type SignedExtra = ( frame_system::CheckNonce, frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, + frame_babel::UnifyAccount, cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim, frame_metadata_hash_extension::CheckMetadataHash, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = - generic::UncheckedExtrinsic; + np_runtime::generic::UncheckedExtrinsic; /// Executive: handles dispatch to the various modules. pub type Executive = frame_executive::Executive< @@ -188,9 +192,9 @@ pub const HOURS: BlockNumber = MINUTES * 60; pub const DAYS: BlockNumber = HOURS * 24; // Unit = the base number of indivisible units for balances -pub const UNIT: Balance = 1_000_000_000_000; -pub const MILLIUNIT: Balance = 1_000_000_000; -pub const MICROUNIT: Balance = 1_000_000; +pub const UNIT: Balance = 1_000_000_000_000_000_000; +pub const MILLIUNIT: Balance = 1_000_000_000_000_000; +pub const MICROUNIT: Balance = 1_000_000_000_000; /// The existential deposit. Set to 1/10 of the Connected Relay Chain. pub const EXISTENTIAL_DEPOSIT: Balance = MILLIUNIT; @@ -295,6 +299,9 @@ mod runtime { pub type CumulusXcm = cumulus_pallet_xcm; #[runtime::pallet_index(33)] pub type MessageQueue = pallet_message_queue; + + #[runtime::pallet_index(128)] + pub type AddressMap = pallet_multimap; } #[docify::export(register_validate_block)] From ef1caa80345e8c58bad95d748e03b807409b2763 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 16 Sep 2024 19:54:59 +0900 Subject: [PATCH 003/133] feat: Add Ethereum compatibility to ziggurat --- build.log | 44 +++++ ziggurat/node/Cargo.toml | 11 ++ ziggurat/node/src/cli.rs | 3 + ziggurat/node/src/command.rs | 1 + ziggurat/node/src/eth/backend.rs | 79 ++++++++ ziggurat/node/src/eth/config.rs | 71 ++++++++ ziggurat/node/src/eth/mod.rs | 199 +++++++++++++++++++++ ziggurat/node/src/eth/rpc.rs | 247 +++++++++++++++++++++++++ ziggurat/node/src/main.rs | 1 + ziggurat/node/src/service.rs | 79 +++++++- ziggurat/runtime/Cargo.toml | 20 +++ ziggurat/runtime/src/apis.rs | 268 +++++++++++++++++++++++++++- ziggurat/runtime/src/call.rs | 134 ++++++++++++++ ziggurat/runtime/src/configs/mod.rs | 87 ++++++++- ziggurat/runtime/src/lib.rs | 13 +- 15 files changed, 1245 insertions(+), 12 deletions(-) create mode 100644 build.log create mode 100644 ziggurat/node/src/eth/backend.rs create mode 100644 ziggurat/node/src/eth/config.rs create mode 100644 ziggurat/node/src/eth/mod.rs create mode 100644 ziggurat/node/src/eth/rpc.rs create mode 100644 ziggurat/runtime/src/call.rs diff --git a/build.log b/build.log new file mode 100644 index 00000000..4fcdac75 --- /dev/null +++ b/build.log @@ -0,0 +1,44 @@ +⚡ Found 3 strongly connected components which includes at least one cycle each +cycle(001) ∈ α: DisputeCoordinator ~~{"DisputeDistributionMessage"}~~> DisputeDistribution ~~{"DisputeCoordinatorMessage"}~~> * +cycle(002) ∈ β: CandidateBacking ~~{"CollatorProtocolMessage"}~~> CollatorProtocol ~~{"CandidateBackingMessage"}~~> * +cycle(003) ∈ γ: NetworkBridgeRx ~~{"GossipSupportMessage"}~~> GossipSupport ~~{"NetworkBridgeRxMessage"}~~> * + Checking ziggurat-node v0.1.0 (/Users/conr2d/Projects/noir/ziggurat/node) +warning: unused import: `backend::*` + --> ziggurat/node/src/eth/mod.rs:23:9 + | +23 | pub use backend::*; + | ^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused variable: `eth_rpc_builder` + --> ziggurat/node/src/service.rs:303:6 + | +303 | let eth_rpc_builder = {}; + | ^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_eth_rpc_builder` + | + = note: `#[warn(unused_variables)]` on by default + +error[E0277]: the trait bound `C: ProvideRuntimeApi<_>` is not satisfied + --> ziggurat/node/src/eth/backend.rs:67:5 + | +67 | overrides, + | ^^^^^^^^^ the trait `ProvideRuntimeApi<_>` is not implemented for `C`, which is required by `StorageOverrideHandler<_, C, _>: StorageOverride<_>` + | + = help: the trait `ProvideRuntimeApi` is implemented for `sc_service::client::client::Client` + = note: required for `StorageOverrideHandler<_, C, _>` to implement `StorageOverride<_>` + = note: required for the cast from `Arc>` to `Arc<(dyn StorageOverride<_> + 'static)>` + +error[E0277]: the trait bound `C: StorageProvider<_, _>` is not satisfied + --> ziggurat/node/src/eth/backend.rs:67:5 + | +67 | overrides, + | ^^^^^^^^^ the trait `StorageProvider<_, _>` is not implemented for `C`, which is required by `StorageOverrideHandler<_, C, _>: StorageOverride<_>` + | + = help: the trait `StorageProvider` is implemented for `sc_service::client::client::Client` + = note: required for `StorageOverrideHandler<_, C, _>` to implement `StorageOverride<_>` + = note: required for the cast from `Arc>` to `Arc<(dyn StorageOverride<_> + 'static)>` + +For more information about this error, try `rustc --explain E0277`. +warning: `ziggurat-node` (bin "ziggurat") generated 2 warnings +error: could not compile `ziggurat-node` (bin "ziggurat") due to 2 previous errors; 2 warnings emitted diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml index e01b4440..8062cac9 100644 --- a/ziggurat/node/Cargo.toml +++ b/ziggurat/node/Cargo.toml @@ -25,6 +25,14 @@ serde_json = "1.0" ziggurat-runtime = { path = "../runtime", default-features = true } +fc-api = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } +fc-db = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } +fc-mapping-sync = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["sql"] } +fc-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["txpool"] } +fc-rpc-core = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } +fp-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } +pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } + frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } @@ -37,6 +45,7 @@ sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "s sc-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sc-network = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sc-network-sync = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sc-rpc-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sc-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sc-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sc-sysinfo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } @@ -67,8 +76,10 @@ cumulus-client-service = { git = "https://github.com/paritytech/polkadot-sdk", b cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } cumulus-relay-chain-interface = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cumulus-test-relay-sproof-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } [build-dependencies] +docify = "0.2" substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } [features] diff --git a/ziggurat/node/src/cli.rs b/ziggurat/node/src/cli.rs index 16417e77..ccf36d91 100644 --- a/ziggurat/node/src/cli.rs +++ b/ziggurat/node/src/cli.rs @@ -66,6 +66,9 @@ pub struct Cli { #[command(flatten)] pub run: cumulus_client_cli::RunCmd, + #[command(flatten)] + pub eth_config: crate::eth::Configuration, + /// Disable automatic hardware benchmarks. /// /// By default these benchmarks are automatically ran at startup and measure diff --git a/ziggurat/node/src/command.rs b/ziggurat/node/src/command.rs index ffbf8340..cf7cd45e 100644 --- a/ziggurat/node/src/command.rs +++ b/ziggurat/node/src/command.rs @@ -246,6 +246,7 @@ pub fn run() -> Result<()> { crate::service::start_parachain_node( config, polkadot_config, + cli.eth_config, collator_options, id, hwbench, diff --git a/ziggurat/node/src/eth/backend.rs b/ziggurat/node/src/eth/backend.rs new file mode 100644 index 00000000..4d003afe --- /dev/null +++ b/ziggurat/node/src/eth/backend.rs @@ -0,0 +1,79 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ziggurat_runtime::opaque::Block; + +use fc_db::Backend; +use fc_rpc::StorageOverrideHandler; +use fp_rpc::EthereumRuntimeRPCApi; +use sc_client_api::{HeaderBackend, StorageProvider}; +use sc_service::Configuration; +use sp_api::ProvideRuntimeApi; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use crate::{eth::FrontierBackendType, service::ParachainBackend}; + +pub fn db_config_dir(config: &Configuration) -> PathBuf { + config.base_path.config_dir(config.chain_spec.id()) +} + +pub fn open_frontier_backend( + client: Arc, + config: &Configuration, + eth_config: &super::Configuration, +) -> Result, String> +where + C: HeaderBackend + + ProvideRuntimeApi + + StorageProvider + + 'static, + C::Api: EthereumRuntimeRPCApi, +{ + Ok(match eth_config.frontier_backend_type { + FrontierBackendType::KeyValue => Backend::KeyValue(Arc::new(fc_db::kv::Backend::open( + client.clone(), + &config.database, + &db_config_dir(config), + )?)), + FrontierBackendType::Sql => { + let overrides = Arc::new(StorageOverrideHandler::new(client.clone())); + let db_path = db_config_dir(config).join("sql"); + std::fs::create_dir_all(&db_path).expect("failed creating sql db directory"); + let backend = futures::executor::block_on(fc_db::sql::Backend::new( + fc_db::sql::BackendConfig::Sqlite(fc_db::sql::SqliteBackendConfig { + path: Path::new("sqlite:///") + .join(db_path) + .join("frontier.db3") + .to_str() + .unwrap(), + create_if_missing: true, + thread_count: eth_config.frontier_sql_backend_thread_count, + cache_size: eth_config.frontier_sql_backend_cache_size, + }), + eth_config.frontier_sql_backend_pool_size, + std::num::NonZeroU32::new(eth_config.frontier_sql_backend_num_ops_timeout), + overrides, + )) + .unwrap_or_else(|err| panic!("failed creating sql backend: {:?}", err)); + Backend::Sql(Arc::new(backend)) + }, + }) +} diff --git a/ziggurat/node/src/eth/config.rs b/ziggurat/node/src/eth/config.rs new file mode 100644 index 00000000..8241cd50 --- /dev/null +++ b/ziggurat/node/src/eth/config.rs @@ -0,0 +1,71 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// Available frontier backend types. +#[derive(Debug, Copy, Clone, Default, clap::ValueEnum)] +pub enum FrontierBackendType { + /// Either RocksDb or ParityDb as per inherited from the global backend settings. + #[default] + KeyValue, + /// Sql database with custom log indexing. + Sql, +} + +#[derive(Debug, clap::Parser)] +pub struct Configuration { + /// Maximum number of logs in a query. + #[arg(long, default_value = "10000")] + pub max_past_logs: u32, + + /// Maximum fee history cache size. + #[arg(long, default_value = "2048")] + pub fee_history_limit: u64, + + /// The dynamic-fee pallet target gas price set by block author + #[arg(long, default_value = "1")] + pub target_gas_price: u64, + + /// Size in bytes of the LRU cache for block data. + #[arg(long, default_value = "50")] + pub eth_log_block_cache: usize, + + /// Size in bytes of the LRU cache for transactions statuses data. + #[arg(long, default_value = "50")] + pub eth_statuses_cache: usize, + + /// Sets the frontier backend type (KeyValue or Sql) + #[arg(long, value_enum, ignore_case = true, default_value_t = FrontierBackendType::default())] + pub frontier_backend_type: FrontierBackendType, + + // Sets the SQL backend's pool size. + #[arg(long, default_value = "100")] + pub frontier_sql_backend_pool_size: u32, + + /// Sets the SQL backend's query timeout in number of VM ops. + #[arg(long, default_value = "10000000")] + pub frontier_sql_backend_num_ops_timeout: u32, + + /// Sets the SQL backend's auxiliary thread limit. + #[arg(long, default_value = "4")] + pub frontier_sql_backend_thread_count: u32, + + /// Sets the SQL backend's query timeout in number of VM ops. + /// Default value is 200MB. + #[arg(long, default_value = "209715200")] + pub frontier_sql_backend_cache_size: u64, +} diff --git a/ziggurat/node/src/eth/mod.rs b/ziggurat/node/src/eth/mod.rs new file mode 100644 index 00000000..243fb4aa --- /dev/null +++ b/ziggurat/node/src/eth/mod.rs @@ -0,0 +1,199 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +mod backend; +mod config; +pub mod rpc; + +pub use backend::*; +pub use config::*; + +use fc_mapping_sync::{kv::MappingSyncWorker, SyncStrategy}; +use fc_rpc::{EthTask, StorageOverride}; +use fc_rpc_core::types::{FeeHistoryCache, FeeHistoryCacheLimit, FilterPool}; +use fp_rpc::EthereumRuntimeRPCApi; +use futures::prelude::*; +use jsonrpsee::RpcModule; +use sc_client_api::{ + backend::{Backend, StateBackend, StorageProvider}, + blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}, + client::BlockchainEvents, + BlockOf, +}; +use sc_network_sync::SyncingService; +use sc_rpc_api::DenyUnsafe; +use sc_service::{Error as ServiceError, TaskManager}; +use sp_api::ProvideRuntimeApi; +use sp_block_builder::BlockBuilder; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Header as HeaderT}; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, + time::Duration, +}; + +pub struct SpawnTasksParams<'a, B: BlockT, C, BE> { + pub config: sc_service::Configuration, + pub rpc_builder: Box Result, ServiceError>>, + pub task_manager: &'a mut TaskManager, + pub client: Arc, + pub substrate_backend: Arc, + pub frontier_backend: Arc>, + pub filter_pool: Option, + pub storage_override: Arc>, + pub fee_history_cache: FeeHistoryCache, + pub fee_history_cache_limit: FeeHistoryCacheLimit, + pub sync: Arc>, + pub pubsub_notification_sinks: Arc< + fc_mapping_sync::EthereumBlockNotificationSinks< + fc_mapping_sync::EthereumBlockNotification, + >, + >, +} + +pub fn spawn_tasks( + params: SpawnTasksParams, +) -> Result +where + C: ProvideRuntimeApi + BlockOf, + C: HeaderBackend + HeaderMetadata + 'static, + C: BlockchainEvents + StorageProvider, + C: Send + Sync + 'static, + C::Api: EthereumRuntimeRPCApi, + C::Api: BlockBuilder, + B: BlockT + Send + Sync + 'static, + B::Header: HeaderT, + BE: Backend + 'static, + BE::State: StateBackend, +{ + let SpawnTasksParams { + mut config, + rpc_builder, + task_manager, + client, + substrate_backend, + frontier_backend, + filter_pool, + storage_override, + fee_history_cache, + fee_history_cache_limit, + sync, + pubsub_notification_sinks, + } = params; + + let rpc_port = config.rpc_port; + // TODO: Make the Ethereum RPC port configurable. + config.rpc_port = 8545; + + let rpc = sc_service::start_rpc_servers( + &config, + rpc_builder, + Some(Box::new(fc_rpc::EthereumSubIdProvider)), + ); + if rpc.is_ok() { + log::info!("Ethereum RPC started: {}", config.rpc_port); + } else { + log::warn!("Ethereum RPC not started"); + } + task_manager.keep_alive(rpc); + + // Spawn main mapping sync worker background task. + match *frontier_backend { + fc_db::Backend::KeyValue(ref b) => { + task_manager.spawn_essential_handle().spawn( + "frontier-mapping-sync-worker", + Some("frontier"), + MappingSyncWorker::new( + client.import_notification_stream(), + Duration::new(6, 0), + client.clone(), + substrate_backend.clone(), + storage_override.clone(), + b.clone(), + 3, + 0, + SyncStrategy::Parachain, + sync.clone(), + pubsub_notification_sinks.clone(), + ) + .for_each(|()| future::ready(())), + ); + }, + fc_db::Backend::Sql(ref b) => { + task_manager.spawn_essential_handle().spawn_blocking( + "frontier-mapping-sync-worker", + Some("frontier"), + fc_mapping_sync::sql::SyncWorker::run( + client.clone(), + substrate_backend.clone(), + b.clone(), + client.import_notification_stream(), + fc_mapping_sync::sql::SyncWorkerConfig { + read_notification_timeout: Duration::from_secs(10), + check_indexed_blocks_interval: Duration::from_secs(60), + }, + fc_mapping_sync::SyncStrategy::Parachain, + sync.clone(), + pubsub_notification_sinks.clone(), + ), + ); + }, + } + + // Spawn Frontier EthFilterApi maintenance task. + if let Some(filter_pool) = filter_pool { + // Each filter is allowed to stay in the pool for 100 blocks. + const FILTER_RETAIN_THRESHOLD: u64 = 100; + task_manager.spawn_essential_handle().spawn( + "frontier-filter-pool", + Some("frontier"), + EthTask::filter_pool_task(client.clone(), filter_pool, FILTER_RETAIN_THRESHOLD), + ); + } + + // Spawn Frontier FeeHistory cache maintenance task. + task_manager.spawn_essential_handle().spawn( + "frontier-fee-history", + Some("frontier"), + EthTask::fee_history_task( + client.clone(), + storage_override.clone(), + fee_history_cache, + fee_history_cache_limit, + ), + ); + + config.rpc_port = rpc_port; + + Ok(config) +} + +pub struct PartialComponents { + pub filter_pool: Option, + pub fee_history_cache: FeeHistoryCache, + pub fee_history_cache_limit: FeeHistoryCacheLimit, +} + +pub fn new_partial(config: &Configuration) -> Result { + Ok(PartialComponents { + filter_pool: Some(Arc::new(Mutex::new(BTreeMap::new()))), + fee_history_cache: Arc::new(Mutex::new(BTreeMap::new())), + fee_history_cache_limit: config.fee_history_limit, + }) +} diff --git a/ziggurat/node/src/eth/rpc.rs b/ziggurat/node/src/eth/rpc.rs new file mode 100644 index 00000000..1e908344 --- /dev/null +++ b/ziggurat/node/src/eth/rpc.rs @@ -0,0 +1,247 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ziggurat_runtime::opaque::Block; + +use cumulus_primitives_core::PersistedValidationData; +use cumulus_primitives_parachain_inherent::ParachainInherentData; +use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; +use fc_rpc::{EthBlockDataCacheTask, StorageOverride}; +use fc_rpc_core::types::{FeeHistoryCache, FeeHistoryCacheLimit, FilterPool}; +use fp_rpc::{ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi}; +use jsonrpsee::RpcModule; +use sc_client_api::{ + client::BlockchainEvents, AuxStore, Backend, HeaderBackend, StorageProvider, UsageProvider, +}; +use sc_network::service::traits::NetworkService; +use sc_network_sync::SyncingService; +use sc_rpc::SubscriptionTaskExecutor; +use sc_transaction_pool::{ChainApi, Pool}; +use sc_transaction_pool_api::TransactionPool; +use sp_api::{CallApiAt, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder; +use sp_consensus_aura::{sr25519::AuthorityId as AuraId, AuraApi}; +use sp_core::H256; +use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; + +pub struct EthConfig(PhantomData<(C, BE)>); + +impl fc_rpc::EthConfig for EthConfig +where + C: StorageProvider + Send + Sync + 'static, + BE: Backend + 'static, +{ + type EstimateGasAdapter = (); + type RuntimeStorageOverride = + fc_rpc::frontier_backend_client::SystemAccountId20StorageOverride; +} + +/// Extra dependencies for Ethereum compatibility. +pub struct FullDeps { + /// The client instance to use. + pub client: Arc, + /// Transaction pool instance. + pub pool: Arc

, + /// Graph pool instance. + pub graph: Arc>, + /// The Node authority flag + pub is_authority: bool, + /// Network service + pub network: Arc, + /// Chain syncing service + pub sync: Arc>, + /// Frontier Backend. + pub frontier_backend: Arc>, + /// Ethereum data access overrides. + pub storage_override: Arc>, + /// Cache for Ethereum block data. + pub block_data_cache: Arc>, + /// EthFilterApi pool. + pub filter_pool: Option, + /// Maximum number of logs in a query. + pub max_past_logs: u32, + /// Fee history cache. + pub fee_history_cache: FeeHistoryCache, + /// Maximum fee history cache size. + pub fee_history_cache_limit: FeeHistoryCacheLimit, + /// Mandated parent hashes for a given block hash. + pub forced_parent_hashes: Option>, + /// + pub subscription_task_executor: SubscriptionTaskExecutor, + /// + pub pubsub_notification_sinks: Arc< + fc_mapping_sync::EthereumBlockNotificationSinks< + fc_mapping_sync::EthereumBlockNotification, + >, + >, +} + +/// Instantiate Ethereum-compatible RPC extensions. +pub fn create_full( + deps: FullDeps, +) -> Result, Box> +where + C: CallApiAt + + ProvideRuntimeApi + + AuxStore + + StorageProvider + + UsageProvider + + HeaderBackend + + BlockchainEvents + + 'static, + C::Api: AuraApi + + EthereumRuntimeRPCApi + + BlockBuilder + + ConvertTransactionRuntimeApi, + P: TransactionPool + 'static, + A: ChainApi + 'static, + BE: Backend + 'static, +{ + use fc_rpc::{ + pending::AuraConsensusDataProvider, Debug, DebugApiServer, Eth, EthApiServer, EthFilter, + EthFilterApiServer, EthPubSub, EthPubSubApiServer, Net, NetApiServer, TxPool, + TxPoolApiServer, Web3, Web3ApiServer, + }; + + let mut io = RpcModule::new(()); + let FullDeps { + client, + pool, + graph, + is_authority, + network, + sync, + frontier_backend, + storage_override, + block_data_cache, + filter_pool, + max_past_logs, + fee_history_cache, + fee_history_cache_limit, + forced_parent_hashes, + subscription_task_executor, + pubsub_notification_sinks, + } = deps; + + let signers = Vec::new(); + + enum Never {} + impl fp_rpc::ConvertTransaction for Never { + fn convert_transaction(&self, _transaction: pallet_ethereum::Transaction) -> T { + // The Never type is not instantiable, but this method requires the type to be + // instantiated to be called (`&self` parameter), so if the code compiles we + // have the guarantee that this function will never be called. + unreachable!() + } + } + let convert_transaction: Option = None; + + let pending_create_inherent_data_providers = move |_, _| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + // Create a dummy parachain inherent data provider which is required to pass + // the checks by the para chain system. We use dummy values because in the 'pending + // context' neither do we have access to the real values nor do we need them. + let (relay_parent_storage_root, relay_chain_state) = + RelayStateSproofBuilder::default().into_state_root_and_proof(); + let vfp = PersistedValidationData { + // This is a hack to make + // `cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases` happy. Relay + // parent number can't be bigger than u32::MAX. + relay_parent_number: u32::MAX, + relay_parent_storage_root, + ..Default::default() + }; + let parachain_inherent_data = ParachainInherentData { + validation_data: vfp, + relay_chain_state, + downward_messages: Default::default(), + horizontal_messages: Default::default(), + }; + Ok((timestamp, parachain_inherent_data)) + }; + + io.merge( + Eth::<_, _, _, _, _, _, _, EthConfig<_, _>>::new( + client.clone(), + pool.clone(), + graph.clone(), + convert_transaction, + sync.clone(), + signers, + storage_override.clone(), + frontier_backend.clone(), + is_authority, + block_data_cache.clone(), + fee_history_cache, + fee_history_cache_limit, + 10, /* execute_gas_limit_multiplier */ + forced_parent_hashes, + pending_create_inherent_data_providers, + Some(Box::new(AuraConsensusDataProvider::new(client.clone()))), + ) + .replace_config::>() + .into_rpc(), + )?; + + if let Some(filter_pool) = filter_pool { + io.merge( + EthFilter::new( + client.clone(), + frontier_backend.clone(), + graph.clone(), + filter_pool, + 500_usize, /* max stored filters */ + max_past_logs, + block_data_cache.clone(), + ) + .into_rpc(), + )?; + } + + io.merge( + Net::new( + client.clone(), + network.clone(), + // Whether to format the `peer_count` response as Hex (default) or not. + true, + ) + .into_rpc(), + )?; + + io.merge(Web3::new(client.clone()).into_rpc())?; + + io.merge( + EthPubSub::new( + pool, + client.clone(), + sync.clone(), + subscription_task_executor, + storage_override.clone(), + pubsub_notification_sinks.clone(), + ) + .into_rpc(), + )?; + + io.merge( + Debug::new(client.clone(), frontier_backend, storage_override, block_data_cache).into_rpc(), + )?; + + io.merge(TxPool::new(client, graph).into_rpc())?; + + Ok(io) +} diff --git a/ziggurat/node/src/main.rs b/ziggurat/node/src/main.rs index 12738a67..9b660805 100644 --- a/ziggurat/node/src/main.rs +++ b/ziggurat/node/src/main.rs @@ -5,6 +5,7 @@ mod chain_spec; mod cli; mod command; +mod eth; mod rpc; mod service; diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs index 6a21646e..85963d52 100644 --- a/ziggurat/node/src/service.rs +++ b/ziggurat/node/src/service.rs @@ -10,6 +10,10 @@ use ziggurat_runtime::{ opaque::{Block, Hash}, }; +// Frontier Imports +use crate::eth; +use fc_rpc::StorageOverrideHandler; + // Cumulus Imports use cumulus_client_collator::service::CollatorService; #[docify::export(lookahead_collator)] @@ -45,7 +49,7 @@ type ParachainExecutor = WasmExecutor; type ParachainClient = TFullClient; -type ParachainBackend = TFullBackend; +pub type ParachainBackend = TFullBackend; type ParachainBlockImport = TParachainBlockImport, ParachainBackend>; @@ -227,6 +231,7 @@ fn start_consensus( pub async fn start_parachain_node( parachain_config: Configuration, polkadot_config: Configuration, + eth_config: eth::Configuration, collator_options: CollatorOptions, para_id: ParaId, hwbench: Option, @@ -300,6 +305,78 @@ pub async fn start_parachain_node( ); } + let eth::PartialComponents { filter_pool, fee_history_cache, fee_history_cache_limit } = + eth::new_partial(ð_config)?; + + let frontier_backend = + Arc::new(eth::open_frontier_backend(client.clone(), ¶chain_config, ð_config)?); + let storage_override = Arc::new(StorageOverrideHandler::new(client.clone())); + let pubsub_notification_sinks = Arc::new(fc_mapping_sync::EthereumBlockNotificationSinks::< + fc_mapping_sync::EthereumBlockNotification, + >::default()); + + let eth_rpc_builder = { + let client = client.clone(); + let pool = transaction_pool.clone(); + let network = network.clone(); + let sync_service = sync_service.clone(); + let subscription_task_executor = Arc::new(task_manager.spawn_handle()); + let is_authority = validator; + let max_past_logs = eth_config.max_past_logs; + let filter_pool = filter_pool.clone(); + let frontier_backend = frontier_backend.clone(); + let pubsub_notificatino_sinks = pubsub_notification_sinks.clone(); + let storage_override = storage_override.clone(); + let fee_history_cache = fee_history_cache.clone(); + let block_data_cache = Arc::new(fc_rpc::EthBlockDataCacheTask::new( + task_manager.spawn_handle(), + storage_override.clone(), + eth_config.eth_log_block_cache, + eth_config.eth_statuses_cache, + prometheus_registry.clone(), + )); + + Box::new(move |_deny_unsafe| { + eth::rpc::create_full(eth::rpc::FullDeps { + client: client.clone(), + pool: pool.clone(), + graph: pool.pool().clone(), + is_authority, + network: network.clone(), + sync: sync_service.clone(), + frontier_backend: match *frontier_backend { + fc_db::Backend::KeyValue(ref b) => b.clone(), + fc_db::Backend::Sql(ref b) => b.clone(), + }, + storage_override: storage_override.clone(), + block_data_cache: block_data_cache.clone(), + filter_pool: filter_pool.clone(), + max_past_logs, + fee_history_cache: fee_history_cache.clone(), + fee_history_cache_limit, + forced_parent_hashes: None, + subscription_task_executor: subscription_task_executor.clone(), + pubsub_notification_sinks: pubsub_notificatino_sinks.clone(), + }) + .map_err(Into::into) + }) + }; + + let parachain_config = eth::spawn_tasks(eth::SpawnTasksParams { + config: parachain_config, + rpc_builder: eth_rpc_builder, + task_manager: &mut task_manager, + client: client.clone(), + substrate_backend: backend.clone(), + frontier_backend, + filter_pool, + storage_override, + fee_history_cache, + fee_history_cache_limit, + sync: sync_service.clone(), + pubsub_notification_sinks, + })?; + let rpc_builder = { let client = client.clone(); let transaction_pool = transaction_pool.clone(); diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 06ab674b..a68663d7 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -17,9 +17,17 @@ scale-info = { version = "2.11", default-features = false, features = ["derive"] smallvec = "1.13" frame-babel = { workspace = true, features = ["ethereum"] } +np-ethereum = { workspace = true } np-runtime = { workspace = true, features = ["serde"] } pallet-multimap = { workspace = true } +fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +fp-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +fp-self-contained = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +pallet-base-fee = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } + frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -83,6 +91,9 @@ std = [ "cumulus-primitives-core/std", "cumulus-primitives-storage-weight-reclaim/std", "cumulus-primitives-utility/std", + "fp-evm/std", + "fp-rpc/std", + "fp-self-contained/std", "frame-babel/std", "frame-benchmarking?/std", "frame-executive/std", @@ -93,11 +104,15 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "log/std", + "np-ethereum/std", "np-runtime/std", "pallet-aura/std", "pallet-authorship/std", "pallet-balances/std", + "pallet-base-fee/std", "pallet-collator-selection/std", + "pallet-ethereum/std", + "pallet-evm/std", "pallet-multimap/std", "pallet-message-queue/std", "pallet-session/std", @@ -141,6 +156,8 @@ runtime-benchmarks = [ "hex-literal", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-ethereum/runtime-benchmarks", + "pallet-evm/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", @@ -165,7 +182,10 @@ try-runtime = [ "pallet-aura/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", + "pallet-base-fee/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-ethereum/try-runtime", + "pallet-evm/try-runtime", "pallet-message-queue/try-runtime", "pallet-session/try-runtime", "pallet-sudo/try-runtime", diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index f5d5d3e6..3fe31ced 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -25,26 +25,30 @@ // External crates imports use alloc::vec::Vec; +use codec::Encode; use frame_support::{ genesis_builder_helper::{build_state, get_preset}, + traits::OnFinalize, weights::Weight, }; use pallet_aura::Authorities; +use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; +use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ traits::Block as BlockT, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, + ApplyExtrinsicResult, Permill, SaturatedConversion, }; use sp_version::RuntimeVersion; // Local module imports use super::{ - AccountId, Balance, Block, ConsensusHook, Executive, InherentDataExt, Nonce, ParachainSystem, - Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, - SLOT_DURATION, VERSION, + AccountId, Balance, Block, ConsensusHook, Ethereum, Executive, InherentDataExt, Nonce, + ParachainSystem, Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, + TransactionPayment, UncheckedExtrinsic, SLOT_DURATION, VERSION, }; // we move some impls outside so we can easily use them with `docify`. @@ -210,6 +214,260 @@ impl_runtime_apis! { } } + impl fp_rpc::EthereumRuntimeRPCApi for Runtime { + fn chain_id() -> u64 { + ::ChainId::get() + } + + fn account_basic(address: H160) -> EVMAccount { + let (account, _) = pallet_evm::Pallet::::account_basic(&address); + account + } + + fn gas_price() -> U256 { + let (gas_price, _) = ::FeeCalculator::min_gas_price(); + gas_price + } + + fn account_code_at(address: H160) -> Vec { + pallet_evm::AccountCodes::::get(address) + } + + fn author() -> H160 { + >::find_author() + } + + fn storage_at(address: H160, index: U256) -> H256 { + let mut tmp = [0u8; 32]; + index.to_big_endian(&mut tmp); + pallet_evm::AccountStorages::::get(address, H256::from_slice(&tmp[..])) + } + + fn call( + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + estimate: bool, + access_list: Option)>>, + ) -> Result { + use pallet_evm::GasWeightMapping as _; + + let config = if estimate { + let mut config = ::config().clone(); + config.estimate = true; + Some(config) + } else { + None + }; + + // Estimated encoded transaction size must be based on the heaviest transaction + // type (EIP1559Transaction) to be compatible with all transaction types. + let mut estimated_transaction_len = data.len() + + // pallet ethereum index: 1 + // transact call index: 1 + // Transaction enum variant: 1 + // chain_id 8 bytes + // nonce: 32 + // max_priority_fee_per_gas: 32 + // max_fee_per_gas: 32 + // gas_limit: 32 + // action: 21 (enum varianrt + call address) + // value: 32 + // access_list: 1 (empty vec size) + // 65 bytes signature + 258; + + if access_list.is_some() { + estimated_transaction_len += access_list.encoded_size(); + } + + + let gas_limit = if gas_limit > U256::from(u64::MAX) { + u64::MAX + } else { + gas_limit.low_u64() + }; + let without_base_extrinsic_weight = true; + + let (weight_limit, proof_size_base_cost) = + match ::GasWeightMapping::gas_to_weight( + gas_limit, + without_base_extrinsic_weight + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } + _ => (None, None), + }; + + ::Runner::call( + from, + to, + data, + value, + gas_limit.saturated_into(), + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + access_list.unwrap_or_default(), + false, + true, + weight_limit, + proof_size_base_cost, + config.as_ref().unwrap_or(::config()), + ).map_err(|err| err.error.into()) + } + + fn create( + from: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + estimate: bool, + access_list: Option)>>, + ) -> Result { + use pallet_evm::GasWeightMapping as _; + + let config = if estimate { + let mut config = ::config().clone(); + config.estimate = true; + Some(config) + } else { + None + }; + + + let mut estimated_transaction_len = data.len() + + // from: 20 + // value: 32 + // gas_limit: 32 + // nonce: 32 + // 1 byte transaction action variant + // chain id 8 bytes + // 65 bytes signature + 190; + + if max_fee_per_gas.is_some() { + estimated_transaction_len += 32; + } + if max_priority_fee_per_gas.is_some() { + estimated_transaction_len += 32; + } + if access_list.is_some() { + estimated_transaction_len += access_list.encoded_size(); + } + + + let gas_limit = if gas_limit > U256::from(u64::MAX) { + u64::MAX + } else { + gas_limit.low_u64() + }; + let without_base_extrinsic_weight = true; + + let (weight_limit, proof_size_base_cost) = + match ::GasWeightMapping::gas_to_weight( + gas_limit, + without_base_extrinsic_weight + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } + _ => (None, None), + }; + + ::Runner::create( + from, + data, + value, + gas_limit.saturated_into(), + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + access_list.unwrap_or_default(), + false, + true, + weight_limit, + proof_size_base_cost, + config.as_ref().unwrap_or(::config()), + ).map_err(|err| err.error.into()) + } + + fn current_transaction_statuses() -> Option> { + pallet_ethereum::CurrentTransactionStatuses::::get() + } + + fn current_block() -> Option { + pallet_ethereum::CurrentBlock::::get() + } + + fn current_receipts() -> Option> { + pallet_ethereum::CurrentReceipts::::get() + } + + fn current_all() -> ( + Option, + Option>, + Option> + ) { + ( + pallet_ethereum::CurrentBlock::::get(), + pallet_ethereum::CurrentReceipts::::get(), + pallet_ethereum::CurrentTransactionStatuses::::get() + ) + } + + fn extrinsic_filter( + xts: Vec<::Extrinsic>, + ) -> Vec { + xts.into_iter().filter_map(|xt| match xt.0.function { + RuntimeCall::Ethereum(pallet_ethereum::Call::transact { transaction }) => Some(transaction), + _ => None + }).collect::>() + } + + fn elasticity() -> Option { + Some(pallet_base_fee::Elasticity::::get()) + } + + fn gas_limit_multiplier_support() {} + + fn pending_block( + xts: Vec<::Extrinsic>, + ) -> (Option, Option>) { + for ext in xts.into_iter() { + let _ = Executive::apply_extrinsic(ext); + } + + Ethereum::on_finalize(System::block_number() + 1); + + ( + pallet_ethereum::CurrentBlock::::get(), + pallet_ethereum::CurrentTransactionStatuses::::get() + ) + } + + fn initialize_pending_block(header: &::Header) { + Executive::initialize_block(header); + } + } + + impl fp_rpc::ConvertTransactionRuntimeApi for Runtime { + fn convert_transaction(transaction: EthereumTransaction) -> ::Extrinsic { + UncheckedExtrinsic::new_unsigned( + pallet_ethereum::Call::::transact { transaction }.into(), + ) + } + } + impl cumulus_primitives_core::CollectCollationInfo for Runtime { fn collect_collation_info(header: &::Header) -> cumulus_primitives_core::CollationInfo { ParachainSystem::collect_collation_info(header) diff --git a/ziggurat/runtime/src/call.rs b/ziggurat/runtime/src/call.rs new file mode 100644 index 00000000..4c8b1290 --- /dev/null +++ b/ziggurat/runtime/src/call.rs @@ -0,0 +1,134 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use fp_evm::TransactionValidationError; +use fp_self_contained::SelfContainedCall; +use frame_babel::{ethereum::TransactionExt, UnifyAccount}; +use np_ethereum::Address as EthereumAddress; +use sp_core::ecdsa; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf}, + transaction_validity::{ + InvalidTransaction, TransactionValidity, TransactionValidityError, UnknownTransaction, + }, + DispatchResultWithInfo, +}; + +use super::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; + +impl SelfContainedCall for RuntimeCall { + type SignedInfo = AccountId; + + fn is_self_contained(&self) -> bool { + match self { + RuntimeCall::Ethereum(call) => call.is_self_contained(), + _ => false, + } + } + + fn check_self_contained(&self) -> Option> { + match self { + RuntimeCall::Ethereum(call) => { + if let pallet_ethereum::Call::transact { transaction } = call { + let check = || { + let origin = transaction.recover_key().map(Self::SignedInfo::from).ok_or( + InvalidTransaction::Custom( + TransactionValidationError::InvalidSignature as u8, + ), + )?; + Ok(origin) + }; + Some(check()) + } else { + None + } + }, + _ => None, + } + } + + fn validate_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option { + match self { + RuntimeCall::Ethereum(call) => { + if let pallet_ethereum::Call::transact { transaction } = call { + if transaction.nonce() == 0 { + match UnifyAccount::::unify_ecdsa(info) { + Ok(_) => (), + Err(_) => + return Some(Err(TransactionValidityError::Unknown( + UnknownTransaction::CannotLookup, + ))), + } + } + } + let public: ecdsa::Public = info.clone().try_into().unwrap(); + let address: EthereumAddress = public.into(); + call.validate_self_contained(&address.into(), dispatch_info, len) + }, + _ => None, + } + } + + fn pre_dispatch_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option> { + match self { + RuntimeCall::Ethereum(call) => { + if let pallet_ethereum::Call::transact { transaction } = &call { + if transaction.nonce() == 0 { + match UnifyAccount::::unify_ecdsa(info) { + Ok(_) => (), + Err(_) => + return Some(Err(TransactionValidityError::Unknown( + UnknownTransaction::CannotLookup, + ))), + } + } + } + let public: ecdsa::Public = info.clone().try_into().unwrap(); + let address: EthereumAddress = public.into(); + call.pre_dispatch_self_contained(&address.into(), dispatch_info, len) + }, + _ => None, + } + } + + fn apply_self_contained( + self, + info: Self::SignedInfo, + ) -> Option>> { + match self { + call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { + let public: ecdsa::Public = info.clone().try_into().unwrap(); + let address: EthereumAddress = public.into(); + Some(call.dispatch(RuntimeOrigin::from( + pallet_ethereum::RawOrigin::EthereumTransaction(address.into()), + ))) + }, + _ => None, + } + } +} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index b133dbae..559eed0d 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -28,7 +28,11 @@ mod xcm_config; // Substrate and Polkadot dependencies use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; -use frame_babel::extensions::unify_account; +use fp_evm::weight_per_gas; +use frame_babel::{ + ethereum::{AddressMapping, EnsureAddress, FrontierPrecompiles}, + extensions::unify_account, +}; use frame_support::{ derive_impl, dispatch::DispatchClass, @@ -43,23 +47,25 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_ethereum::PostLogContent; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use polkadot_runtime_common::{ xcm_sender::NoPriceForMessageDelivery, BlockHashCount, SlowAdjustingFeeUpdate, }; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_runtime::Perbill; +use sp_core::U256; +use sp_runtime::{Perbill, Permill}; use sp_version::RuntimeVersion; use xcm::latest::prelude::BodyId; // Local module imports use super::{ weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, - AccountId, AddressMap, Aura, Balance, Balances, Block, BlockNumber, CollatorSelection, + AccountId, AddressMap, Aura, Balance, Balances, BaseFee, Block, BlockNumber, CollatorSelection, ConsensusHook, Hash, Instance1, MessageQueue, Nonce, PalletInfo, ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, - Session, SessionKeys, System, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, + Session, SessionKeys, System, Timestamp, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; @@ -318,3 +324,76 @@ impl unify_account::Config for Runtime { type AddressMap = AddressMap; type DrainBalance = Balances; } + +const BLOCK_GAS_LIMIT: u64 = 75_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +const WEIGHT_MILLISECS_PER_BLOCK: u64 = 2000; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const ChainId: u64 = 1337; + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub PrecompilesValue: FrontierPrecompiles = FrontierPrecompiles::<_>::new(); + pub WeightPerGas: Weight = Weight::from_parts(weight_per_gas(BLOCK_GAS_LIMIT, NORMAL_DISPATCH_RATIO, WEIGHT_MILLISECS_PER_BLOCK), 0); + pub SuicideQuickClearLimit: u32 = 0; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = BaseFee; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = pallet_ethereum::EthereumBlockHashMapping; + type CallOrigin = EnsureAddress; + type WithdrawOrigin = EnsureAddress; + type AddressMapping = AddressMapping; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = FrontierPrecompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = ChainId; + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = SuicideQuickClearLimit; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const PostBlockAndTxnHashes: PostLogContent = PostLogContent::BlockAndTxnHashes; +} + +impl pallet_ethereum::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type StateRoot = pallet_ethereum::IntermediateStateRoot; + type PostLogContent = PostBlockAndTxnHashes; + type ExtraDataLength = ConstU32<30>; +} + +parameter_types! { + pub DefaultBaseFeePerGas: U256 = U256::from(1_000_000_000); + pub DefaultElasticity: Permill = Permill::from_parts(125_000); +} + +pub struct BaseFeeThreshold; +impl pallet_base_fee::BaseFeeThreshold for BaseFeeThreshold { + fn lower() -> Permill { + Permill::zero() + } + fn ideal() -> Permill { + Permill::from_parts(500_000) + } + fn upper() -> Permill { + Permill::from_parts(1_000_000) + } +} + +impl pallet_base_fee::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Threshold = BaseFeeThreshold; + type DefaultBaseFeePerGas = DefaultBaseFeePerGas; + type DefaultElasticity = DefaultElasticity; +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs index 11cc02a6..79cff117 100644 --- a/ziggurat/runtime/src/lib.rs +++ b/ziggurat/runtime/src/lib.rs @@ -9,12 +9,13 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); pub mod apis; #[cfg(feature = "runtime-benchmarks")] mod benchmarks; +mod call; mod configs; mod weights; extern crate alloc; use alloc::vec::Vec; -use np_runtime::MultiSignature; +use np_runtime::{self_contained, MultiSignature}; use smallvec::smallvec; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, @@ -92,7 +93,7 @@ pub type SignedExtra = ( /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = - np_runtime::generic::UncheckedExtrinsic; + self_contained::UncheckedExtrinsic; /// Executive: handles dispatch to the various modules. pub type Executive = frame_executive::Executive< @@ -300,6 +301,14 @@ mod runtime { #[runtime::pallet_index(33)] pub type MessageQueue = pallet_message_queue; + // Ethereum compatibility. + #[runtime::pallet_index(60)] + pub type Ethereum = pallet_ethereum; + #[runtime::pallet_index(61)] + pub type Evm = pallet_evm; + #[runtime::pallet_index(62)] + pub type BaseFee = pallet_base_fee; + #[runtime::pallet_index(128)] pub type AddressMap = pallet_multimap; } From 99a52afb9f52cb95452e6e5d38c5d21ae7b3f804 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Thu, 19 Sep 2024 10:30:40 +0900 Subject: [PATCH 004/133] chore: Update CLI description --- ziggurat/node/src/command.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ziggurat/node/src/command.rs b/ziggurat/node/src/command.rs index cf7cd45e..0acacfe8 100644 --- a/ziggurat/node/src/command.rs +++ b/ziggurat/node/src/command.rs @@ -17,6 +17,8 @@ use crate::{ service::new_partial, }; +const IMPL_NAME: &str = "Ziggurat Node"; + fn load_spec(id: &str) -> std::result::Result, String> { Ok(match id { "dev" => Box::new(chain_spec::development_config()), @@ -27,7 +29,7 @@ fn load_spec(id: &str) -> std::result::Result, String> { impl SubstrateCli for Cli { fn impl_name() -> String { - "Parachain Collator Template".into() + IMPL_NAME.into() } fn impl_version() -> String { @@ -36,10 +38,11 @@ impl SubstrateCli for Cli { fn description() -> String { format!( - "Parachain Collator Template\n\nThe command-line arguments provided first will be \ + "{}\n\nThe command-line arguments provided first will be \ passed to the parachain node, while the arguments provided after -- will be passed \ to the relay chain node.\n\n\ {} -- ", + IMPL_NAME, Self::executable_name() ) } @@ -49,11 +52,11 @@ impl SubstrateCli for Cli { } fn support_url() -> String { - "https://github.com/paritytech/polkadot-sdk/issues/new".into() + "https://github.com/noirhq/noir/issues/new".into() } fn copyright_start_year() -> i32 { - 2020 + 2021 } fn load_spec(&self, id: &str) -> std::result::Result, String> { @@ -63,7 +66,7 @@ impl SubstrateCli for Cli { impl SubstrateCli for RelayChainCli { fn impl_name() -> String { - "Parachain Collator Template".into() + IMPL_NAME.into() } fn impl_version() -> String { @@ -72,10 +75,11 @@ impl SubstrateCli for RelayChainCli { fn description() -> String { format!( - "Parachain Collator Template\n\nThe command-line arguments provided first will be \ + "{}\n\nThe command-line arguments provided first will be \ passed to the parachain node, while the arguments provided after -- will be passed \ to the relay chain node.\n\n\ {} -- ", + IMPL_NAME, Self::executable_name() ) } @@ -85,11 +89,11 @@ impl SubstrateCli for RelayChainCli { } fn support_url() -> String { - "https://github.com/paritytech/polkadot-sdk/issues/new".into() + "https://github.com/noirhq/noir/issues/new".into() } fn copyright_start_year() -> i32 { - 2020 + 2021 } fn load_spec(&self, id: &str) -> std::result::Result, String> { From caaafbe645688f6b40c73f056e7a142ed4f4eb15 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Thu, 19 Sep 2024 10:37:57 +0900 Subject: [PATCH 005/133] fix: Fix Ethereum RPC server --- ziggurat/node/src/eth/mod.rs | 9 ++++++--- ziggurat/node/src/service.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ziggurat/node/src/eth/mod.rs b/ziggurat/node/src/eth/mod.rs index 243fb4aa..76686016 100644 --- a/ziggurat/node/src/eth/mod.rs +++ b/ziggurat/node/src/eth/mod.rs @@ -97,9 +97,11 @@ where pubsub_notification_sinks, } = params; - let rpc_port = config.rpc_port; + let rpc_port = config.rpc_addr.map(|addr| addr.port()).unwrap_or(config.rpc_port); + let prometheus_config = config.prometheus_config.take(); + // TODO: Make the Ethereum RPC port configurable. - config.rpc_port = 8545; + config.rpc_addr.as_mut().map(|addr| addr.set_port(8545)); let rpc = sc_service::start_rpc_servers( &config, @@ -179,7 +181,8 @@ where ), ); - config.rpc_port = rpc_port; + config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); + config.prometheus_config = prometheus_config; Ok(config) } diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs index 85963d52..3f8654cf 100644 --- a/ziggurat/node/src/service.rs +++ b/ziggurat/node/src/service.rs @@ -333,7 +333,7 @@ pub async fn start_parachain_node( storage_override.clone(), eth_config.eth_log_block_cache, eth_config.eth_statuses_cache, - prometheus_registry.clone(), + None, )); Box::new(move |_deny_unsafe| { From 2e7c7410f5c94849593f18479b402ea58ac3835e Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 11:26:34 +0900 Subject: [PATCH 006/133] feat: Add pallet-cosmos-types --- Cargo.toml | 2 + frame/cosmos/types/Cargo.toml | 40 +++++++++++++++++ frame/cosmos/types/src/address.rs | 47 +++++++++++++++++++ frame/cosmos/types/src/coin.rs | 62 +++++++++++++++++++++++++ frame/cosmos/types/src/context.rs | 30 +++++++++++++ frame/cosmos/types/src/errors.rs | 60 +++++++++++++++++++++++++ frame/cosmos/types/src/events.rs | 53 ++++++++++++++++++++++ frame/cosmos/types/src/gas.rs | 67 ++++++++++++++++++++++++++++ frame/cosmos/types/src/handler.rs | 38 ++++++++++++++++ frame/cosmos/types/src/lib.rs | 31 +++++++++++++ frame/cosmos/types/src/msgservice.rs | 28 ++++++++++++ frame/cosmos/types/src/tx.rs | 27 +++++++++++ frame/cosmos/types/src/tx_msgs.rs | 23 ++++++++++ 13 files changed, 508 insertions(+) create mode 100644 frame/cosmos/types/Cargo.toml create mode 100644 frame/cosmos/types/src/address.rs create mode 100644 frame/cosmos/types/src/coin.rs create mode 100644 frame/cosmos/types/src/context.rs create mode 100644 frame/cosmos/types/src/errors.rs create mode 100644 frame/cosmos/types/src/events.rs create mode 100644 frame/cosmos/types/src/gas.rs create mode 100644 frame/cosmos/types/src/handler.rs create mode 100644 frame/cosmos/types/src/lib.rs create mode 100644 frame/cosmos/types/src/msgservice.rs create mode 100644 frame/cosmos/types/src/tx.rs create mode 100644 frame/cosmos/types/src/tx_msgs.rs diff --git a/Cargo.toml b/Cargo.toml index b0511770..01303695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "core-primitives", "frame/babel", + "frame/cosmos/types", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -19,6 +20,7 @@ noir-runtime-common = { path = "runtime/common", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } +pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } [profile.release] diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml new file mode 100644 index 00000000..8d3d911f --- /dev/null +++ b/frame/cosmos/types/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "pallet-cosmos-types" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] } +cosmos-sdk-proto = { version = "0.24.0", default-features = false } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +impl-trait-for-tuples = { version = "0.2.2" } +parity-scale-codec = { version = "3.2.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.3.0", default-features = false, features = [ + "derive", +] } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.127", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[dev-dependencies] +hex = { version = "0.4.3", default-features = false } + +[features] +default = ["std"] +std = [ + "bech32/std", + "cosmos-sdk-proto/std", + "frame-support/std", + "parity-scale-codec/std", + "scale-info/std", + "serde/std", + "serde_json/std", + "sp-runtime/std", + "hex/std", +] diff --git a/frame/cosmos/types/src/address.rs b/frame/cosmos/types/src/address.rs new file mode 100644 index 00000000..acc36314 --- /dev/null +++ b/frame/cosmos/types/src/address.rs @@ -0,0 +1,47 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Error { + DecodeError(bech32::DecodeError), +} + +pub fn acc_address_from_bech32(address: &str) -> Result<(String, Vec), Error> { + bech32::decode(address) + .map(|(hrp, data)| (hrp.to_string(), data)) + .map_err(Error::DecodeError) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn acc_address_from_bech32_test() { + let address = "cosmos1qd69nuwj95gta4akjgyxtj9ujmz4w8edmqysqw"; + let (hrp, address_raw) = acc_address_from_bech32(address).unwrap(); + assert_eq!(hrp, "cosmos"); + + let address_raw = hex::encode(address_raw); + assert_eq!(address_raw, "037459f1d22d10bed7b6920865c8bc96c5571f2d"); + } +} diff --git a/frame/cosmos/types/src/coin.rs b/frame/cosmos/types/src/coin.rs new file mode 100644 index 00000000..a045d18c --- /dev/null +++ b/frame/cosmos/types/src/coin.rs @@ -0,0 +1,62 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::string::{String, ToString}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Coin { + pub amount: String, + pub denom: String, +} + +impl From<&cosmos_sdk_proto::cosmos::base::v1beta1::Coin> for Coin { + fn from(coin: &cosmos_sdk_proto::cosmos::base::v1beta1::Coin) -> Self { + Self { amount: coin.amount.clone(), denom: coin.denom.clone() } + } +} + +pub fn amount_to_string(amount: &[cosmos_sdk_proto::cosmos::base::v1beta1::Coin]) -> String { + let mut ret = "".to_string(); + for (i, coin) in amount.iter().enumerate() { + ret.push_str(&coin.amount); + ret.push_str(&coin.denom); + if i < amount.len() - 1 { + ret.push(','); + } + } + ret +} + +#[cfg(test)] +mod tests { + use crate::coin::amount_to_string; + use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; + + #[test] + fn amount_to_string_test() { + let mut amounts = Vec::::new(); + assert_eq!(amount_to_string(&amounts), ""); + + amounts.push(Coin { denom: "uatom".to_string(), amount: "1000".to_string() }); + assert_eq!(amount_to_string(&amounts), "1000uatom"); + + amounts.push(Coin { denom: "stake".to_string(), amount: "2000".to_string() }); + + assert_eq!(amount_to_string(&amounts), "1000uatom,2000stake"); + } +} diff --git a/frame/cosmos/types/src/context.rs b/frame/cosmos/types/src/context.rs new file mode 100644 index 00000000..1accfe77 --- /dev/null +++ b/frame/cosmos/types/src/context.rs @@ -0,0 +1,30 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + events::EventManager, + gas::{Gas, GasMeter}, +}; + +pub trait Context { + type GasMeter: GasMeter; + type EventManager: EventManager; + + fn new(limit: Gas) -> Self; + fn gas_meter(&mut self) -> &mut Self::GasMeter; + fn event_manager(&mut self) -> &mut Self::EventManager; +} diff --git a/frame/cosmos/types/src/errors.rs b/frame/cosmos/types/src/errors.rs new file mode 100644 index 00000000..d6de7b6d --- /dev/null +++ b/frame/cosmos/types/src/errors.rs @@ -0,0 +1,60 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use frame_support::PalletError; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; + +#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo, PalletError)] +pub struct CosmosError { + pub codespace: u8, + pub code: u8, +} + +pub const ROOT_CODESPACE: u8 = 0; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum RootError { + TxDecodeError = 2, + InsufficientFunds = 5, + UnknownRequest = 6, + InvalidAddress = 7, + InvalidCoins = 10, + OutOfGas = 11, + UnpackAnyError = 34, +} + +impl From for CosmosError { + fn from(error: RootError) -> Self { + CosmosError { codespace: ROOT_CODESPACE, code: error as u8 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::ROOT_CODESPACE; + + #[test] + fn cosmos_error_test() { + let error: CosmosError = RootError::InvalidAddress.into(); + assert_eq!( + error, + CosmosError { codespace: ROOT_CODESPACE, code: RootError::InvalidAddress as u8 } + ); + } +} diff --git a/frame/cosmos/types/src/events.rs b/frame/cosmos/types/src/events.rs new file mode 100644 index 00000000..da4e2ede --- /dev/null +++ b/frame/cosmos/types/src/events.rs @@ -0,0 +1,53 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::vec::Vec; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +pub const EVENT_TYPE_TX: &str = "tx"; + +pub const ATTRIBUTE_KEY_FEE: &str = "fee"; +pub const ATTRIBUTE_KEY_FEE_PAYER: &str = "fee_payer"; + +pub const EVENT_TYPE_MESSAGE: &str = "message"; + +pub const ATTRIBUTE_KEY_SENDER: &str = "sender"; +pub const ATTRIBUTE_KEY_AMOUNT: &str = "amount"; + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, Serialize, Deserialize)] +pub struct CosmosEvent { + #[serde(rename = "type")] + pub r#type: Vec, + pub attributes: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, Serialize, Deserialize)] +pub struct EventAttribute { + pub key: Vec, + pub value: Vec, +} + +pub type CosmosEvents = Vec; + +pub trait EventManager { + fn new() -> Self; + fn events(&self) -> CosmosEvents; + fn emit_event(&mut self, event: CosmosEvent); + fn emit_events(&mut self, events: CosmosEvents); +} diff --git a/frame/cosmos/types/src/gas.rs b/frame/cosmos/types/src/gas.rs new file mode 100644 index 00000000..86a372a5 --- /dev/null +++ b/frame/cosmos/types/src/gas.rs @@ -0,0 +1,67 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub type Gas = u64; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + GasOverflow, + OutOfGas, +} + +pub trait GasMeter { + fn new(limit: Gas) -> Self; + fn consumed_gas(&self) -> Gas; + fn gas_remaining(&self) -> Gas; + fn limit(&self) -> Gas; + fn consume_gas(&mut self, amount: Gas, descriptor: &str) -> Result; +} + +#[derive(Clone, Debug)] +pub struct BasicGasMeter { + pub limit: Gas, + pub consumed: Gas, +} + +impl GasMeter for BasicGasMeter { + fn new(limit: Gas) -> Self { + Self { limit, consumed: 0 } + } + + fn consumed_gas(&self) -> Gas { + self.consumed + } + + fn gas_remaining(&self) -> Gas { + self.limit.saturating_sub(self.consumed) + } + + fn limit(&self) -> Gas { + self.limit + } + + // TODO: Handle or remove descriptor + fn consume_gas(&mut self, amount: Gas, _descriptor: &str) -> Result { + let consumed = self.consumed.checked_add(amount).ok_or(Error::GasOverflow)?; + if consumed > self.limit { + return Err(Error::OutOfGas); + } + + self.consumed = consumed; + Ok(self.consumed) + } +} diff --git a/frame/cosmos/types/src/handler.rs b/frame/cosmos/types/src/handler.rs new file mode 100644 index 00000000..db10e6b5 --- /dev/null +++ b/frame/cosmos/types/src/handler.rs @@ -0,0 +1,38 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; +use sp_runtime::transaction_validity::{TransactionValidity, ValidTransaction}; + +pub trait AnteDecorator { + fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity; +} + +impl AnteDecorator for () { + fn ante_handle(_tx: &Tx, _simulate: bool) -> TransactionValidity { + Ok(ValidTransaction::default()) + } +} + +#[impl_trait_for_tuples::impl_for_tuples(1, 12)] +impl AnteDecorator for Tuple { + fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { + let valid = ValidTransaction::default(); + for_tuples!( #( let valid = valid.combine_with(Tuple::ante_handle(tx, simulate)?); )* ); + Ok(valid) + } +} diff --git a/frame/cosmos/types/src/lib.rs b/frame/cosmos/types/src/lib.rs new file mode 100644 index 00000000..6faf02c8 --- /dev/null +++ b/frame/cosmos/types/src/lib.rs @@ -0,0 +1,31 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod address; +pub mod coin; +pub mod context; +pub mod errors; +pub mod events; +pub mod gas; +pub mod handler; +pub mod msgservice; +pub mod tx; +pub mod tx_msgs; diff --git a/frame/cosmos/types/src/msgservice.rs b/frame/cosmos/types/src/msgservice.rs new file mode 100644 index 00000000..167c487d --- /dev/null +++ b/frame/cosmos/types/src/msgservice.rs @@ -0,0 +1,28 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::CosmosError; +use alloc::boxed::Box; +use cosmos_sdk_proto::Any; + +pub trait MsgHandler { + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError>; +} + +pub trait MsgServiceRouter { + fn route(msg: &Any) -> Option>>; +} diff --git a/frame/cosmos/types/src/tx.rs b/frame/cosmos/types/src/tx.rs new file mode 100644 index 00000000..7dc7e2b6 --- /dev/null +++ b/frame/cosmos/types/src/tx.rs @@ -0,0 +1,27 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; + +pub type SequenceNumber = u64; + +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct Account { + pub sequence: SequenceNumber, + pub amount: u128, +} diff --git a/frame/cosmos/types/src/tx_msgs.rs b/frame/cosmos/types/src/tx_msgs.rs new file mode 100644 index 00000000..4a8e3430 --- /dev/null +++ b/frame/cosmos/types/src/tx_msgs.rs @@ -0,0 +1,23 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec::Vec}; + +pub trait Msg { + // get_signers returns the addresses of signers that must sign. + fn get_signers(self) -> Vec; +} From 9ba8ce87d227cbb30fe31d46b46997e99b7a9737 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 14:37:27 +0900 Subject: [PATCH 007/133] feat: Add cosmos-runtime-api --- Cargo.toml | 2 ++ frame/cosmos/runtime-api/Cargo.toml | 31 ++++++++++++++++ frame/cosmos/runtime-api/src/lib.rs | 55 +++++++++++++++++++++++++++++ frame/cosmos/types/Cargo.toml | 6 ++-- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 frame/cosmos/runtime-api/Cargo.toml create mode 100644 frame/cosmos/runtime-api/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 01303695..56370d13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "core-primitives", "frame/babel", "frame/cosmos/types", + "frame/cosmos/runtime-api", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -14,6 +15,7 @@ members = [ ] [workspace.dependencies] +cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } diff --git a/frame/cosmos/runtime-api/Cargo.toml b/frame/cosmos/runtime-api/Cargo.toml new file mode 100644 index 00000000..ff1bca3e --- /dev/null +++ b/frame/cosmos/runtime-api/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cosmos-runtime-api" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +pallet-cosmos-types = { workspace = true, default-features = false } +parity-scale-codec = { version = "3.6", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.11", default-features = false, features = [ + "derive", +] } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = [ + "pallet-cosmos-types/std", + "parity-scale-codec/std", + "scale-info/std", + "serde/std", + "sp-api/std", + "sp-runtime/std", +] diff --git a/frame/cosmos/runtime-api/src/lib.rs b/frame/cosmos/runtime-api/src/lib.rs new file mode 100644 index 00000000..b461934c --- /dev/null +++ b/frame/cosmos/runtime-api/src/lib.rs @@ -0,0 +1,55 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; +use pallet_cosmos_types::{events::CosmosEvent, gas::Gas}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_api::decl_runtime_apis; +use sp_runtime::traits::Block as BlockT; + +#[derive(Clone, Decode, Encode, Debug, TypeInfo, Serialize, Deserialize)] +pub struct GasInfo { + pub gas_wanted: Gas, + pub gas_used: Gas, +} + +#[derive(Clone, Decode, Encode, Debug, TypeInfo, Serialize, Deserialize)] +pub struct SimulateResponse { + pub gas_info: GasInfo, + pub events: Vec, +} + +#[derive(Clone, Decode, Encode, Debug, Eq, PartialEq, TypeInfo)] +pub enum SimulateError { + InvalidTx, + InternalError(Vec), +} + +pub type SimulateResult = Result; + +decl_runtime_apis! { + pub trait CosmosRuntimeApi { + fn convert_tx(tx_bytes: Vec) -> ::Extrinsic; + fn simulate(tx_bytes: Vec) -> SimulateResult; + } +} diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml index 8d3d911f..5982c5f2 100644 --- a/frame/cosmos/types/Cargo.toml +++ b/frame/cosmos/types/Cargo.toml @@ -8,14 +8,14 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] } +bech32 = { version = "0.11", default-features = false, features = ["alloc"] } cosmos-sdk-proto = { version = "0.24.0", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } impl-trait-for-tuples = { version = "0.2.2" } -parity-scale-codec = { version = "3.2.0", default-features = false, features = [ +parity-scale-codec = { version = "3.6", default-features = false, features = [ "derive", ] } -scale-info = { version = "2.3.0", default-features = false, features = [ +scale-info = { version = "2.11", default-features = false, features = [ "derive", ] } serde = { version = "1.0.210", default-features = false, features = ["derive"] } From fd592789953983bc6e821c4635ef56b2bf604733 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 16:07:48 +0900 Subject: [PATCH 008/133] feat: Add cosmos-rpc --- Cargo.toml | 2 + frame/cosmos/rpc/Cargo.toml | 19 +++++++ frame/cosmos/rpc/src/cosmos.rs | 90 ++++++++++++++++++++++++++++++++++ frame/cosmos/rpc/src/lib.rs | 45 +++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 frame/cosmos/rpc/Cargo.toml create mode 100644 frame/cosmos/rpc/src/cosmos.rs create mode 100644 frame/cosmos/rpc/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 56370d13..858e8775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "core-primitives", "frame/babel", "frame/cosmos/types", + "frame/cosmos/rpc", "frame/cosmos/runtime-api", "frame/multimap", "primitives/cosmos", @@ -15,6 +16,7 @@ members = [ ] [workspace.dependencies] +cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } diff --git a/frame/cosmos/rpc/Cargo.toml b/frame/cosmos/rpc/Cargo.toml new file mode 100644 index 00000000..1d4e460b --- /dev/null +++ b/frame/cosmos/rpc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cosmos-rpc" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-runtime-api = { workspace = true, features = ["std"] } +futures = { version = "0.3" } +hex = { version = "0.4.3" } +jsonrpsee = { version = "0.23", features = ["server", "macros"] } +sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } diff --git a/frame/cosmos/rpc/src/cosmos.rs b/frame/cosmos/rpc/src/cosmos.rs new file mode 100644 index 00000000..c045367a --- /dev/null +++ b/frame/cosmos/rpc/src/cosmos.rs @@ -0,0 +1,90 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{internal_error, request_error}; +use cosmos_runtime_api::{CosmosRuntimeApi, SimulateError, SimulateResponse}; +use futures::future::TryFutureExt; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + proc_macros::rpc, +}; +use sc_transaction_pool_api::TransactionPool; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_core::{sha2_256, Bytes, H256}; +use sp_runtime::{traits::Block as BlockT, transaction_validity::TransactionSource}; +use std::sync::Arc; + +#[rpc(server)] +#[async_trait] +pub trait CosmosApi { + #[method(name = "cosmos_broadcastTx")] + async fn broadcast_tx(&self, tx_bytes: Bytes) -> RpcResult; + + #[method(name = "cosmos_simulate")] + async fn simulate(&self, tx_bytes: Bytes) -> RpcResult; +} + +pub struct Cosmos { + client: Arc, + pool: Arc

, +} + +impl Cosmos { + pub fn new(client: Arc, pool: Arc

) -> Self { + Self { client, pool } + } +} + +#[async_trait] +impl CosmosApiServer for Cosmos +where + Block: BlockT, + C: Send + Sync + 'static, + C: ProvideRuntimeApi, + C: HeaderBackend + 'static, + C::Api: cosmos_runtime_api::CosmosRuntimeApi, + P: TransactionPool + 'static, +{ + async fn broadcast_tx(&self, tx_bytes: Bytes) -> RpcResult { + let best_hash = self.client.info().best_hash; + let extrinsic = self + .client + .runtime_api() + .convert_tx(best_hash, tx_bytes.to_vec()) + .map_err(internal_error)?; + + self.pool + .submit_one(best_hash, TransactionSource::Local, extrinsic) + .map_ok(move |_| H256(sha2_256(&tx_bytes))) + .map_err(internal_error) + .await + } + + async fn simulate(&self, tx_bytes: Bytes) -> RpcResult { + let best_hash = self.client.info().best_hash; + + self.client + .runtime_api() + .simulate(best_hash, tx_bytes.to_vec()) + .map_err(internal_error)? + .map_err(|e| match e { + SimulateError::InvalidTx => request_error("Invalid tx"), + SimulateError::InternalError(e) => internal_error(String::from_utf8_lossy(&e)), + }) + } +} diff --git a/frame/cosmos/rpc/src/lib.rs b/frame/cosmos/rpc/src/lib.rs new file mode 100644 index 00000000..94e1444b --- /dev/null +++ b/frame/cosmos/rpc/src/lib.rs @@ -0,0 +1,45 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod cosmos; + +use jsonrpsee::{ + core::to_json_raw_value, + types::{ + error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, + ErrorObject, ErrorObjectOwned, + }, +}; + +pub fn error(code: i32, message: T, data: Option<&[u8]>) -> ErrorObjectOwned { + ErrorObject::owned( + code, + message.to_string(), + data.map(|bytes| { + to_json_raw_value(&format!("0x{}", hex::encode(bytes))) + .expect("Failed to serialize data") + }), + ) +} + +pub fn request_error(message: T) -> ErrorObjectOwned { + error(INVALID_REQUEST_CODE, message, None) +} + +pub fn internal_error(message: T) -> ErrorObjectOwned { + error(INTERNAL_ERROR_CODE, message, None) +} From 4cc9cd566e9e66c0eedf0a8bc89ede775021598c Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 17:13:37 +0900 Subject: [PATCH 009/133] feat: Add pallet-cosmos-x-auth-migrations --- Cargo.toml | 2 + frame/cosmos/x/auth/migrations/Cargo.toml | 23 ++++++++ .../x/auth/migrations/src/legacytx/mod.rs | 18 ++++++ .../x/auth/migrations/src/legacytx/stdsign.rs | 58 +++++++++++++++++++ frame/cosmos/x/auth/migrations/src/lib.rs | 22 +++++++ 5 files changed, 123 insertions(+) create mode 100644 frame/cosmos/x/auth/migrations/Cargo.toml create mode 100644 frame/cosmos/x/auth/migrations/src/legacytx/mod.rs create mode 100644 frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs create mode 100644 frame/cosmos/x/auth/migrations/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 858e8775..6792f11a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "frame/cosmos/types", "frame/cosmos/rpc", "frame/cosmos/runtime-api", + "frame/cosmos/x/auth/migrations", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -24,6 +25,7 @@ noir-runtime-common = { path = "runtime/common", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } +pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/cosmos/x/auth/migrations/Cargo.toml b/frame/cosmos/x/auth/migrations/Cargo.toml new file mode 100644 index 00000000..8121ad68 --- /dev/null +++ b/frame/cosmos/x/auth/migrations/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pallet-cosmos-x-auth-migrations" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +serde = { version = "1.0.210", default-features = false } +serde_json = { version = "1.0.127", default-features = false } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "pallet-cosmos-types/std", + "serde/std", + "serde_json/std", +] diff --git a/frame/cosmos/x/auth/migrations/src/legacytx/mod.rs b/frame/cosmos/x/auth/migrations/src/legacytx/mod.rs new file mode 100644 index 00000000..c8d31e0f --- /dev/null +++ b/frame/cosmos/x/auth/migrations/src/legacytx/mod.rs @@ -0,0 +1,18 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod stdsign; diff --git a/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs b/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs new file mode 100644 index 00000000..cdaad810 --- /dev/null +++ b/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs @@ -0,0 +1,58 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use cosmos_sdk_proto::cosmos::tx::v1beta1::Fee; +use pallet_cosmos_types::coin::Coin; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StdSignDoc { + pub account_number: String, + pub chain_id: String, + pub fee: StdFee, + pub memo: String, + pub msgs: Vec, + pub sequence: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StdFee { + pub amount: Vec, + pub gas: String, +} + +impl From<&Fee> for StdFee { + fn from(fee: &Fee) -> Self { + Self { amount: fee.amount.iter().map(Into::into).collect(), gas: fee.gas_limit.to_string() } + } +} + +pub trait LegacyMsg { + const AMINO_NAME: &'static str; + + fn get_sign_bytes(self) -> Value + where + Self: Sized + Serialize, + { + serde_json::json!({ "type": Self::AMINO_NAME.to_string(), "value": serde_json::to_value(self).unwrap() }) + } +} diff --git a/frame/cosmos/x/auth/migrations/src/lib.rs b/frame/cosmos/x/auth/migrations/src/lib.rs new file mode 100644 index 00000000..628f2d08 --- /dev/null +++ b/frame/cosmos/x/auth/migrations/src/lib.rs @@ -0,0 +1,22 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod legacytx; From 8b256999e54db9232ec8346c383a0731013fbedc Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 17:34:48 +0900 Subject: [PATCH 010/133] feat: Add pallet-cosmos-x-bank-types --- Cargo.toml | 2 + frame/cosmos/x/bank/types/Cargo.toml | 23 ++++++++ frame/cosmos/x/bank/types/src/events.rs | 20 +++++++ frame/cosmos/x/bank/types/src/lib.rs | 23 ++++++++ frame/cosmos/x/bank/types/src/msgs/mod.rs | 18 +++++++ .../cosmos/x/bank/types/src/msgs/msg_send.rs | 54 +++++++++++++++++++ 6 files changed, 140 insertions(+) create mode 100644 frame/cosmos/x/bank/types/Cargo.toml create mode 100644 frame/cosmos/x/bank/types/src/events.rs create mode 100644 frame/cosmos/x/bank/types/src/lib.rs create mode 100644 frame/cosmos/x/bank/types/src/msgs/mod.rs create mode 100644 frame/cosmos/x/bank/types/src/msgs/msg_send.rs diff --git a/Cargo.toml b/Cargo.toml index 6792f11a..4d7b02a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "frame/cosmos/rpc", "frame/cosmos/runtime-api", "frame/cosmos/x/auth/migrations", + "frame/cosmos/x/bank/types", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -26,6 +27,7 @@ np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } +pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/cosmos/x/bank/types/Cargo.toml b/frame/cosmos/x/bank/types/Cargo.toml new file mode 100644 index 00000000..12754a02 --- /dev/null +++ b/frame/cosmos/x/bank/types/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pallet-cosmos-x-bank-types" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-auth-migrations = { workspace = true, default-features = false } +serde = { version = "1.0.210", default-features = false } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-auth-migrations/std", + "serde/std", +] diff --git a/frame/cosmos/x/bank/types/src/events.rs b/frame/cosmos/x/bank/types/src/events.rs new file mode 100644 index 00000000..d8bc5484 --- /dev/null +++ b/frame/cosmos/x/bank/types/src/events.rs @@ -0,0 +1,20 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub const EVENT_TYPE_TRANSFER: &str = "transfer"; + +pub const ATTRIBUTE_KEY_RECIPIENT: &str = "recipient"; diff --git a/frame/cosmos/x/bank/types/src/lib.rs b/frame/cosmos/x/bank/types/src/lib.rs new file mode 100644 index 00000000..0797570e --- /dev/null +++ b/frame/cosmos/x/bank/types/src/lib.rs @@ -0,0 +1,23 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod events; +pub mod msgs; diff --git a/frame/cosmos/x/bank/types/src/msgs/mod.rs b/frame/cosmos/x/bank/types/src/msgs/mod.rs new file mode 100644 index 00000000..a994ce26 --- /dev/null +++ b/frame/cosmos/x/bank/types/src/msgs/mod.rs @@ -0,0 +1,18 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod msg_send; diff --git a/frame/cosmos/x/bank/types/src/msgs/msg_send.rs b/frame/cosmos/x/bank/types/src/msgs/msg_send.rs new file mode 100644 index 00000000..c63cc067 --- /dev/null +++ b/frame/cosmos/x/bank/types/src/msgs/msg_send.rs @@ -0,0 +1,54 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::{coin::Coin, tx_msgs::Msg}; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgSend { + pub amount: Vec, + pub from_address: String, + pub to_address: String, +} + +impl TryFrom<&Any> for MsgSend { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend::decode(&mut &*any.value) + .map_err(|_| ())?; + + Ok(Self { + amount: msg.amount.iter().map(Into::into).collect(), + from_address: msg.from_address, + to_address: msg.to_address, + }) + } +} + +impl Msg for MsgSend { + fn get_signers(self) -> Vec { + vec![self.from_address.clone()] + } +} + +impl LegacyMsg for MsgSend { + const AMINO_NAME: &'static str = "cosmos-sdk/MsgSend"; +} From 175fd2c1f327d66934e453a2a42cc889f5628d13 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 17:46:05 +0900 Subject: [PATCH 011/133] feat: Add pallet-cosmos-x-wasm-types --- Cargo.toml | 4 +- frame/cosmos/x/auth/migrations/Cargo.toml | 2 +- frame/cosmos/x/bank/types/Cargo.toml | 2 +- frame/cosmos/x/wasm/types/Cargo.toml | 25 +++++++ frame/cosmos/x/wasm/types/src/errors.rs | 50 ++++++++++++++ frame/cosmos/x/wasm/types/src/events.rs | 27 ++++++++ frame/cosmos/x/wasm/types/src/lib.rs | 24 +++++++ frame/cosmos/x/wasm/types/src/tx/mod.rs | 22 +++++++ .../wasm/types/src/tx/msg_execute_contract.rs | 56 ++++++++++++++++ .../types/src/tx/msg_instantiate_contract2.rs | 64 ++++++++++++++++++ .../wasm/types/src/tx/msg_migrate_contract.rs | 51 +++++++++++++++ .../x/wasm/types/src/tx/msg_store_code.rs | 65 +++++++++++++++++++ .../x/wasm/types/src/tx/msg_update_admin.rs | 49 ++++++++++++++ 13 files changed, 438 insertions(+), 3 deletions(-) create mode 100644 frame/cosmos/x/wasm/types/Cargo.toml create mode 100644 frame/cosmos/x/wasm/types/src/errors.rs create mode 100644 frame/cosmos/x/wasm/types/src/events.rs create mode 100644 frame/cosmos/x/wasm/types/src/lib.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/mod.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/msg_execute_contract.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/msg_instantiate_contract2.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/msg_migrate_contract.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/msg_store_code.rs create mode 100644 frame/cosmos/x/wasm/types/src/tx/msg_update_admin.rs diff --git a/Cargo.toml b/Cargo.toml index 4d7b02a7..fe32b52b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "frame/cosmos/runtime-api", "frame/cosmos/x/auth/migrations", "frame/cosmos/x/bank/types", + "frame/cosmos/x/wasm/types", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -26,9 +27,10 @@ noir-runtime-common = { path = "runtime/common", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } +pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } -pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } +pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } [profile.release] diff --git a/frame/cosmos/x/auth/migrations/Cargo.toml b/frame/cosmos/x/auth/migrations/Cargo.toml index 8121ad68..bebd7c4f 100644 --- a/frame/cosmos/x/auth/migrations/Cargo.toml +++ b/frame/cosmos/x/auth/migrations/Cargo.toml @@ -10,7 +10,7 @@ publish = false [dependencies] cosmos-sdk-proto = { version = "0.24.0", default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } -serde = { version = "1.0.210", default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } serde_json = { version = "1.0.127", default-features = false } [features] diff --git a/frame/cosmos/x/bank/types/Cargo.toml b/frame/cosmos/x/bank/types/Cargo.toml index 12754a02..63267148 100644 --- a/frame/cosmos/x/bank/types/Cargo.toml +++ b/frame/cosmos/x/bank/types/Cargo.toml @@ -11,7 +11,7 @@ publish = false cosmos-sdk-proto = { version = "0.24.0", default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } pallet-cosmos-x-auth-migrations = { workspace = true, default-features = false } -serde = { version = "1.0.210", default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } [features] default = ["std"] diff --git a/frame/cosmos/x/wasm/types/Cargo.toml b/frame/cosmos/x/wasm/types/Cargo.toml new file mode 100644 index 00000000..11d7c676 --- /dev/null +++ b/frame/cosmos/x/wasm/types/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pallet-cosmos-x-wasm-types" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ + "cosmwasm", +] } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-auth-migrations = { workspace = true, default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-auth-migrations/std", + "serde/std", +] diff --git a/frame/cosmos/x/wasm/types/src/errors.rs b/frame/cosmos/x/wasm/types/src/errors.rs new file mode 100644 index 00000000..0aa9a1cd --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/errors.rs @@ -0,0 +1,50 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pallet_cosmos_types::errors::CosmosError; + +pub const WASM_CODESPACE: u8 = 1; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum WasmError { + CreateFailed = 2, + InstantiateFailed = 4, + ExecuteFailed = 5, + MigrationFailed = 11, + Empty = 12, +} + +impl From for CosmosError { + fn from(error: WasmError) -> Self { + CosmosError { codespace: WASM_CODESPACE, code: error as u8 } + } +} + +#[cfg(test)] +mod tests { + use super::{CosmosError, WasmError}; + use crate::errors::WASM_CODESPACE; + + #[test] + fn wasm_error_test() { + let error: CosmosError = WasmError::CreateFailed.into(); + assert_eq!( + error, + CosmosError { codespace: WASM_CODESPACE, code: WasmError::CreateFailed as u8 } + ); + } +} diff --git a/frame/cosmos/x/wasm/types/src/events.rs b/frame/cosmos/x/wasm/types/src/events.rs new file mode 100644 index 00000000..ab9b3286 --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/events.rs @@ -0,0 +1,27 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub const EVENT_TYPE_STORE_CODE: &str = "store_code"; +pub const EVENT_TYPE_INSTANTIATE: &str = "instantiate"; +pub const EVENT_TYPE_EXECUTE: &str = "execute"; +pub const EVENT_TYPE_MIGRATE: &str = "migrate"; +pub const EVENT_TYPE_UPDATE_CONTRACT_ADMIN: &str = "update_contract_admin"; + +pub const ATTRIBUTE_KEY_CONTRACT_ADDR: &str = "_contract_address"; +pub const ATTRIBUTE_KEY_CODE_ID: &str = "code_id"; +pub const ATTRIBUTE_KEY_CHECKSUM: &str = "code_checksum"; +pub const ATTRIBUTE_KEY_NEW_ADMIN: &str = "new_admin_address"; diff --git a/frame/cosmos/x/wasm/types/src/lib.rs b/frame/cosmos/x/wasm/types/src/lib.rs new file mode 100644 index 00000000..757c5189 --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/lib.rs @@ -0,0 +1,24 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod errors; +pub mod events; +pub mod tx; diff --git a/frame/cosmos/x/wasm/types/src/tx/mod.rs b/frame/cosmos/x/wasm/types/src/tx/mod.rs new file mode 100644 index 00000000..11a21cb8 --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/mod.rs @@ -0,0 +1,22 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod msg_execute_contract; +pub mod msg_instantiate_contract2; +pub mod msg_migrate_contract; +pub mod msg_store_code; +pub mod msg_update_admin; diff --git a/frame/cosmos/x/wasm/types/src/tx/msg_execute_contract.rs b/frame/cosmos/x/wasm/types/src/tx/msg_execute_contract.rs new file mode 100644 index 00000000..e7c6ad88 --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/msg_execute_contract.rs @@ -0,0 +1,56 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::{coin::Coin, tx_msgs::Msg}; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgExecuteContract { + pub contract: String, + pub funds: Vec, + pub msg: Vec, + pub sender: String, +} + +impl TryFrom<&Any> for MsgExecuteContract { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = + cosmos_sdk_proto::cosmwasm::wasm::v1::MsgExecuteContract::decode(&mut &*any.value) + .map_err(|_| ())?; + Ok(Self { + contract: msg.contract, + funds: msg.funds.iter().map(Into::into).collect(), + msg: msg.msg, + sender: msg.sender, + }) + } +} + +impl LegacyMsg for MsgExecuteContract { + const AMINO_NAME: &'static str = "wasm/MsgExecuteContract"; +} + +impl Msg for MsgExecuteContract { + fn get_signers(self) -> Vec { + vec![self.sender.clone()] + } +} diff --git a/frame/cosmos/x/wasm/types/src/tx/msg_instantiate_contract2.rs b/frame/cosmos/x/wasm/types/src/tx/msg_instantiate_contract2.rs new file mode 100644 index 00000000..8c60befc --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/msg_instantiate_contract2.rs @@ -0,0 +1,64 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::{coin::Coin, tx_msgs::Msg}; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgInstantiateContract2 { + pub admin: String, + pub code_id: u64, + pub fix_msg: bool, + pub funds: Vec, + pub label: String, + pub msg: Vec, + pub salt: Vec, + pub sender: String, +} + +impl TryFrom<&Any> for MsgInstantiateContract2 { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = + cosmos_sdk_proto::cosmwasm::wasm::v1::MsgInstantiateContract2::decode(&mut &*any.value) + .map_err(|_| ())?; + Ok(Self { + admin: msg.admin, + code_id: msg.code_id, + fix_msg: msg.fix_msg, + funds: msg.funds.iter().map(Into::into).collect(), + label: msg.label, + msg: msg.msg, + salt: msg.salt, + sender: msg.sender, + }) + } +} + +impl LegacyMsg for MsgInstantiateContract2 { + const AMINO_NAME: &'static str = "wasm/MsgInstantiateContract2"; +} + +impl Msg for MsgInstantiateContract2 { + fn get_signers(self) -> Vec { + vec![self.sender.clone()] + } +} diff --git a/frame/cosmos/x/wasm/types/src/tx/msg_migrate_contract.rs b/frame/cosmos/x/wasm/types/src/tx/msg_migrate_contract.rs new file mode 100644 index 00000000..fd34b43e --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/msg_migrate_contract.rs @@ -0,0 +1,51 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::tx_msgs::Msg; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgMigrateContract { + pub code_id: u64, + pub contract: String, + pub msg: Vec, + pub sender: String, +} + +impl TryFrom<&Any> for MsgMigrateContract { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = + cosmos_sdk_proto::cosmwasm::wasm::v1::MsgMigrateContract::decode(&mut &*any.value) + .map_err(|_| ())?; + Ok(Self { code_id: msg.code_id, contract: msg.contract, msg: msg.msg, sender: msg.sender }) + } +} + +impl LegacyMsg for MsgMigrateContract { + const AMINO_NAME: &'static str = "wasm/MsgMigrateContract"; +} + +impl Msg for MsgMigrateContract { + fn get_signers(self) -> Vec { + vec![self.sender.clone()] + } +} diff --git a/frame/cosmos/x/wasm/types/src/tx/msg_store_code.rs b/frame/cosmos/x/wasm/types/src/tx/msg_store_code.rs new file mode 100644 index 00000000..e0537840 --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/msg_store_code.rs @@ -0,0 +1,65 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::tx_msgs::Msg; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccessConfig { + pub addresses: Vec, + pub permission: i32, +} + +impl From for AccessConfig { + fn from(config: cosmos_sdk_proto::cosmwasm::wasm::v1::AccessConfig) -> Self { + Self { addresses: config.addresses, permission: config.permission } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgStoreCode { + pub instantiate_permission: Option, + pub sender: String, + pub wasm_byte_code: Vec, +} + +impl TryFrom<&Any> for MsgStoreCode { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = cosmos_sdk_proto::cosmwasm::wasm::v1::MsgStoreCode::decode(&mut &*any.value) + .map_err(|_| ())?; + Ok(Self { + instantiate_permission: msg.instantiate_permission.map(Into::into), + sender: msg.sender, + wasm_byte_code: msg.wasm_byte_code, + }) + } +} + +impl LegacyMsg for MsgStoreCode { + const AMINO_NAME: &'static str = "wasm/MsgStoreCode"; +} + +impl Msg for MsgStoreCode { + fn get_signers(self) -> Vec { + vec![self.sender.clone()] + } +} diff --git a/frame/cosmos/x/wasm/types/src/tx/msg_update_admin.rs b/frame/cosmos/x/wasm/types/src/tx/msg_update_admin.rs new file mode 100644 index 00000000..cc7938ea --- /dev/null +++ b/frame/cosmos/x/wasm/types/src/tx/msg_update_admin.rs @@ -0,0 +1,49 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::{string::String, vec, vec::Vec}; +use cosmos_sdk_proto::{prost::Message, Any}; +use pallet_cosmos_types::tx_msgs::Msg; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::LegacyMsg; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MsgUpdateAdmin { + pub contract: String, + pub new_admin: String, + pub sender: String, +} + +impl TryFrom<&Any> for MsgUpdateAdmin { + type Error = (); + + fn try_from(any: &Any) -> Result { + let msg = cosmos_sdk_proto::cosmwasm::wasm::v1::MsgUpdateAdmin::decode(&mut &*any.value) + .map_err(|_| ())?; + Ok(Self { contract: msg.contract, new_admin: msg.new_admin, sender: msg.sender }) + } +} + +impl LegacyMsg for MsgUpdateAdmin { + const AMINO_NAME: &'static str = "wasm/MsgUpdateAdmin"; +} + +impl Msg for MsgUpdateAdmin { + fn get_signers(self) -> Vec { + vec![self.sender.clone()] + } +} From c9d556e04988e40c263b94490eecc07ff3707dd0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 18:07:29 +0900 Subject: [PATCH 012/133] feat: Add any_match macro to pallet-cosmos-types --- frame/cosmos/types/Cargo.toml | 4 +- frame/cosmos/types/src/lib.rs | 1 + frame/cosmos/types/src/macros.rs | 70 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 frame/cosmos/types/src/macros.rs diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml index 5982c5f2..6b5fd125 100644 --- a/frame/cosmos/types/Cargo.toml +++ b/frame/cosmos/types/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] bech32 = { version = "0.11", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = ["cosmwasm"] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } impl-trait-for-tuples = { version = "0.2.2" } parity-scale-codec = { version = "3.6", default-features = false, features = [ @@ -19,7 +19,6 @@ scale-info = { version = "2.11", default-features = false, features = [ "derive", ] } serde = { version = "1.0.210", default-features = false, features = ["derive"] } -serde_json = { version = "1.0.127", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [dev-dependencies] @@ -34,7 +33,6 @@ std = [ "parity-scale-codec/std", "scale-info/std", "serde/std", - "serde_json/std", "sp-runtime/std", "hex/std", ] diff --git a/frame/cosmos/types/src/lib.rs b/frame/cosmos/types/src/lib.rs index 6faf02c8..bb812a99 100644 --- a/frame/cosmos/types/src/lib.rs +++ b/frame/cosmos/types/src/lib.rs @@ -26,6 +26,7 @@ pub mod errors; pub mod events; pub mod gas; pub mod handler; +pub mod macros; pub mod msgservice; pub mod tx; pub mod tx_msgs; diff --git a/frame/cosmos/types/src/macros.rs b/frame/cosmos/types/src/macros.rs new file mode 100644 index 00000000..acd55374 --- /dev/null +++ b/frame/cosmos/types/src/macros.rs @@ -0,0 +1,70 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_export] +macro_rules! any_match { + ($msg:expr, { $( $msg_type:ty => $handler:expr ),* $(,)? }, $default:expr) => { + { + { + $( + if $msg.type_url == <$msg_type as cosmos_sdk_proto::traits::Name>::type_url() { + $handler + } else + )* { + $default + } + } + } + }; +} + +#[cfg(test)] +mod tests { + use cosmos_sdk_proto::{ + cosmos::bank::v1beta1::MsgSend, + cosmwasm::wasm::v1::{MsgExecuteContract, MsgStoreCode}, + prost::Name, + traits::Message, + Any, + }; + + #[test] + fn any_match_test() { + let any = Any { type_url: MsgSend::type_url(), value: MsgSend::default().encode_to_vec() }; + let result = any_match!( + any, { + MsgSend => any.type_url, + MsgStoreCode => any.type_url, + }, + "Unsupported msg".to_string() + ); + assert_eq!(result, MsgSend::type_url()); + + let any = Any { + type_url: MsgExecuteContract::type_url(), + value: MsgExecuteContract::default().encode_to_vec(), + }; + let result = any_match!( + any, { + MsgSend => any.type_url, + MsgStoreCode => any.type_url, + }, + "Unsupported msg".to_string() + ); + assert_eq!(result, "Unsupported msg".to_string()); + } +} From 17f375547a2a920770291bb88ff752c80ef5f634 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 18:17:02 +0900 Subject: [PATCH 013/133] refactor: Remove type SequenceNumber from pallet-cosmos-types --- frame/cosmos/types/src/tx.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frame/cosmos/types/src/tx.rs b/frame/cosmos/types/src/tx.rs index 7dc7e2b6..270d5946 100644 --- a/frame/cosmos/types/src/tx.rs +++ b/frame/cosmos/types/src/tx.rs @@ -18,10 +18,8 @@ use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; -pub type SequenceNumber = u64; - #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct Account { - pub sequence: SequenceNumber, + pub sequence: u64, pub amount: u128, } From 70a2a054193552115c84927698389c312437b311 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 19:00:25 +0900 Subject: [PATCH 014/133] refactor: Remove type Account from pallet-cosmos-types --- frame/cosmos/types/src/lib.rs | 1 - frame/cosmos/types/src/tx.rs | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 frame/cosmos/types/src/tx.rs diff --git a/frame/cosmos/types/src/lib.rs b/frame/cosmos/types/src/lib.rs index bb812a99..a4b6aa4a 100644 --- a/frame/cosmos/types/src/lib.rs +++ b/frame/cosmos/types/src/lib.rs @@ -28,5 +28,4 @@ pub mod gas; pub mod handler; pub mod macros; pub mod msgservice; -pub mod tx; pub mod tx_msgs; diff --git a/frame/cosmos/types/src/tx.rs b/frame/cosmos/types/src/tx.rs deleted file mode 100644 index 270d5946..00000000 --- a/frame/cosmos/types/src/tx.rs +++ /dev/null @@ -1,25 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; - -#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] -pub struct Account { - pub sequence: u64, - pub amount: u128, -} From 474100953c1993f023c43b5dd71f69037c8cf92e Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 19:37:45 +0900 Subject: [PATCH 015/133] feat: Add pallet-cosmos-x-auth-signing --- Cargo.toml | 2 + frame/cosmos/x/auth/signing/Cargo.toml | 37 ++++ frame/cosmos/x/auth/signing/src/lib.rs | 23 ++ .../auth/signing/src/sign_mode_handler/mod.rs | 201 ++++++++++++++++++ .../signing/src/sign_mode_handler/traits.rs | 28 +++ .../signing/src/sign_verifiable_tx/mod.rs | 122 +++++++++++ .../signing/src/sign_verifiable_tx/traits.rs | 26 +++ 7 files changed, 439 insertions(+) create mode 100644 frame/cosmos/x/auth/signing/Cargo.toml create mode 100644 frame/cosmos/x/auth/signing/src/lib.rs create mode 100644 frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs create mode 100644 frame/cosmos/x/auth/signing/src/sign_mode_handler/traits.rs create mode 100644 frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs create mode 100644 frame/cosmos/x/auth/signing/src/sign_verifiable_tx/traits.rs diff --git a/Cargo.toml b/Cargo.toml index fe32b52b..ed412626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "frame/cosmos/rpc", "frame/cosmos/runtime-api", "frame/cosmos/x/auth/migrations", + "frame/cosmos/x/auth/signing", "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm/types", "frame/multimap", @@ -29,6 +30,7 @@ np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } +pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-features = false } pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/cosmos/x/auth/signing/Cargo.toml b/frame/cosmos/x/auth/signing/Cargo.toml new file mode 100644 index 00000000..3e878df9 --- /dev/null +++ b/frame/cosmos/x/auth/signing/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-cosmos-x-auth-signing" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ + "cosmwasm", +] } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-auth-migrations = { workspace = true, default-features = false } +pallet-cosmos-x-bank-types = { workspace = true, default-features = false } +pallet-cosmos-x-wasm-types = { workspace = true, default-features = false } +serde_json = { version = "1.0.127", default-features = false } + +[dev-dependencies] +base64ct = { version = "1.6.0", default-features = false } +hex = { version = "0.4.3", default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-auth-migrations/std", + "pallet-cosmos-x-bank-types/std", + "pallet-cosmos-x-wasm-types/std", + "serde_json/std", + "base64ct/std", + "hex/std", + "sp-core/std", +] diff --git a/frame/cosmos/x/auth/signing/src/lib.rs b/frame/cosmos/x/auth/signing/src/lib.rs new file mode 100644 index 00000000..fb81d86e --- /dev/null +++ b/frame/cosmos/x/auth/signing/src/lib.rs @@ -0,0 +1,23 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod sign_mode_handler; +pub mod sign_verifiable_tx; diff --git a/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs new file mode 100644 index 00000000..ce1d547d --- /dev/null +++ b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs @@ -0,0 +1,201 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod traits; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use cosmos_sdk_proto::{ + cosmos::{ + bank, + tx::v1beta1::{ + mode_info::{Single, Sum}, + ModeInfo, SignDoc, Tx, TxRaw, + }, + }, + cosmwasm::wasm, + traits::Message, + Any, +}; +use pallet_cosmos_types::any_match; +use pallet_cosmos_x_auth_migrations::legacytx::stdsign::{LegacyMsg, StdSignDoc}; +use pallet_cosmos_x_bank_types::msgs::msg_send::MsgSend; +use pallet_cosmos_x_wasm_types::tx::{ + msg_execute_contract::MsgExecuteContract, msg_instantiate_contract2::MsgInstantiateContract2, + msg_migrate_contract::MsgMigrateContract, msg_store_code::MsgStoreCode, + msg_update_admin::MsgUpdateAdmin, +}; +use serde_json::Value; + +#[derive(Clone)] +pub struct SignerData { + pub address: String, + pub chain_id: String, + pub account_number: u64, + pub sequence: u64, + pub pub_key: Any, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum SignModeHandlerError { + EmptyTxBody, + EmptyFee, + EmptyModeInfo, + DecodeTxError, + InvalidMsg, + SerializeError, + UnsupportedMode, +} + +pub struct SignModeHandler; +impl traits::SignModeHandler for SignModeHandler { + fn get_sign_bytes( + mode: &ModeInfo, + data: &SignerData, + tx: &Tx, + ) -> Result, SignModeHandlerError> { + let sum = mode.sum.as_ref().ok_or(SignModeHandlerError::EmptyModeInfo)?; + let sign_bytes = match sum { + Sum::Single(Single { mode }) => match mode { + 1 /* SIGN_MODE_DIRECT */ => { + let tx_raw = TxRaw::decode(&mut &*tx.encode_to_vec()).map_err(|_| SignModeHandlerError::DecodeTxError)?; + SignDoc { + body_bytes: tx_raw.body_bytes, + auth_info_bytes: tx_raw.auth_info_bytes, + chain_id: data.chain_id.clone(), + account_number: data.account_number, + }.encode_to_vec() + }, + 127 /* SIGN_MODE_LEGACY_AMINO_JSON */ => { + let body = tx.body.as_ref().ok_or(SignModeHandlerError::EmptyTxBody)?; + let mut msgs = Vec::::new(); + for msg in body.messages.iter() { + let legacy_msg = any_match!( + msg, { + bank::v1beta1::MsgSend => MsgSend::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + wasm::v1::MsgStoreCode => MsgStoreCode::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + wasm::v1::MsgInstantiateContract2 => MsgInstantiateContract2::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + wasm::v1::MsgExecuteContract => MsgExecuteContract::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + wasm::v1::MsgMigrateContract => MsgMigrateContract::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + wasm::v1::MsgUpdateAdmin => MsgUpdateAdmin::try_from(msg).map(LegacyMsg::get_sign_bytes).map_err(|_| SignModeHandlerError::InvalidMsg), + }, + Err(SignModeHandlerError::InvalidMsg))?; + + msgs.push(legacy_msg); + } + let fee = tx.auth_info.as_ref().and_then(|auth_info| auth_info.fee.as_ref()).ok_or(SignModeHandlerError::EmptyFee)?; + let sign_doc = StdSignDoc { + account_number: data.account_number.to_string(), + chain_id: data.chain_id.clone(), + fee: fee.into(), + memo: body.memo.clone(), + msgs, + sequence: data.sequence.to_string(), + }; + + serde_json::to_vec(&sign_doc).map_err(|_| SignModeHandlerError::SerializeError)? + }, + _ => return Err(SignModeHandlerError::UnsupportedMode), + }, + _ => return Err(SignModeHandlerError::UnsupportedMode), + }; + + Ok(sign_bytes) + } +} + +#[cfg(test)] +mod tests { + use crate::sign_mode_handler::{traits::SignModeHandler as _, SignModeHandler, SignerData}; + use base64ct::{Base64, Encoding}; + use cosmos_sdk_proto::{ + cosmos::tx::v1beta1::{ + mode_info::{Single, Sum}, + ModeInfo, Tx, + }, + prost::Message, + }; + use sp_core::sha2_256; + + #[test] + fn get_sign_bytes_test() { + let tx_raw = "CpMBCpABChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEnAKLWNvc21vczFxZDY5bnV3ajk1Z3RhNGFramd5eHRqOXVqbXo0dzhlZG1xeXNxdxItY29zbW9zMWdtajJleGFnMDN0dGdhZnBya2RjM3Q4ODBncm1hOW53ZWZjZDJ3GhAKBXVhdG9tEgcxMDAwMDAwEnEKTgpGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQIKEJE0H+VmS/oXgtXgR3lokGjJFrBMs2XsMVN1VoTZoRIECgIIARIfChUKBXVhdG9tEgw4ODY4ODAwMDAwMDAQgMDxxZSVFBpA9+DRmMYoIcxYF8jpNfUjMIMB4pgZ9diC8ySbnhc6YU84AA3b/0RsCr+nx9AZ27FwcrKJM/yBh8lz+/A9BFn3bg=="; + + let tx_raw = Base64::decode_vec(&tx_raw).unwrap(); + let tx = Tx::decode(&mut &*tx_raw).unwrap(); + + let public_key = tx + .auth_info + .as_ref() + .unwrap() + .signer_infos + .first() + .unwrap() + .public_key + .as_ref() + .unwrap(); + + let mode = ModeInfo { sum: Some(Sum::Single(Single { mode: 1 })) }; + let data = SignerData { + address: "cosmos1qd69nuwj95gta4akjgyxtj9ujmz4w8edmqysqw".to_string(), + chain_id: "theta-testnet-001".to_string(), + account_number: 754989, + sequence: 0, + pub_key: public_key.clone(), + }; + let expected_hash = sha2_256(&SignModeHandler::get_sign_bytes(&mode, &data, &tx).unwrap()); + + let sign_doc_raw = + "CpMBCpABChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEnAKLWNvc21vczFxZDY5bnV3ajk1Z3RhNGFramd5eHRqOXVqbXo0dzhlZG1xeXNxdxItY29zbW9zMWdtajJleGFnMDN0dGdhZnBya2RjM3Q4ODBncm1hOW53ZWZjZDJ3GhAKBXVhdG9tEgcxMDAwMDAwEnEKTgpGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQIKEJE0H+VmS/oXgtXgR3lokGjJFrBMs2XsMVN1VoTZoRIECgIIARIfChUKBXVhdG9tEgw4ODY4ODAwMDAwMDAQgMDxxZSVFBoRdGhldGEtdGVzdG5ldC0wMDEgrYou"; + let hash = sha2_256(&Base64::decode_vec(&sign_doc_raw).unwrap()); + + assert_eq!(expected_hash, hash); + } + + #[test] + fn get_std_sign_bytes_test() { + let tx_raw = "CpoBCpcBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEncKLWNvc21vczFxZDY5bnV3ajk1Z3RhNGFramd5eHRqOXVqbXo0dzhlZG1xeXNxdxItY29zbW9zMW41amd4NjR6dzM4c3M3Nm16dXU0dWM3amV5cXcydmZqazYwZmR6GhcKBGFjZHQSDzEwMDAwMDAwMDAwMDAwMBJsCk4KRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECChCRNB/lZkv6F4LV4Ed5aJBoyRawTLNl7DFTdVaE2aESBAoCCH8SGgoSCgRhY2R0EgoxMDQwMDAwMDAwEIDa8esEGkBgXIiPoBpecG7QpKDJPaztFogqvmxjDHF5ORfWBrOoSzf0+AAmch1CXrG4OmiKL0y8v9ITx0QzWYUc7ueXcdIm"; + let tx_raw = Base64::decode_vec(tx_raw).unwrap(); + let tx = Tx::decode(&mut &*tx_raw).unwrap(); + + let public_key = tx + .auth_info + .as_ref() + .unwrap() + .signer_infos + .first() + .unwrap() + .public_key + .as_ref() + .unwrap(); + + let mode = ModeInfo { sum: Some(Sum::Single(Single { mode: 127 })) }; + let data = SignerData { + address: "cosmos1qd69nuwj95gta4akjgyxtj9ujmz4w8edmqysqw".to_string(), + chain_id: "dev".to_string(), + account_number: 0, + sequence: 0, + pub_key: public_key.clone(), + }; + let hash = sha2_256(&SignModeHandler::get_sign_bytes(&mode, &data, &tx).unwrap()); + let hash = hex::encode(&hash); + + assert_eq!(hash, "714d4bdfdbd0bd630ebdf93b1f6eba7d3c752e92bbab6c9d3d9c93e1777348bb"); + } +} diff --git a/frame/cosmos/x/auth/signing/src/sign_mode_handler/traits.rs b/frame/cosmos/x/auth/signing/src/sign_mode_handler/traits.rs new file mode 100644 index 00000000..e24dad03 --- /dev/null +++ b/frame/cosmos/x/auth/signing/src/sign_mode_handler/traits.rs @@ -0,0 +1,28 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{SignModeHandlerError, SignerData}; +use alloc::vec::Vec; +use cosmos_sdk_proto::cosmos::tx::v1beta1::{ModeInfo, Tx}; + +pub trait SignModeHandler { + fn get_sign_bytes( + mode: &ModeInfo, + data: &SignerData, + tx: &Tx, + ) -> Result, SignModeHandlerError>; +} diff --git a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs new file mode 100644 index 00000000..c77c7469 --- /dev/null +++ b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs @@ -0,0 +1,122 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod traits; + +use alloc::{string::String, vec::Vec}; +use cosmos_sdk_proto::{ + cosmos::{bank, tx::v1beta1::Tx}, + cosmwasm::wasm, +}; +use pallet_cosmos_types::{any_match, tx_msgs::Msg}; +use pallet_cosmos_x_bank_types::msgs::msg_send::MsgSend; +use pallet_cosmos_x_wasm_types::tx::{ + msg_execute_contract::MsgExecuteContract, msg_instantiate_contract2::MsgInstantiateContract2, + msg_migrate_contract::MsgMigrateContract, msg_store_code::MsgStoreCode, + msg_update_admin::MsgUpdateAdmin, +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum SigVerifiableTxError { + EmptyAuthInfo, + EmptyFee, + EmptySigners, + EmptyTxBody, + InvalidMsg, +} + +pub struct SigVerifiableTx; +impl traits::SigVerifiableTx for SigVerifiableTx { + fn get_signers(tx: &Tx) -> Result, SigVerifiableTxError> { + let mut signers = Vec::::new(); + + let body = tx.body.as_ref().ok_or(SigVerifiableTxError::EmptyTxBody)?; + for msg in body.messages.iter() { + let msg_signers = any_match!( + msg, { + bank::v1beta1::MsgSend => MsgSend::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + wasm::v1::MsgStoreCode => MsgStoreCode::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + wasm::v1::MsgInstantiateContract2 => MsgInstantiateContract2::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + wasm::v1::MsgExecuteContract => MsgExecuteContract::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + wasm::v1::MsgMigrateContract => MsgMigrateContract::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + wasm::v1::MsgUpdateAdmin => MsgUpdateAdmin::try_from(msg).map(Msg::get_signers).map_err(|_| SigVerifiableTxError::InvalidMsg), + }, + Err(SigVerifiableTxError::InvalidMsg) + )?; + + for msg_signer in msg_signers.iter() { + if !signers.contains(msg_signer) { + signers.push(msg_signer.clone()); + } + } + } + + let fee_payer = &tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(SigVerifiableTxError::EmptyFee)? + .payer; + + if !fee_payer.is_empty() && !signers.contains(fee_payer) { + signers.push(fee_payer.clone()); + } + + Ok(signers) + } + + fn fee_payer(tx: &Tx) -> Result { + let fee = tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(SigVerifiableTxError::EmptyFee)?; + + let fee_payer = if !fee.payer.is_empty() { + fee.payer.clone() + } else { + Self::get_signers(tx)? + .first() + .ok_or(SigVerifiableTxError::EmptySigners)? + .clone() + }; + + Ok(fee_payer) + } + + fn sequence(tx: &Tx) -> Result { + let auth_info = tx.auth_info.as_ref().ok_or(SigVerifiableTxError::EmptyAuthInfo)?; + let fee = auth_info.fee.as_ref().ok_or(SigVerifiableTxError::EmptyFee)?; + + let sequence = if !fee.payer.is_empty() { + // TODO: Verify that the last signer is the fee payer. + auth_info + .signer_infos + .last() + .ok_or(SigVerifiableTxError::EmptySigners)? + .sequence + } else { + auth_info + .signer_infos + .first() + .ok_or(SigVerifiableTxError::EmptySigners)? + .sequence + }; + + Ok(sequence) + } +} diff --git a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/traits.rs b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/traits.rs new file mode 100644 index 00000000..fc20614b --- /dev/null +++ b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/traits.rs @@ -0,0 +1,26 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::SigVerifiableTxError; +use alloc::{string::String, vec::Vec}; +use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; + +pub trait SigVerifiableTx { + fn get_signers(tx: &Tx) -> Result, SigVerifiableTxError>; + fn fee_payer(tx: &Tx) -> Result; + fn sequence(tx: &Tx) -> Result; +} From a378f8a0053cfa93c2cf19c7cc6e6edeca453b00 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 22:51:29 +0900 Subject: [PATCH 016/133] feat: Add cosmwasm-std --- Cargo.toml | 2 + cosmwasm/std/.cargo/config.toml | 4 + cosmwasm/std/Cargo.toml | 85 + cosmwasm/std/README.md | 12 + cosmwasm/std/src/__internal.rs | 8 + cosmwasm/std/src/addresses.rs | 767 +++++ cosmwasm/std/src/assertions.rs | 211 ++ cosmwasm/std/src/binary.rs | 548 ++++ cosmwasm/std/src/checksum.rs | 237 ++ cosmwasm/std/src/coin.rs | 204 ++ cosmwasm/std/src/coins.rs | 514 ++++ cosmwasm/std/src/conversion.rs | 17 + cosmwasm/std/src/deps.rs | 151 + cosmwasm/std/src/encoding.rs | 94 + cosmwasm/std/src/errors/backtrace.rs | 89 + cosmwasm/std/src/errors/mod.rs | 16 + .../std/src/errors/recover_pubkey_error.rs | 64 + cosmwasm/std/src/errors/std_error.rs | 639 ++++ cosmwasm/std/src/errors/system_error.rs | 100 + cosmwasm/std/src/errors/verification_error.rs | 145 + cosmwasm/std/src/exports.rs | 802 +++++ cosmwasm/std/src/forward_ref.rs | 85 + cosmwasm/std/src/hex_binary.rs | 612 ++++ cosmwasm/std/src/ibc.rs | 869 ++++++ cosmwasm/std/src/ibc/callbacks.rs | 222 ++ cosmwasm/std/src/ibc/transfer_msg_builder.rs | 281 ++ cosmwasm/std/src/import_helpers.rs | 30 + cosmwasm/std/src/imports.rs | 732 +++++ cosmwasm/std/src/iterator.rs | 57 + cosmwasm/std/src/lib.rs | 153 + cosmwasm/std/src/math/conversion.rs | 336 ++ cosmwasm/std/src/math/decimal.rs | 1965 ++++++++++++ cosmwasm/std/src/math/decimal256.rs | 2039 +++++++++++++ cosmwasm/std/src/math/fraction.rs | 187 ++ cosmwasm/std/src/math/int128.rs | 1201 ++++++++ cosmwasm/std/src/math/int256.rs | 1310 ++++++++ cosmwasm/std/src/math/int512.rs | 1294 ++++++++ cosmwasm/std/src/math/int64.rs | 1156 +++++++ cosmwasm/std/src/math/isqrt.rs | 130 + cosmwasm/std/src/math/mod.rs | 111 + cosmwasm/std/src/math/num_consts.rs | 7 + cosmwasm/std/src/math/signed_decimal.rs | 2534 ++++++++++++++++ cosmwasm/std/src/math/signed_decimal_256.rs | 2691 +++++++++++++++++ cosmwasm/std/src/math/uint128.rs | 1462 +++++++++ cosmwasm/std/src/math/uint256.rs | 2022 +++++++++++++ cosmwasm/std/src/math/uint512.rs | 1367 +++++++++ cosmwasm/std/src/math/uint64.rs | 1362 +++++++++ cosmwasm/std/src/memory.rs | 168 + cosmwasm/std/src/metadata.rs | 25 + cosmwasm/std/src/never.rs | 45 + cosmwasm/std/src/pagination.rs | 12 + cosmwasm/std/src/panic.rs | 14 + cosmwasm/std/src/prelude.rs | 5 + cosmwasm/std/src/query/bank.rs | 114 + cosmwasm/std/src/query/distribution.rs | 109 + cosmwasm/std/src/query/ibc.rs | 52 + cosmwasm/std/src/query/mod.rs | 166 + cosmwasm/std/src/query/query_response.rs | 19 + cosmwasm/std/src/query/staking.rs | 196 ++ cosmwasm/std/src/query/wasm.rs | 144 + cosmwasm/std/src/results/contract_result.rs | 173 ++ cosmwasm/std/src/results/cosmos_msg.rs | 682 +++++ cosmwasm/std/src/results/empty.rs | 37 + cosmwasm/std/src/results/events.rs | 155 + cosmwasm/std/src/results/mod.rs | 28 + cosmwasm/std/src/results/query.rs | 1 + cosmwasm/std/src/results/response.rs | 409 +++ cosmwasm/std/src/results/submessages.rs | 615 ++++ cosmwasm/std/src/results/system_result.rs | 76 + cosmwasm/std/src/sections.rs | 144 + cosmwasm/std/src/serde.rs | 172 ++ cosmwasm/std/src/stdack.rs | 163 + cosmwasm/std/src/storage.rs | 321 ++ .../std/src/storage_keys/length_prefixed.rs | 225 ++ cosmwasm/std/src/storage_keys/mod.rs | 5 + cosmwasm/std/src/testing/assertions.rs | 183 ++ cosmwasm/std/src/testing/message_info.rs | 69 + cosmwasm/std/src/testing/mock.rs | 2442 +++++++++++++++ cosmwasm/std/src/testing/mod.rs | 32 + cosmwasm/std/src/timestamp.rs | 361 +++ cosmwasm/std/src/traits.rs | 737 +++++ cosmwasm/std/src/types.rs | 110 + .../std/testdata/instantiate2_addresses.json | 386 +++ 83 files changed, 37519 insertions(+) create mode 100644 cosmwasm/std/.cargo/config.toml create mode 100644 cosmwasm/std/Cargo.toml create mode 100644 cosmwasm/std/README.md create mode 100644 cosmwasm/std/src/__internal.rs create mode 100644 cosmwasm/std/src/addresses.rs create mode 100644 cosmwasm/std/src/assertions.rs create mode 100644 cosmwasm/std/src/binary.rs create mode 100644 cosmwasm/std/src/checksum.rs create mode 100644 cosmwasm/std/src/coin.rs create mode 100644 cosmwasm/std/src/coins.rs create mode 100644 cosmwasm/std/src/conversion.rs create mode 100644 cosmwasm/std/src/deps.rs create mode 100644 cosmwasm/std/src/encoding.rs create mode 100644 cosmwasm/std/src/errors/backtrace.rs create mode 100644 cosmwasm/std/src/errors/mod.rs create mode 100644 cosmwasm/std/src/errors/recover_pubkey_error.rs create mode 100644 cosmwasm/std/src/errors/std_error.rs create mode 100644 cosmwasm/std/src/errors/system_error.rs create mode 100644 cosmwasm/std/src/errors/verification_error.rs create mode 100644 cosmwasm/std/src/exports.rs create mode 100644 cosmwasm/std/src/forward_ref.rs create mode 100644 cosmwasm/std/src/hex_binary.rs create mode 100644 cosmwasm/std/src/ibc.rs create mode 100644 cosmwasm/std/src/ibc/callbacks.rs create mode 100644 cosmwasm/std/src/ibc/transfer_msg_builder.rs create mode 100644 cosmwasm/std/src/import_helpers.rs create mode 100644 cosmwasm/std/src/imports.rs create mode 100644 cosmwasm/std/src/iterator.rs create mode 100644 cosmwasm/std/src/lib.rs create mode 100644 cosmwasm/std/src/math/conversion.rs create mode 100644 cosmwasm/std/src/math/decimal.rs create mode 100644 cosmwasm/std/src/math/decimal256.rs create mode 100644 cosmwasm/std/src/math/fraction.rs create mode 100644 cosmwasm/std/src/math/int128.rs create mode 100644 cosmwasm/std/src/math/int256.rs create mode 100644 cosmwasm/std/src/math/int512.rs create mode 100644 cosmwasm/std/src/math/int64.rs create mode 100644 cosmwasm/std/src/math/isqrt.rs create mode 100644 cosmwasm/std/src/math/mod.rs create mode 100644 cosmwasm/std/src/math/num_consts.rs create mode 100644 cosmwasm/std/src/math/signed_decimal.rs create mode 100644 cosmwasm/std/src/math/signed_decimal_256.rs create mode 100644 cosmwasm/std/src/math/uint128.rs create mode 100644 cosmwasm/std/src/math/uint256.rs create mode 100644 cosmwasm/std/src/math/uint512.rs create mode 100644 cosmwasm/std/src/math/uint64.rs create mode 100644 cosmwasm/std/src/memory.rs create mode 100644 cosmwasm/std/src/metadata.rs create mode 100644 cosmwasm/std/src/never.rs create mode 100644 cosmwasm/std/src/pagination.rs create mode 100644 cosmwasm/std/src/panic.rs create mode 100644 cosmwasm/std/src/prelude.rs create mode 100644 cosmwasm/std/src/query/bank.rs create mode 100644 cosmwasm/std/src/query/distribution.rs create mode 100644 cosmwasm/std/src/query/ibc.rs create mode 100644 cosmwasm/std/src/query/mod.rs create mode 100644 cosmwasm/std/src/query/query_response.rs create mode 100644 cosmwasm/std/src/query/staking.rs create mode 100644 cosmwasm/std/src/query/wasm.rs create mode 100644 cosmwasm/std/src/results/contract_result.rs create mode 100644 cosmwasm/std/src/results/cosmos_msg.rs create mode 100644 cosmwasm/std/src/results/empty.rs create mode 100644 cosmwasm/std/src/results/events.rs create mode 100644 cosmwasm/std/src/results/mod.rs create mode 100644 cosmwasm/std/src/results/query.rs create mode 100644 cosmwasm/std/src/results/response.rs create mode 100644 cosmwasm/std/src/results/submessages.rs create mode 100644 cosmwasm/std/src/results/system_result.rs create mode 100644 cosmwasm/std/src/sections.rs create mode 100644 cosmwasm/std/src/serde.rs create mode 100644 cosmwasm/std/src/stdack.rs create mode 100644 cosmwasm/std/src/storage.rs create mode 100644 cosmwasm/std/src/storage_keys/length_prefixed.rs create mode 100644 cosmwasm/std/src/storage_keys/mod.rs create mode 100644 cosmwasm/std/src/testing/assertions.rs create mode 100644 cosmwasm/std/src/testing/message_info.rs create mode 100644 cosmwasm/std/src/testing/mock.rs create mode 100644 cosmwasm/std/src/testing/mod.rs create mode 100644 cosmwasm/std/src/timestamp.rs create mode 100644 cosmwasm/std/src/traits.rs create mode 100644 cosmwasm/std/src/types.rs create mode 100644 cosmwasm/std/testdata/instantiate2_addresses.json diff --git a/Cargo.toml b/Cargo.toml index ed412626..91bf1ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "core-primitives", + "cosmwasm/std", "frame/babel", "frame/cosmos/types", "frame/cosmos/rpc", @@ -22,6 +23,7 @@ members = [ [workspace.dependencies] cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } +cosmwasm-std = { path = "cosmwasm/std", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } diff --git a/cosmwasm/std/.cargo/config.toml b/cosmwasm/std/.cargo/config.toml new file mode 100644 index 00000000..b93207d6 --- /dev/null +++ b/cosmwasm/std/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" diff --git a/cosmwasm/std/Cargo.toml b/cosmwasm/std/Cargo.toml new file mode 100644 index 00000000..4434b4c4 --- /dev/null +++ b/cosmwasm/std/Cargo.toml @@ -0,0 +1,85 @@ +[package] +name = "cosmwasm-std" +version = "2.1.3" +authors = ["Ethan Frey "] +edition = "2021" +description = "Standard library for Wasm based smart contracts on Cosmos blockchains" +repository = "https://github.com/CosmWasm/cosmwasm/tree/main/packages/std" +license = "Apache-2.0" +readme = "README.md" + +[package.metadata.docs.rs] +features = ["abort", "stargate", "staking", "cosmwasm_2_1"] + +[features] +default = ["iterator", "abort", "std"] +abort = [] +std = [] +# iterator allows us to iterate over all DB items in a given range +# optional as some merkle stores (like tries) don't support this +# given Ethereum 1.0, 2.0, Substrate, and other major projects use Tries +# we keep this optional, to allow possible future integration (or different Cosmos Backends) +iterator = [] +# staking exposes bindings to a required staking moudle in the runtime, via new +# CosmosMsg types, and new QueryRequest types. This should only be enabled on contracts +# that require these types, so other contracts can be used on systems with eg. PoA consensus +staking = [] +# stargate enables stargate-dependent messages and queries, like raw protobuf messages +# as well as ibc-related functionality +stargate = [] +# This feature makes `BankQuery::Supply` available for the contract to call, but requires +# the host blockchain to run CosmWasm `1.1.0` or higher. +cosmwasm_1_1 = [] +# This feature makes `GovMsg::VoteWeighted` available for the contract to call, but requires +# the host blockchain to run CosmWasm `1.2.0` or higher. +cosmwasm_1_2 = ["cosmwasm_1_1"] +# This feature makes `BankQuery::DenomMetadata` available for the contract to call, but requires +# the host blockchain to run CosmWasm `1.3.0` or higher. +cosmwasm_1_3 = ["cosmwasm_1_2"] +# Together with the `iterator` feature this enables additional imports for more +# efficient iteration over DB keys or values. +# It also makes `DistributionQuery::{DelegationRewards, DelegationTotalRewards, DelegatorValidators}` +# available for the contract to call. +# It requires the host blockchain to run CosmWasm `1.4.0` or higher. +cosmwasm_1_4 = ["cosmwasm_1_3"] +# This enables functionality that is only available on 2.0 chains. +# It adds `CosmosMsg::Any`, replacing `CosmosMsg::Stargate`. It also adds `QueryRequest::Grpc`. +cosmwasm_2_0 = ["cosmwasm_1_4"] +# This enables functionality that is only available on 2.1 chains. +# It adds verification and key recovery for the secp256r1 elliptic curve. +cosmwasm_2_1 = ["cosmwasm_2_0"] + +[dependencies] +base64 = { version = "0.22.0", default-features = false, features = ["alloc"] } +bnum = { version = "0.11.0", default-features = false } +cosmwasm-core = { version = "2.1.3", default-features = false } +cosmwasm-derive = { version = "2.1.3", default-features = false } +derive_more = { version = "1.0.0-beta.6", default-features = false, features = [ + "debug", +] } +hex = { version = "0.4", default-features = false } +schemars = { version = "1.0.0-alpha.5", default-features = false, features = [ + "derive", +] } +sha2 = { version = "0.10.3", default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +serde-json-wasm = { version = "1.0.1", default-features = false } +static_assertions = "1.1.0" +thiserror = { version = "1.0.50", package = "thiserror-core", default-features = false } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +bech32 = "0.11.0" +cosmwasm-crypto = { version = "2.1.3" } +rand_core = { version = "0.6.4", features = ["getrandom"] } + +[dev-dependencies] +cosmwasm-core = { version = "2.1.3" } +cosmwasm-schema = { version = "2.1.3" } +# The chrono dependency is only used in an example, which Rust compiles for us. If this causes trouble, remove it. +chrono = { version = "0.4", default-features = false, features = [ + "alloc", + "std", +] } +crc32fast = "1.3.2" +hex-literal = "0.4.1" +serde_json = "1.0.81" diff --git a/cosmwasm/std/README.md b/cosmwasm/std/README.md new file mode 100644 index 00000000..5878fa15 --- /dev/null +++ b/cosmwasm/std/README.md @@ -0,0 +1,12 @@ +# cosmwasm + +[![cosmwasm-std on crates.io](https://img.shields.io/crates/v/cosmwasm-std.svg)](https://crates.io/crates/cosmwasm-std) + +The standard library for building CosmWasm smart contracts. Code in this package +is compiled into the smart contract. + +## License + +This package is part of the cosmwasm repository, licensed under the Apache +License 2.0 (see [NOTICE](https://github.com/CosmWasm/cosmwasm/blob/main/NOTICE) +and [LICENSE](https://github.com/CosmWasm/cosmwasm/blob/main/LICENSE)). diff --git a/cosmwasm/std/src/__internal.rs b/cosmwasm/std/src/__internal.rs new file mode 100644 index 00000000..f46e14e6 --- /dev/null +++ b/cosmwasm/std/src/__internal.rs @@ -0,0 +1,8 @@ +//! +//! # ⚠ DO NOT DEPEND ON THIS AS AN OUTSIDE CONSUMER +//! +//! **THIS MODULE IS SEMVER EXEMPT AND ONLY MEANT TO SHARE TYPES BETWEEN CORE AND STD** +//! +//! Module for re-exporting implementation details from `core` to `std` + +pub use crate::__internal__forward_ref_partial_eq as forward_ref_partial_eq; diff --git a/cosmwasm/std/src/addresses.rs b/cosmwasm/std/src/addresses.rs new file mode 100644 index 00000000..c0bb1651 --- /dev/null +++ b/cosmwasm/std/src/addresses.rs @@ -0,0 +1,767 @@ +use alloc::{borrow::Cow, string::String, vec::Vec}; +use core::{fmt, ops::Deref}; +use serde::{Deserialize, Serialize}; +use sha2::{ + digest::{Digest, Update}, + Sha256, +}; + +use crate::{Binary, HexBinary, __internal::forward_ref_partial_eq}; + +/// A human readable address. +/// +/// In Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no +/// assumptions should be made other than being UTF-8 encoded and of reasonable length. +/// +/// This type represents a validated address. It can be created in the following ways +/// 1. Use `Addr::unchecked(input)` +/// 2. Use `let checked: Addr = deps.api.addr_validate(input)?` +/// 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` +/// 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a +/// contract's state. `Addr` must not be used in messages sent by the user because this would +/// result in unvalidated instances. +/// +/// This type is immutable. If you really need to mutate it (Really? Are you sure?), create +/// a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` +/// instance. +#[derive( + Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, schemars::JsonSchema, +)] +pub struct Addr(String); + +forward_ref_partial_eq!(Addr, Addr); + +impl Addr { + /// Creates a new `Addr` instance from the given input without checking the validity + /// of the input. Since `Addr` must always contain valid addresses, the caller is + /// responsible for ensuring the input is valid. + /// + /// Use this in cases where the address was validated before or in test code. + /// If you see this in contract code, it should most likely be replaced with + /// `let checked: Addr = deps.api.addr_humanize(canonical_addr)?`. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Addr}; + /// let address = Addr::unchecked("foobar"); + /// assert_eq!(address.as_str(), "foobar"); + /// ``` + pub fn unchecked(input: impl Into) -> Addr { + Addr(input.into()) + } + + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Returns the UTF-8 encoded address string as a byte array. + /// + /// This is equivalent to `address.as_str().as_bytes()`. + #[inline] + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Utility for explicit conversion to `String`. + #[inline] + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for Addr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +impl AsRef for Addr { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +// Addr->String is a safe conversion. +// However, the opposite direction is unsafe and must not be implemented. + +impl From for String { + fn from(addr: Addr) -> Self { + addr.0 + } +} + +impl From<&Addr> for String { + fn from(addr: &Addr) -> Self { + addr.0.clone() + } +} + +impl From for Cow<'_, Addr> { + fn from(addr: Addr) -> Self { + Cow::Owned(addr) + } +} + +impl<'a> From<&'a Addr> for Cow<'a, Addr> { + fn from(addr: &'a Addr) -> Self { + Cow::Borrowed(addr) + } +} + +/// A blockchain address in its binary form. +/// +/// The specific implementation is up to the underlying chain and CosmWasm as well as +/// contracts should not make assumptions on that data. In Ethereum for example, an +/// `Addr` would contain a user visible address like 0x14d3cc818735723ab86eaf9502376e847a64ddad +/// and the corresponding `CanonicalAddr` would store the 20 bytes 0x14, 0xD3, ..., 0xAD. +/// In Cosmos, the bech32 format is used for `Addr`s and the `CanonicalAddr` holds the +/// encoded bech32 data without the checksum. Typical sizes are 20 bytes for externally +/// owned addresses and 32 bytes for module addresses (such as x/wasm contract addresses). +/// That being said, a chain might decide to use any size other than 20 or 32 bytes. +/// +/// The safe way to obtain a valid `CanonicalAddr` is using `Api::addr_canonicalize`. In +/// addition to that there are many unsafe ways to convert any binary data into an instance. +/// So the type should be treated as a marker to express the intended data type, not as +/// a validity guarantee of any sort. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, schemars::JsonSchema)] +pub struct CanonicalAddr(Binary); + +/// Implement `CanonicalAddr == Binary` +impl PartialEq for CanonicalAddr { + fn eq(&self, rhs: &Binary) -> bool { + &self.0 == rhs + } +} + +/// Implement `Binary == CanonicalAddr` +impl PartialEq for Binary { + fn eq(&self, rhs: &CanonicalAddr) -> bool { + self == &rhs.0 + } +} + +/// Implement `CanonicalAddr == HexBinary` +impl PartialEq for CanonicalAddr { + fn eq(&self, rhs: &HexBinary) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `HexBinary == CanonicalAddr` +impl PartialEq for HexBinary { + fn eq(&self, rhs: &CanonicalAddr) -> bool { + self.as_slice() == rhs.0.as_slice() + } +} + +impl From<&[u8]> for CanonicalAddr { + fn from(source: &[u8]) -> Self { + Self(source.into()) + } +} + +// Array reference +impl From<&[u8; LENGTH]> for CanonicalAddr { + fn from(source: &[u8; LENGTH]) -> Self { + Self(source.into()) + } +} + +// Owned array +impl From<[u8; LENGTH]> for CanonicalAddr { + fn from(source: [u8; LENGTH]) -> Self { + Self(source.into()) + } +} + +// Owned vector -> CanonicalAddr +impl From> for CanonicalAddr { + fn from(source: Vec) -> Self { + Self(source.into()) + } +} + +// CanonicalAddr -> Owned vector +impl From for Vec { + fn from(source: CanonicalAddr) -> Vec { + source.0.into() + } +} + +// Owned Binary -> CanonicalAddr +impl From for CanonicalAddr { + fn from(source: Binary) -> Self { + Self(source) + } +} + +// CanonicalAddr -> Owned Binary +impl From for Binary { + fn from(source: CanonicalAddr) -> Binary { + source.0 + } +} + +// Owned HexBinary -> CanonicalAddr +impl From for CanonicalAddr { + fn from(source: HexBinary) -> Self { + Self(source.into()) + } +} + +// CanonicalAddr -> Owned HexBinary +impl From for HexBinary { + fn from(source: CanonicalAddr) -> HexBinary { + source.0.into() + } +} + +/// Just like Vec, CanonicalAddr is a smart pointer to [u8]. +/// This implements `*canonical_address` for us and allows us to +/// do `&*canonical_address`, returning a `&[u8]` from a `&CanonicalAddr`. +/// With [deref coercions](https://doc.rust-lang.org/1.22.1/book/first-edition/deref-coercions.html#deref-coercions), +/// this allows us to use `&canonical_address` whenever a `&[u8]` is required. +impl Deref for CanonicalAddr { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl CanonicalAddr { + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl fmt::Display for CanonicalAddr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for byte in self.0.as_slice() { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum Instantiate2AddressError { + /// Checksum must be 32 bytes + InvalidChecksumLength, + /// Salt must be between 1 and 64 bytes + InvalidSaltLength, +} + +impl fmt::Display for Instantiate2AddressError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Instantiate2AddressError::InvalidChecksumLength => write!(f, "invalid checksum length"), + Instantiate2AddressError::InvalidSaltLength => write!(f, "invalid salt length"), + } + } +} + +/// Creates a contract address using the predictable address format introduced with +/// wasmd 0.29. When using instantiate2, this is a way to precompute the address. +/// When using instantiate, the contract address will use a different algorithm and +/// cannot be pre-computed as it contains inputs from the chain's state at the time of +/// message execution. +/// +/// The predicable address format of instantiate2 is stable. But bear in mind this is +/// a powerful tool that requires multiple software components to work together smoothly. +/// It should be used carefully and tested thoroughly to avoid the loss of funds. +/// +/// This method operates on [`CanonicalAddr`] to be implemented without chain interaction. +/// The typical usage looks like this: +/// +/// ``` +/// # use cosmwasm_std::{ +/// # HexBinary, +/// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, +/// # Response, QueryResponse, +/// # }; +/// # type ExecuteMsg = (); +/// use cosmwasm_std::instantiate2_address; +/// +/// #[entry_point] +/// pub fn execute( +/// deps: DepsMut, +/// env: Env, +/// info: MessageInfo, +/// msg: ExecuteMsg, +/// ) -> Result { +/// let canonical_creator = deps.api.addr_canonicalize(env.contract.address.as_str())?; +/// let checksum = HexBinary::from_hex("9af782a3a1bcbcd22dbb6a45c751551d9af782a3a1bcbcd22dbb6a45c751551d")?; +/// let salt = b"instance 1231"; +/// let canonical_addr = instantiate2_address(&checksum, &canonical_creator, salt) +/// .map_err(|_| StdError::generic_err("Could not calculate addr"))?; +/// let addr = deps.api.addr_humanize(&canonical_addr)?; +/// +/// # Ok(Default::default()) +/// } +/// ``` +pub fn instantiate2_address( + checksum: &[u8], + creator: &CanonicalAddr, + salt: &[u8], +) -> Result { + // Non-empty msg values are discouraged. + // See https://medium.com/cosmwasm/dev-note-3-limitations-of-instantiate2-and-how-to-deal-with-them-a3f946874230. + let msg = b""; + instantiate2_address_impl(checksum, creator, salt, msg) +} + +/// The instantiate2 address derivation implementation. This API is used for +/// testing purposes only. The `msg` field is discouraged and should not be used. +/// Use [`instantiate2_address`]. +#[doc(hidden)] +fn instantiate2_address_impl( + checksum: &[u8], + creator: &CanonicalAddr, + salt: &[u8], + msg: &[u8], +) -> Result { + if checksum.len() != 32 { + return Err(Instantiate2AddressError::InvalidChecksumLength); + } + + if salt.is_empty() || salt.len() > 64 { + return Err(Instantiate2AddressError::InvalidSaltLength); + }; + + let mut key = Vec::::new(); + key.extend_from_slice(b"wasm\0"); + key.extend_from_slice(&(checksum.len() as u64).to_be_bytes()); + key.extend_from_slice(checksum); + key.extend_from_slice(&(creator.len() as u64).to_be_bytes()); + key.extend_from_slice(creator); + key.extend_from_slice(&(salt.len() as u64).to_be_bytes()); + key.extend_from_slice(salt); + key.extend_from_slice(&(msg.len() as u64).to_be_bytes()); + key.extend_from_slice(msg); + let address_data = hash("module", &key); + Ok(address_data.into()) +} + +/// The "Basic Address" Hash from +/// https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/docs/architecture/adr-028-public-key-addresses.md +fn hash(ty: &str, key: &[u8]) -> Vec { + let inner = Sha256::digest(ty.as_bytes()); + Sha256::new().chain(inner).chain(key).finalize().to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_hash_works, HexBinary}; + + use hex_literal::hex; + + #[test] + fn addr_unchecked_works() { + let a = Addr::unchecked("123"); + let aa = Addr::unchecked(String::from("123")); + let b = Addr::unchecked("be"); + assert_eq!(a, aa); + assert_ne!(a, b); + } + + #[test] + fn addr_as_str_works() { + let addr = Addr::unchecked("literal-string"); + assert_eq!(addr.as_str(), "literal-string"); + } + + #[test] + fn addr_as_bytes_works() { + let addr = Addr::unchecked("literal-string"); + assert_eq!( + addr.as_bytes(), + [108, 105, 116, 101, 114, 97, 108, 45, 115, 116, 114, 105, 110, 103] + ); + } + + #[test] + fn addr_implements_display() { + let addr = Addr::unchecked("cos934gh9034hg04g0h134"); + let embedded = format!("Address: {addr}"); + assert_eq!(embedded, "Address: cos934gh9034hg04g0h134"); + assert_eq!(addr.to_string(), "cos934gh9034hg04g0h134"); + } + + #[test] + fn addr_implements_as_ref_for_str() { + let addr = Addr::unchecked("literal-string"); + assert_eq!(addr.as_ref(), "literal-string"); + } + + #[test] + fn addr_implements_partial_eq_addr_ref() { + let addr = Addr::unchecked("cos934gh9034hg04g0h134"); + let addr_ref = &addr; + let addr_ref2 = &addr; + + // `Addr == &Addr` + assert_eq!(addr, addr_ref); + // `&Addr == Addr` + assert_eq!(addr_ref, addr); + // `&Addr == &Addr` + assert_eq!(addr_ref, addr_ref2); + } + + #[test] + fn addr_implements_into_string() { + // owned Addr + let addr = Addr::unchecked("cos934gh9034hg04g0h134"); + let string: String = addr.into(); + assert_eq!(string, "cos934gh9034hg04g0h134"); + + // &Addr + let addr = Addr::unchecked("cos934gh9034hg04g0h134"); + let addr_ref = &addr; + let string: String = addr_ref.into(); + assert_eq!(string, "cos934gh9034hg04g0h134"); + } + + // Test CanonicalAddr as_slice() for each CanonicalAddr::from input type + #[test] + fn canonical_addr_from_slice() { + // slice + let bytes: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let canonical_addr_slice = CanonicalAddr::from(bytes); + assert_eq!(canonical_addr_slice.as_slice(), &[0u8, 187, 61, 11, 250, 0]); + + // Vector + let bytes: Vec = vec![0u8, 187, 61, 11, 250, 0]; + let canonical_addr_vec = CanonicalAddr::from(bytes); + assert_eq!(canonical_addr_vec.as_slice(), &[0u8, 187, 61, 11, 250, 0]); + } + + #[test] + fn canonical_addr_implements_partial_eq_with_binary() { + let addr = CanonicalAddr::from([1, 2, 3]); + let bin1 = Binary::from([1, 2, 3]); + let bin2 = Binary::from([42, 43]); + + assert_eq!(addr, bin1); + assert_eq!(bin1, addr); + assert_ne!(addr, bin2); + assert_ne!(bin2, addr); + } + + #[test] + fn canonical_addr_implements_partial_eq_with_hex_binary() { + let addr = CanonicalAddr::from([1, 2, 3]); + let bin1 = HexBinary::from([1, 2, 3]); + let bin2 = HexBinary::from([42, 43]); + + assert_eq!(addr, bin1); + assert_eq!(bin1, addr); + assert_ne!(addr, bin2); + assert_ne!(bin2, addr); + } + + #[test] + fn canonical_addr_implements_from_array() { + let array = [1, 2, 3]; + let addr = CanonicalAddr::from(array); + assert_eq!(addr.as_slice(), [1, 2, 3]); + + let array_ref = b"foo"; + let addr = CanonicalAddr::from(array_ref); + assert_eq!(addr.as_slice(), [0x66, 0x6f, 0x6f]); + } + + #[test] + fn canonical_addr_implements_from_and_to_vector() { + // Into for Vec + // This test is a bit pointless because we get Into from the From implementation + let original = vec![0u8, 187, 61, 11, 250, 0]; + let original_ptr = original.as_ptr(); + let addr: CanonicalAddr = original.into(); + assert_eq!(addr.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!((addr.0).as_slice().as_ptr(), original_ptr, "must not be copied"); + + // From> for CanonicalAddr + let original = vec![0u8, 187, 61, 11, 250, 0]; + let original_ptr = original.as_ptr(); + let addr = CanonicalAddr::from(original); + assert_eq!(addr.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!((addr.0).as_slice().as_ptr(), original_ptr, "must not be copied"); + + // Into> for CanonicalAddr + // This test is a bit pointless because we get Into from the From implementation + let original = CanonicalAddr::from(vec![0u8, 187, 61, 11, 250, 0]); + let original_ptr = (original.0).as_slice().as_ptr(); + let vec: Vec = original.into(); + assert_eq!(vec.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(vec.as_ptr(), original_ptr, "must not be copied"); + + // From for Vec + let original = CanonicalAddr::from(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = (original.0).as_slice().as_ptr(); + let vec = Vec::::from(original); + assert_eq!(vec.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(vec.as_ptr(), original_ptr, "must not be copied"); + } + + #[test] + fn canonical_addr_implements_from_and_to_binary() { + // From for CanonicalAddr + let original = Binary::from([0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.as_ptr(); + let addr = CanonicalAddr::from(original); + assert_eq!(addr.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!((addr.0).as_slice().as_ptr(), original_ptr, "must not be copied"); + + // From for Binary + let original = CanonicalAddr::from(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = (original.0).as_slice().as_ptr(); + let bin = Binary::from(original); + assert_eq!(bin.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(bin.as_ptr(), original_ptr, "must not be copied"); + } + + #[test] + fn canonical_addr_implements_from_and_to_hex_binary() { + // From for CanonicalAddr + let original = HexBinary::from([0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.as_ptr(); + let addr = CanonicalAddr::from(original); + assert_eq!(addr.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!((addr.0).as_slice().as_ptr(), original_ptr, "must not be copied"); + + // From for HexBinary + let original = CanonicalAddr::from(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = (original.0).as_slice().as_ptr(); + let bin = HexBinary::from(original); + assert_eq!(bin.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(bin.as_ptr(), original_ptr, "must not be copied"); + } + + #[test] + fn canonical_addr_len() { + let bytes: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let canonical_addr = CanonicalAddr::from(bytes); + assert_eq!(canonical_addr.len(), bytes.len()); + } + + #[test] + fn canonical_addr_is_empty() { + let bytes: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let canonical_addr = CanonicalAddr::from(bytes); + assert!(!canonical_addr.is_empty()); + let empty_canonical_addr = CanonicalAddr::from(vec![]); + assert!(empty_canonical_addr.is_empty()); + } + + #[test] + fn canonical_addr_implements_display() { + let bytes: &[u8] = &[ + 0x12, // two hex digits + 0x03, // small values must be padded to two digits + 0xab, // ensure we get upper case + 0x00, // always test extreme values + 0xff, + ]; + let address = CanonicalAddr::from(bytes); + let embedded = format!("Address: {address}"); + assert_eq!(embedded, "Address: 1203AB00FF"); + assert_eq!(address.to_string(), "1203AB00FF"); + } + + #[test] + fn canonical_addr_implements_deref() { + // Dereference to [u8] + let bytes: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let canonical_addr = CanonicalAddr::from(bytes); + assert_eq!(*canonical_addr, [0u8, 187, 61, 11, 250, 0]); + + // This checks deref coercions from &CanonicalAddr to &[u8] works + let bytes: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let canonical_addr = CanonicalAddr::from(bytes); + assert_eq!(canonical_addr.len(), 6); + let canonical_addr_slice: &[u8] = &canonical_addr; + assert_eq!(canonical_addr_slice, &[0u8, 187, 61, 11, 250, 0]); + } + + /// Tests that `CanonicalAddr` implements `EQ` and `Hash` correctly and thus + /// can be used with hash maps and sets. + #[test] + fn canonical_addr_implements_hash_eq() { + let alice = CanonicalAddr::from([0, 187, 61, 11, 250, 0]); + let bob = CanonicalAddr::from([16, 21, 33, 0, 255, 9]); + assert_hash_works!(alice, bob); + } + + // helper to show we can handle Addr and &Addr equally + fn flexible<'a>(a: impl Into>) -> String { + a.into().into_owned().to_string() + } + + #[test] + fn addr_into_cow() { + // owned Addr + let value = "wasmeucn0ur0ncny2308ry"; + let addr = Addr::unchecked(value); + + // pass by ref + assert_eq!(value, &flexible(&addr)); + // pass by value + assert_eq!(value, &flexible(addr)); + } + + #[test] + fn instantiate2_address_impl_works() { + let checksum1 = + HexBinary::from_hex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5") + .unwrap(); + let creator1 = CanonicalAddr::from(hex!("9999999999aaaaaaaaaabbbbbbbbbbcccccccccc")); + let salt1 = hex!("61"); + let salt2 = hex!("aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae"); + let msg1: &[u8] = b""; + let msg2: &[u8] = b"{}"; + let msg3: &[u8] = b"{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}"; + + // No msg + let expected = CanonicalAddr::from(hex!( + "5e865d3e45ad3e961f77fd77d46543417ced44d924dc3e079b5415ff6775f847" + )); + assert_eq!( + instantiate2_address_impl(&checksum1, &creator1, &salt1, msg1).unwrap(), + expected + ); + + // With msg + let expected = CanonicalAddr::from(hex!( + "0995499608947a5281e2c7ebd71bdb26a1ad981946dad57f6c4d3ee35de77835" + )); + assert_eq!( + instantiate2_address_impl(&checksum1, &creator1, &salt1, msg2).unwrap(), + expected + ); + + // Long msg + let expected = CanonicalAddr::from(hex!( + "83326e554723b15bac664ceabc8a5887e27003abe9fbd992af8c7bcea4745167" + )); + assert_eq!( + instantiate2_address_impl(&checksum1, &creator1, &salt1, msg3).unwrap(), + expected + ); + + // Long salt + let expected = CanonicalAddr::from(hex!( + "9384c6248c0bb171e306fd7da0993ec1e20eba006452a3a9e078883eb3594564" + )); + assert_eq!( + instantiate2_address_impl(&checksum1, &creator1, &salt2, b"").unwrap(), + expected + ); + + // Salt too short or too long + let empty = Vec::::new(); + assert!(matches!( + instantiate2_address_impl(&checksum1, &creator1, &empty, b"").unwrap_err(), + Instantiate2AddressError::InvalidSaltLength + )); + let too_long = vec![0x11; 65]; + assert!(matches!( + instantiate2_address_impl(&checksum1, &creator1, &too_long, b"").unwrap_err(), + Instantiate2AddressError::InvalidSaltLength + )); + + // invalid checksum length + let broken_cs = hex!("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2"); + assert!(matches!( + instantiate2_address_impl(&broken_cs, &creator1, &salt1, b"").unwrap_err(), + Instantiate2AddressError::InvalidChecksumLength + )); + let broken_cs = hex!(""); + assert!(matches!( + instantiate2_address_impl(&broken_cs, &creator1, &salt1, b"").unwrap_err(), + Instantiate2AddressError::InvalidChecksumLength + )); + let broken_cs = hex!("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2aaaa"); + assert!(matches!( + instantiate2_address_impl(&broken_cs, &creator1, &salt1, b"").unwrap_err(), + Instantiate2AddressError::InvalidChecksumLength + )); + } + + #[test] + fn instantiate2_address_impl_works_for_cosmjs_test_vectors() { + // Test data from https://github.com/cosmos/cosmjs/pull/1253 + const COSMOS_ED25519_TESTS_JSON: &str = "./testdata/instantiate2_addresses.json"; + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + #[allow(dead_code)] + struct In { + checksum: HexBinary, + creator: String, + creator_data: HexBinary, + salt: HexBinary, + msg: Option, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + #[allow(dead_code)] + struct Intermediate { + key: HexBinary, + address_data: HexBinary, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + #[allow(dead_code)] + struct Out { + address: String, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + struct Row { + #[serde(rename = "in")] + input: In, + intermediate: Intermediate, + out: Out, + } + + fn read_tests() -> Vec { + use std::{fs::File, io::BufReader}; + + // Open the file in read-only mode with buffer. + let file = File::open(COSMOS_ED25519_TESTS_JSON).unwrap(); + let reader = BufReader::new(file); + + serde_json::from_reader(reader).unwrap() + } + + for Row { input, intermediate, out: _ } in read_tests() { + let msg = input.msg.map(|msg| msg.into_bytes()).unwrap_or_default(); + let addr = instantiate2_address_impl( + &input.checksum, + &input.creator_data.into(), + &input.salt, + &msg, + ) + .unwrap(); + assert_eq!(addr, intermediate.address_data); + } + } + + #[test] + fn hash_works() { + // Test case from https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-alpha1/types/address/hash_test.go#L19-L24 + let expected = [ + 195, 235, 23, 251, 9, 99, 177, 195, 81, 122, 182, 124, 36, 113, 245, 156, 76, 188, 221, + 83, 181, 192, 227, 82, 100, 177, 161, 133, 240, 160, 5, 25, + ]; + assert_eq!(hash("1", &[1]), expected); + } +} diff --git a/cosmwasm/std/src/assertions.rs b/cosmwasm/std/src/assertions.rs new file mode 100644 index 00000000..1af8df9a --- /dev/null +++ b/cosmwasm/std/src/assertions.rs @@ -0,0 +1,211 @@ +//! A module containing an assertion framework for CosmWasm contracts. +//! The methods in here never panic but return errors instead. + +/// Quick check for a guard. If the condition (first argument) is false, +/// then return the second argument `x` wrapped in `Err(x)`. +/// +/// ``` +/// # enum ContractError { +/// # DelegatePerm {}, +/// # } +/// # +/// # struct Permissions { +/// # delegate: bool, +/// # } +/// # +/// # fn body() -> Result<(), ContractError> { +/// # let permissions = Permissions { delegate: true }; +/// use cosmwasm_std::ensure; +/// ensure!(permissions.delegate, ContractError::DelegatePerm {}); +/// +/// // is the same as +/// +/// if !permissions.delegate { +/// return Err(ContractError::DelegatePerm {}); +/// } +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! ensure { + ($cond:expr, $e:expr) => { + if !($cond) { + return Err(core::convert::From::from($e)); + } + }; +} + +/// Quick check for a guard. Like `assert_eq!`, but rather than panic, +/// it returns the third argument `x` wrapped in `Err(x)`. +/// +/// ``` +/// # use cosmwasm_std::{MessageInfo, Addr}; +/// # +/// # enum ContractError { +/// # Unauthorized {}, +/// # } +/// # struct Config { +/// # admin: Addr, +/// # } +/// # +/// # fn body() -> Result<(), ContractError> { +/// # let info = MessageInfo { sender: Addr::unchecked("foo"), funds: Vec::new() }; +/// # let cfg = Config { admin: Addr::unchecked("foo") }; +/// use cosmwasm_std::ensure_eq; +/// +/// ensure_eq!(info.sender, cfg.admin, ContractError::Unauthorized {}); +/// +/// // is the same as +/// +/// if info.sender != cfg.admin { +/// return Err(ContractError::Unauthorized {}); +/// } +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! ensure_eq { + ($a:expr, $b:expr, $e:expr) => { + // Not implemented via `ensure!` because the caller would have to import both macros. + if !($a == $b) { + return Err(core::convert::From::from($e)); + } + }; +} + +/// Quick check for a guard. Like `assert_ne!`, but rather than panic, +/// it returns the third argument `x` wrapped in Err(x). +/// +/// ``` +/// # enum ContractError { +/// # NotAVoter {}, +/// # } +/// # +/// # fn body() -> Result<(), ContractError> { +/// # let voting_power = 123; +/// use cosmwasm_std::ensure_ne; +/// +/// ensure_ne!(voting_power, 0, ContractError::NotAVoter {}); +/// +/// // is the same as +/// +/// if voting_power != 0 { +/// return Err(ContractError::NotAVoter {}); +/// } +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! ensure_ne { + ($a:expr, $b:expr, $e:expr) => { + // Not implemented via `ensure!` because the caller would have to import both macros. + if !($a != $b) { + return Err(core::convert::From::from($e)); + } + }; +} + +#[cfg(test)] +mod tests { + use crate::StdError; + + #[test] + fn ensure_works() { + fn check(a: usize, b: usize) -> Result<(), StdError> { + ensure!(a == b, StdError::generic_err("foobar")); + Ok(()) + } + + let err = check(5, 6).unwrap_err(); + assert!(matches!(err, StdError::GenericErr { .. })); + + check(5, 5).unwrap(); + } + + #[test] + fn ensure_can_infer_error_type() { + let check = |a, b| { + ensure!(a == b, StdError::generic_err("foobar")); + Ok(()) + }; + + let err = check(5, 6).unwrap_err(); + assert!(matches!(err, StdError::GenericErr { .. })); + + check(5, 5).unwrap(); + } + + #[test] + fn ensure_can_convert_into() { + #[derive(Debug)] + struct ContractError; + + impl From for ContractError { + fn from(_original: StdError) -> Self { + ContractError + } + } + + fn check(a: usize, b: usize) -> Result<(), ContractError> { + ensure!(a == b, StdError::generic_err("foobar")); + Ok(()) + } + + let err = check(5, 6).unwrap_err(); + assert!(matches!(err, ContractError)); + + check(5, 5).unwrap(); + } + + #[test] + fn ensure_eq_works() { + let check = |a, b| { + ensure_eq!(a, b, StdError::generic_err("foobar")); + Ok(()) + }; + + let err = check("123", "456").unwrap_err(); + assert!(matches!(err, StdError::GenericErr { .. })); + check("123", "123").unwrap(); + } + + #[test] + fn ensure_eq_gets_precedence_right() { + // If this was expanded to `true || false == false` we'd get equality. + // It must be expanded to `(true || false) == false` and we expect inequality. + + #[allow(clippy::nonminimal_bool)] + fn check() -> Result<(), StdError> { + ensure_eq!(true || false, false, StdError::generic_err("foobar")); + Ok(()) + } + + let _err = check().unwrap_err(); + } + + #[test] + fn ensure_ne_works() { + let check = |a, b| { + ensure_ne!(a, b, StdError::generic_err("foobar")); + Ok(()) + }; + + let err = check("123", "123").unwrap_err(); + assert!(matches!(err, StdError::GenericErr { .. })); + check("123", "456").unwrap(); + } + + #[test] + fn ensure_ne_gets_precedence_right() { + // If this was expanded to `true || false == false` we'd get equality. + // It must be expanded to `(true || false) == false` and we expect inequality. + + #[allow(clippy::nonminimal_bool)] + fn check() -> Result<(), StdError> { + ensure_ne!(true || false, false, StdError::generic_err("foobar")); + Ok(()) + } + + check().unwrap(); + } +} diff --git a/cosmwasm/std/src/binary.rs b/cosmwasm/std/src/binary.rs new file mode 100644 index 00000000..322dce7d --- /dev/null +++ b/cosmwasm/std/src/binary.rs @@ -0,0 +1,548 @@ +use alloc::{string::String, vec::Vec}; +use core::{fmt, ops::Deref}; + +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + encoding::{from_base64, to_base64}, + errors::{StdError, StdResult}, +}; + +/// Binary is a wrapper around Vec to add base64 de/serialization +/// with serde. It also adds some helper methods to help encode inline. +/// +/// This is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. +/// See also . +#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Binary(#[schemars(with = "String")] Vec); + +impl Binary { + /// Creates a new `Binary` containing the given data. + pub const fn new(data: Vec) -> Self { + Self(data) + } + + /// take an (untrusted) string and decode it into bytes. + /// fails if it is not valid base64 + pub fn from_base64(encoded: &str) -> StdResult { + from_base64(encoded).map(Self::new) + } + + /// encode to base64 string (guaranteed to be success as we control the data inside). + /// this returns normalized form (with trailing = if needed) + pub fn to_base64(&self) -> String { + to_base64(&self.0) + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } + + /// Copies content into fixed-sized array. + /// + /// # Examples + /// + /// Copy to array of explicit length + /// + /// ``` + /// # use cosmwasm_std::Binary; + /// let binary = Binary::from(&[0xfb, 0x1f, 0x37]); + /// let array: [u8; 3] = binary.to_array().unwrap(); + /// assert_eq!(array, [0xfb, 0x1f, 0x37]); + /// ``` + /// + /// Copy to integer + /// + /// ``` + /// # use cosmwasm_std::Binary; + /// let binary = Binary::from(&[0x8b, 0x67, 0x64, 0x84, 0xb5, 0xfb, 0x1f, 0x37]); + /// let num = u64::from_be_bytes(binary.to_array().unwrap()); + /// assert_eq!(num, 10045108015024774967); + /// ``` + pub fn to_array(&self) -> StdResult<[u8; LENGTH]> { + if self.len() != LENGTH { + return Err(StdError::invalid_data_size(LENGTH, self.len())); + } + + let mut out: [u8; LENGTH] = [0; LENGTH]; + out.copy_from_slice(&self.0); + Ok(out) + } +} + +impl fmt::Display for Binary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_base64()) + } +} + +impl fmt::Debug for Binary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Use an output inspired by tuples (https://doc.rust-lang.org/std/fmt/struct.Formatter.html#method.debug_tuple) + // but with a custom implementation to avoid the need for an intemediate hex string. + write!(f, "Binary(")?; + for byte in self.0.iter() { + write!(f, "{byte:02x}")?; + } + write!(f, ")")?; + Ok(()) + } +} + +/// Just like Vec, Binary is a smart pointer to [u8]. +/// This implements `*binary` for us and allows us to +/// do `&*binary`, returning a `&[u8]` from a `&Binary`. +/// With [deref coercions](https://doc.rust-lang.org/1.22.1/book/first-edition/deref-coercions.html#deref-coercions), +/// this allows us to use `&binary` whenever a `&[u8]` is required. +impl Deref for Binary { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl AsRef<[u8]> for Binary { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +// Slice +impl From<&[u8]> for Binary { + fn from(binary: &[u8]) -> Self { + Self(binary.to_vec()) + } +} + +// Array reference +impl From<&[u8; LENGTH]> for Binary { + fn from(source: &[u8; LENGTH]) -> Self { + Self(source.to_vec()) + } +} + +// Owned array +impl From<[u8; LENGTH]> for Binary { + fn from(source: [u8; LENGTH]) -> Self { + Self(source.into()) + } +} + +impl From> for Binary { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl From for Vec { + fn from(original: Binary) -> Vec { + original.0 + } +} + +/// Implement `encoding::Binary == alloc::vec::Vec` +impl PartialEq> for Binary { + fn eq(&self, rhs: &Vec) -> bool { + // Use Vec == Vec + self.0 == *rhs + } +} + +/// Implement `alloc::vec::Vec == encoding::Binary` +impl PartialEq for Vec { + fn eq(&self, rhs: &Binary) -> bool { + // Use Vec == Vec + *self == rhs.0 + } +} + +/// Implement `Binary == &[u8]` +impl PartialEq<&[u8]> for Binary { + fn eq(&self, rhs: &&[u8]) -> bool { + // Use &[u8] == &[u8] + self.as_slice() == *rhs + } +} + +/// Implement `&[u8] == Binary` +impl PartialEq for &[u8] { + fn eq(&self, rhs: &Binary) -> bool { + // Use &[u8] == &[u8] + *self == rhs.as_slice() + } +} + +/// Implement `Binary == &[u8; LENGTH]` +impl PartialEq<&[u8; LENGTH]> for Binary { + fn eq(&self, rhs: &&[u8; LENGTH]) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `&[u8; LENGTH] == Binary` +impl PartialEq for &[u8; LENGTH] { + fn eq(&self, rhs: &Binary) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `Binary == [u8; LENGTH]` +impl PartialEq<[u8; LENGTH]> for Binary { + fn eq(&self, rhs: &[u8; LENGTH]) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `[u8; LENGTH] == Binary` +impl PartialEq for [u8; LENGTH] { + fn eq(&self, rhs: &Binary) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Serializes as a base64 string +impl Serialize for Binary { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_base64()) + } else { + panic!("Binary is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +/// Deserializes as a base64 string +impl<'de> Deserialize<'de> for Binary { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + deserializer.deserialize_str(Base64Visitor) + } else { + panic!("Binary is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +struct Base64Visitor; + +impl<'de> de::Visitor<'de> for Base64Visitor { + type Value = Binary; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("valid base64 encoded string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match Binary::from_base64(v) { + Ok(binary) => Ok(binary), + Err(_) => Err(E::custom(format_args!("invalid base64: {v}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{assert_hash_works, errors::StdError}; + + #[test] + fn to_array_works() { + // simple + let binary = Binary::from(&[1, 2, 3]); + let array: [u8; 3] = binary.to_array().unwrap(); + assert_eq!(array, [1, 2, 3]); + + // empty + let binary = Binary::from(&[]); + let array: [u8; 0] = binary.to_array().unwrap(); + assert_eq!(array, [] as [u8; 0]); + + // invalid size + let binary = Binary::from(&[1, 2, 3]); + let error = binary.to_array::<8>().unwrap_err(); + match error { + StdError::InvalidDataSize { expected, actual, .. } => { + assert_eq!(expected, 8); + assert_eq!(actual, 3); + }, + err => panic!("Unexpected error: {err:?}"), + } + + // long array (32 bytes) + let binary = Binary::from_base64("t119JOQox4WUQEmO/nyqOZfO+wjJm91YG2sfn4ZglvA=").unwrap(); + let array: [u8; 32] = binary.to_array().unwrap(); + assert_eq!( + array, + [ + 0xb7, 0x5d, 0x7d, 0x24, 0xe4, 0x28, 0xc7, 0x85, 0x94, 0x40, 0x49, 0x8e, 0xfe, 0x7c, + 0xaa, 0x39, 0x97, 0xce, 0xfb, 0x08, 0xc9, 0x9b, 0xdd, 0x58, 0x1b, 0x6b, 0x1f, 0x9f, + 0x86, 0x60, 0x96, 0xf0, + ] + ); + + // very long array > 32 bytes (requires Rust 1.47+) + let binary = + Binary::from_base64("t119JOQox4WUQEmO/nyqOZfO+wjJm91YG2sfn4ZglvBzyMOwMWq+").unwrap(); + let array: [u8; 39] = binary.to_array().unwrap(); + assert_eq!( + array, + [ + 0xb7, 0x5d, 0x7d, 0x24, 0xe4, 0x28, 0xc7, 0x85, 0x94, 0x40, 0x49, 0x8e, 0xfe, 0x7c, + 0xaa, 0x39, 0x97, 0xce, 0xfb, 0x08, 0xc9, 0x9b, 0xdd, 0x58, 0x1b, 0x6b, 0x1f, 0x9f, + 0x86, 0x60, 0x96, 0xf0, 0x73, 0xc8, 0xc3, 0xb0, 0x31, 0x6a, 0xbe, + ] + ); + } + + #[test] + fn test_base64_encoding_success() { + for (value, encoded, encoded_no_pad) in [ + (&b""[..], "", ""), + (&b"hello"[..], "aGVsbG8=", "aGVsbG8"), + (&b"\x0C\xBB\x00\x11\xFA\x01"[..], "DLsAEfoB", "DLsAEfoB"), + (&b"rand"[..], "cmFuZA==", "cmFuZA"), + (&b"rand"[..], "cmFuZA==", "cmFuZA="), + (&b"randomiZ"[..], "cmFuZG9taVo=", "cmFuZG9taVo"), + ] { + let value = Binary::from(value); + assert_eq!(encoded, value.to_base64()); + assert_eq!(Ok(value.clone()), Binary::from_base64(encoded)); + assert_eq!(Ok(value.clone()), Binary::from_base64(encoded_no_pad)); + } + } + + #[test] + fn test_base64_encoding_error() { + for (invalid_base64, want) in + [("cm%uZG9taVo", "Invalid symbol 37, offset 2."), ("cmFuZ", "Invalid input length: 5")] + { + match Binary::from_base64(invalid_base64) { + Err(StdError::InvalidBase64 { msg, .. }) => assert_eq!(want, msg), + result => panic!("Unexpected result: {result:?}"), + } + } + } + + #[test] + fn from_slice_works() { + let original: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + } + + #[test] + fn from_fixed_length_array_works() { + let original = &[]; + let binary: Binary = original.into(); + assert_eq!(binary.len(), 0); + + let original = &[0u8]; + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8]); + + let original = &[0u8, 187, 61, 11, 250, 0]; + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + + let original = &[ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]; + let binary: Binary = original.into(); + assert_eq!( + binary.as_slice(), + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ] + ); + } + + #[test] + fn from_owned_fixed_length_array_works() { + let original = []; + let binary: Binary = original.into(); + assert_eq!(binary.len(), 0); + + let original = [0u8]; + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8]); + + let original = [0u8, 187, 61, 11, 250, 0]; + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + + let original = [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]; + let binary: Binary = original.into(); + assert_eq!( + binary.as_slice(), + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ] + ); + } + + #[test] + fn from_literal_works() { + let a: Binary = b"".into(); + assert_eq!(a.len(), 0); + + let a: Binary = b".".into(); + assert_eq!(a.len(), 1); + + let a: Binary = b"...".into(); + assert_eq!(a.len(), 3); + + let a: Binary = b"...............................".into(); + assert_eq!(a.len(), 31); + + let a: Binary = b"................................".into(); + assert_eq!(a.len(), 32); + + let a: Binary = b".................................".into(); + assert_eq!(a.len(), 33); + } + + #[test] + fn from_vec_works() { + let original = vec![0u8, 187, 61, 11, 250, 0]; + let original_ptr = original.as_ptr(); + let binary: Binary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(binary.0.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn into_vec_works() { + // Into> for Binary + let original = Binary::new(vec![0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.0.as_ptr(); + let vec: Vec = original.into(); + assert_eq!(vec.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(vec.as_ptr(), original_ptr, "vector must not be copied"); + + // From for Vec + let original = Binary::new(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = original.0.as_ptr(); + let vec = Vec::::from(original); + assert_eq!(vec.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(vec.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn serialization_works() { + let binary = Binary::new(vec![0u8, 187, 61, 11, 250, 0]); + + let json = serde_json::to_vec(&binary).unwrap(); + let deserialized: Binary = serde_json::from_slice(&json).unwrap(); + + assert_eq!(binary, deserialized); + } + + #[test] + fn deserialize_from_valid_string() { + let b64_str = "ALs9C/oA"; + // this is the binary behind above string + let expected = vec![0u8, 187, 61, 11, 250, 0]; + + let serialized = serde_json::to_vec(&b64_str).unwrap(); + let deserialized: Binary = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(expected, deserialized.as_slice()); + } + + #[test] + fn deserialize_from_invalid_string() { + let invalid_str = "**BAD!**"; + let serialized = serde_json::to_vec(&invalid_str).unwrap(); + let res = serde_json::from_slice::(&serialized); + assert!(res.is_err()); + } + + #[test] + fn binary_implements_debug() { + // Some data + let binary = Binary::new(vec![0x07, 0x35, 0xAA, 0xcb, 0x00, 0xff]); + assert_eq!(format!("{binary:?}"), "Binary(0735aacb00ff)",); + + // Empty + let binary = Binary::new(vec![]); + assert_eq!(format!("{binary:?}"), "Binary()",); + } + + #[test] + fn binary_implements_deref() { + // Dereference to [u8] + let binary = Binary::new(vec![7u8, 35, 49, 101, 0, 255]); + assert_eq!(*binary, [7u8, 35, 49, 101, 0, 255]); + + // This checks deref coercions from &Binary to &[u8] works + let binary = Binary::new(vec![7u8, 35, 49, 101, 0, 255]); + assert_eq!(binary.len(), 6); + let binary_slice: &[u8] = &binary; + assert_eq!(binary_slice, &[7u8, 35, 49, 101, 0, 255]); + } + + #[test] + fn binary_implements_as_ref() { + let want = &[7u8, 35, 49, 101, 0, 255]; + let data = Binary::new(want.to_vec()); + assert_eq!(want, AsRef::<[u8]>::as_ref(&data)); + assert_eq!(want, AsRef::<[u8]>::as_ref(&&data)); + } + + /// Tests that `Binary` implements `EQ` and `Hash` correctly and thus can be + /// used with hash maps and sets. + #[test] + fn binary_implements_hash_eq() { + let a = Binary::from([0, 187, 61, 11, 250, 0]); + let b = Binary::from([16, 21, 33, 0, 255, 9]); + assert_hash_works!(a, b); + } + + #[test] + fn binary_implements_partial_eq_with_vector() { + let a = Binary::new(vec![5u8; 3]); + let b = vec![5u8; 3]; + let c = vec![9u8; 3]; + assert_eq!(a, b); + assert_eq!(b, a); + assert_ne!(a, c); + assert_ne!(c, a); + } + + #[test] + fn binary_implements_partial_eq_with_slice_and_array() { + let a = Binary::new(vec![0xAA, 0xBB]); + + // Slice: &[u8] + assert_eq!(a, b"\xAA\xBB" as &[u8]); + assert_eq!(b"\xAA\xBB" as &[u8], a); + assert_ne!(a, b"\x11\x22" as &[u8]); + assert_ne!(b"\x11\x22" as &[u8], a); + + // Array reference: &[u8; 2] + assert_eq!(a, b"\xAA\xBB"); + assert_eq!(b"\xAA\xBB", a); + assert_ne!(a, b"\x11\x22"); + assert_ne!(b"\x11\x22", a); + + // Array: [u8; 2] + assert_eq!(a, [0xAA, 0xBB]); + assert_eq!([0xAA, 0xBB], a); + assert_ne!(a, [0x11, 0x22]); + assert_ne!([0x11, 0x22], a); + } +} diff --git a/cosmwasm/std/src/checksum.rs b/cosmwasm/std/src/checksum.rs new file mode 100644 index 00000000..92e7439d --- /dev/null +++ b/cosmwasm/std/src/checksum.rs @@ -0,0 +1,237 @@ +use core::fmt; + +use schemars::JsonSchema; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::{prelude::*, StdError, StdResult}; + +/// A SHA-256 checksum of a Wasm blob, used to identify a Wasm code. +/// This must remain stable since this checksum is stored in the blockchain state. +/// +/// This is often referred to as "code ID" in go-cosmwasm, even if code ID +/// usually refers to an auto-incrementing number. +#[derive(JsonSchema, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Checksum(#[schemars(with = "String")] [u8; 32]); + +impl Checksum { + pub fn generate(wasm: &[u8]) -> Self { + Checksum(Sha256::digest(wasm).into()) + } + + /// Tries to parse the given hex string into a checksum. + /// Errors if the string contains non-hex characters or does not contain 32 bytes. + pub fn from_hex(input: &str) -> StdResult { + let mut binary = [0u8; 32]; + hex::decode_to_slice(input, &mut binary).map_err(StdError::invalid_hex)?; + + Ok(Self(binary)) + } + + /// Creates a lowercase hex encoded copy of this checksum. + /// + /// This takes an owned `self` instead of a reference because `Checksum` is cheap to `Copy`. + pub fn to_hex(self) -> String { + self.to_string() + } + + /// Returns a reference to the inner bytes of this checksum as a slice. + /// If you need a reference to the array, use [`AsRef::as_ref`]. + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for Checksum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for byte in self.0.iter() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl From<[u8; 32]> for Checksum { + fn from(data: [u8; 32]) -> Self { + Checksum(data) + } +} + +impl AsRef<[u8; 32]> for Checksum { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Serializes as a hex string +impl Serialize for Checksum { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_hex()) + } else { + panic!("Checksum is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +/// Deserializes as a hex string +impl<'de> Deserialize<'de> for Checksum { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + deserializer.deserialize_str(ChecksumVisitor) + } else { + panic!("Checksum is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +struct ChecksumVisitor; + +impl<'de> de::Visitor<'de> for ChecksumVisitor { + type Value = Checksum; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("valid hex encoded 32 byte checksum") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match Checksum::from_hex(v) { + Ok(data) => Ok(data), + Err(_) => Err(E::custom(format!("invalid checksum: {v}"))), + } + } +} + +#[derive(Error, Debug)] +#[error("Checksum not of length 32")] +pub struct ChecksumError; + +impl TryFrom<&[u8]> for Checksum { + type Error = ChecksumError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != 32 { + return Err(ChecksumError); + } + let mut data = [0u8; 32]; + data.copy_from_slice(value); + Ok(Checksum(data)) + } +} + +impl From for Vec { + fn from(original: Checksum) -> Vec { + original.0.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::to_json_string; + + #[test] + fn generate_works() { + let wasm = vec![0x68, 0x69, 0x6a]; + let checksum = Checksum::generate(&wasm); + + // echo -n "hij" | sha256sum + let expected = [ + 0x72, 0x2c, 0x8c, 0x99, 0x3f, 0xd7, 0x5a, 0x76, 0x27, 0xd6, 0x9e, 0xd9, 0x41, 0x34, + 0x4f, 0xe2, 0xa1, 0x42, 0x3a, 0x3e, 0x75, 0xef, 0xd3, 0xe6, 0x77, 0x8a, 0x14, 0x28, + 0x84, 0x22, 0x71, 0x04, + ]; + assert_eq!(checksum.0, expected); + } + + #[test] + fn implemented_display() { + let wasm = vec![0x68, 0x69, 0x6a]; + let checksum = Checksum::generate(&wasm); + // echo -n "hij" | sha256sum + let embedded = format!("Check: {checksum}"); + assert_eq!( + embedded, + "Check: 722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104" + ); + assert_eq!( + checksum.to_string(), + "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104" + ); + } + + #[test] + fn from_hex_works() { + // echo -n "hij" | sha256sum + let checksum = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104"; + let parsed = Checksum::from_hex(checksum).unwrap(); + assert_eq!(parsed, Checksum::generate(b"hij")); + // should be inverse of `to_hex` + assert_eq!(parsed.to_hex(), checksum); + + // invalid hex + let too_short = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a1428842271"; + assert!(Checksum::from_hex(too_short).is_err()); + let invalid_char = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a1428842271g4"; + assert!(Checksum::from_hex(invalid_char).is_err()); + let too_long = "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a14288422710400"; + assert!(Checksum::from_hex(too_long).is_err()); + } + + #[test] + fn to_hex_works() { + let wasm = vec![0x68, 0x69, 0x6a]; + let checksum = Checksum::generate(&wasm); + // echo -n "hij" | sha256sum + assert_eq!( + checksum.to_hex(), + "722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104" + ); + } + + #[test] + fn into_vec_works() { + let checksum = Checksum::generate(&[12u8; 17]); + let as_vec: Vec = checksum.into(); + assert_eq!(as_vec, checksum.0); + } + + #[test] + fn ref_conversions_work() { + let checksum = Checksum::generate(&[12u8; 17]); + // as_ref + let _: &[u8; 32] = checksum.as_ref(); + let _: &[u8] = checksum.as_ref(); + // as_slice + let _: &[u8; 32] = checksum.as_ref(); + let _: &[u8] = checksum.as_ref(); + } + + #[test] + fn serde_works() { + // echo -n "hij" | sha256sum + let checksum = + Checksum::from_hex("722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104") + .unwrap(); + + let serialized = to_json_string(&checksum).unwrap(); + assert_eq!( + serialized, + "\"722c8c993fd75a7627d69ed941344fe2a1423a3e75efd3e6778a142884227104\"" + ); + + let deserialized: Checksum = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, checksum); + } +} diff --git a/cosmwasm/std/src/coin.rs b/cosmwasm/std/src/coin.rs new file mode 100644 index 00000000..65b19e4c --- /dev/null +++ b/cosmwasm/std/src/coin.rs @@ -0,0 +1,204 @@ +use core::{fmt, str::FromStr}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, CoinFromStrError, Uint128}; + +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct Coin { + pub denom: String, + pub amount: Uint128, +} + +impl Coin { + pub fn new(amount: impl Into, denom: impl Into) -> Self { + Coin { amount: amount.into(), denom: denom.into() } + } +} + +impl fmt::Debug for Coin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Coin {{ {} \"{}\" }}", self.amount, self.denom) + } +} + +impl FromStr for Coin { + type Err = CoinFromStrError; + + fn from_str(s: &str) -> Result { + let pos = s.find(|c: char| !c.is_ascii_digit()).ok_or(CoinFromStrError::MissingDenom)?; + let (amount, denom) = s.split_at(pos); + + if amount.is_empty() { + return Err(CoinFromStrError::MissingAmount); + } + + Ok(Coin { amount: amount.parse::()?.into(), denom: denom.to_string() }) + } +} + +impl fmt::Display for Coin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // We use the formatting without a space between amount and denom, + // which is common in the Cosmos SDK ecosystem: + // https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/types/coin.go#L643-L645 + // For communication to end users, Coin needs to transformed anways (e.g. convert integer + // uatom to decimal ATOM). + write!(f, "{}{}", self.amount, self.denom) + } +} + +/// A shortcut constructor for a set of one denomination of coins +/// +/// # Examples +/// +/// ``` +/// # use cosmwasm_std::{coins, BankMsg, CosmosMsg, Response, SubMsg}; +/// # use cosmwasm_std::testing::mock_env; +/// # let env = mock_env(); +/// # let recipient = "blub".to_string(); +/// let tip = coins(123, "ucosm"); +/// +/// let mut response: Response = Default::default(); +/// response.messages = vec![SubMsg::new(BankMsg::Send { +/// to_address: recipient, +/// amount: tip, +/// })]; +/// ``` +pub fn coins(amount: u128, denom: impl Into) -> Vec { + vec![coin(amount, denom)] +} + +/// A shorthand constructor for Coin +/// +/// # Examples +/// +/// ``` +/// # use cosmwasm_std::{coin, BankMsg, CosmosMsg, Response, SubMsg}; +/// # use cosmwasm_std::testing::{mock_env, mock_info}; +/// # let env = mock_env(); +/// # let recipient = "blub".to_string(); +/// let tip = vec![ +/// coin(123, "ucosm"), +/// coin(24, "ustake"), +/// ]; +/// +/// let mut response: Response = Default::default(); +/// response.messages = vec![SubMsg::new(BankMsg::Send { +/// to_address: recipient, +/// amount: tip, +/// })]; +/// ``` +pub fn coin(amount: u128, denom: impl Into) -> Coin { + Coin::new(amount, denom) +} + +/// has_coins returns true if the list of coins has at least the required amount +pub fn has_coins(coins: &[Coin], required: &Coin) -> bool { + coins + .iter() + .find(|c| c.denom == required.denom) + .map(|m| m.amount >= required.amount) + .unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn coin_implements_display() { + let a = Coin { amount: Uint128::new(123), denom: "ucosm".to_string() }; + + let embedded = format!("Amount: {a}"); + assert_eq!(embedded, "Amount: 123ucosm"); + assert_eq!(a.to_string(), "123ucosm"); + } + + #[test] + fn coin_works() { + let a = coin(123, "ucosm"); + assert_eq!(a, Coin { amount: Uint128::new(123), denom: "ucosm".to_string() }); + + let zero = coin(0, "ucosm"); + assert_eq!(zero, Coin { amount: Uint128::new(0), denom: "ucosm".to_string() }); + + let string_denom = coin(42, String::from("ucosm")); + assert_eq!(string_denom, Coin { amount: Uint128::new(42), denom: "ucosm".to_string() }); + } + + #[test] + fn coins_works() { + let a = coins(123, "ucosm"); + assert_eq!(a, vec![Coin { amount: Uint128::new(123), denom: "ucosm".to_string() }]); + + let zero = coins(0, "ucosm"); + assert_eq!(zero, vec![Coin { amount: Uint128::new(0), denom: "ucosm".to_string() }]); + + let string_denom = coins(42, String::from("ucosm")); + assert_eq!( + string_denom, + vec![Coin { amount: Uint128::new(42), denom: "ucosm".to_string() }] + ); + } + + #[test] + fn has_coins_matches() { + let wallet = vec![coin(12345, "ETH"), coin(555, "BTC")]; + + // less than same type + assert!(has_coins(&wallet, &coin(777, "ETH"))); + } + + #[test] + fn parse_coin() { + let expected = Coin::new(123u128, "ucosm"); + assert_eq!("123ucosm".parse::().unwrap(), expected); + // leading zeroes should be ignored + assert_eq!("00123ucosm".parse::().unwrap(), expected); + // 0 amount parses correctly + assert_eq!("0ucosm".parse::().unwrap(), Coin::new(0u128, "ucosm")); + // ibc denom should work + let ibc_str = "11111ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"; + let ibc_coin = Coin::new( + 11111u128, + "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", + ); + assert_eq!(ibc_str.parse::().unwrap(), ibc_coin); + + // error cases + assert_eq!(Coin::from_str("123").unwrap_err(), CoinFromStrError::MissingDenom); + assert_eq!( + Coin::from_str("ucosm").unwrap_err(), // no amount + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("-123ucosm").unwrap_err(), // negative amount + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("").unwrap_err(), // empty input + CoinFromStrError::MissingDenom + ); + assert_eq!( + Coin::from_str(" 1ucosm").unwrap_err(), // unsupported whitespace + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("�1ucosm").unwrap_err(), // other broken data + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("340282366920938463463374607431768211456ucosm") + .unwrap_err() + .to_string(), + "Invalid amount: number too large to fit in target type" + ); + } + + #[test] + fn debug_coin() { + let coin = Coin::new(123u128, "ucosm"); + assert_eq!(format!("{coin:?}"), r#"Coin { 123 "ucosm" }"#); + } +} diff --git a/cosmwasm/std/src/coins.rs b/cosmwasm/std/src/coins.rs new file mode 100644 index 00000000..a539d781 --- /dev/null +++ b/cosmwasm/std/src/coins.rs @@ -0,0 +1,514 @@ +use alloc::collections::BTreeMap; +use core::{fmt, str::FromStr}; + +use crate::{ + prelude::*, Coin, CoinsError, OverflowError, OverflowOperation, StdError, StdResult, Uint128, +}; + +/// A collection of coins, similar to Cosmos SDK's `sdk.Coins` struct. +/// +/// Differently from `sdk.Coins`, which is a vector of `sdk.Coin`, here we +/// implement Coins as a BTreeMap that maps from coin denoms to `Coin`. +/// This has a number of advantages: +/// +/// - coins are naturally sorted alphabetically by denom +/// - duplicate denoms are automatically removed +/// - cheaper for searching/inserting/deleting: O(log(n)) compared to O(n) +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Coins(BTreeMap); + +/// Casting a Vec to Coins. +/// The Vec can be out of order, but must not contain duplicate denoms. +/// If you want to sum up duplicates, create an empty instance using `Coins::default` and +/// use `Coins::add` to add your coins. +impl TryFrom> for Coins { + type Error = CoinsError; + + fn try_from(vec: Vec) -> Result { + let mut map = BTreeMap::new(); + for coin in vec { + if coin.amount.is_zero() { + continue; + } + + // if the insertion returns a previous value, we have a duplicate denom + if map.insert(coin.denom.clone(), coin).is_some() { + return Err(CoinsError::DuplicateDenom); + } + } + + Ok(Self(map)) + } +} + +impl TryFrom<&[Coin]> for Coins { + type Error = CoinsError; + + fn try_from(slice: &[Coin]) -> Result { + slice.to_vec().try_into() + } +} + +impl From for Coins { + fn from(value: Coin) -> Self { + let mut coins = Coins::default(); + // this can never overflow (because there are no coins in there yet), so we can unwrap + coins.add(value).unwrap(); + coins + } +} + +impl TryFrom<[Coin; N]> for Coins { + type Error = CoinsError; + + fn try_from(slice: [Coin; N]) -> Result { + slice.to_vec().try_into() + } +} + +impl From for Vec { + fn from(value: Coins) -> Self { + value.into_vec() + } +} + +impl FromStr for Coins { + type Err = StdError; + + fn from_str(s: &str) -> StdResult { + if s.is_empty() { + return Ok(Self::default()); + } + + Ok(s.split(',').map(Coin::from_str).collect::, _>>()?.try_into()?) + } +} + +impl fmt::Display for Coins { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = self.0.values().map(|coin| coin.to_string()).collect::>().join(","); + write!(f, "{s}") + } +} + +impl Coins { + /// Conversion to Vec, while NOT consuming the original object. + /// + /// This produces a vector of coins that is sorted alphabetically by denom with + /// no duplicate denoms. + pub fn to_vec(&self) -> Vec { + self.0.values().cloned().collect() + } + + /// Conversion to Vec, consuming the original object. + /// + /// This produces a vector of coins that is sorted alphabetically by denom with + /// no duplicate denoms. + pub fn into_vec(self) -> Vec { + self.0.into_values().collect() + } + + /// Returns the number of different denoms in this collection. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if this collection contains no coins. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the denoms as a vector of strings. + /// The vector is guaranteed to not contain duplicates and sorted alphabetically. + pub fn denoms(&self) -> Vec { + self.0.keys().cloned().collect() + } + + /// Returns the amount of the given denom or zero if the denom is not present. + pub fn amount_of(&self, denom: &str) -> Uint128 { + self.0.get(denom).map(|c| c.amount).unwrap_or_else(Uint128::zero) + } + + /// Returns the amount of the given denom if and only if this collection contains only + /// the given denom. Otherwise `None` is returned. + /// + /// # Examples + /// + /// ```rust + /// use cosmwasm_std::{Coin, Coins, coin}; + /// + /// let coins: Coins = [coin(100, "uatom")].try_into().unwrap(); + /// assert_eq!(coins.contains_only("uatom").unwrap().u128(), 100); + /// assert_eq!(coins.contains_only("uluna"), None); + /// ``` + /// + /// ```rust + /// use cosmwasm_std::{Coin, Coins, coin}; + /// + /// let coins: Coins = [coin(100, "uatom"), coin(200, "uusd")].try_into().unwrap(); + /// assert_eq!(coins.contains_only("uatom"), None); + /// ``` + pub fn contains_only(&self, denom: &str) -> Option { + if self.len() == 1 { + self.0.get(denom).map(|c| c.amount) + } else { + None + } + } + + /// Adds the given coin to this `Coins` instance. + /// Errors in case of overflow. + pub fn add(&mut self, coin: Coin) -> StdResult<()> { + if coin.amount.is_zero() { + return Ok(()); + } + + // if the coin is not present yet, insert it, otherwise add to existing amount + match self.0.get_mut(&coin.denom) { + None => { + self.0.insert(coin.denom.clone(), coin); + }, + Some(existing) => { + existing.amount = existing.amount.checked_add(coin.amount)?; + }, + } + Ok(()) + } + + /// Subtracts the given coin from this `Coins` instance. + /// Errors in case of overflow or if the denom is not present. + pub fn sub(&mut self, coin: Coin) -> StdResult<()> { + match self.0.get_mut(&coin.denom) { + Some(existing) => { + existing.amount = existing.amount.checked_sub(coin.amount)?; + // make sure to remove zero coin + if existing.amount.is_zero() { + self.0.remove(&coin.denom); + } + }, + None => { + // ignore zero subtraction + if coin.amount.is_zero() { + return Ok(()); + } + return Err(OverflowError::new(OverflowOperation::Sub).into()); + }, + } + + Ok(()) + } + + /// Returns an iterator over the coins. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{coin, Coin, Coins, Uint128}; + /// let mut coins = Coins::default(); + /// coins.add(coin(500, "uluna")).unwrap(); + /// coins.add(coin(1000, "uatom")).unwrap(); + /// let mut iterator = coins.iter(); + /// + /// let uatom = iterator.next().unwrap(); + /// assert_eq!(uatom.denom, "uatom"); + /// assert_eq!(uatom.amount.u128(), 1000); + /// + /// let uluna = iterator.next().unwrap(); + /// assert_eq!(uluna.denom, "uluna"); + /// assert_eq!(uluna.amount.u128(), 500); + /// + /// assert_eq!(iterator.next(), None); + /// ``` + pub fn iter(&self) -> CoinsIter<'_> { + CoinsIter(self.0.iter()) + } +} + +impl IntoIterator for Coins { + type Item = Coin; + type IntoIter = CoinsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + CoinsIntoIter(self.0.into_iter()) + } +} + +impl<'a> IntoIterator for &'a Coins { + type Item = &'a Coin; + type IntoIter = CoinsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +#[derive(Debug)] +pub struct CoinsIntoIter(alloc::collections::btree_map::IntoIter); + +impl Iterator for CoinsIntoIter { + type Item = Coin; + + fn next(&mut self) -> Option { + self.0.next().map(|(_, coin)| coin) + } + + fn size_hint(&self) -> (usize, Option) { + // Since btree_map::IntoIter implements ExactSizeIterator, this is guaranteed to return the + // exact length + self.0.size_hint() + } +} + +impl DoubleEndedIterator for CoinsIntoIter { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, coin)| coin) + } +} + +impl ExactSizeIterator for CoinsIntoIter { + fn len(&self) -> usize { + self.0.len() + } +} + +#[derive(Debug)] +pub struct CoinsIter<'a>(alloc::collections::btree_map::Iter<'a, String, Coin>); + +impl<'a> Iterator for CoinsIter<'a> { + type Item = &'a Coin; + + fn next(&mut self) -> Option { + self.0.next().map(|(_, coin)| coin) + } + + fn size_hint(&self) -> (usize, Option) { + // Since btree_map::Iter implements ExactSizeIterator, this is guaranteed to return the + // exact length + self.0.size_hint() + } +} + +impl<'a> DoubleEndedIterator for CoinsIter<'a> { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, coin)| coin) + } +} + +impl<'a> ExactSizeIterator for CoinsIter<'a> { + fn len(&self) -> usize { + self.0.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::coin; + + /// Sort a Vec by denom alphabetically + fn sort_by_denom(vec: &mut [Coin]) { + vec.sort_by(|a, b| a.denom.cmp(&b.denom)); + } + + /// Returns a mockup Vec. In this example, the coins are not in order + fn mock_vec() -> Vec { + vec![ + coin(12345, "uatom"), + coin(69420, "ibc/1234ABCD"), + coin(88888, "factory/osmo1234abcd/subdenom"), + ] + } + + /// Return a mockup Coins that contains the same coins as in `mock_vec` + fn mock_coins() -> Coins { + let mut coins = Coins::default(); + for coin in mock_vec() { + coins.add(coin).unwrap(); + } + coins + } + + #[test] + fn converting_vec() { + let mut vec = mock_vec(); + let coins = mock_coins(); + + // &[Coin] --> Coins + assert_eq!(Coins::try_from(vec.as_slice()).unwrap(), coins); + // Vec --> Coins + assert_eq!(Coins::try_from(vec.clone()).unwrap(), coins); + + sort_by_denom(&mut vec); + + // &Coins --> Vec + // NOTE: the returned vec should be sorted + assert_eq!(coins.to_vec(), vec); + // Coins --> Vec + // NOTE: the returned vec should be sorted + assert_eq!(coins.into_vec(), vec); + } + + #[test] + fn converting_str() { + // not in order + let s1 = "88888factory/osmo1234abcd/subdenom,12345uatom,69420ibc/1234ABCD"; + // in order + let s2 = "88888factory/osmo1234abcd/subdenom,69420ibc/1234ABCD,12345uatom"; + + let invalid = "12345uatom,noamount"; + + let coins = mock_coins(); + + // &str --> Coins + // NOTE: should generate the same Coins, regardless of input order + assert_eq!(Coins::from_str(s1).unwrap(), coins); + assert_eq!(Coins::from_str(s2).unwrap(), coins); + assert_eq!(Coins::from_str("").unwrap(), Coins::default()); + + // Coins --> String + // NOTE: the generated string should be sorted + assert_eq!(coins.to_string(), s2); + assert_eq!(Coins::default().to_string(), ""); + assert_eq!( + Coins::from_str(invalid).unwrap_err().to_string(), + "Generic error: Parsing Coin: Missing amount or non-digit characters in amount" + ); + } + + #[test] + fn handling_duplicates() { + // create a Vec that contains duplicate denoms + let mut vec = mock_vec(); + vec.push(coin(67890, "uatom")); + + let err = Coins::try_from(vec).unwrap_err(); + assert_eq!(err, CoinsError::DuplicateDenom); + } + + #[test] + fn handling_zero_amount() { + // create a Vec that contains zero amounts + let mut vec = mock_vec(); + vec[0].amount = Uint128::zero(); + + let coins = Coins::try_from(vec).unwrap(); + assert_eq!(coins.len(), 2); + assert_ne!(coins.amount_of("ibc/1234ABCD"), Uint128::zero()); + assert_ne!(coins.amount_of("factory/osmo1234abcd/subdenom"), Uint128::zero()); + + // adding a coin with zero amount should not be added + let mut coins = Coins::default(); + coins.add(coin(0, "uusd")).unwrap(); + assert!(coins.is_empty()); + } + + #[test] + fn length() { + let coins = Coins::default(); + assert_eq!(coins.len(), 0); + assert!(coins.is_empty()); + + let coins = mock_coins(); + assert_eq!(coins.len(), 3); + assert!(!coins.is_empty()); + } + + #[test] + fn add_coin() { + let mut coins = mock_coins(); + + // existing denom + coins.add(coin(12345, "uatom")).unwrap(); + assert_eq!(coins.len(), 3); + assert_eq!(coins.amount_of("uatom").u128(), 24690); + + // new denom + coins.add(coin(123, "uusd")).unwrap(); + assert_eq!(coins.len(), 4); + + // zero amount + coins.add(coin(0, "uusd")).unwrap(); + assert_eq!(coins.amount_of("uusd").u128(), 123); + + // zero amount, new denom + coins.add(coin(0, "utest")).unwrap(); + assert_eq!(coins.len(), 4); + } + + #[test] + fn sub_coins() { + let mut coins: Coins = coin(12345, "uatom").into(); + + // sub more than available + let err = coins.sub(coin(12346, "uatom")).unwrap_err(); + assert!(matches!(err, StdError::Overflow { .. })); + + // sub non-existent denom + let err = coins.sub(coin(12345, "uusd")).unwrap_err(); + assert!(matches!(err, StdError::Overflow { .. })); + + // partial sub + coins.sub(coin(1, "uatom")).unwrap(); + assert_eq!(coins.len(), 1); + assert_eq!(coins.amount_of("uatom").u128(), 12344); + + // full sub + coins.sub(coin(12344, "uatom")).unwrap(); + assert!(coins.is_empty()); + + // sub zero, existing denom + coins.sub(coin(0, "uusd")).unwrap(); + assert!(coins.is_empty()); + let mut coins: Coins = coin(12345, "uatom").into(); + + // sub zero, non-existent denom + coins.sub(coin(0, "uatom")).unwrap(); + assert_eq!(coins.len(), 1); + assert_eq!(coins.amount_of("uatom").u128(), 12345); + } + + #[test] + fn coin_to_coins() { + // zero coin results in empty collection + let coins: Coins = coin(0, "uusd").into(); + assert!(coins.is_empty()); + + // happy path + let coins = Coins::from(coin(12345, "uatom")); + assert_eq!(coins.len(), 1); + assert_eq!(coins.amount_of("uatom").u128(), 12345); + } + + #[test] + fn exact_size_iterator() { + let coins = mock_coins(); + let iter = coins.iter(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + + let iter = coins.into_iter(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + } + + #[test] + fn can_iterate_owned() { + let coins = mock_coins(); + let mut moved = Coins::default(); + for c in coins { + moved.add(c).unwrap(); + } + assert_eq!(moved.len(), 3); + + assert!(mock_coins().into_iter().eq(mock_coins().to_vec())); + } + + #[test] + fn can_iterate_borrowed() { + let coins = mock_coins(); + assert!(coins.iter().map(|c| &c.denom).eq(coins.to_vec().iter().map(|c| &c.denom))); + + // can still use the coins afterwards + assert_eq!(coins.amount_of("uatom").u128(), 12345); + } +} diff --git a/cosmwasm/std/src/conversion.rs b/cosmwasm/std/src/conversion.rs new file mode 100644 index 00000000..3e75386d --- /dev/null +++ b/cosmwasm/std/src/conversion.rs @@ -0,0 +1,17 @@ +/// Converts an input of type usize to u32. +/// +/// On 32 bit platforms such as wasm32 this is just a safe cast. +/// On other platforms the conversion panics for values larger than +/// `u32::MAX`. +#[inline] +pub fn force_to_u32(input: usize) -> u32 { + #[cfg(target_pointer_width = "32")] + { + // usize = u32 on this architecture + input as u32 + } + #[cfg(not(target_pointer_width = "32"))] + { + input.try_into().expect("Input exceeds u32 range") + } +} diff --git a/cosmwasm/std/src/deps.rs b/cosmwasm/std/src/deps.rs new file mode 100644 index 00000000..d879c8d4 --- /dev/null +++ b/cosmwasm/std/src/deps.rs @@ -0,0 +1,151 @@ +use core::marker::PhantomData; + +use crate::{ + query::CustomQuery, + results::Empty, + traits::{Api, Querier, Storage}, + QuerierWrapper, +}; + +/// Holds all external dependencies of the contract. +/// Designed to allow easy dependency injection at runtime. +/// This cannot be copied or cloned since it would behave differently +/// for mock storages and a bridge storage in the VM. +pub struct OwnedDeps { + pub storage: S, + pub api: A, + pub querier: Q, + pub custom_query_type: PhantomData, +} + +pub struct DepsMut<'a, C: CustomQuery = Empty> { + pub storage: &'a mut dyn Storage, + pub api: &'a dyn Api, + pub querier: QuerierWrapper<'a, C>, +} + +#[derive(Clone)] +pub struct Deps<'a, C: CustomQuery = Empty> { + pub storage: &'a dyn Storage, + pub api: &'a dyn Api, + pub querier: QuerierWrapper<'a, C>, +} + +// Use custom implementation on order to implement Copy in case `C` is not `Copy`. +// See "There is a small difference between the two: the derive strategy will also +// place a Copy bound on type parameters, which isn’t always desired." +// https://doc.rust-lang.org/std/marker/trait.Copy.html +impl<'a, C: CustomQuery> Copy for Deps<'a, C> {} + +impl OwnedDeps { + pub fn as_ref(&'_ self) -> Deps<'_, C> { + Deps { storage: &self.storage, api: &self.api, querier: QuerierWrapper::new(&self.querier) } + } + + pub fn as_mut(&'_ mut self) -> DepsMut<'_, C> { + DepsMut { + storage: &mut self.storage, + api: &self.api, + querier: QuerierWrapper::new(&self.querier), + } + } +} + +impl<'a, C: CustomQuery> DepsMut<'a, C> { + pub fn as_ref(&'_ self) -> Deps<'_, C> { + Deps { storage: self.storage, api: self.api, querier: self.querier } + } + + pub fn branch(&'_ mut self) -> DepsMut<'_, C> { + DepsMut { storage: self.storage, api: self.api, querier: self.querier } + } + + /// This allows to convert any `DepsMut` into one generic over `Empty` custom + /// query type. + pub fn into_empty(self) -> DepsMut<'a, Empty> { + DepsMut { storage: self.storage, api: self.api, querier: self.querier.into_empty() } + } +} + +impl<'a, C: CustomQuery> Deps<'a, C> { + /// This allows to convert any `Deps` into one generic over `Empty` custom + /// query type. + pub fn into_empty(self) -> Deps<'a, Empty> { + Deps { storage: self.storage, api: self.api, querier: self.querier.into_empty() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{mock_dependencies, MockApi, MockQuerier, MockStorage}; + use serde::{Deserialize, Serialize}; + + // ensure we can call these many times, eg. as sub-calls + fn execute(mut deps: DepsMut) { + execute2(deps.branch()); + query(deps.as_ref()); + execute2(deps.branch()); + } + fn execute2(_deps: DepsMut) {} + + fn query(deps: Deps) { + query2(deps); + query2(deps); + } + fn query2(_deps: Deps) {} + + #[test] + fn ensure_easy_reuse() { + let mut deps = mock_dependencies(); + execute(deps.as_mut()); + query(deps.as_ref()) + } + + #[derive(Clone, Serialize, Deserialize)] + struct MyQuery; + impl CustomQuery for MyQuery {} + + #[test] + fn deps_implements_copy() { + impl CustomQuery for u64 {} + + // With C: Copy + let owned = OwnedDeps::<_, _, _, u64> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; + let deps: Deps = owned.as_ref(); + let _copy1 = deps; + let _copy2 = deps; + + // Without C: Copy + let owned = OwnedDeps::<_, _, _, MyQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; + let deps: Deps = owned.as_ref(); + let _copy1 = deps; + let _copy2 = deps; + } + + #[test] + fn deps_to_empty() { + let mut owned = OwnedDeps::<_, _, _, MyQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; + + let deps_mut: DepsMut = owned.as_mut(); + let _: DepsMut = deps_mut.into_empty(); + + let deps: Deps = owned.as_ref(); + let _: Deps = deps.into_empty(); + } +} diff --git a/cosmwasm/std/src/encoding.rs b/cosmwasm/std/src/encoding.rs new file mode 100644 index 00000000..5edf812d --- /dev/null +++ b/cosmwasm/std/src/encoding.rs @@ -0,0 +1,94 @@ +use alloc::{string::String, vec::Vec}; +use base64::{engine::GeneralPurpose, Engine}; + +use crate::{StdError, StdResult}; + +/// Base64 encoding engine used in conversion to/from base64. +/// +/// The engine adds padding when encoding and accepts strings with or +/// without padding when decoding. +const B64_ENGINE: GeneralPurpose = GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new() + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), +); + +/// Deserialize a bag of bytes from Base64 into a vector of bytes +pub fn from_base64(input: I) -> StdResult> +where + I: AsRef<[u8]>, +{ + B64_ENGINE.decode(input).map_err(StdError::invalid_base64) +} + +/// Encode a bag of bytes into the Base64 format +pub fn to_base64(input: I) -> String +where + I: AsRef<[u8]>, +{ + B64_ENGINE.encode(input) +} + +/// Decode a bag of bytes from hex into a vector of bytes +pub fn from_hex(input: I) -> StdResult> +where + I: AsRef<[u8]>, +{ + hex::decode(input).map_err(StdError::invalid_hex) +} + +/// Encode a bag of bytes into the hex format +pub fn to_hex(input: I) -> String +where + I: AsRef<[u8]>, +{ + hex::encode(input) +} + +#[cfg(test)] +mod test { + use crate::{from_base64, from_hex, to_base64, to_hex}; + + const BASE64_FOOBAR: &str = "Zm9vYmFy"; // utf-8 encoded "foobar" + const HEX_FOOBAR: &str = "666f6f626172"; // utf-8 encoded "foobar" + + #[test] + fn from_base64_works() { + let decoded = from_base64(BASE64_FOOBAR).unwrap(); + assert_eq!(decoded, b"foobar"); + } + + #[test] + fn to_base64_works() { + let encoded = to_base64("foobar"); + assert_eq!(encoded, BASE64_FOOBAR); + } + + #[test] + fn base64_roundtrip_works() { + let decoded = from_base64(BASE64_FOOBAR).unwrap(); + assert_eq!(decoded, b"foobar"); + let encoded = to_base64(decoded); + assert_eq!(encoded, BASE64_FOOBAR); + } + + #[test] + fn from_hex_works() { + let decoded = from_hex(HEX_FOOBAR).unwrap(); + assert_eq!(decoded, b"foobar"); + } + + #[test] + fn to_hex_works() { + let encoded = to_hex("foobar"); + assert_eq!(encoded, HEX_FOOBAR); + } + + #[test] + fn hex_roundtrip_works() { + let decoded = from_hex(HEX_FOOBAR).unwrap(); + assert_eq!(decoded, b"foobar"); + let encoded = to_hex(decoded); + assert_eq!(encoded, HEX_FOOBAR); + } +} diff --git a/cosmwasm/std/src/errors/backtrace.rs b/cosmwasm/std/src/errors/backtrace.rs new file mode 100644 index 00000000..ad7827cf --- /dev/null +++ b/cosmwasm/std/src/errors/backtrace.rs @@ -0,0 +1,89 @@ +use alloc::boxed::Box; +use core::fmt::{Debug, Display, Formatter, Result}; + +/// This wraps an actual backtrace to achieve two things: +/// - being able to fill this with a stub implementation in `no_std` environments +/// - being able to use this in conjunction with [`thiserror::Error`] +pub struct BT(Box); + +impl BT { + #[track_caller] + pub fn capture() -> Self { + // in case of no_std, we can fill with a stub here + #[cfg(feature = "std")] + { + #[cfg(target_arch = "wasm32")] + return BT(Box::new(std::backtrace::Backtrace::disabled())); + #[cfg(not(target_arch = "wasm32"))] + return BT(Box::new(std::backtrace::Backtrace::capture())); + } + #[cfg(not(feature = "std"))] + { + BT(Box::new(Stub)) + } + } +} + +trait Printable: Debug + Display {} +impl Printable for T where T: Debug + Display {} + +impl Debug for BT { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for BT { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + Display::fmt(&self.0, f) + } +} + +#[allow(unused)] +struct Stub; + +impl Debug for Stub { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "") + } +} + +impl Display for Stub { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "") + } +} + +/// This macro implements `From` for a given error type to a given error type where +/// the target error has a `backtrace` field. +/// This is meant as a replacement for `thiserror`'s `#[from]` attribute, which does not +/// work with our custom backtrace wrapper. +macro_rules! impl_from_err { + ($from:ty, $to:ty, $map:path) => { + impl From<$from> for $to { + fn from(err: $from) -> Self { + $map { source: err, backtrace: $crate::errors::backtrace::BT::capture() } + } + } + }; +} +pub(crate) use impl_from_err; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bt_works_without_std() { + #[derive(Debug)] + struct BacktraceStub; + + impl Display for BacktraceStub { + fn fmt(&self, _f: &mut Formatter<'_>) -> Result { + Ok(()) + } + } + + _ = BT(Box::new(BacktraceStub)); + } +} diff --git a/cosmwasm/std/src/errors/mod.rs b/cosmwasm/std/src/errors/mod.rs new file mode 100644 index 00000000..c1623899 --- /dev/null +++ b/cosmwasm/std/src/errors/mod.rs @@ -0,0 +1,16 @@ +mod backtrace; +mod recover_pubkey_error; +mod std_error; +mod system_error; +mod verification_error; + +pub(crate) use backtrace::{impl_from_err, BT}; +pub use recover_pubkey_error::RecoverPubkeyError; +pub use std_error::{ + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, + CoinFromStrError, CoinsError, ConversionOverflowError, DivideByZeroError, DivisionError, + OverflowError, OverflowOperation, RoundDownOverflowError, RoundUpOverflowError, StdError, + StdResult, +}; +pub use system_error::SystemError; +pub use verification_error::{AggregationError, PairingEqualityError, VerificationError}; diff --git a/cosmwasm/std/src/errors/recover_pubkey_error.rs b/cosmwasm/std/src/errors/recover_pubkey_error.rs new file mode 100644 index 00000000..1b2d2782 --- /dev/null +++ b/cosmwasm/std/src/errors/recover_pubkey_error.rs @@ -0,0 +1,64 @@ +use core::fmt::Debug; +#[cfg(not(target_arch = "wasm32"))] +use cosmwasm_crypto::CryptoError; + +use super::BT; + +#[derive(Debug, thiserror::Error)] +pub enum RecoverPubkeyError { + #[error("Invalid hash format")] + InvalidHashFormat, + #[error("Invalid signature format")] + InvalidSignatureFormat, + #[error("Invalid recovery parameter. Supported values: 0 and 1.")] + InvalidRecoveryParam, + #[error("Unknown error: {error_code}")] + UnknownErr { error_code: u32, backtrace: BT }, +} + +impl RecoverPubkeyError { + pub fn unknown_err(error_code: u32) -> Self { + RecoverPubkeyError::UnknownErr { error_code, backtrace: BT::capture() } + } +} + +impl PartialEq for RecoverPubkeyError { + fn eq(&self, rhs: &RecoverPubkeyError) -> bool { + match self { + RecoverPubkeyError::InvalidHashFormat => { + matches!(rhs, RecoverPubkeyError::InvalidHashFormat) + }, + RecoverPubkeyError::InvalidSignatureFormat => { + matches!(rhs, RecoverPubkeyError::InvalidSignatureFormat) + }, + RecoverPubkeyError::InvalidRecoveryParam => { + matches!(rhs, RecoverPubkeyError::InvalidRecoveryParam) + }, + RecoverPubkeyError::UnknownErr { error_code, .. } => + if let RecoverPubkeyError::UnknownErr { error_code: rhs_error_code, .. } = rhs { + error_code == rhs_error_code + } else { + false + }, + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for RecoverPubkeyError { + fn from(original: CryptoError) -> Self { + match original { + CryptoError::InvalidHashFormat { .. } => RecoverPubkeyError::InvalidHashFormat, + CryptoError::InvalidSignatureFormat { .. } => + RecoverPubkeyError::InvalidSignatureFormat, + CryptoError::GenericErr { .. } => RecoverPubkeyError::unknown_err(original.code()), + CryptoError::InvalidRecoveryParam { .. } => RecoverPubkeyError::InvalidRecoveryParam, + CryptoError::Aggregation { .. } | + CryptoError::PairingEquality { .. } | + CryptoError::BatchErr { .. } | + CryptoError::InvalidPubkeyFormat { .. } | + CryptoError::InvalidPoint { .. } | + CryptoError::UnknownHashFunction { .. } => panic!("Conversion not supported"), + } + } +} diff --git a/cosmwasm/std/src/errors/std_error.rs b/cosmwasm/std/src/errors/std_error.rs new file mode 100644 index 00000000..a4b578dc --- /dev/null +++ b/cosmwasm/std/src/errors/std_error.rs @@ -0,0 +1,639 @@ +use alloc::string::{String, ToString}; +use core::fmt; + +use super::{impl_from_err, BT}; + +use crate::errors::{RecoverPubkeyError, VerificationError}; + +/// Structured error type for init, execute and query. +/// +/// This can be serialized and passed over the Wasm/VM boundary, which allows us to use structured +/// error types in e.g. integration tests. In that process backtraces are stripped off. +/// +/// The prefix "Std" means "the standard error within the standard library". This is not the only +/// result/error type in cosmwasm-std. +/// +/// When new cases are added, they should describe the problem rather than what was attempted (e.g. +/// InvalidBase64 is preferred over Base64DecodingErr). In the long run this allows us to get rid of +/// the duplication in "StdError::FooErr". +/// +/// Checklist for adding a new error: +/// - Add enum case +/// - Add creator function in std_error_helpers.rs +#[derive(Debug, thiserror::Error)] +pub enum StdError { + #[error("Verification error: {source}")] + VerificationErr { source: VerificationError, backtrace: BT }, + #[error("Recover pubkey error: {source}")] + RecoverPubkeyErr { source: RecoverPubkeyError, backtrace: BT }, + /// Whenever there is no specific error type available + #[error("Generic error: {msg}")] + GenericErr { msg: String, backtrace: BT }, + #[error("Invalid Base64 string: {msg}")] + InvalidBase64 { msg: String, backtrace: BT }, + #[error("Invalid data size: expected={expected} actual={actual}")] + InvalidDataSize { expected: u64, actual: u64, backtrace: BT }, + #[error("Invalid hex string: {msg}")] + InvalidHex { msg: String, backtrace: BT }, + /// Whenever UTF-8 bytes cannot be decoded into a unicode string, e.g. in String::from_utf8 or + /// str::from_utf8. + #[error("Cannot decode UTF8 bytes into string: {msg}")] + InvalidUtf8 { msg: String, backtrace: BT }, + #[error("{kind} not found")] + NotFound { kind: String, backtrace: BT }, + #[error("Error parsing into type {target_type}: {msg}")] + ParseErr { + /// the target type that was attempted + target_type: String, + msg: String, + backtrace: BT, + }, + #[error("Error serializing type {source_type}: {msg}")] + SerializeErr { + /// the source type that was attempted + source_type: String, + msg: String, + backtrace: BT, + }, + #[error("Overflow: {source}")] + Overflow { source: OverflowError, backtrace: BT }, + #[error("Divide by zero: {source}")] + DivideByZero { source: DivideByZeroError, backtrace: BT }, + #[error("Conversion error: ")] + ConversionOverflow { source: ConversionOverflowError, backtrace: BT }, +} + +impl_from_err!(ConversionOverflowError, StdError, StdError::ConversionOverflow); + +impl StdError { + pub fn verification_err(source: VerificationError) -> Self { + StdError::VerificationErr { source, backtrace: BT::capture() } + } + + pub fn recover_pubkey_err(source: RecoverPubkeyError) -> Self { + StdError::RecoverPubkeyErr { source, backtrace: BT::capture() } + } + + pub fn generic_err(msg: impl Into) -> Self { + StdError::GenericErr { msg: msg.into(), backtrace: BT::capture() } + } + + pub fn invalid_base64(msg: impl ToString) -> Self { + StdError::InvalidBase64 { msg: msg.to_string(), backtrace: BT::capture() } + } + + pub fn invalid_data_size(expected: usize, actual: usize) -> Self { + StdError::InvalidDataSize { + // Cast is safe because usize is 32 or 64 bit large in all environments we support + expected: expected as u64, + actual: actual as u64, + backtrace: BT::capture(), + } + } + + pub fn invalid_hex(msg: impl ToString) -> Self { + StdError::InvalidHex { msg: msg.to_string(), backtrace: BT::capture() } + } + + pub fn invalid_utf8(msg: impl ToString) -> Self { + StdError::InvalidUtf8 { msg: msg.to_string(), backtrace: BT::capture() } + } + + pub fn not_found(kind: impl Into) -> Self { + StdError::NotFound { kind: kind.into(), backtrace: BT::capture() } + } + + pub fn parse_err(target: impl Into, msg: impl ToString) -> Self { + StdError::ParseErr { + target_type: target.into(), + msg: msg.to_string(), + backtrace: BT::capture(), + } + } + + pub fn serialize_err(source: impl Into, msg: impl ToString) -> Self { + StdError::SerializeErr { + source_type: source.into(), + msg: msg.to_string(), + backtrace: BT::capture(), + } + } + + pub fn overflow(source: OverflowError) -> Self { + StdError::Overflow { source, backtrace: BT::capture() } + } + + pub fn divide_by_zero(source: DivideByZeroError) -> Self { + StdError::DivideByZero { source, backtrace: BT::capture() } + } +} + +impl PartialEq for StdError { + fn eq(&self, rhs: &StdError) -> bool { + match self { + StdError::VerificationErr { source, backtrace: _ } => + if let StdError::VerificationErr { source: rhs_source, backtrace: _ } = rhs { + source == rhs_source + } else { + false + }, + StdError::RecoverPubkeyErr { source, backtrace: _ } => + if let StdError::RecoverPubkeyErr { source: rhs_source, backtrace: _ } = rhs { + source == rhs_source + } else { + false + }, + StdError::GenericErr { msg, backtrace: _ } => { + if let StdError::GenericErr { msg: rhs_msg, backtrace: _ } = rhs { + msg == rhs_msg + } else { + false + } + }, + StdError::InvalidBase64 { msg, backtrace: _ } => { + if let StdError::InvalidBase64 { msg: rhs_msg, backtrace: _ } = rhs { + msg == rhs_msg + } else { + false + } + }, + StdError::InvalidDataSize { expected, actual, backtrace: _ } => + if let StdError::InvalidDataSize { + expected: rhs_expected, + actual: rhs_actual, + backtrace: _, + } = rhs + { + expected == rhs_expected && actual == rhs_actual + } else { + false + }, + StdError::InvalidHex { msg, backtrace: _ } => { + if let StdError::InvalidHex { msg: rhs_msg, backtrace: _ } = rhs { + msg == rhs_msg + } else { + false + } + }, + StdError::InvalidUtf8 { msg, backtrace: _ } => { + if let StdError::InvalidUtf8 { msg: rhs_msg, backtrace: _ } = rhs { + msg == rhs_msg + } else { + false + } + }, + StdError::NotFound { kind, backtrace: _ } => { + if let StdError::NotFound { kind: rhs_kind, backtrace: _ } = rhs { + kind == rhs_kind + } else { + false + } + }, + StdError::ParseErr { target_type, msg, backtrace: _ } => { + if let StdError::ParseErr { + target_type: rhs_target_type, + msg: rhs_msg, + backtrace: _, + } = rhs + { + target_type == rhs_target_type && msg == rhs_msg + } else { + false + } + }, + StdError::SerializeErr { source_type, msg, backtrace: _ } => + if let StdError::SerializeErr { + source_type: rhs_source_type, + msg: rhs_msg, + backtrace: _, + } = rhs + { + source_type == rhs_source_type && msg == rhs_msg + } else { + false + }, + StdError::Overflow { source, backtrace: _ } => { + if let StdError::Overflow { source: rhs_source, backtrace: _ } = rhs { + source == rhs_source + } else { + false + } + }, + StdError::DivideByZero { source, backtrace: _ } => { + if let StdError::DivideByZero { source: rhs_source, backtrace: _ } = rhs { + source == rhs_source + } else { + false + } + }, + StdError::ConversionOverflow { source, backtrace: _ } => + if let StdError::ConversionOverflow { source: rhs_source, backtrace: _ } = rhs { + source == rhs_source + } else { + false + }, + } + } +} + +impl From for StdError { + fn from(source: core::str::Utf8Error) -> Self { + Self::invalid_utf8(source) + } +} + +impl From for StdError { + fn from(source: alloc::string::FromUtf8Error) -> Self { + Self::invalid_utf8(source) + } +} + +impl From for StdError { + fn from(source: VerificationError) -> Self { + Self::verification_err(source) + } +} + +impl From for StdError { + fn from(source: RecoverPubkeyError) -> Self { + Self::recover_pubkey_err(source) + } +} + +impl From for StdError { + fn from(source: OverflowError) -> Self { + Self::overflow(source) + } +} + +impl From for StdError { + fn from(source: DivideByZeroError) -> Self { + Self::divide_by_zero(source) + } +} + +/// The return type for init, execute and query. Since the error type cannot be serialized to JSON, +/// this is only available within the contract and its unit tests. +/// +/// The prefix "Core"/"Std" means "the standard result within the core/standard library". This is +/// not the only result/error type in cosmwasm-core/cosmwasm-std. +pub type StdResult = core::result::Result; + +#[derive(Debug, PartialEq, Eq)] +pub enum OverflowOperation { + Add, + Sub, + Mul, + Pow, + Shr, + Shl, +} + +impl fmt::Display for OverflowOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Cannot {operation} with given operands")] +pub struct OverflowError { + pub operation: OverflowOperation, +} + +impl OverflowError { + pub fn new(operation: OverflowOperation) -> Self { + Self { operation } + } +} + +/// The error returned by [`TryFrom`] conversions that overflow, for example +/// when converting from [`Uint256`] to [`Uint128`]. +/// +/// [`TryFrom`]: core::convert::TryFrom +/// [`Uint256`]: crate::Uint256 +/// [`Uint128`]: crate::Uint128 +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Error converting {source_type} to {target_type}")] +pub struct ConversionOverflowError { + pub source_type: &'static str, + pub target_type: &'static str, +} + +impl ConversionOverflowError { + pub fn new(source_type: &'static str, target_type: &'static str) -> Self { + Self { source_type, target_type } + } +} + +#[derive(Debug, Default, PartialEq, Eq, thiserror::Error)] +#[error("Cannot divide by zero")] +pub struct DivideByZeroError; + +impl DivideByZeroError { + pub fn new() -> Self { + Self + } +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum DivisionError { + #[error("Divide by zero")] + DivideByZero, + + #[error("Overflow in division")] + Overflow, +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum CheckedMultiplyFractionError { + #[error("{_0}")] + DivideByZero(#[from] DivideByZeroError), + + #[error("{_0}")] + ConversionOverflow(#[from] ConversionOverflowError), + + #[error("{_0}")] + Overflow(#[from] OverflowError), +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum CheckedMultiplyRatioError { + #[error("Denominator must not be zero")] + DivideByZero, + + #[error("Multiplication overflow")] + Overflow, +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum CheckedFromRatioError { + #[error("Denominator must not be zero")] + DivideByZero, + + #[error("Overflow")] + Overflow, +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Round up operation failed because of overflow")] +pub struct RoundUpOverflowError; + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Round down operation failed because of overflow")] +pub struct RoundDownOverflowError; + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum CoinsError { + #[error("Duplicate denom")] + DuplicateDenom, +} + +impl From for StdError { + fn from(value: CoinsError) -> Self { + Self::generic_err(format!("Creating Coins: {value}")) + } +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum CoinFromStrError { + #[error("Missing denominator")] + MissingDenom, + #[error("Missing amount or non-digit characters in amount")] + MissingAmount, + #[error("Invalid amount: {_0}")] + InvalidAmount(core::num::ParseIntError), +} + +impl From for CoinFromStrError { + fn from(value: core::num::ParseIntError) -> Self { + Self::InvalidAmount(value) + } +} + +impl From for StdError { + fn from(value: CoinFromStrError) -> Self { + Self::generic_err(format!("Parsing Coin: {value}")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::str; + + // constructors + + // example of reporting contract errors with format! + #[test] + fn generic_err_owned() { + let guess = 7; + let error = StdError::generic_err(format!("{guess} is too low")); + match error { + StdError::GenericErr { msg, .. } => { + assert_eq!(msg, String::from("7 is too low")); + }, + e => panic!("unexpected error, {e:?}"), + } + } + + // example of reporting static contract errors + #[test] + fn generic_err_ref() { + let error = StdError::generic_err("not implemented"); + match error { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "not implemented"), + e => panic!("unexpected error, {e:?}"), + } + } + + #[test] + fn invalid_base64_works_for_strings() { + let error = StdError::invalid_base64("my text"); + match error { + StdError::InvalidBase64 { msg, .. } => { + assert_eq!(msg, "my text"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_base64_works_for_errors() { + let original = base64::DecodeError::InvalidLength(10); + let error = StdError::invalid_base64(original); + match error { + StdError::InvalidBase64 { msg, .. } => { + assert_eq!(msg, "Invalid input length: 10"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_data_size_works() { + let error = StdError::invalid_data_size(31, 14); + match error { + StdError::InvalidDataSize { expected, actual, .. } => { + assert_eq!(expected, 31); + assert_eq!(actual, 14); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_hex_works_for_strings() { + let error = StdError::invalid_hex("my text"); + match error { + StdError::InvalidHex { msg, .. } => { + assert_eq!(msg, "my text"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_hex_works_for_errors() { + let original = hex::FromHexError::OddLength; + let error = StdError::invalid_hex(original); + match error { + StdError::InvalidHex { msg, .. } => { + assert_eq!(msg, "Odd number of digits"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_utf8_works_for_strings() { + let error = StdError::invalid_utf8("my text"); + match error { + StdError::InvalidUtf8 { msg, .. } => { + assert_eq!(msg, "my text"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn invalid_utf8_works_for_errors() { + let original = String::from_utf8(vec![0x80]).unwrap_err(); + let error = StdError::invalid_utf8(original); + match error { + StdError::InvalidUtf8 { msg, .. } => { + assert_eq!(msg, "invalid utf-8 sequence of 1 bytes from index 0"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn not_found_works() { + let error = StdError::not_found("gold"); + match error { + StdError::NotFound { kind, .. } => assert_eq!(kind, "gold"), + _ => panic!("expect different error"), + } + } + + #[test] + fn parse_err_works() { + let error = StdError::parse_err("Book", "Missing field: title"); + match error { + StdError::ParseErr { target_type, msg, .. } => { + assert_eq!(target_type, "Book"); + assert_eq!(msg, "Missing field: title"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn serialize_err_works() { + let error = StdError::serialize_err("Book", "Content too long"); + match error { + StdError::SerializeErr { source_type, msg, .. } => { + assert_eq!(source_type, "Book"); + assert_eq!(msg, "Content too long"); + }, + _ => panic!("expect different error"), + } + } + + #[test] + fn underflow_works_for_u128() { + let error = StdError::overflow(OverflowError::new(OverflowOperation::Sub)); + assert!(matches!( + error, + StdError::Overflow { source: OverflowError { operation: OverflowOperation::Sub }, .. } + )); + } + + #[test] + fn overflow_works_for_i64() { + let error = StdError::overflow(OverflowError::new(OverflowOperation::Sub)); + assert!(matches!( + error, + StdError::Overflow { source: OverflowError { operation: OverflowOperation::Sub }, .. } + )); + } + + #[test] + fn divide_by_zero_works() { + let error = StdError::divide_by_zero(DivideByZeroError); + assert!(matches!(error, StdError::DivideByZero { source: DivideByZeroError, .. })); + } + + #[test] + fn implements_debug() { + let error: StdError = StdError::from(OverflowError::new(OverflowOperation::Sub)); + let embedded = format!("Debug: {error:?}"); + let expected = r#"Debug: Overflow { source: OverflowError { operation: Sub }, backtrace: }"#; + assert_eq!(embedded, expected); + } + + #[test] + fn implements_display() { + let error: StdError = StdError::from(OverflowError::new(OverflowOperation::Sub)); + let embedded = format!("Display: {error}"); + assert_eq!(embedded, "Display: Overflow: Cannot Sub with given operands"); + } + + #[test] + fn implements_partial_eq() { + let u1 = StdError::from(OverflowError::new(OverflowOperation::Sub)); + let u2 = StdError::from(OverflowError::new(OverflowOperation::Sub)); + let s1 = StdError::serialize_err("Book", "Content too long"); + let s2 = StdError::serialize_err("Book", "Content too long"); + let s3 = StdError::serialize_err("Book", "Title too long"); + assert_eq!(u1, u2); + assert_ne!(u1, s1); + assert_eq!(s1, s2); + assert_ne!(s1, s3); + } + + #[test] + fn from_std_str_utf8error_works() { + let broken = Vec::from(b"Hello \xF0\x90\x80World" as &[u8]); + let error: StdError = str::from_utf8(&broken).unwrap_err().into(); + match error { + StdError::InvalidUtf8 { msg, .. } => { + assert_eq!(msg, "invalid utf-8 sequence of 3 bytes from index 6") + }, + err => panic!("Unexpected error: {err:?}"), + } + } + + #[test] + fn from_std_string_from_utf8error_works() { + let error: StdError = + String::from_utf8(b"Hello \xF0\x90\x80World".to_vec()).unwrap_err().into(); + match error { + StdError::InvalidUtf8 { msg, .. } => { + assert_eq!(msg, "invalid utf-8 sequence of 3 bytes from index 6") + }, + err => panic!("Unexpected error: {err:?}"), + } + } +} diff --git a/cosmwasm/std/src/errors/system_error.rs b/cosmwasm/std/src/errors/system_error.rs new file mode 100644 index 00000000..ac41283a --- /dev/null +++ b/cosmwasm/std/src/errors/system_error.rs @@ -0,0 +1,100 @@ +use alloc::string::String; +use serde::{Deserialize, Serialize}; + +use crate::Binary; + +/// SystemError is used for errors inside the VM and is API friendly (i.e. serializable). +/// +/// This is used on return values for Querier as a nested result: Result, SystemError> +/// The first wrap (SystemError) will trigger if the contract address doesn't exist, +/// the QueryRequest is malformed, etc. The second wrap will be an error message from +/// the contract itself. +/// +/// Such errors are only created by the VM. The error type is defined in the standard library, to +/// ensure the contract understands the error format without creating a dependency on cosmwasm-vm. +#[derive( + Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, thiserror::Error, +)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum SystemError { + InvalidRequest { + error: String, + request: Binary, + }, + InvalidResponse { + error: String, + response: Binary, + }, + NoSuchContract { + /// The address that was attempted to query + addr: String, + }, + /// A Wasm code was not found. + NoSuchCode { + /// The code ID that is missing + code_id: u64, + }, + Unknown {}, + UnsupportedRequest { + kind: String, + }, +} + +impl core::fmt::Display for SystemError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SystemError::InvalidRequest { error, request } => write!( + f, + "Cannot parse request: {} in: {}", + error, + String::from_utf8_lossy(request) + ), + SystemError::InvalidResponse { error, response } => write!( + f, + "Cannot parse response: {} in: {}", + error, + String::from_utf8_lossy(response) + ), + SystemError::NoSuchContract { addr } => write!(f, "No such contract: {addr}"), + SystemError::NoSuchCode { code_id } => write!(f, "No such code: {code_id}"), + SystemError::Unknown {} => write!(f, "Unknown system error"), + SystemError::UnsupportedRequest { kind } => { + write!(f, "Unsupported query type: {kind}") + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn system_error_no_such_contract_serialization() { + let err = SystemError::NoSuchContract { addr: "gibtsnicht".to_string() }; + + // ser + let json = serde_json::to_vec(&err).unwrap(); + assert_eq!(String::from_utf8_lossy(&json), r#"{"no_such_contract":{"addr":"gibtsnicht"}}"#,); + + // de + let err: SystemError = + serde_json::from_slice(br#"{"no_such_contract":{"addr":"nada"}}"#).unwrap(); + assert_eq!(err, SystemError::NoSuchContract { addr: "nada".to_string() }); + } + + #[test] + fn system_error_no_such_code_serialization() { + let err = SystemError::NoSuchCode { code_id: 13 }; + + // ser + let json = serde_json::to_vec(&err).unwrap(); + assert_eq!(String::from_utf8_lossy(&json), r#"{"no_such_code":{"code_id":13}}"#,); + + // de + let err: SystemError = + serde_json::from_slice(br#"{"no_such_code":{"code_id":987}}"#).unwrap(); + assert_eq!(err, SystemError::NoSuchCode { code_id: 987 },); + } +} diff --git a/cosmwasm/std/src/errors/verification_error.rs b/cosmwasm/std/src/errors/verification_error.rs new file mode 100644 index 00000000..cf0ff6f7 --- /dev/null +++ b/cosmwasm/std/src/errors/verification_error.rs @@ -0,0 +1,145 @@ +use core::fmt::Debug; + +use super::BT; + +#[cfg(not(target_arch = "wasm32"))] +use cosmwasm_crypto::CryptoError; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum AggregationError { + #[error("List of points is empty")] + Empty, + #[error("List is not an expected multiple")] + NotMultiple, +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum PairingEqualityError { + #[error("List is not a multiple of 48")] + NotMultipleG1, + #[error("List is not a multiple of 96")] + NotMultipleG2, + #[error("Not the same amount of points passed")] + UnequalPointAmount, +} + +#[derive(Debug, thiserror::Error)] +pub enum VerificationError { + #[error("Aggregation error: {source}")] + Aggregation { source: AggregationError }, + #[error("Batch error")] + BatchErr, + #[error("Generic error")] + GenericErr, + #[error("Invalid hash format")] + InvalidHashFormat, + #[error("Invalid signature format")] + InvalidSignatureFormat, + #[error("Invalid public key format")] + InvalidPubkeyFormat, + #[error("Invalid recovery parameter. Supported values: 0 and 1.")] + InvalidRecoveryParam, + #[error("Invalid point")] + InvalidPoint, + #[error("Unknown hash function")] + UnknownHashFunction, + #[error("Aggregation pairing equality error: {source}")] + PairingEquality { source: PairingEqualityError }, + #[error("Unknown error: {error_code}")] + UnknownErr { error_code: u32, backtrace: BT }, +} + +impl VerificationError { + pub fn unknown_err(error_code: u32) -> Self { + VerificationError::UnknownErr { error_code, backtrace: BT::capture() } + } +} + +impl PartialEq for VerificationError { + fn eq(&self, rhs: &VerificationError) -> bool { + match self { + VerificationError::Aggregation { source: lhs_source } => { + matches!(rhs, VerificationError::Aggregation { source: rhs_source } if rhs_source == lhs_source) + }, + VerificationError::PairingEquality { source: lhs_source } => { + matches!(rhs, VerificationError::PairingEquality { source: rhs_source } if rhs_source == lhs_source) + }, + VerificationError::BatchErr => matches!(rhs, VerificationError::BatchErr), + VerificationError::GenericErr => matches!(rhs, VerificationError::GenericErr), + VerificationError::InvalidHashFormat => { + matches!(rhs, VerificationError::InvalidHashFormat) + }, + VerificationError::InvalidPubkeyFormat => { + matches!(rhs, VerificationError::InvalidPubkeyFormat) + }, + VerificationError::InvalidSignatureFormat => { + matches!(rhs, VerificationError::InvalidSignatureFormat) + }, + VerificationError::InvalidRecoveryParam => { + matches!(rhs, VerificationError::InvalidRecoveryParam) + }, + VerificationError::InvalidPoint => matches!(rhs, VerificationError::InvalidPoint), + VerificationError::UnknownHashFunction => { + matches!(rhs, VerificationError::UnknownHashFunction) + }, + VerificationError::UnknownErr { error_code, .. } => + if let VerificationError::UnknownErr { error_code: rhs_error_code, .. } = rhs { + error_code == rhs_error_code + } else { + false + }, + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for VerificationError { + fn from(original: CryptoError) -> Self { + match original { + CryptoError::Aggregation { + source: cosmwasm_crypto::AggregationError::Empty, .. + } => VerificationError::Aggregation { source: AggregationError::Empty }, + CryptoError::Aggregation { + source: cosmwasm_crypto::AggregationError::NotMultiple { .. }, + .. + } => VerificationError::Aggregation { source: AggregationError::NotMultiple }, + CryptoError::PairingEquality { + source: cosmwasm_crypto::PairingEqualityError::NotMultipleG1 { .. }, + .. + } => VerificationError::PairingEquality { source: PairingEqualityError::NotMultipleG1 }, + CryptoError::PairingEquality { + source: cosmwasm_crypto::PairingEqualityError::NotMultipleG2 { .. }, + .. + } => VerificationError::PairingEquality { source: PairingEqualityError::NotMultipleG2 }, + CryptoError::PairingEquality { + source: cosmwasm_crypto::PairingEqualityError::UnequalPointAmount { .. }, + .. + } => VerificationError::PairingEquality { + source: PairingEqualityError::UnequalPointAmount, + }, + CryptoError::InvalidHashFormat { .. } => VerificationError::InvalidHashFormat, + CryptoError::InvalidPubkeyFormat { .. } => VerificationError::InvalidPubkeyFormat, + CryptoError::InvalidSignatureFormat { .. } => VerificationError::InvalidSignatureFormat, + CryptoError::GenericErr { .. } => VerificationError::GenericErr, + CryptoError::InvalidRecoveryParam { .. } => VerificationError::InvalidRecoveryParam, + CryptoError::InvalidPoint { .. } => VerificationError::InvalidPoint, + CryptoError::BatchErr { .. } => VerificationError::BatchErr, + CryptoError::UnknownHashFunction { .. } => VerificationError::UnknownHashFunction, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // constructors + #[test] + fn unknown_err_works() { + let error = VerificationError::unknown_err(123); + match error { + VerificationError::UnknownErr { error_code, .. } => assert_eq!(error_code, 123), + _ => panic!("wrong error type!"), + } + } +} diff --git a/cosmwasm/std/src/exports.rs b/cosmwasm/std/src/exports.rs new file mode 100644 index 00000000..a56ad7f0 --- /dev/null +++ b/cosmwasm/std/src/exports.rs @@ -0,0 +1,802 @@ +//! exports exposes the public wasm API +//! +//! interface_version_8, allocate and deallocate turn into Wasm exports +//! as soon as cosmwasm_std is `use`d in the contract, even privately. +//! +//! `do_execute`, `do_instantiate`, `do_migrate`, `do_query`, `do_reply` +//! and `do_sudo` should be wrapped with a extern "C" entry point including +//! the contract-specific function pointer. This is done via the `#[entry_point]` +//! macro attribute from cosmwasm-derive. +use crate::prelude::*; +use core::marker::PhantomData; + +use serde::de::DeserializeOwned; + +#[cfg(feature = "stargate")] +use crate::ibc::{ + IbcChannelCloseMsg, IbcChannelConnectMsg, IbcPacketAckMsg, IbcPacketReceiveMsg, + IbcPacketTimeoutMsg, IbcReceiveResponse, +}; +#[cfg(feature = "abort")] +use crate::panic::install_panic_handler; +use crate::{ + deps::OwnedDeps, + ibc::{ + IbcBasicResponse, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcDestinationCallbackMsg, + IbcSourceCallbackMsg, + }, + imports::{ExternalApi, ExternalQuerier, ExternalStorage}, + memory::{Owned, Region}, + query::CustomQuery, + results::{ContractResult, QueryResponse, Reply, Response}, + serde::{from_json, to_json_vec}, + types::Env, + CustomMsg, Deps, DepsMut, MessageInfo, +}; + +// These functions are used as markers for the chain to know which features this contract requires. +// If the chain does not support all the required features, it will reject storing the contract. +// See `docs/CAPABILITIES.md` for more details. +#[cfg(feature = "iterator")] +#[no_mangle] +extern "C" fn requires_iterator() -> () {} + +#[cfg(feature = "staking")] +#[no_mangle] +extern "C" fn requires_staking() -> () {} + +#[cfg(feature = "stargate")] +#[no_mangle] +extern "C" fn requires_stargate() -> () {} + +#[cfg(feature = "cosmwasm_1_1")] +#[no_mangle] +extern "C" fn requires_cosmwasm_1_1() -> () {} + +#[cfg(feature = "cosmwasm_1_2")] +#[no_mangle] +extern "C" fn requires_cosmwasm_1_2() -> () {} + +#[cfg(feature = "cosmwasm_1_3")] +#[no_mangle] +extern "C" fn requires_cosmwasm_1_3() -> () {} + +#[cfg(feature = "cosmwasm_1_4")] +#[no_mangle] +extern "C" fn requires_cosmwasm_1_4() -> () {} + +#[cfg(feature = "cosmwasm_2_0")] +#[no_mangle] +extern "C" fn requires_cosmwasm_2_0() -> () {} + +#[cfg(feature = "cosmwasm_2_1")] +#[no_mangle] +extern "C" fn requires_cosmwasm_2_1() -> () {} + +/// interface_version_* exports mark which Wasm VM interface level this contract is compiled for. +/// They can be checked by cosmwasm_vm. +/// Update this whenever the Wasm VM interface breaks. +#[no_mangle] +extern "C" fn interface_version_8() -> () {} + +/// allocate reserves the given number of bytes in wasm memory and returns a pointer +/// to a Region defining this data. This space is managed by the calling process +/// and should be accompanied by a corresponding deallocate +#[no_mangle] +extern "C" fn allocate(size: usize) -> u32 { + Region::with_capacity(size).to_heap_ptr() as u32 +} + +/// deallocate expects a pointer to a Region created with allocate. +/// It will free both the Region and the memory referenced by the Region. +#[no_mangle] +extern "C" fn deallocate(pointer: u32) { + // auto-drop Region on function end + let _ = unsafe { Region::from_heap_ptr(pointer as *mut Region) }; +} + +// TODO: replace with https://doc.rust-lang.org/std/ops/trait.Try.html once stabilized +macro_rules! r#try_into_contract_result { + ($expr:expr) => { + match $expr { + Ok(val) => val, + Err(err) => { + return ContractResult::Err(err.to_string()); + }, + } + }; + ($expr:expr,) => { + $crate::try_into_contract_result!($expr) + }; +} + +/// This should be wrapped in an external "C" export, containing a contract-specific function as an +/// argument. +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_instantiate( + instantiate_fn: &dyn Fn(DepsMut, Env, MessageInfo, M) -> Result, E>, + env_ptr: u32, + info_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_instantiate( + instantiate_fn, + env_ptr as *mut Region, + info_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_execute should be wrapped in an external "C" export, containing a contract-specific function +/// as arg +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_execute( + execute_fn: &dyn Fn(DepsMut, Env, MessageInfo, M) -> Result, E>, + env_ptr: u32, + info_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_execute( + execute_fn, + env_ptr as *mut Region, + info_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_migrate should be wrapped in an external "C" export, containing a contract-specific function +/// as arg +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_migrate( + migrate_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_migrate(migrate_fn, env_ptr as *mut Region, msg_ptr as *mut Region); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_sudo should be wrapped in an external "C" export, containing a contract-specific function as +/// arg +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_sudo( + sudo_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_sudo(sudo_fn, env_ptr as *mut Region, msg_ptr as *mut Region); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_reply should be wrapped in an external "C" export, containing a contract-specific function as +/// arg message body is always `SubcallResult` +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_reply( + reply_fn: &dyn Fn(DepsMut, Env, Reply) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_reply(reply_fn, env_ptr as *mut Region, msg_ptr as *mut Region); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_query should be wrapped in an external "C" export, containing a contract-specific function as +/// arg +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `E`: error type for responses +pub fn do_query( + query_fn: &dyn Fn(Deps, Env, M) -> Result, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_query(query_fn, env_ptr as *mut Region, msg_ptr as *mut Region); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_channel_open is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn does the protocol version negotiation during channel handshake phase +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_channel_open( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelOpenMsg) -> Result, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_channel_open( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_channel_connect is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn is a callback when a IBC channel is established (after both sides agree in open) +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_channel_connect( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelConnectMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_channel_connect( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_channel_close is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn is a callback when a IBC channel belonging to this contract is closed +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_channel_close( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelCloseMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_channel_close( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_packet_receive is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn is called when this chain receives an IBC Packet on a channel belonging +/// to this contract +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_packet_receive( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketReceiveMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_packet_receive( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_packet_ack is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn is called when this chain receives an IBC Acknowledgement for a packet +/// that this contract previously sent +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_packet_ack( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketAckMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_packet_ack( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +/// do_ibc_packet_timeout is designed for use with #[entry_point] to make a "C" extern +/// +/// contract_fn is called when a packet that this contract previously sent has provably +/// timedout and will never be relayed to the calling chain. This generally behaves +/// like ick_ack_fn upon an acknowledgement containing an error. +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "stargate")] +pub fn do_ibc_packet_timeout( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketTimeoutMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_packet_timeout( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +pub fn do_ibc_source_callback( + contract_fn: &dyn Fn(DepsMut, Env, IbcSourceCallbackMsg) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_source_callback( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +pub fn do_ibc_destination_callback( + contract_fn: &dyn Fn( + DepsMut, + Env, + IbcDestinationCallbackMsg, + ) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_ibc_destination_callback( + contract_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + +fn _do_instantiate( + instantiate_fn: &dyn Fn(DepsMut, Env, MessageInfo, M) -> Result, E>, + env_ptr: *mut Region, + info_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let info: Vec = unsafe { Region::from_heap_ptr(info_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let info: MessageInfo = try_into_contract_result!(from_json(info)); + let msg: M = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + instantiate_fn(deps.as_mut(), env, info, msg).into() +} + +fn _do_execute( + execute_fn: &dyn Fn(DepsMut, Env, MessageInfo, M) -> Result, E>, + env_ptr: *mut Region, + info_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let info: Vec = unsafe { Region::from_heap_ptr(info_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let info: MessageInfo = try_into_contract_result!(from_json(info)); + let msg: M = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + execute_fn(deps.as_mut(), env, info, msg).into() +} + +fn _do_migrate( + migrate_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: M = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + migrate_fn(deps.as_mut(), env, msg).into() +} + +fn _do_sudo( + sudo_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: M = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + sudo_fn(deps.as_mut(), env, msg).into() +} + +fn _do_reply( + reply_fn: &dyn Fn(DepsMut, Env, Reply) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: Reply = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + reply_fn(deps.as_mut(), env, msg).into() +} + +fn _do_query( + query_fn: &dyn Fn(Deps, Env, M) -> Result, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult +where + Q: CustomQuery, + M: DeserializeOwned, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: M = try_into_contract_result!(from_json(msg)); + + let deps = make_dependencies(); + query_fn(deps.as_ref(), env, msg).into() +} + +fn _do_ibc_channel_open( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelOpenMsg) -> Result, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult +where + Q: CustomQuery, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcChannelOpenMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +#[cfg(feature = "stargate")] +fn _do_ibc_channel_connect( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelConnectMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcChannelConnectMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +#[cfg(feature = "stargate")] +fn _do_ibc_channel_close( + contract_fn: &dyn Fn(DepsMut, Env, IbcChannelCloseMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcChannelCloseMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +#[cfg(feature = "stargate")] +fn _do_ibc_packet_receive( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketReceiveMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcPacketReceiveMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +#[cfg(feature = "stargate")] +fn _do_ibc_packet_ack( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketAckMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcPacketAckMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +#[cfg(feature = "stargate")] +fn _do_ibc_packet_timeout( + contract_fn: &dyn Fn(DepsMut, Env, IbcPacketTimeoutMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcPacketTimeoutMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +fn _do_ibc_source_callback( + contract_fn: &dyn Fn(DepsMut, Env, IbcSourceCallbackMsg) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcSourceCallbackMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +fn _do_ibc_destination_callback( + contract_fn: &dyn Fn( + DepsMut, + Env, + IbcDestinationCallbackMsg, + ) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: IbcDestinationCallbackMsg = try_into_contract_result!(from_json(msg)); + + let mut deps = make_dependencies(); + contract_fn(deps.as_mut(), env, msg).into() +} + +/// Makes all bridges to external dependencies (i.e. Wasm imports) that are injected by the VM +pub(crate) fn make_dependencies() -> OwnedDeps +where + Q: CustomQuery, +{ + OwnedDeps { + storage: ExternalStorage::new(), + api: ExternalApi::new(), + querier: ExternalQuerier::new(), + custom_query_type: PhantomData, + } +} diff --git a/cosmwasm/std/src/forward_ref.rs b/cosmwasm/std/src/forward_ref.rs new file mode 100644 index 00000000..5ecf2982 --- /dev/null +++ b/cosmwasm/std/src/forward_ref.rs @@ -0,0 +1,85 @@ +/// # ⚠ THIS IS AN INTERNAL IMPLEMENTATION DETAIL. DO NOT USE. +/// +/// Given an implementation of `T == U`, implements: +/// - `&T == U` +/// - `T == &U` +/// +/// We don't need to add `&T == &U` here because this is implemented automatically. +#[doc(hidden)] +#[macro_export] +macro_rules! __internal__forward_ref_partial_eq { + ($t:ty, $u:ty) => { + // `&T == U` + impl<'a> PartialEq<$u> for &'a $t { + #[inline] + fn eq(&self, rhs: &$u) -> bool { + **self == *rhs // Implement via T == U + } + } + + // `T == &U` + impl PartialEq<&$u> for $t { + #[inline] + fn eq(&self, rhs: &&$u) -> bool { + *self == **rhs // Implement via T == U + } + } + }; +} + +/// implements binary operators "&T op U", "T op &U", "&T op &U" +/// based on "T op U" where T and U are expected to be `Copy`able +/// +/// Copied from `libcore` +macro_rules! forward_ref_binop { + (impl $imp:ident, $method:ident for $t:ty, $u:ty) => { + impl<'a> $imp<$u> for &'a $t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + #[track_caller] + fn $method(self, other: $u) -> <$t as $imp<$u>>::Output { + $imp::$method(*self, other) + } + } + + impl $imp<&$u> for $t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + #[track_caller] + fn $method(self, other: &$u) -> <$t as $imp<$u>>::Output { + $imp::$method(self, *other) + } + } + + impl $imp<&$u> for &$t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + #[track_caller] + fn $method(self, other: &$u) -> <$t as $imp<$u>>::Output { + $imp::$method(*self, *other) + } + } + }; +} + +/// implements "T op= &U", based on "T op= U" +/// where U is expected to be `Copy`able +/// +/// Copied from `libcore` +macro_rules! forward_ref_op_assign { + (impl $imp:ident, $method:ident for $t:ty, $u:ty) => { + impl $imp<&$u> for $t { + #[inline] + #[track_caller] + fn $method(&mut self, other: &$u) { + $imp::$method(self, *other); + } + } + }; +} + +pub(crate) use forward_ref_binop; +pub(crate) use forward_ref_op_assign; diff --git a/cosmwasm/std/src/hex_binary.rs b/cosmwasm/std/src/hex_binary.rs new file mode 100644 index 00000000..3ab4a9c2 --- /dev/null +++ b/cosmwasm/std/src/hex_binary.rs @@ -0,0 +1,612 @@ +use alloc::{string::String, vec::Vec}; +use core::{fmt, ops::Deref}; + +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + encoding::{from_hex, to_hex}, + Binary, StdError, StdResult, +}; + +/// This is a wrapper around Vec to add hex de/serialization +/// with serde. It also adds some helper methods to help encode inline. +/// +/// This is similar to `cosmwasm_std::Binary` but uses hex. +/// See also . +#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, schemars::JsonSchema)] +pub struct HexBinary(#[schemars(with = "String")] Vec); + +impl HexBinary { + pub fn from_hex(input: &str) -> StdResult { + from_hex(input).map(Self) + } + + pub fn to_hex(&self) -> String { + to_hex(&self.0) + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } + + /// Copies content into fixed-sized array. + /// + /// # Examples + /// + /// Copy to array of explicit length + /// + /// ``` + /// # use cosmwasm_std::HexBinary; + /// let data = HexBinary::from(&[0xfb, 0x1f, 0x37]); + /// let array: [u8; 3] = data.to_array().unwrap(); + /// assert_eq!(array, [0xfb, 0x1f, 0x37]); + /// ``` + /// + /// Copy to integer + /// + /// ``` + /// # use cosmwasm_std::HexBinary; + /// let data = HexBinary::from(&[0x8b, 0x67, 0x64, 0x84, 0xb5, 0xfb, 0x1f, 0x37]); + /// let num = u64::from_be_bytes(data.to_array().unwrap()); + /// assert_eq!(num, 10045108015024774967); + /// ``` + pub fn to_array(&self) -> StdResult<[u8; LENGTH]> { + if self.len() != LENGTH { + return Err(StdError::invalid_data_size(LENGTH, self.len())); + } + + let mut out: [u8; LENGTH] = [0; LENGTH]; + out.copy_from_slice(&self.0); + Ok(out) + } +} + +impl fmt::Display for HexBinary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl fmt::Debug for HexBinary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Use an output inspired by tuples (https://doc.rust-lang.org/std/fmt/struct.Formatter.html#method.debug_tuple) + // but with a custom implementation to avoid the need for an intemediate hex string. + write!(f, "HexBinary(")?; + for byte in self.0.iter() { + write!(f, "{byte:02x}")?; + } + write!(f, ")")?; + Ok(()) + } +} + +/// Just like Vec, HexBinary is a smart pointer to [u8]. +/// This implements `*data` for us and allows us to +/// do `&*data`, returning a `&[u8]` from a `&HexBinary`. +/// With [deref coercions](https://doc.rust-lang.org/1.22.1/book/first-edition/deref-coercions.html#deref-coercions), +/// this allows us to use `&data` whenever a `&[u8]` is required. +impl Deref for HexBinary { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl AsRef<[u8]> for HexBinary { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +// Slice +impl From<&[u8]> for HexBinary { + fn from(binary: &[u8]) -> Self { + Self(binary.to_vec()) + } +} + +// Array reference +impl From<&[u8; LENGTH]> for HexBinary { + fn from(source: &[u8; LENGTH]) -> Self { + Self(source.to_vec()) + } +} + +// Owned array +impl From<[u8; LENGTH]> for HexBinary { + fn from(source: [u8; LENGTH]) -> Self { + Self(source.into()) + } +} + +impl From> for HexBinary { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl From for Vec { + fn from(original: HexBinary) -> Vec { + original.0 + } +} + +impl From for HexBinary { + fn from(original: Binary) -> Self { + Self(original.into()) + } +} + +impl From for Binary { + fn from(original: HexBinary) -> Binary { + Binary::from(original.0) + } +} + +/// Implement `HexBinary == alloc::vec::Vec` +impl PartialEq> for HexBinary { + fn eq(&self, rhs: &Vec) -> bool { + // Use Vec == Vec + self.0 == *rhs + } +} + +/// Implement `alloc::vec::Vec == HexBinary` +impl PartialEq for Vec { + fn eq(&self, rhs: &HexBinary) -> bool { + // Use Vec == Vec + *self == rhs.0 + } +} + +/// Implement `HexBinary == &[u8]` +impl PartialEq<&[u8]> for HexBinary { + fn eq(&self, rhs: &&[u8]) -> bool { + // Use &[u8] == &[u8] + self.as_slice() == *rhs + } +} + +/// Implement `&[u8] == HexBinary` +impl PartialEq for &[u8] { + fn eq(&self, rhs: &HexBinary) -> bool { + // Use &[u8] == &[u8] + *self == rhs.as_slice() + } +} + +/// Implement `HexBinary == [u8; LENGTH]` +impl PartialEq<[u8; LENGTH]> for HexBinary { + fn eq(&self, rhs: &[u8; LENGTH]) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `[u8; LENGTH] == HexBinary` +impl PartialEq for [u8; LENGTH] { + fn eq(&self, rhs: &HexBinary) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `HexBinary == &[u8; LENGTH]` +impl PartialEq<&[u8; LENGTH]> for HexBinary { + fn eq(&self, rhs: &&[u8; LENGTH]) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Implement `&[u8; LENGTH] == HexBinary` +impl PartialEq for &[u8; LENGTH] { + fn eq(&self, rhs: &HexBinary) -> bool { + self.as_slice() == rhs.as_slice() + } +} + +/// Serializes as a hex string +impl Serialize for HexBinary { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_hex()) + } else { + panic!("HexBinary is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +/// Deserializes as a hex string +impl<'de> Deserialize<'de> for HexBinary { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + deserializer.deserialize_str(HexVisitor) + } else { + panic!("HexBinary is only intended to be used with JSON serialization for now. If you are hitting this panic please open an issue at https://github.com/CosmWasm/cosmwasm describing your use case.") + } + } +} + +struct HexVisitor; + +impl<'de> de::Visitor<'de> for HexVisitor { + type Value = HexBinary; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("valid hex encoded string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match HexBinary::from_hex(v) { + Ok(data) => Ok(data), + Err(_) => Err(E::custom(format!("invalid hex: {v}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{assert_hash_works, StdError}; + + #[test] + fn from_hex_works() { + let data = HexBinary::from_hex("").unwrap(); + assert_eq!(data, b""); + let data = HexBinary::from_hex("61").unwrap(); + assert_eq!(data, b"a"); + let data = HexBinary::from_hex("00").unwrap(); + assert_eq!(data, b"\0"); + + let data = HexBinary::from_hex("68656c6c6f").unwrap(); + assert_eq!(data, b"hello"); + let data = HexBinary::from_hex("68656C6C6F").unwrap(); + assert_eq!(data, b"hello"); + let data = HexBinary::from_hex("72616e646f6d695a").unwrap(); + assert_eq!(data.as_slice(), b"randomiZ"); + + // odd + match HexBinary::from_hex("123").unwrap_err() { + StdError::InvalidHex { msg, .. } => { + assert_eq!(msg, "Odd number of digits") + }, + _ => panic!("Unexpected error type"), + } + // non-hex + match HexBinary::from_hex("efgh").unwrap_err() { + StdError::InvalidHex { msg, .. } => { + assert_eq!(msg, "Invalid character 'g' at position 2") + }, + _ => panic!("Unexpected error type"), + } + // 0x prefixed + match HexBinary::from_hex("0xaa").unwrap_err() { + StdError::InvalidHex { msg, .. } => { + assert_eq!(msg, "Invalid character 'x' at position 1") + }, + _ => panic!("Unexpected error type"), + } + // spaces + assert!(matches!(HexBinary::from_hex("aa ").unwrap_err(), StdError::InvalidHex { .. })); + assert!(matches!(HexBinary::from_hex(" aa").unwrap_err(), StdError::InvalidHex { .. })); + assert!(matches!(HexBinary::from_hex("a a").unwrap_err(), StdError::InvalidHex { .. })); + assert!(matches!(HexBinary::from_hex(" aa ").unwrap_err(), StdError::InvalidHex { .. })); + } + + #[test] + fn to_hex_works() { + let binary: &[u8] = b""; + let encoded = HexBinary::from(binary).to_hex(); + assert_eq!(encoded, ""); + + let binary: &[u8] = b"hello"; + let encoded = HexBinary::from(binary).to_hex(); + assert_eq!(encoded, "68656c6c6f"); + + let binary = vec![12u8, 187, 0, 17, 250, 1]; + let encoded = HexBinary(binary).to_hex(); + assert_eq!(encoded, "0cbb0011fa01"); + } + + #[test] + fn to_array_works() { + // simple + let binary = HexBinary::from(&[1, 2, 3]); + let array: [u8; 3] = binary.to_array().unwrap(); + assert_eq!(array, [1, 2, 3]); + + // empty + let binary = HexBinary::from(&[]); + let array: [u8; 0] = binary.to_array().unwrap(); + assert_eq!(array, [] as [u8; 0]); + + // invalid size + let binary = HexBinary::from(&[1, 2, 3]); + let error = binary.to_array::<8>().unwrap_err(); + match error { + StdError::InvalidDataSize { expected, actual, .. } => { + assert_eq!(expected, 8); + assert_eq!(actual, 3); + }, + err => panic!("Unexpected error: {err:?}"), + } + + // long array (32 bytes) + let binary = + HexBinary::from_hex("b75d7d24e428c7859440498efe7caa3997cefb08c99bdd581b6b1f9f866096f0") + .unwrap(); + let array: [u8; 32] = binary.to_array().unwrap(); + assert_eq!( + array, + [ + 0xb7, 0x5d, 0x7d, 0x24, 0xe4, 0x28, 0xc7, 0x85, 0x94, 0x40, 0x49, 0x8e, 0xfe, 0x7c, + 0xaa, 0x39, 0x97, 0xce, 0xfb, 0x08, 0xc9, 0x9b, 0xdd, 0x58, 0x1b, 0x6b, 0x1f, 0x9f, + 0x86, 0x60, 0x96, 0xf0, + ] + ); + + // very long array > 32 bytes (requires Rust 1.47+) + let binary = HexBinary::from_hex( + "b75d7d24e428c7859440498efe7caa3997cefb08c99bdd581b6b1f9f866096f073c8c3b0316abe", + ) + .unwrap(); + let array: [u8; 39] = binary.to_array().unwrap(); + assert_eq!( + array, + [ + 0xb7, 0x5d, 0x7d, 0x24, 0xe4, 0x28, 0xc7, 0x85, 0x94, 0x40, 0x49, 0x8e, 0xfe, 0x7c, + 0xaa, 0x39, 0x97, 0xce, 0xfb, 0x08, 0xc9, 0x9b, 0xdd, 0x58, 0x1b, 0x6b, 0x1f, 0x9f, + 0x86, 0x60, 0x96, 0xf0, 0x73, 0xc8, 0xc3, 0xb0, 0x31, 0x6a, 0xbe, + ] + ); + } + + #[test] + fn from_json_works() { + let original: &[u8] = &[0u8, 187, 61, 11, 250, 0]; + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + } + + #[test] + fn from_fixed_length_array_works() { + let original = &[]; + let binary: HexBinary = original.into(); + assert_eq!(binary.len(), 0); + + let original = &[0u8]; + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8]); + + let original = &[0u8, 187, 61, 11, 250, 0]; + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + + let original = &[ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]; + let binary: HexBinary = original.into(); + assert_eq!( + binary.as_slice(), + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ] + ); + } + + #[test] + fn from_owned_fixed_length_array_works() { + let original = []; + let binary: HexBinary = original.into(); + assert_eq!(binary.len(), 0); + + let original = [0u8]; + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8]); + + let original = [0u8, 187, 61, 11, 250, 0]; + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + + let original = [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]; + let binary: HexBinary = original.into(); + assert_eq!( + binary.as_slice(), + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ] + ); + } + + #[test] + fn from_literal_works() { + let a: HexBinary = b"".into(); + assert_eq!(a.len(), 0); + + let a: HexBinary = b".".into(); + assert_eq!(a.len(), 1); + + let a: HexBinary = b"...".into(); + assert_eq!(a.len(), 3); + + let a: HexBinary = b"...............................".into(); + assert_eq!(a.len(), 31); + + let a: HexBinary = b"................................".into(); + assert_eq!(a.len(), 32); + + let a: HexBinary = (b".................................").into(); + assert_eq!(a.len(), 33); + } + + #[test] + fn from_vec_works() { + let original = vec![0u8, 187, 61, 11, 250, 0]; + let original_ptr = original.as_ptr(); + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(binary.0.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn into_vec_works() { + // Into> for HexBinary + let original = HexBinary(vec![0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.0.as_ptr(); + let vec: Vec = original.into(); + assert_eq!(vec.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(vec.as_ptr(), original_ptr, "vector must not be copied"); + + // From for Vec + let original = HexBinary(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = original.0.as_ptr(); + let vec = Vec::::from(original); + assert_eq!(vec.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(vec.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn from_binary_works() { + let original = Binary::from([0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.as_ptr(); + let binary: HexBinary = original.into(); + assert_eq!(binary.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(binary.0.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn into_binary_works() { + // Into for HexBinary + let original = HexBinary(vec![0u8, 187, 61, 11, 250, 0]); + let original_ptr = original.0.as_ptr(); + let bin: Binary = original.into(); + assert_eq!(bin.as_slice(), [0u8, 187, 61, 11, 250, 0]); + assert_eq!(bin.as_ptr(), original_ptr, "vector must not be copied"); + + // From for Binary + let original = HexBinary(vec![7u8, 35, 49, 101, 0, 255]); + let original_ptr = original.0.as_ptr(); + let bin = Binary::from(original); + assert_eq!(bin.as_slice(), [7u8, 35, 49, 101, 0, 255]); + assert_eq!(bin.as_ptr(), original_ptr, "vector must not be copied"); + } + + #[test] + fn serialization_works() { + let binary = HexBinary(vec![0u8, 187, 61, 11, 250, 0]); + + let json = serde_json::to_vec(&binary).unwrap(); + let deserialized: HexBinary = serde_json::from_slice(&json).unwrap(); + + assert_eq!(binary, deserialized); + } + + #[test] + fn deserialize_from_valid_string() { + let hex = "00bb3d0bfa00"; + // this is the binary behind above string + let expected = vec![0u8, 187, 61, 11, 250, 0]; + + let serialized = serde_json::to_vec(&hex).unwrap(); + let deserialized: HexBinary = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(expected, deserialized.as_slice()); + } + + #[test] + fn deserialize_from_invalid_string() { + let invalid_str = "**BAD!**"; + let serialized = serde_json::to_vec(&invalid_str).unwrap(); + let res = serde_json::from_slice::(&serialized); + assert!(res.is_err()); + } + + #[test] + fn hex_binary_implements_debug() { + // Some data + let data = HexBinary(vec![0x07, 0x35, 0xAA, 0xcb, 0x00, 0xff]); + assert_eq!(format!("{data:?}"), "HexBinary(0735aacb00ff)",); + + // Empty + let data = HexBinary(vec![]); + assert_eq!(format!("{data:?}"), "HexBinary()",); + } + + #[test] + fn hex_binary_implements_deref() { + // Dereference to [u8] + let data = HexBinary(vec![7u8, 35, 49, 101, 0, 255]); + assert_eq!(*data, [7u8, 35, 49, 101, 0, 255]); + + // This checks deref coercions from &Binary to &[u8] works + let data = HexBinary(vec![7u8, 35, 49, 101, 0, 255]); + assert_eq!(data.len(), 6); + let data_slice: &[u8] = &data; + assert_eq!(data_slice, &[7u8, 35, 49, 101, 0, 255]); + } + + #[test] + fn hex_binary_implements_as_ref() { + let want = &[7u8, 35, 49, 101, 0, 255]; + let data = HexBinary(want.to_vec()); + assert_eq!(want, AsRef::<[u8]>::as_ref(&data)); + assert_eq!(want, AsRef::<[u8]>::as_ref(&&data)); + } + + /// Tests that `HexBinary` implements `EQ` and `Hash` correctly and thus can + /// be used with hash maps and sets. + #[test] + fn hex_binary_implements_hash_eq() { + let a = HexBinary::from([0, 187, 61, 11, 250, 0]); + let b = HexBinary::from([16, 21, 33, 0, 255, 9]); + assert_hash_works!(a, b); + } + + #[test] + fn hex_binary_implements_partial_eq_with_vector() { + let a = HexBinary(vec![5u8; 3]); + let b = vec![5u8; 3]; + let c = vec![9u8; 3]; + assert_eq!(a, b); + assert_eq!(b, a); + assert_ne!(a, c); + assert_ne!(c, a); + } + + #[test] + fn hex_binary_implements_partial_eq_with_slice_and_array() { + let a = HexBinary(vec![0xAA, 0xBB]); + + // Slice: &[u8] + assert_eq!(a, b"\xAA\xBB" as &[u8]); + assert_eq!(b"\xAA\xBB" as &[u8], a); + assert_ne!(a, b"\x11\x22" as &[u8]); + assert_ne!(b"\x11\x22" as &[u8], a); + + // Array reference: &[u8; 2] + assert_eq!(a, b"\xAA\xBB"); + assert_eq!(b"\xAA\xBB", a); + assert_ne!(a, b"\x11\x22"); + assert_ne!(b"\x11\x22", a); + + // Array: [u8; 2] + assert_eq!(a, [0xAA, 0xBB]); + assert_eq!([0xAA, 0xBB], a); + assert_ne!(a, [0x11, 0x22]); + assert_ne!([0x11, 0x22], a); + } +} diff --git a/cosmwasm/std/src/ibc.rs b/cosmwasm/std/src/ibc.rs new file mode 100644 index 00000000..d335e9e1 --- /dev/null +++ b/cosmwasm/std/src/ibc.rs @@ -0,0 +1,869 @@ +// The CosmosMsg variants are defined in results/cosmos_msg.rs +// The rest of the IBC related functionality is defined here + +use core::cmp::{Ord, Ordering, PartialOrd}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + coin::Coin, + prelude::*, + results::{Attribute, CosmosMsg, Empty, Event, SubMsg}, + to_json_binary, Addr, Binary, StdResult, Timestamp, +}; + +mod callbacks; +mod transfer_msg_builder; + +pub use callbacks::*; +pub use transfer_msg_builder::*; + +/// These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts +/// (contracts that directly speak the IBC protocol via 6 entry points) +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcMsg { + /// Sends bank tokens owned by the contract to the given address on another chain. + /// The channel must already be established between the ibctransfer module on this chain + /// and a matching module on the remote chain. + /// We cannot select the port_id, this is whatever the local chain has bound the ibctransfer + /// module to. + Transfer { + /// existing channel to send the tokens over + channel_id: String, + /// address on the remote chain to receive these tokens + to_address: String, + /// packet data only supports one coin + /// https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20 + amount: Coin, + /// when packet times out, measured on remote chain + timeout: IbcTimeout, + /// An optional memo. See the blog post + /// ["Moving Beyond Simple Token Transfers"](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b) + /// for more information. + /// + /// There is no difference between setting this to `None` or an empty string. + /// + /// This field is only supported on chains with CosmWasm >= 2.0 and silently + /// ignored on older chains. + /// If you need support for both 1.x and 2.x chain with the same codebase, + /// it is recommended to use `CosmosMsg::Stargate` with a custom MsgTransfer + /// protobuf encoder instead. + memo: Option, + }, + /// Sends an IBC packet with given data over the existing channel. + /// Data should be encoded in a format defined by the channel version, + /// and the module on the other side should know how to parse this. + SendPacket { + channel_id: String, + data: Binary, + /// when packet times out, measured on remote chain + timeout: IbcTimeout, + }, + /// Acknowledges a packet that this contract received over IBC. + /// This allows acknowledging a packet that was not acknowledged yet in the + /// `ibc_packet_receive` call. + #[cfg(feature = "cosmwasm_2_1")] + WriteAcknowledgement { + /// Existing channel where the packet was received + channel_id: String, + /// Sequence number of the packet that was received + packet_sequence: u64, + /// The acknowledgement to send back + ack: IbcAcknowledgement, + }, + /// This will close an existing channel that is owned by this contract. + /// Port is auto-assigned to the contract's IBC port + CloseChannel { channel_id: String }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcEndpoint { + pub port_id: String, + pub channel_id: String, +} + +/// In IBC each package must set at least one type of timeout: +/// the timestamp or the block height. Using this rather complex enum instead of +/// two timeout fields we ensure that at least one timeout is set. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct IbcTimeout { + // use private fields to enforce the use of constructors, which ensure that at least one is set + block: Option, + timestamp: Option, +} + +impl IbcTimeout { + pub fn with_block(block: IbcTimeoutBlock) -> Self { + IbcTimeout { block: Some(block), timestamp: None } + } + + pub fn with_timestamp(timestamp: Timestamp) -> Self { + IbcTimeout { block: None, timestamp: Some(timestamp) } + } + + pub fn with_both(block: IbcTimeoutBlock, timestamp: Timestamp) -> Self { + IbcTimeout { block: Some(block), timestamp: Some(timestamp) } + } + + pub fn block(&self) -> Option { + self.block + } + + pub fn timestamp(&self) -> Option { + self.timestamp + } +} + +impl From for IbcTimeout { + fn from(timestamp: Timestamp) -> IbcTimeout { + IbcTimeout::with_timestamp(timestamp) + } +} + +impl From for IbcTimeout { + fn from(original: IbcTimeoutBlock) -> IbcTimeout { + IbcTimeout::with_block(original) + } +} + +// These are various messages used in the callbacks + +/// IbcChannel defines all information on a channel. +/// This is generally used in the hand-shake process, but can be queried directly. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcChannel { + pub endpoint: IbcEndpoint, + pub counterparty_endpoint: IbcEndpoint, + pub order: IbcOrder, + /// Note: in ibcv3 this may be "", in the IbcOpenChannel handshake messages + pub version: String, + /// The connection upon which this channel was created. If this is a multi-hop + /// channel, we only expose the first hop. + pub connection_id: String, +} + +impl IbcChannel { + /// Construct a new IbcChannel. + pub fn new( + endpoint: IbcEndpoint, + counterparty_endpoint: IbcEndpoint, + order: IbcOrder, + version: impl Into, + connection_id: impl Into, + ) -> Self { + Self { + endpoint, + counterparty_endpoint, + order, + version: version.into(), + connection_id: connection_id.into(), + } + } +} + +/// IbcOrder defines if a channel is ORDERED or UNORDERED +/// Values come from https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/core/channel/v1/channel.proto#L69-L80 +/// Naming comes from the protobuf files and go translations. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub enum IbcOrder { + #[serde(rename = "ORDER_UNORDERED")] + Unordered, + #[serde(rename = "ORDER_ORDERED")] + Ordered, +} + +/// IBCTimeoutHeight Height is a monotonically increasing data type +/// that can be compared against another Height for the purposes of updating and +/// freezing clients. +/// Ordering is (revision_number, timeout_height) +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcTimeoutBlock { + /// the version that the client is currently on + /// (e.g. after resetting the chain this could increment 1 as height drops to 0) + pub revision: u64, + /// block height after which the packet times out. + /// the height within the given revision + pub height: u64, +} + +impl IbcTimeoutBlock { + pub fn is_zero(&self) -> bool { + self.revision == 0 && self.height == 0 + } +} + +impl PartialOrd for IbcTimeoutBlock { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for IbcTimeoutBlock { + fn cmp(&self, other: &Self) -> Ordering { + match self.revision.cmp(&other.revision) { + Ordering::Equal => self.height.cmp(&other.height), + other => other, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcPacket { + /// The raw data sent from the other side in the packet + pub data: Binary, + /// identifies the channel and port on the sending chain. + pub src: IbcEndpoint, + /// identifies the channel and port on the receiving chain. + pub dest: IbcEndpoint, + /// The sequence number of the packet on the given channel + pub sequence: u64, + pub timeout: IbcTimeout, +} + +impl IbcPacket { + /// Construct a new IbcPacket. + pub fn new( + data: impl Into, + src: IbcEndpoint, + dest: IbcEndpoint, + sequence: u64, + timeout: IbcTimeout, + ) -> Self { + Self { data: data.into(), src, dest, sequence, timeout } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcAcknowledgement { + pub data: Binary, + // we may add more info here in the future (meta-data from the acknowledgement) + // there have been proposals to extend this type in core ibc for future versions +} + +impl IbcAcknowledgement { + pub fn new(data: impl Into) -> Self { + IbcAcknowledgement { data: data.into() } + } + + pub fn encode_json(data: &impl Serialize) -> StdResult { + Ok(IbcAcknowledgement { data: to_json_binary(data)? }) + } +} + +/// The message that is passed into `ibc_channel_open` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcChannelOpenMsg { + /// The ChanOpenInit step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + OpenInit { channel: IbcChannel }, + /// The ChanOpenTry step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + OpenTry { channel: IbcChannel, counterparty_version: String }, +} + +impl IbcChannelOpenMsg { + pub fn new_init(channel: IbcChannel) -> Self { + Self::OpenInit { channel } + } + + pub fn new_try(channel: IbcChannel, counterparty_version: impl Into) -> Self { + Self::OpenTry { channel, counterparty_version: counterparty_version.into() } + } + + pub fn channel(&self) -> &IbcChannel { + match self { + Self::OpenInit { channel } => channel, + Self::OpenTry { channel, .. } => channel, + } + } + + pub fn counterparty_version(&self) -> Option<&str> { + match self { + Self::OpenTry { counterparty_version, .. } => Some(counterparty_version), + _ => None, + } + } +} + +impl From for IbcChannel { + fn from(msg: IbcChannelOpenMsg) -> IbcChannel { + match msg { + IbcChannelOpenMsg::OpenInit { channel } => channel, + IbcChannelOpenMsg::OpenTry { channel, .. } => channel, + } + } +} + +/// This serializes either as `null` or a JSON object. +/// Within the response, a channel version can be specified. +/// If `null` is provided instead, the incoming channel version is accepted. +pub type IbcChannelOpenResponse = Option; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Ibc3ChannelOpenResponse { + /// We can set the channel version to a different one than we were called with + pub version: String, +} + +/// The message that is passed into `ibc_channel_connect` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcChannelConnectMsg { + /// The ChanOpenAck step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + OpenAck { channel: IbcChannel, counterparty_version: String }, + /// The ChanOpenConfirm step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + OpenConfirm { channel: IbcChannel }, +} + +impl IbcChannelConnectMsg { + pub fn new_ack(channel: IbcChannel, counterparty_version: impl Into) -> Self { + Self::OpenAck { channel, counterparty_version: counterparty_version.into() } + } + + pub fn new_confirm(channel: IbcChannel) -> Self { + Self::OpenConfirm { channel } + } + + pub fn channel(&self) -> &IbcChannel { + match self { + Self::OpenAck { channel, .. } => channel, + Self::OpenConfirm { channel } => channel, + } + } + + pub fn counterparty_version(&self) -> Option<&str> { + match self { + Self::OpenAck { counterparty_version, .. } => Some(counterparty_version), + _ => None, + } + } +} + +impl From for IbcChannel { + fn from(msg: IbcChannelConnectMsg) -> IbcChannel { + match msg { + IbcChannelConnectMsg::OpenAck { channel, .. } => channel, + IbcChannelConnectMsg::OpenConfirm { channel } => channel, + } + } +} + +/// The message that is passed into `ibc_channel_close` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcChannelCloseMsg { + /// The ChanCloseInit step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + CloseInit { channel: IbcChannel }, + /// The ChanCloseConfirm step from https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#channel-lifecycle-management + CloseConfirm { channel: IbcChannel }, // pub channel: IbcChannel, +} + +impl IbcChannelCloseMsg { + pub fn new_init(channel: IbcChannel) -> Self { + Self::CloseInit { channel } + } + + pub fn new_confirm(channel: IbcChannel) -> Self { + Self::CloseConfirm { channel } + } + + pub fn channel(&self) -> &IbcChannel { + match self { + Self::CloseInit { channel } => channel, + Self::CloseConfirm { channel } => channel, + } + } +} + +impl From for IbcChannel { + fn from(msg: IbcChannelCloseMsg) -> IbcChannel { + match msg { + IbcChannelCloseMsg::CloseInit { channel } => channel, + IbcChannelCloseMsg::CloseConfirm { channel } => channel, + } + } +} + +/// The message that is passed into `ibc_packet_receive` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcPacketReceiveMsg { + pub packet: IbcPacket, + pub relayer: Addr, +} + +impl IbcPacketReceiveMsg { + pub fn new(packet: IbcPacket, relayer: Addr) -> Self { + Self { packet, relayer } + } +} + +/// The message that is passed into `ibc_packet_ack` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcPacketAckMsg { + pub acknowledgement: IbcAcknowledgement, + pub original_packet: IbcPacket, + pub relayer: Addr, +} + +impl IbcPacketAckMsg { + pub fn new( + acknowledgement: IbcAcknowledgement, + original_packet: IbcPacket, + relayer: Addr, + ) -> Self { + Self { acknowledgement, original_packet, relayer } + } +} + +/// The message that is passed into `ibc_packet_timeout` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcPacketTimeoutMsg { + pub packet: IbcPacket, + pub relayer: Addr, +} + +impl IbcPacketTimeoutMsg { + pub fn new(packet: IbcPacket, relayer: Addr) -> Self { + Self { packet, relayer } + } +} + +/// This is the return value for the majority of the ibc handlers. +/// That are able to dispatch messages / events on their own, +/// but have no meaningful return value to the calling code. +/// +/// Callbacks that have return values (like receive_packet) +/// or that cannot redispatch messages (like the handshake callbacks) +/// will use other Response types +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcBasicResponse { + /// Optional list of messages to pass. These will be executed in order. + /// If the ReplyOn member is set, they will invoke this contract's `reply` entry point + /// after execution. Otherwise, they act like "fire and forget". + /// Use `SubMsg::new` to create messages with the older "fire and forget" semantics. + pub messages: Vec>, + /// The attributes that will be emitted as part of a `wasm` event. + /// + /// More info about events (and their attributes) can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub attributes: Vec, + /// Extra, custom events separate from the main `wasm` one. These will have + /// `wasm-` prepended to the type. + /// + /// More info about events can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub events: Vec, +} + +// Custom implementation in order to implement it for all `T`, even if `T` is not `Default`. +impl Default for IbcBasicResponse { + fn default() -> Self { + IbcBasicResponse { messages: vec![], attributes: vec![], events: vec![] } + } +} + +impl IbcBasicResponse { + pub fn new() -> Self { + Self::default() + } + + /// Add an attribute included in the main `wasm` event. + pub fn add_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.attributes.push(Attribute::new(key, value)); + self + } + + /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it, + /// and adds it to the list of messages to process. + pub fn add_message(mut self, msg: impl Into>) -> Self { + self.messages.push(SubMsg::new(msg)); + self + } + + /// This takes an explicit SubMsg (creates via e.g. `reply_on_error`) + /// and adds it to the list of messages to process. + pub fn add_submessage(mut self, msg: SubMsg) -> Self { + self.messages.push(msg); + self + } + + /// Adds an extra event to the response, separate from the main `wasm` event + /// that is always created. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided type + /// of event. + pub fn add_event(mut self, event: Event) -> Self { + self.events.push(event); + self + } + + /// Bulk add attributes included in the main `wasm` event. + /// + /// Anything that can be turned into an iterator and yields something + /// that can be converted into an `Attribute` is accepted. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{attr, IbcBasicResponse}; + /// + /// let attrs = vec![ + /// ("action", "reaction"), + /// ("answer", "42"), + /// ("another", "attribute"), + /// ]; + /// let res: IbcBasicResponse = IbcBasicResponse::new().add_attributes(attrs.clone()); + /// assert_eq!(res.attributes, attrs); + /// ``` + pub fn add_attributes>( + mut self, + attrs: impl IntoIterator, + ) -> Self { + self.attributes.extend(attrs.into_iter().map(A::into)); + self + } + + /// Bulk add "fire and forget" messages to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{CosmosMsg, IbcBasicResponse}; + /// + /// fn make_response_with_msgs(msgs: Vec) -> IbcBasicResponse { + /// IbcBasicResponse::new().add_messages(msgs) + /// } + /// ``` + pub fn add_messages>>(self, msgs: impl IntoIterator) -> Self { + self.add_submessages(msgs.into_iter().map(SubMsg::new)) + } + + /// Bulk add explicit SubMsg structs to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{SubMsg, IbcBasicResponse}; + /// + /// fn make_response_with_submsgs(msgs: Vec) -> IbcBasicResponse { + /// IbcBasicResponse::new().add_submessages(msgs) + /// } + /// ``` + pub fn add_submessages(mut self, msgs: impl IntoIterator>) -> Self { + self.messages.extend(msgs); + self + } + + /// Bulk add custom events to the response. These are separate from the main + /// `wasm` event. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided types + /// of events. + pub fn add_events(mut self, events: impl IntoIterator) -> Self { + self.events.extend(events); + self + } +} + +/// This defines the return value on packet response processing. +/// This "success" case should be returned even in application-level errors, +/// Where the acknowledgement bytes contain an encoded error message to be returned to +/// the calling chain. (Returning ContractResult::Err will abort processing of this packet +/// and not inform the calling chain). +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcReceiveResponse { + /// The bytes we return to the contract that sent the packet. + /// This may represent a success or error of execution. + /// In case of `None`, no acknowledgement is written. + pub acknowledgement: Option, + /// Optional list of messages to pass. These will be executed in order. + /// If the ReplyOn member is set, they will invoke this contract's `reply` entry point + /// after execution. Otherwise, they act like "fire and forget". + /// Use `call` or `msg.into()` to create messages with the older "fire and forget" semantics. + pub messages: Vec>, + /// The attributes that will be emitted as part of a "wasm" event. + /// + /// More info about events (and their attributes) can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub attributes: Vec, + /// Extra, custom events separate from the main `wasm` one. These will have + /// `wasm-` prepended to the type. + /// + /// More info about events can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub events: Vec, +} + +impl IbcReceiveResponse { + /// Create a new response with the given acknowledgement. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{ + /// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, + /// # Never, Response, QueryResponse, StdAck, IbcPacketReceiveMsg + /// # }; + /// use cosmwasm_std::IbcReceiveResponse; + /// + /// #[entry_point] + /// pub fn ibc_packet_receive( + /// deps: DepsMut, + /// env: Env, + /// msg: IbcPacketReceiveMsg, + /// ) -> Result { + /// // ... + /// + /// // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// Ok(IbcReceiveResponse::new(StdAck::success(b"\x01"))) + /// } + /// ``` + pub fn new(ack: impl Into) -> Self { + Self { + acknowledgement: Some(ack.into()), + messages: vec![], + attributes: vec![], + events: vec![], + } + } + + /// Creates a new response without an acknowledgement. + /// + /// This allows you to send the acknowledgement asynchronously later using + /// [`IbcMsg::WriteAcknowledgement`][self::IbcMsg#variant.WriteAcknowledgement]. + /// If you want to send the acknowledgement immediately, use [`IbcReceiveResponse::new`]. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{ + /// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, + /// # Never, Response, QueryResponse, StdAck, IbcPacketReceiveMsg + /// # }; + /// use cosmwasm_std::IbcReceiveResponse; + /// + /// #[entry_point] + /// pub fn ibc_packet_receive( + /// deps: DepsMut, + /// env: Env, + /// msg: IbcPacketReceiveMsg, + /// ) -> Result { + /// // ... + /// + /// Ok(IbcReceiveResponse::without_ack()) + /// } + /// ``` + pub fn without_ack() -> Self { + Self { acknowledgement: None, messages: vec![], attributes: vec![], events: vec![] } + } + + /// Add an attribute included in the main `wasm` event. + pub fn add_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.attributes.push(Attribute::new(key, value)); + self + } + + /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it, + /// and adds it to the list of messages to process. + pub fn add_message(mut self, msg: impl Into>) -> Self { + self.messages.push(SubMsg::new(msg)); + self + } + + /// This takes an explicit SubMsg (creates via e.g. `reply_on_error`) + /// and adds it to the list of messages to process. + pub fn add_submessage(mut self, msg: SubMsg) -> Self { + self.messages.push(msg); + self + } + + /// Adds an extra event to the response, separate from the main `wasm` event + /// that is always created. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided type + /// of event. + pub fn add_event(mut self, event: Event) -> Self { + self.events.push(event); + self + } + + /// Bulk add attributes included in the main `wasm` event. + /// + /// Anything that can be turned into an iterator and yields something + /// that can be converted into an `Attribute` is accepted. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{attr, IbcReceiveResponse, StdAck}; + /// + /// let attrs = vec![ + /// ("action", "reaction"), + /// ("answer", "42"), + /// ("another", "attribute"), + /// ]; + /// let res: IbcReceiveResponse = IbcReceiveResponse::new(StdAck::success(b"\x01")).add_attributes(attrs.clone()); + /// assert_eq!(res.attributes, attrs); + /// ``` + pub fn add_attributes>( + mut self, + attrs: impl IntoIterator, + ) -> Self { + self.attributes.extend(attrs.into_iter().map(A::into)); + self + } + + /// Bulk add "fire and forget" messages to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{CosmosMsg, IbcReceiveResponse, StdAck}; + /// + /// fn make_response_with_msgs(msgs: Vec) -> IbcReceiveResponse { + /// IbcReceiveResponse::new(StdAck::success(b"\x01")).add_messages(msgs) + /// } + /// ``` + pub fn add_messages>>(self, msgs: impl IntoIterator) -> Self { + self.add_submessages(msgs.into_iter().map(SubMsg::new)) + } + + /// Bulk add explicit SubMsg structs to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{SubMsg, StdAck, IbcReceiveResponse}; + /// + /// fn make_response_with_submsgs(msgs: Vec) -> IbcReceiveResponse { + /// IbcReceiveResponse::new(StdAck::success(b"\x01")).add_submessages(msgs) + /// } + /// ``` + pub fn add_submessages(mut self, msgs: impl IntoIterator>) -> Self { + self.messages.extend(msgs); + self + } + + /// Bulk add custom events to the response. These are separate from the main + /// `wasm` event. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided types + /// of events. + pub fn add_events(mut self, events: impl IntoIterator) -> Self { + self.events.extend(events); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json_wasm::to_string; + + #[test] + // added this to check json format for go compat, as I was unsure how some messages are snake + // encoded + fn serialize_msg() { + let msg = IbcMsg::Transfer { + channel_id: "channel-123".to_string(), + to_address: "my-special-addr".into(), + amount: Coin::new(12345678u128, "uatom"), + timeout: IbcTimeout::with_timestamp(Timestamp::from_nanos(1234567890)), + memo: None, + }; + let encoded = to_string(&msg).unwrap(); + let expected = r#"{"transfer":{"channel_id":"channel-123","to_address":"my-special-addr","amount":{"denom":"uatom","amount":"12345678"},"timeout":{"block":null,"timestamp":"1234567890"},"memo":null}}"#; + assert_eq!(encoded.as_str(), expected); + } + + #[test] + fn ibc_timeout_serialize() { + let timestamp = IbcTimeout::with_timestamp(Timestamp::from_nanos(684816844)); + let expected = r#"{"block":null,"timestamp":"684816844"}"#; + assert_eq!(to_string(×tamp).unwrap(), expected); + + let block = IbcTimeout::with_block(IbcTimeoutBlock { revision: 12, height: 129 }); + let expected = r#"{"block":{"revision":12,"height":129},"timestamp":null}"#; + assert_eq!(to_string(&block).unwrap(), expected); + + let both = IbcTimeout::with_both( + IbcTimeoutBlock { revision: 12, height: 129 }, + Timestamp::from_nanos(684816844), + ); + let expected = r#"{"block":{"revision":12,"height":129},"timestamp":"684816844"}"#; + assert_eq!(to_string(&both).unwrap(), expected); + } + + #[test] + #[allow(clippy::eq_op)] + fn ibc_timeout_block_ord() { + let epoch1a = IbcTimeoutBlock { revision: 1, height: 1000 }; + let epoch1b = IbcTimeoutBlock { revision: 1, height: 3000 }; + let epoch2a = IbcTimeoutBlock { revision: 2, height: 500 }; + let epoch2b = IbcTimeoutBlock { revision: 2, height: 2500 }; + + // basic checks + assert_eq!(epoch1a, epoch1a); + assert!(epoch1a < epoch1b); + assert!(epoch1b > epoch1a); + assert!(epoch2a > epoch1a); + assert!(epoch2b > epoch1a); + + // ensure epoch boundaries are correctly handled + assert!(epoch1b > epoch1a); + assert!(epoch2a > epoch1b); + assert!(epoch2b > epoch2a); + assert!(epoch2b > epoch1b); + // and check the inverse compare + assert!(epoch1a < epoch1b); + assert!(epoch1b < epoch2a); + assert!(epoch2a < epoch2b); + assert!(epoch1b < epoch2b); + } + + #[test] + fn ibc_packet_serialize() { + let packet = IbcPacket { + data: b"foo".into(), + src: IbcEndpoint { + port_id: "their-port".to_string(), + channel_id: "channel-1234".to_string(), + }, + dest: IbcEndpoint { port_id: "our-port".to_string(), channel_id: "chan33".into() }, + sequence: 27, + timeout: IbcTimeout::with_both( + IbcTimeoutBlock { revision: 1, height: 12345678 }, + Timestamp::from_nanos(4611686018427387904), + ), + }; + let expected = r#"{"data":"Zm9v","src":{"port_id":"their-port","channel_id":"channel-1234"},"dest":{"port_id":"our-port","channel_id":"chan33"},"sequence":27,"timeout":{"block":{"revision":1,"height":12345678},"timestamp":"4611686018427387904"}}"#; + assert_eq!(to_string(&packet).unwrap(), expected); + + let no_timestamp = IbcPacket { + data: b"foo".into(), + src: IbcEndpoint { + port_id: "their-port".to_string(), + channel_id: "channel-1234".to_string(), + }, + dest: IbcEndpoint { port_id: "our-port".to_string(), channel_id: "chan33".into() }, + sequence: 27, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { revision: 1, height: 12345678 }), + }; + let expected = r#"{"data":"Zm9v","src":{"port_id":"their-port","channel_id":"channel-1234"},"dest":{"port_id":"our-port","channel_id":"chan33"},"sequence":27,"timeout":{"block":{"revision":1,"height":12345678},"timestamp":null}}"#; + assert_eq!(to_string(&no_timestamp).unwrap(), expected); + } +} diff --git a/cosmwasm/std/src/ibc/callbacks.rs b/cosmwasm/std/src/ibc/callbacks.rs new file mode 100644 index 00000000..ae3451b6 --- /dev/null +++ b/cosmwasm/std/src/ibc/callbacks.rs @@ -0,0 +1,222 @@ +//! This module contains types for the IBC callbacks defined in +//! [ADR-8](https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-008-app-caller-cbs.md). + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Addr, IbcAcknowledgement, IbcPacket, Uint64}; + +/// This is just a type representing the data that has to be sent with the IBC message to receive +/// callbacks. It should be serialized and sent with the IBC message. +/// The specific field and format to send it in can vary depending on the IBC message, +/// but is usually the `memo` field by convention. +/// +/// See [`IbcSourceCallbackMsg`] and [`IbcDestinationCallbackMsg`] for more details. +/// +/// # Example +/// +/// Using [`TransferMsgBuilder`](crate::TransferMsgBuilder): +/// ```rust +/// use cosmwasm_std::{ +/// to_json_string, Coin, IbcCallbackRequest, TransferMsgBuilder, IbcSrcCallback, IbcTimeout, Response, +/// Timestamp, +/// }; +/// # use cosmwasm_std::testing::mock_env; +/// # let env = mock_env(); +/// +/// let _msg = TransferMsgBuilder::new( +/// "channel-0".to_string(), +/// "cosmos1example".to_string(), +/// Coin::new(10u32, "ucoin"), +/// Timestamp::from_seconds(12345), +/// ) +/// .with_src_callback(IbcSrcCallback { +/// address: env.contract.address, +/// gas_limit: None, +/// }) +/// .build(); +/// ``` +/// +/// Manual serialization: +/// ```rust +/// use cosmwasm_std::{ +/// to_json_string, Coin, IbcCallbackRequest, IbcMsg, IbcSrcCallback, IbcTimeout, Response, +/// Timestamp, +/// }; +/// # use cosmwasm_std::testing::mock_env; +/// # let env = mock_env(); +/// +/// let _transfer = IbcMsg::Transfer { +/// to_address: "cosmos1example".to_string(), +/// channel_id: "channel-0".to_string(), +/// amount: Coin::new(10u32, "ucoin"), +/// timeout: Timestamp::from_seconds(12345).into(), +/// memo: Some(to_json_string(&IbcCallbackRequest::source(IbcSrcCallback { +/// address: env.contract.address, +/// gas_limit: None, +/// })).unwrap()), +/// }; +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcCallbackRequest { + // using private fields to force use of the constructors + #[serde(skip_serializing_if = "Option::is_none")] + src_callback: Option, + #[serde(skip_serializing_if = "Option::is_none")] + dest_callback: Option, +} + +impl IbcCallbackRequest { + /// Use this if you want to execute callbacks on both the source and destination chain. + pub fn both(src_callback: IbcSrcCallback, dest_callback: IbcDstCallback) -> Self { + IbcCallbackRequest { src_callback: Some(src_callback), dest_callback: Some(dest_callback) } + } + + /// Use this if you want to execute callbacks on the source chain, but not the destination + /// chain. + pub fn source(src_callback: IbcSrcCallback) -> Self { + IbcCallbackRequest { src_callback: Some(src_callback), dest_callback: None } + } + + /// Use this if you want to execute callbacks on the destination chain, but not the source + /// chain. + pub fn destination(dest_callback: IbcDstCallback) -> Self { + IbcCallbackRequest { src_callback: None, dest_callback: Some(dest_callback) } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcSrcCallback { + /// The source chain address that should receive the callback. + /// For CosmWasm contracts, this *must* be `env.contract.address`. + /// Other addresses are not allowed and will effectively be ignored. + pub address: Addr, + /// Optional gas limit for the callback (in Cosmos SDK gas units) + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcDstCallback { + /// The destination chain address that should receive the callback. + pub address: String, + /// Optional gas limit for the callback (in Cosmos SDK gas units) + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, +} + +/// The type of IBC source callback that is being called. +/// +/// IBC source callbacks are needed for cases where your contract triggers the sending of an +/// IBC packet through some other message (i.e. not through [`crate::IbcMsg::SendPacket`]) and needs +/// to know whether or not the packet was successfully received on the other chain. +/// A prominent example is the [`crate::IbcMsg::Transfer`] message. Without callbacks, you cannot +/// know whether the transfer was successful or not. +/// +/// Note that there are some prerequisites that need to be fulfilled to receive source callbacks: +/// - The contract must implement the `ibc_source_callback` entrypoint. +/// - The IBC application in the source chain must have support for the callbacks middleware. +/// - You have to add serialized [`IbcCallbackRequest`] to a specific field of the message. For +/// `IbcMsg::Transfer`, this is the `memo` field and it needs to be json-encoded. +/// - The receiver of the callback must also be the sender of the message. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcSourceCallbackMsg { + Acknowledgement(IbcAckCallbackMsg), + Timeout(IbcTimeoutCallbackMsg), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcAckCallbackMsg { + pub acknowledgement: IbcAcknowledgement, + pub original_packet: IbcPacket, + pub relayer: Addr, +} + +impl IbcAckCallbackMsg { + pub fn new( + acknowledgement: IbcAcknowledgement, + original_packet: IbcPacket, + relayer: Addr, + ) -> Self { + Self { acknowledgement, original_packet, relayer } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct IbcTimeoutCallbackMsg { + pub packet: IbcPacket, + pub relayer: Addr, +} + +impl IbcTimeoutCallbackMsg { + pub fn new(packet: IbcPacket, relayer: Addr) -> Self { + Self { packet, relayer } + } +} + +/// The message type of the IBC destination callback. +/// +/// The IBC destination callback is needed for cases where someone triggers the sending of an +/// IBC packet through some other message (i.e. not through [`crate::IbcMsg::SendPacket`]) and +/// your contract needs to know that it received this. +/// A prominent example is the [`crate::IbcMsg::Transfer`] message. Without callbacks, you cannot +/// know that someone sent you IBC coins. +/// +/// It is important to validate that the packet and acknowledgement are what you expect them to be. +/// For example for a transfer message, the receiver is not necessarily the contract itself. +/// +/// The callback is called when the packet is being acknowledged on the destination chain. +/// This happens for both synchronous and asynchronous acknowledgements. +/// +/// Note that there are some prerequisites that need to be fulfilled to receive destination +/// callbacks: +/// - The contract must implement the `ibc_destination_callback` entrypoint. +/// - The IBC application in the destination chain must have support for the callbacks middleware. +/// - You have to add serialized [`IbcCallbackRequest`] to a specific field of the message. For +/// `IbcMsg::Transfer`, this is the `memo` field and it needs to be json-encoded. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct IbcDestinationCallbackMsg { + pub packet: IbcPacket, + pub ack: IbcAcknowledgement, +} + +#[cfg(test)] +mod tests { + use crate::to_json_string; + + use super::*; + + #[test] + fn ibc_callback_data_serialization() { + let mut data = IbcCallbackRequest::both( + IbcSrcCallback { + address: Addr::unchecked("src_address"), + gas_limit: Some(123u64.into()), + }, + IbcDstCallback { address: "dst_address".to_string(), gas_limit: Some(1234u64.into()) }, + ); + + // both + let json = to_json_string(&data).unwrap(); + assert_eq!( + json, + r#"{"src_callback":{"address":"src_address","gas_limit":"123"},"dest_callback":{"address":"dst_address","gas_limit":"1234"}}"# + ); + + // dst only, without gas limit + let mut src = data.src_callback.take().unwrap(); + data.dest_callback.as_mut().unwrap().gas_limit = None; + let json = to_json_string(&data).unwrap(); + assert_eq!(json, r#"{"dest_callback":{"address":"dst_address"}}"#); + + // source only, without gas limit + src.gas_limit = None; + data.src_callback = Some(src); + data.dest_callback = None; + let json = to_json_string(&data).unwrap(); + assert_eq!(json, r#"{"src_callback":{"address":"src_address"}}"#); + } +} diff --git a/cosmwasm/std/src/ibc/transfer_msg_builder.rs b/cosmwasm/std/src/ibc/transfer_msg_builder.rs new file mode 100644 index 00000000..129d4a3a --- /dev/null +++ b/cosmwasm/std/src/ibc/transfer_msg_builder.rs @@ -0,0 +1,281 @@ +use crate::{ + prelude::*, to_json_string, Coin, IbcCallbackRequest, IbcDstCallback, IbcMsg, IbcSrcCallback, + IbcTimeout, +}; + +// these are the different memo types and at the same time the states +// the TransferMsgBuilder can be in +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EmptyMemo; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WithMemo { + memo: String, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WithSrcCallback { + src_callback: IbcSrcCallback, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WithDstCallback { + dst_callback: IbcDstCallback, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WithCallbacks { + src_callback: IbcSrcCallback, + dst_callback: IbcDstCallback, +} + +pub trait MemoSource { + fn into_memo(self) -> Option; +} + +impl MemoSource for EmptyMemo { + fn into_memo(self) -> Option { + None + } +} + +impl MemoSource for WithMemo { + fn into_memo(self) -> Option { + Some(self.memo) + } +} + +impl MemoSource for WithSrcCallback { + fn into_memo(self) -> Option { + Some(to_json_string(&IbcCallbackRequest::source(self.src_callback)).unwrap()) + } +} + +impl MemoSource for WithDstCallback { + fn into_memo(self) -> Option { + Some(to_json_string(&IbcCallbackRequest::destination(self.dst_callback)).unwrap()) + } +} + +impl MemoSource for WithCallbacks { + fn into_memo(self) -> Option { + Some( + to_json_string(&IbcCallbackRequest::both(self.src_callback, self.dst_callback)) + .unwrap(), + ) + } +} + +impl TransferMsgBuilder { + pub fn build(self) -> IbcMsg { + IbcMsg::Transfer { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: self.memo.into_memo(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TransferMsgBuilder { + channel_id: String, + to_address: String, + amount: Coin, + timeout: IbcTimeout, + memo: MemoData, +} + +impl TransferMsgBuilder { + /// Creates a new transfer message with the given parameters and no memo. + pub fn new( + channel_id: impl Into, + to_address: impl Into, + amount: Coin, + timeout: impl Into, + ) -> Self { + Self { + channel_id: channel_id.into(), + to_address: to_address.into(), + amount, + timeout: timeout.into(), + memo: EmptyMemo, + } + } + + /// Adds a memo text to the transfer message. + pub fn with_memo(self, memo: impl Into) -> TransferMsgBuilder { + TransferMsgBuilder { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: WithMemo { memo: memo.into() }, + } + } + + /// Adds an IBC source callback entry to the memo field. + /// Use this if you want to receive IBC callbacks on the source chain. + /// + /// For more info check out [`crate::IbcSourceCallbackMsg`]. + pub fn with_src_callback( + self, + src_callback: IbcSrcCallback, + ) -> TransferMsgBuilder { + TransferMsgBuilder { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: WithSrcCallback { src_callback }, + } + } + + /// Adds an IBC destination callback entry to the memo field. + /// Use this if you want to receive IBC callbacks on the destination chain. + /// + /// For more info check out [`crate::IbcDestinationCallbackMsg`]. + pub fn with_dst_callback( + self, + dst_callback: IbcDstCallback, + ) -> TransferMsgBuilder { + TransferMsgBuilder { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: WithDstCallback { dst_callback }, + } + } +} + +impl TransferMsgBuilder { + /// Adds an IBC destination callback entry to the memo field. + /// Use this if you want to receive IBC callbacks on the destination chain. + /// + /// For more info check out [`crate::IbcDestinationCallbackMsg`]. + pub fn with_dst_callback( + self, + dst_callback: IbcDstCallback, + ) -> TransferMsgBuilder { + TransferMsgBuilder { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: WithCallbacks { src_callback: self.memo.src_callback, dst_callback }, + } + } +} + +impl TransferMsgBuilder { + /// Adds an IBC source callback entry to the memo field. + /// Use this if you want to receive IBC callbacks on the source chain. + /// + /// For more info check out [`crate::IbcSourceCallbackMsg`]. + pub fn with_src_callback( + self, + src_callback: IbcSrcCallback, + ) -> TransferMsgBuilder { + TransferMsgBuilder { + channel_id: self.channel_id, + to_address: self.to_address, + amount: self.amount, + timeout: self.timeout, + memo: WithCallbacks { src_callback, dst_callback: self.memo.dst_callback }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{coin, Addr, Timestamp, Uint64}; + + use super::*; + + #[test] + fn test_transfer_msg_builder() { + let src_callback = + IbcSrcCallback { address: Addr::unchecked("src"), gas_limit: Some(Uint64::new(12345)) }; + let dst_callback = IbcDstCallback { address: "dst".to_string(), gas_limit: None }; + + let empty_memo_builder = TransferMsgBuilder::new( + "channel-0", + "cosmos1example", + coin(10, "ucoin"), + Timestamp::from_seconds(12345), + ); + + let empty = empty_memo_builder.clone().build(); + let with_memo = empty_memo_builder.clone().with_memo("memo").build(); + + let with_src_callback_builder = + empty_memo_builder.clone().with_src_callback(src_callback.clone()); + let with_src_callback = with_src_callback_builder.clone().build(); + let with_dst_callback_builder = + empty_memo_builder.clone().with_dst_callback(dst_callback.clone()); + let with_dst_callback = with_dst_callback_builder.clone().build(); + + let with_both_callbacks1 = + with_src_callback_builder.with_dst_callback(dst_callback.clone()).build(); + + let with_both_callbacks2 = + with_dst_callback_builder.with_src_callback(src_callback.clone()).build(); + + // assert all the different messages + assert_eq!( + empty, + IbcMsg::Transfer { + channel_id: "channel-0".to_string(), + to_address: "cosmos1example".to_string(), + amount: coin(10, "ucoin"), + timeout: Timestamp::from_seconds(12345).into(), + memo: None, + } + ); + assert_eq!( + with_memo, + IbcMsg::Transfer { + channel_id: "channel-0".to_string(), + to_address: "cosmos1example".to_string(), + amount: coin(10, "ucoin"), + timeout: Timestamp::from_seconds(12345).into(), + memo: Some("memo".to_string()), + } + ); + assert_eq!( + with_src_callback, + IbcMsg::Transfer { + channel_id: "channel-0".to_string(), + to_address: "cosmos1example".to_string(), + amount: coin(10, "ucoin"), + timeout: Timestamp::from_seconds(12345).into(), + memo: Some( + to_json_string(&IbcCallbackRequest::source(src_callback.clone())).unwrap() + ), + } + ); + assert_eq!( + with_dst_callback, + IbcMsg::Transfer { + channel_id: "channel-0".to_string(), + to_address: "cosmos1example".to_string(), + amount: coin(10, "ucoin"), + timeout: Timestamp::from_seconds(12345).into(), + memo: Some( + to_json_string(&IbcCallbackRequest::destination(dst_callback.clone())).unwrap() + ), + } + ); + assert_eq!( + with_both_callbacks1, + IbcMsg::Transfer { + channel_id: "channel-0".to_string(), + to_address: "cosmos1example".to_string(), + amount: coin(10, "ucoin"), + timeout: Timestamp::from_seconds(12345).into(), + memo: Some( + to_json_string(&IbcCallbackRequest::both(src_callback, dst_callback)).unwrap() + ), + } + ); + assert_eq!(with_both_callbacks1, with_both_callbacks2); + } +} diff --git a/cosmwasm/std/src/import_helpers.rs b/cosmwasm/std/src/import_helpers.rs new file mode 100644 index 00000000..457285f8 --- /dev/null +++ b/cosmwasm/std/src/import_helpers.rs @@ -0,0 +1,30 @@ +/// Returns the four most significant bytes +#[allow(dead_code)] // only used in Wasm builds +#[inline] +pub fn from_high_half(data: u64) -> u32 { + (data >> 32).try_into().unwrap() +} + +/// Returns the four least significant bytes +#[allow(dead_code)] // only used in Wasm builds +#[inline] +pub fn from_low_half(data: u64) -> u32 { + (data & 0xFFFFFFFF).try_into().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_high_half_works() { + assert_eq!(from_high_half(0), 0); + assert_eq!(from_high_half(0x1122334455667788), 0x11223344); + } + + #[test] + fn from_low_haf_works() { + assert_eq!(from_low_half(0), 0); + assert_eq!(from_low_half(0x1122334455667788), 0x55667788); + } +} diff --git a/cosmwasm/std/src/imports.rs b/cosmwasm/std/src/imports.rs new file mode 100644 index 00000000..1e8bec77 --- /dev/null +++ b/cosmwasm/std/src/imports.rs @@ -0,0 +1,732 @@ +use crate::prelude::*; + +#[cfg(feature = "iterator")] +use crate::sections::decode_sections2; +use crate::{ + import_helpers::{from_high_half, from_low_half}, + memory::{Owned, Region}, + results::SystemResult, + sections::encode_sections, + serde::from_json, + traits::{Api, Querier, QuerierResult, Storage}, + Addr, CanonicalAddr, RecoverPubkeyError, StdError, StdResult, SystemError, VerificationError, +}; +#[cfg(feature = "iterator")] +use crate::{ + iterator::{Order, Record}, + memory::get_optional_region_address, +}; +#[cfg(feature = "cosmwasm_2_1")] +use crate::{AggregationError, HashFunction, PairingEqualityError}; + +/// An upper bound for typical canonical address lengths (e.g. 20 in Cosmos SDK/Ethereum or 32 in +/// Nano/Substrate) +const CANONICAL_ADDRESS_BUFFER_LENGTH: usize = 64; +/// An upper bound for typical human readable address formats (e.g. 42 for Ethereum hex addresses or +/// 90 for bech32) +const HUMAN_ADDRESS_BUFFER_LENGTH: usize = 90; + +// This interface will compile into required Wasm imports. +// A complete documentation those functions is available in the VM that provides them: +// https://github.com/CosmWasm/cosmwasm/blob/v1.0.0-beta/packages/vm/src/instance.rs#L89-L206 +extern "C" { + #[cfg(feature = "abort")] + fn abort(source_ptr: u32); + + fn db_read(key: u32) -> u32; + fn db_write(key: u32, value: u32); + fn db_remove(key: u32); + + // scan creates an iterator, which can be read by consecutive next() calls + #[cfg(feature = "iterator")] + fn db_scan(start_ptr: u32, end_ptr: u32, order: i32) -> u32; + #[cfg(feature = "iterator")] + fn db_next(iterator_id: u32) -> u32; + #[cfg(all(feature = "iterator", feature = "cosmwasm_1_4"))] + fn db_next_key(iterator_id: u32) -> u32; + #[cfg(all(feature = "iterator", feature = "cosmwasm_1_4"))] + fn db_next_value(iterator_id: u32) -> u32; + + fn addr_validate(source_ptr: u32) -> u32; + fn addr_canonicalize(source_ptr: u32, destination_ptr: u32) -> u32; + fn addr_humanize(source_ptr: u32, destination_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_aggregate_g1(g1s_ptr: u32, out_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_aggregate_g2(g2s_ptr: u32, out_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_pairing_equality(ps_ptr: u32, qs_ptr: u32, r_ptr: u32, s_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_hash_to_g1(hash_function: u32, msg_ptr: u32, dst_ptr: u32, out_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_hash_to_g2(hash_function: u32, msg_ptr: u32, dst_ptr: u32, out_ptr: u32) -> u32; + + /// Verifies message hashes against a signature with a public key, using the + /// secp256k1 ECDSA parametrization. + /// Returns 0 on verification success, 1 on verification failure, and values + /// greater than 1 in case of error. + fn secp256k1_verify(message_hash_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + + fn secp256k1_recover_pubkey( + message_hash_ptr: u32, + signature_ptr: u32, + recovery_param: u32, + ) -> u64; + + /// Verifies message hashes against a signature with a public key, using the + /// secp256r1 ECDSA parametrization. + /// Returns 0 on verification success, 1 on verification failure, and values + /// greater than 1 in case of error. + #[cfg(feature = "cosmwasm_2_1")] + fn secp256r1_verify(message_hash_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + + #[cfg(feature = "cosmwasm_2_1")] + fn secp256r1_recover_pubkey( + message_hash_ptr: u32, + signature_ptr: u32, + recovery_param: u32, + ) -> u64; + + /// Verifies a message against a signature with a public key, using the + /// ed25519 EdDSA scheme. + /// Returns 0 on verification success, 1 on verification failure, and values + /// greater than 1 in case of error. + fn ed25519_verify(message_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + + /// Verifies a batch of messages against a batch of signatures and public keys, using the + /// ed25519 EdDSA scheme. + /// Returns 0 on verification success, 1 on verification failure, and values + /// greater than 1 in case of error. + fn ed25519_batch_verify(messages_ptr: u32, signatures_ptr: u32, public_keys_ptr: u32) -> u32; + + /// Writes a debug message (UFT-8 encoded) to the host for debugging purposes. + /// The host is free to log or process this in any way it considers appropriate. + /// In production environments it is expected that those messages are discarded. + fn debug(source_ptr: u32); + + /// Executes a query on the chain (import). Not to be confused with the + /// query export, which queries the state of the contract. + fn query_chain(request: u32) -> u32; +} + +/// A stateless convenience wrapper around database imports provided by the VM. +/// This cannot be cloned as it would not copy any data. If you need to clone this, it indicates a +/// flaw in your logic. +pub struct ExternalStorage {} + +impl ExternalStorage { + pub fn new() -> ExternalStorage { + ExternalStorage {} + } +} + +impl Storage for ExternalStorage { + fn get(&self, key: &[u8]) -> Option> { + let key = Region::from_slice(key); + let key_ptr = key.as_ptr() as u32; + + let read = unsafe { db_read(key_ptr) }; + if read == 0 { + // key does not exist in external storage + return None; + } + + let value_ptr = read as *mut Region; + let data = unsafe { Region::from_heap_ptr(value_ptr) }; + + Some(data.into_vec()) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + if value.is_empty() { + panic!("TL;DR: Value must not be empty in Storage::set but in most cases you can use Storage::remove instead. Long story: Getting empty values from storage is not well supported at the moment. Some of our internal interfaces cannot differentiate between a non-existent key and an empty value. Right now, you cannot rely on the behaviour of empty values. To protect you from trouble later on, we stop here. Sorry for the inconvenience! We highly welcome you to contribute to CosmWasm, making this more solid one way or the other."); + } + + let key = Region::from_slice(key); + let key_ptr = key.as_ptr() as u32; + + let value = Region::from_slice(value); + let value_ptr = value.as_ptr() as u32; + + unsafe { db_write(key_ptr, value_ptr) }; + } + + fn remove(&mut self, key: &[u8]) { + let key = Region::from_slice(key); + let key_ptr = key.as_ptr() as u32; + + unsafe { db_remove(key_ptr) }; + } + + #[cfg(feature = "iterator")] + fn range( + &self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> { + let iterator_id = create_iter(start, end, order); + let iter = ExternalIterator { iterator_id }; + Box::new(iter) + } + + #[cfg(all(feature = "cosmwasm_1_4", feature = "iterator"))] + fn range_keys<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + let iterator_id = create_iter(start, end, order); + let iter = ExternalPartialIterator { iterator_id, partial_type: PartialType::Keys }; + Box::new(iter) + } + + #[cfg(all(feature = "cosmwasm_1_4", feature = "iterator"))] + fn range_values<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + let iterator_id = create_iter(start, end, order); + let iter = ExternalPartialIterator { iterator_id, partial_type: PartialType::Values }; + Box::new(iter) + } +} + +#[cfg(feature = "iterator")] +fn create_iter(start: Option<&[u8]>, end: Option<&[u8]>, order: Order) -> u32 { + // There is lots of gotchas on turning options into regions for FFI, thus this design + // See: https://github.com/CosmWasm/cosmwasm/pull/509 + let start_region = start.map(Region::from_slice); + let end_region = end.map(Region::from_slice); + let start_region_addr = get_optional_region_address(&start_region.as_ref()); + let end_region_addr = get_optional_region_address(&end_region.as_ref()); + unsafe { db_scan(start_region_addr, end_region_addr, order as i32) } +} + +#[cfg(all(feature = "cosmwasm_1_4", feature = "iterator"))] +enum PartialType { + Keys, + Values, +} + +/// ExternalPartialIterator makes a call out to `next_key` or `next_value` +/// depending on its `partial_type`. +/// Compared to `ExternalIterator`, it allows iterating only over the keys or +/// values instead of both. +#[cfg(all(feature = "cosmwasm_1_4", feature = "iterator"))] +struct ExternalPartialIterator { + iterator_id: u32, + partial_type: PartialType, +} + +#[cfg(all(feature = "cosmwasm_1_4", feature = "iterator"))] +impl Iterator for ExternalPartialIterator { + type Item = Vec; + + /// The default implementation calls `next` repeatedly, + /// which we can do a little more efficiently by using `db_next_key` instead. + /// It is used by `skip`, so it allows cheaper skipping. + #[cfg(feature = "cosmwasm_1_4")] + fn nth(&mut self, n: usize) -> Option { + skip_iter(self.iterator_id, n); + self.next() + } + + fn next(&mut self) -> Option { + // here we differentiate between the two types + let next_result = match self.partial_type { + PartialType::Keys => unsafe { db_next_key(self.iterator_id) }, + PartialType::Values => unsafe { db_next_value(self.iterator_id) }, + }; + + if next_result == 0 { + // iterator is done + return None; + } + + let data_region = next_result as *mut Region; + let data = unsafe { Region::from_heap_ptr(data_region) }; + + Some(data.into_vec()) + } +} + +#[cfg(feature = "iterator")] +/// ExternalIterator makes a call out to next. +/// We use the pointer to differentiate between multiple open iterators. +struct ExternalIterator { + iterator_id: u32, +} + +#[cfg(feature = "iterator")] +impl Iterator for ExternalIterator { + type Item = Record; + + /// The default implementation calls `next` repeatedly, + /// which we can do a little more efficiently by using `db_next_key` instead. + /// It is used by `skip`, so it allows cheaper skipping. + #[cfg(feature = "cosmwasm_1_4")] + fn nth(&mut self, n: usize) -> Option { + skip_iter(self.iterator_id, n); + self.next() + } + + fn next(&mut self) -> Option { + let next_result = unsafe { db_next(self.iterator_id) }; + let kv_region_ptr = next_result as *mut Region; + let kv = unsafe { Region::from_heap_ptr(kv_region_ptr) }; + + let (key, value) = decode_sections2(kv.into_vec()); + + if key.len() == 0 { + None + } else { + Some((key, value)) + } + } +} + +/// Helper function to skip `count` elements of an iterator. +#[cfg(all(feature = "iterator", feature = "cosmwasm_1_4"))] +fn skip_iter(iter_id: u32, count: usize) { + for _ in 0..count { + let region = unsafe { db_next_key(iter_id) }; + if region == 0 { + // early return + return; + } + + // just deallocate the region + unsafe { Region::from_heap_ptr(region as *mut Region) }; + } +} + +/// A stateless convenience wrapper around imports provided by the VM +#[derive(Copy, Clone)] +pub struct ExternalApi {} + +impl ExternalApi { + pub fn new() -> ExternalApi { + ExternalApi {} + } +} + +impl Api for ExternalApi { + fn addr_validate(&self, input: &str) -> StdResult { + let input_bytes = input.as_bytes(); + if input_bytes.len() > 256 { + // See MAX_LENGTH_HUMAN_ADDRESS in the VM. + // In this case, the VM will refuse to read the input from the contract. + // Stop here to allow handling the error in the contract. + return Err(StdError::generic_err("input too long for addr_validate")); + } + let source = Region::from_slice(input_bytes); + let source_ptr = source.as_ptr() as u32; + + let result = unsafe { addr_validate(source_ptr) }; + if result != 0 { + let error = + unsafe { consume_string_region_written_by_vm(result as *mut Region) }; + return Err(StdError::generic_err(format!("addr_validate errored: {}", error))); + } + + Ok(Addr::unchecked(input)) + } + + fn addr_canonicalize(&self, input: &str) -> StdResult { + let input_bytes = input.as_bytes(); + if input_bytes.len() > 256 { + // See MAX_LENGTH_HUMAN_ADDRESS in the VM. + // In this case, the VM will refuse to read the input from the contract. + // Stop here to allow handling the error in the contract. + return Err(StdError::generic_err("input too long for addr_canonicalize")); + } + let send = Region::from_slice(input_bytes); + let send_ptr = send.as_ptr() as u32; + let canon = Region::with_capacity(CANONICAL_ADDRESS_BUFFER_LENGTH); + + let result = unsafe { addr_canonicalize(send_ptr, canon.as_ptr() as u32) }; + if result != 0 { + let error = + unsafe { consume_string_region_written_by_vm(result as *mut Region) }; + return Err(StdError::generic_err(format!("addr_canonicalize errored: {}", error))); + } + + Ok(CanonicalAddr::from(canon.into_vec())) + } + + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { + let send = Region::from_slice(canonical.as_slice()); + let send_ptr = send.as_ptr() as u32; + let human = Region::with_capacity(HUMAN_ADDRESS_BUFFER_LENGTH); + + let result = unsafe { addr_humanize(send_ptr, human.as_ptr() as u32) }; + if result != 0 { + let error = + unsafe { consume_string_region_written_by_vm(result as *mut Region) }; + return Err(StdError::generic_err(format!("addr_humanize errored: {}", error))); + } + + let address = unsafe { String::from_utf8_unchecked(human.into_vec()) }; + Ok(Addr::unchecked(address)) + } + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_aggregate_g1(&self, g1s: &[u8]) -> Result<[u8; 48], VerificationError> { + let point = [0_u8; 48]; + + let send = Region::from_slice(g1s); + let send_ptr = send.as_ptr() as u32; + + let out = Region::from_slice(&point); + let out_ptr = out.as_ptr() as u32; + let result = unsafe { bls12_381_aggregate_g1(send_ptr, out_ptr) }; + match result { + 0 => Ok(point), + 8 => Err(VerificationError::InvalidPoint), + 16 => Err(VerificationError::Aggregation { source: AggregationError::Empty }), + 17 => Err(VerificationError::Aggregation { source: AggregationError::NotMultiple }), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_aggregate_g2(&self, g2s: &[u8]) -> Result<[u8; 96], VerificationError> { + let point = [0_u8; 96]; + + let send = Region::from_slice(g2s); + let send_ptr = send.as_ptr() as u32; + + let out = Region::from_slice(&point); + let out_ptr = out.as_ptr() as u32; + let result = unsafe { bls12_381_aggregate_g2(send_ptr, out_ptr) }; + match result { + 0 => Ok(point), + 8 => Err(VerificationError::InvalidPoint), + 14 => Err(VerificationError::Aggregation { source: AggregationError::Empty }), + 15 => Err(VerificationError::Aggregation { source: AggregationError::NotMultiple }), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_pairing_equality( + &self, + ps: &[u8], + qs: &[u8], + r: &[u8], + s: &[u8], + ) -> Result { + let send_ps = Region::from_slice(ps); + let send_qs = Region::from_slice(qs); + let send_r = Region::from_slice(r); + let send_s = Region::from_slice(s); + + let send_ps_ptr = send_ps.as_ptr() as u32; + let send_qs_ptr = send_qs.as_ptr() as u32; + let send_r_ptr = send_r.as_ptr() as u32; + let send_s_ptr = send_s.as_ptr() as u32; + + let result = + unsafe { bls12_381_pairing_equality(send_ps_ptr, send_qs_ptr, send_r_ptr, send_s_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 8 => Err(VerificationError::InvalidPoint), + 11 => Err(VerificationError::PairingEquality { + source: PairingEqualityError::NotMultipleG1, + }), + 12 => Err(VerificationError::PairingEquality { + source: PairingEqualityError::NotMultipleG2, + }), + 13 => Err(VerificationError::PairingEquality { + source: PairingEqualityError::UnequalPointAmount, + }), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_hash_to_g1( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 48], VerificationError> { + let point = [0_u8; 48]; + + let send_msg = Region::from_slice(msg); + let send_msg_ptr = send_msg.as_ptr() as u32; + + let send_dst = Region::from_slice(dst); + let send_dst_ptr = send_dst.as_ptr() as u32; + + let out = Region::from_slice(&point); + let out_ptr = out.as_ptr() as u32; + let result = unsafe { + bls12_381_hash_to_g1(hash_function as u32, send_msg_ptr, send_dst_ptr, out_ptr) + }; + + match result { + 0 => Ok(point), + 9 => Err(VerificationError::UnknownHashFunction), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn bls12_381_hash_to_g2( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 96], VerificationError> { + let point = [0_u8; 96]; + + let send_msg = Region::from_slice(msg); + let send_msg_ptr = send_msg.as_ptr() as u32; + + let send_dst = Region::from_slice(dst); + let send_dst_ptr = send_dst.as_ptr() as u32; + + let out = Region::from_slice(&point); + let out_ptr = out.as_ptr() as u32; + let result = unsafe { + bls12_381_hash_to_g2(hash_function as u32, send_msg_ptr, send_dst_ptr, out_ptr) + }; + + match result { + 0 => Ok(point), + 9 => Err(VerificationError::UnknownHashFunction), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + let hash_send = Region::from_slice(message_hash); + let hash_send_ptr = hash_send.as_ptr() as u32; + let sig_send = Region::from_slice(signature); + let sig_send_ptr = sig_send.as_ptr() as u32; + let pubkey_send = Region::from_slice(public_key); + let pubkey_send_ptr = pubkey_send.as_ptr() as u32; + + let result = unsafe { secp256k1_verify(hash_send_ptr, sig_send_ptr, pubkey_send_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 2 => panic!("MessageTooLong must not happen. This is a bug in the VM."), + 3 => Err(VerificationError::InvalidHashFormat), + 4 => Err(VerificationError::InvalidSignatureFormat), + 5 => Err(VerificationError::InvalidPubkeyFormat), + 10 => Err(VerificationError::GenericErr), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recover_param: u8, + ) -> Result, RecoverPubkeyError> { + let hash_send = Region::from_slice(message_hash); + let hash_send_ptr = hash_send.as_ptr() as u32; + let sig_send = Region::from_slice(signature); + let sig_send_ptr = sig_send.as_ptr() as u32; + + let result = + unsafe { secp256k1_recover_pubkey(hash_send_ptr, sig_send_ptr, recover_param.into()) }; + let error_code = from_high_half(result); + let pubkey_ptr = from_low_half(result); + match error_code { + 0 => { + let pubkey = + unsafe { Region::from_heap_ptr(pubkey_ptr as *mut Region).into_vec() }; + Ok(pubkey) + }, + 2 => panic!("MessageTooLong must not happen. This is a bug in the VM."), + 3 => Err(RecoverPubkeyError::InvalidHashFormat), + 4 => Err(RecoverPubkeyError::InvalidSignatureFormat), + 6 => Err(RecoverPubkeyError::InvalidRecoveryParam), + error_code => Err(RecoverPubkeyError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn secp256r1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + let hash_send = Region::from_slice(message_hash); + let hash_send_ptr = hash_send.as_ptr() as u32; + let sig_send = Region::from_slice(signature); + let sig_send_ptr = sig_send.as_ptr() as u32; + let pubkey_send = Region::from_slice(public_key); + let pubkey_send_ptr = pubkey_send.as_ptr() as u32; + + let result = unsafe { secp256r1_verify(hash_send_ptr, sig_send_ptr, pubkey_send_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 2 => panic!("MessageTooLong must not happen. This is a bug in the VM."), + 3 => Err(VerificationError::InvalidHashFormat), + 4 => Err(VerificationError::InvalidSignatureFormat), + 5 => Err(VerificationError::InvalidPubkeyFormat), + 10 => Err(VerificationError::GenericErr), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + #[cfg(feature = "cosmwasm_2_1")] + fn secp256r1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recover_param: u8, + ) -> Result, RecoverPubkeyError> { + let hash_send = Region::from_slice(message_hash); + let hash_send_ptr = hash_send.as_ptr() as u32; + let sig_send = Region::from_slice(signature); + let sig_send_ptr = sig_send.as_ptr() as u32; + + let result = + unsafe { secp256r1_recover_pubkey(hash_send_ptr, sig_send_ptr, recover_param.into()) }; + let error_code = from_high_half(result); + let pubkey_ptr = from_low_half(result); + match error_code { + 0 => { + let pubkey = + unsafe { Region::from_heap_ptr(pubkey_ptr as *mut Region).into_vec() }; + Ok(pubkey) + }, + 2 => panic!("MessageTooLong must not happen. This is a bug in the VM."), + 3 => Err(RecoverPubkeyError::InvalidHashFormat), + 4 => Err(RecoverPubkeyError::InvalidSignatureFormat), + 6 => Err(RecoverPubkeyError::InvalidRecoveryParam), + error_code => Err(RecoverPubkeyError::unknown_err(error_code)), + } + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + let msg_send = Region::from_slice(message); + let msg_send_ptr = msg_send.as_ptr() as u32; + let sig_send = Region::from_slice(signature); + let sig_send_ptr = sig_send.as_ptr() as u32; + let pubkey_send = Region::from_slice(public_key); + let pubkey_send_ptr = pubkey_send.as_ptr() as u32; + + let result = unsafe { ed25519_verify(msg_send_ptr, sig_send_ptr, pubkey_send_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 2 => panic!("Error code 2 unused since CosmWasm 0.15. This is a bug in the VM."), + 3 => panic!("InvalidHashFormat must not happen. This is a bug in the VM."), + 4 => Err(VerificationError::InvalidSignatureFormat), + 5 => Err(VerificationError::InvalidPubkeyFormat), + 10 => Err(VerificationError::GenericErr), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + let msgs_encoded = encode_sections(messages); + let msgs_send = Region::from_vec(msgs_encoded); + let msgs_send_ptr = msgs_send.as_ptr() as u32; + + let sigs_encoded = encode_sections(signatures); + let sig_sends = Region::from_vec(sigs_encoded); + let sigs_send_ptr = sig_sends.as_ptr() as u32; + + let pubkeys_encoded = encode_sections(public_keys); + let pubkeys_send = Region::from_vec(pubkeys_encoded); + let pubkeys_send_ptr = pubkeys_send.as_ptr() as u32; + + let result = + unsafe { ed25519_batch_verify(msgs_send_ptr, sigs_send_ptr, pubkeys_send_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 2 => panic!("Error code 2 unused since CosmWasm 0.15. This is a bug in the VM."), + 3 => panic!("InvalidHashFormat must not happen. This is a bug in the VM."), + 4 => Err(VerificationError::InvalidSignatureFormat), + 5 => Err(VerificationError::InvalidPubkeyFormat), + 10 => Err(VerificationError::GenericErr), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + + fn debug(&self, message: &str) { + // keep the boxes in scope, so we free it at the end (don't cast to pointers same line as + // Region::from_slice) + let region = Region::from_slice(message.as_bytes()); + let region_ptr = region.as_ptr() as u32; + unsafe { debug(region_ptr) }; + } +} + +/// Takes a pointer to a Region and reads the data into a String. +/// This is for trusted string sources only. +unsafe fn consume_string_region_written_by_vm(from: *mut Region) -> String { + let data = Region::from_heap_ptr(from).into_vec(); + // We trust the VM/chain to return correct UTF-8, so let's save some gas + String::from_utf8_unchecked(data) +} + +/// A stateless convenience wrapper around imports provided by the VM +pub struct ExternalQuerier {} + +impl ExternalQuerier { + pub fn new() -> ExternalQuerier { + ExternalQuerier {} + } +} + +impl Querier for ExternalQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let req = Region::from_slice(bin_request); + let request_ptr = req.as_ptr() as u32; + + let response_ptr = unsafe { query_chain(request_ptr) }; + let response = + unsafe { Region::from_heap_ptr(response_ptr as *mut Region).into_vec() }; + + from_json(&response).unwrap_or_else(|parsing_err| { + SystemResult::Err(SystemError::InvalidResponse { + error: parsing_err.to_string(), + response: response.into(), + }) + }) + } +} + +#[cfg(feature = "abort")] +pub fn handle_panic(message: &str) { + let region = Region::from_slice(message.as_bytes()); + let region_ptr = region.as_ptr() as u32; + unsafe { abort(region_ptr) }; +} diff --git a/cosmwasm/std/src/iterator.rs b/cosmwasm/std/src/iterator.rs new file mode 100644 index 00000000..b4575afb --- /dev/null +++ b/cosmwasm/std/src/iterator.rs @@ -0,0 +1,57 @@ +use crate::StdError; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +/// A record of a key-value storage that is created through an iterator API. +/// The first element (key) is always raw binary data. The second element +/// (value) is binary by default but can be changed to a custom type. This +/// allows contracts to reuse the type when deserializing database records. +pub type Record> = (Vec, V); + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +// We assign these to integers to provide a stable API for passing over FFI (to wasm and Go) +pub enum Order { + Ascending = 1, + Descending = 2, +} + +impl TryFrom for Order { + type Error = StdError; + + fn try_from(value: i32) -> Result { + match value { + 1 => Ok(Order::Ascending), + 2 => Ok(Order::Descending), + _ => Err(StdError::generic_err("Order must be 1 or 2")), + } + } +} + +impl From for i32 { + fn from(original: Order) -> i32 { + original as _ + } +} + +#[cfg(test)] +mod tests { + use crate::{from_json, to_json_vec}; + + use super::*; + + #[test] + fn order_serde() { + let ascending_bytes = br#""ascending""#; + let descending_bytes = br#""descending""#; + + assert_eq!(to_json_vec(&Order::Ascending).unwrap(), ascending_bytes); + assert_eq!(to_json_vec(&Order::Descending).unwrap(), descending_bytes); + + assert_eq!(from_json::(ascending_bytes).unwrap(), Order::Ascending); + + assert_eq!(from_json::(descending_bytes).unwrap(), Order::Descending); + } +} diff --git a/cosmwasm/std/src/lib.rs b/cosmwasm/std/src/lib.rs new file mode 100644 index 00000000..7670de75 --- /dev/null +++ b/cosmwasm/std/src/lib.rs @@ -0,0 +1,153 @@ +// #[cfg(not(feature = "std"))] +// core::compile_error!( +// r#"Please enable `cosmwasm-std`'s `std` feature, as we might move existing functionality to +// that feature in the future. Builds without the std feature are currently not expected to work. If +// you need no_std support see #1484. "# +// ); + +#![cfg_attr(not(feature = "std"), no_std)] + +#[macro_use] +extern crate alloc; + +// Exposed on all platforms + +mod __internal; +mod addresses; +mod assertions; +mod binary; +mod checksum; +mod coin; +mod coins; +mod conversion; +mod deps; +mod encoding; +mod errors; +mod forward_ref; +mod hex_binary; +mod ibc; +mod import_helpers; +#[cfg(feature = "iterator")] +mod iterator; +mod math; +mod metadata; +mod never; +mod pagination; +mod panic; +mod query; +mod results; +mod sections; +mod serde; +mod stdack; +mod storage; +mod timestamp; +mod traits; +mod types; + +/// This module is to simplify no_std imports +pub(crate) mod prelude; + +/// This modules is very advanced and will not be used directly by the vast majority of users. +/// We want to offer it to ensure a stable storage key composition system but don't encourage +/// contract devs to use it directly. +pub mod storage_keys; + +#[cfg(feature = "iterator")] +pub use crate::iterator::{Order, Record}; +#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] +pub use crate::results::WeightedVoteOption; +#[cfg(feature = "staking")] +pub use crate::results::{DistributionMsg, StakingMsg}; +#[cfg(feature = "stargate")] +pub use crate::results::{GovMsg, VoteOption}; +#[allow(deprecated)] +pub use crate::serde::{ + from_binary, from_json, from_slice, to_binary, to_json_binary, to_json_string, to_json_vec, + to_vec, +}; +pub use crate::{ + addresses::{instantiate2_address, Addr, CanonicalAddr, Instantiate2AddressError}, + binary::Binary, + checksum::{Checksum, ChecksumError}, + coin::{coin, coins, has_coins, Coin}, + coins::Coins, + deps::{Deps, DepsMut, OwnedDeps}, + encoding::{from_base64, from_hex, to_base64, to_hex}, + errors::{ + AggregationError, CheckedFromRatioError, CheckedMultiplyFractionError, + CheckedMultiplyRatioError, CoinFromStrError, CoinsError, ConversionOverflowError, + DivideByZeroError, DivisionError, OverflowError, OverflowOperation, PairingEqualityError, + RecoverPubkeyError, RoundDownOverflowError, RoundUpOverflowError, StdError, StdResult, + SystemError, VerificationError, + }, + hex_binary::HexBinary, + ibc::{ + Ibc3ChannelOpenResponse, IbcAckCallbackMsg, IbcAcknowledgement, IbcBasicResponse, + IbcCallbackRequest, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcChannelOpenResponse, IbcDestinationCallbackMsg, IbcDstCallback, + IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, + IbcPacketTimeoutMsg, IbcReceiveResponse, IbcSourceCallbackMsg, IbcSrcCallback, IbcTimeout, + IbcTimeoutBlock, IbcTimeoutCallbackMsg, TransferMsgBuilder, + }, + math::{ + Decimal, Decimal256, Decimal256RangeExceeded, DecimalRangeExceeded, Fraction, Int128, + Int256, Int512, Int64, Isqrt, SignedDecimal, SignedDecimal256, + SignedDecimal256RangeExceeded, SignedDecimalRangeExceeded, Uint128, Uint256, Uint512, + Uint64, + }, + metadata::{DenomMetadata, DenomUnit}, + never::Never, + pagination::PageRequest, + query::{ + AllBalanceResponse, AllDelegationsResponse, AllDenomMetadataResponse, + AllValidatorsResponse, BalanceResponse, BankQuery, BondedDenomResponse, ChannelResponse, + CodeInfoResponse, ContractInfoResponse, CustomQuery, DecCoin, Delegation, + DelegationResponse, DelegationRewardsResponse, DelegationTotalRewardsResponse, + DelegatorReward, DelegatorValidatorsResponse, DelegatorWithdrawAddressResponse, + DenomMetadataResponse, DistributionQuery, FullDelegation, GrpcQuery, IbcQuery, + ListChannelsResponse, PortIdResponse, QueryRequest, StakingQuery, SupplyResponse, + Validator, ValidatorResponse, WasmQuery, + }, + results::{ + attr, wasm_execute, wasm_instantiate, AnyMsg, Attribute, BankMsg, ContractResult, + CosmosMsg, CustomMsg, Empty, Event, MsgResponse, QueryResponse, Reply, ReplyOn, Response, + SubMsg, SubMsgResponse, SubMsgResult, SystemResult, WasmMsg, + }, + stdack::StdAck, + storage::MemoryStorage, + timestamp::Timestamp, + traits::{Api, HashFunction, Querier, QuerierResult, QuerierWrapper, Storage}, + types::{BlockInfo, ContractInfo, Env, MessageInfo, TransactionInfo}, +}; + +// Exposed in wasm build only + +#[cfg(target_arch = "wasm32")] +mod exports; +#[cfg(target_arch = "wasm32")] +mod imports; +#[cfg(target_arch = "wasm32")] +mod memory; // Used by exports and imports only. This assumes pointers are 32 bit long, which makes it + // untestable on dev machines. + +#[cfg(target_arch = "wasm32")] +pub use crate::exports::{ + do_execute, do_ibc_destination_callback, do_ibc_source_callback, do_instantiate, do_migrate, + do_query, do_reply, do_sudo, +}; +#[cfg(all(feature = "stargate", target_arch = "wasm32"))] +pub use crate::exports::{ + do_ibc_channel_close, do_ibc_channel_connect, do_ibc_channel_open, do_ibc_packet_ack, + do_ibc_packet_receive, do_ibc_packet_timeout, +}; +#[cfg(target_arch = "wasm32")] +pub use crate::imports::{ExternalApi, ExternalQuerier, ExternalStorage}; + +/// Exposed for testing only +/// Both unit tests and integration tests are compiled to native code, so everything in here does +/// not need to compile to Wasm. +#[cfg(not(target_arch = "wasm32"))] +pub mod testing; + +pub use cosmwasm_core::{BLS12_381_G1_GENERATOR, BLS12_381_G2_GENERATOR}; +pub use cosmwasm_derive::entry_point; diff --git a/cosmwasm/std/src/math/conversion.rs b/cosmwasm/std/src/math/conversion.rs new file mode 100644 index 00000000..62cbb4a7 --- /dev/null +++ b/cosmwasm/std/src/math/conversion.rs @@ -0,0 +1,336 @@ +#[cfg(test)] +use alloc::string::String; + +/// Grows a big endian signed integer to a bigger size. +/// See +pub const fn grow_be_int( + input: [u8; INPUT_SIZE], +) -> [u8; OUTPUT_SIZE] { + debug_assert!(INPUT_SIZE <= OUTPUT_SIZE); + // check if sign bit is set + let mut output = if input[0] & 0b10000000 != 0 { + // negative number is filled up with 1s + [0b11111111u8; OUTPUT_SIZE] + } else { + [0u8; OUTPUT_SIZE] + }; + let mut i = 0; + + // copy input to the end of output + // copy_from_slice is not const, so we have to do this manually + while i < INPUT_SIZE { + output[OUTPUT_SIZE - INPUT_SIZE + i] = input[i]; + i += 1; + } + output +} + +/// Shrinks a big endian signed integer to a smaller size. +/// This is the opposite operation of sign extension. +pub fn shrink_be_int( + input: [u8; INPUT_SIZE], +) -> Option<[u8; OUTPUT_SIZE]> { + debug_assert!(INPUT_SIZE >= OUTPUT_SIZE); + + // check bounds + if input[0] & 0b10000000 != 0 { + // a negative number should start with only 1s, otherwise it's too small + for i in &input[0..(INPUT_SIZE - OUTPUT_SIZE)] { + if *i != 0b11111111u8 { + return None; + } + } + // the sign bit also has to be 1 + if input[INPUT_SIZE - OUTPUT_SIZE] & 0b10000000 == 0 { + return None; + } + } else { + // a positive number should start with only 0s, otherwise it's too large + for i in &input[0..(INPUT_SIZE - OUTPUT_SIZE)] { + if *i != 0u8 { + return None; + } + } + // the sign bit also has to be 0 + if input[INPUT_SIZE - OUTPUT_SIZE] & 0b10000000 != 0 { + return None; + } + } + + // Now, we can just copy the last bytes + let mut output = [0u8; OUTPUT_SIZE]; + output.copy_from_slice(&input[(INPUT_SIZE - OUTPUT_SIZE)..]); + Some(output) +} + +/// Helper macro to implement `TryFrom` for a type that is just a wrapper around another type. +/// This can be used for all our integer conversions where `bnum` implements `TryFrom`. +macro_rules! forward_try_from { + ($input: ty, $output: ty) => { + impl TryFrom<$input> for $output { + type Error = $crate::ConversionOverflowError; + + fn try_from(value: $input) -> Result { + value + .0 + .try_into() + .map(Self) + .map_err(|_| Self::Error::new(stringify!($input), stringify!($output))) + } + } + }; +} +pub(crate) use forward_try_from; + +/// Helper macro to implement `TryFrom` for a conversion from a bigger signed int to a smaller one. +/// This is needed because `bnum` does not implement `TryFrom` for those conversions +/// because of limitations of const generics. +macro_rules! try_from_int_to_int { + ($input: ty, $output: ty) => { + // statically assert that the input is bigger than the output + static_assertions::const_assert!( + core::mem::size_of::<$input>() > core::mem::size_of::<$output>() + ); + impl TryFrom<$input> for $output { + type Error = $crate::ConversionOverflowError; + + fn try_from(value: $input) -> Result { + $crate::math::conversion::shrink_be_int(value.to_be_bytes()) + .ok_or_else(|| Self::Error::new(stringify!($input), stringify!($output))) + .map(Self::from_be_bytes) + } + } + }; +} +pub(crate) use try_from_int_to_int; + +/// Helper macro to implement `TryFrom` for a conversion from a unsigned int to a smaller or +/// equal sized signed int. +/// This is needed because `bnum` does not implement `TryFrom` for all of those conversions. +macro_rules! try_from_uint_to_int { + ($input: ty, $output: ty) => { + // statically assert that... + // input is unsigned + static_assertions::const_assert_eq!(stringify!($input).as_bytes()[0], b'U'); + // output is signed + static_assertions::const_assert_eq!(stringify!($output).as_bytes()[0], b'I'); + // input is bigger than output (otherwise we would not need a `TryFrom` impl) + static_assertions::const_assert!( + core::mem::size_of::<$input>() >= core::mem::size_of::<$output>() + ); + + impl TryFrom<$input> for $output { + type Error = $crate::ConversionOverflowError; + + fn try_from(value: $input) -> Result { + use bnum::prelude::As; + // $input::MAX has to be bigger than $output::MAX, so we can just cast it + if value.0 > Self::MAX.0.as_() { + return Err(Self::Error::new(stringify!($input), stringify!($output))); + } + + // at this point we know it fits + Ok(Self(value.0.as_())) + } + } + }; +} +pub(crate) use try_from_uint_to_int; + +#[cfg(test)] +pub(crate) fn test_try_from_uint_to_int(input_type: &'static str, output_type: &'static str) +where + I: super::num_consts::NumConsts + + From + + Copy + + TryFrom + + core::fmt::Debug + + core::ops::Add, + O: TryFrom + + From + + super::num_consts::NumConsts + + core::cmp::PartialEq + + core::fmt::Debug, + String: From, +{ + let v = I::MAX; + assert_eq!( + O::try_from(v), + Err(crate::ConversionOverflowError::new(input_type, output_type)), + "input::MAX value should not fit" + ); + + let max = I::try_from(O::MAX).unwrap(); + assert_eq!(O::try_from(max), Ok(O::MAX), "output::MAX value should fit"); + + // but $output::MAX + 1 should not fit + let v = max + I::ONE; + assert_eq!( + O::try_from(v), + Err(crate::ConversionOverflowError::new(input_type, output_type)), + "output::MAX + 1 should not fit" + ); + + // zero should work + let v = I::ZERO; + assert_eq!(O::try_from(v), Ok(O::ZERO), "zero should fit"); + + // 42 should work + assert_eq!(O::try_from(I::from(42u32)), Ok(O::from(42u32)), "42 should fit") +} + +#[cfg(test)] +pub(crate) fn test_try_from_int_to_uint(input_type: &'static str, output_type: &'static str) +where + I: super::num_consts::NumConsts + + From + + Copy + + TryFrom + + core::fmt::Debug + + core::ops::Add, + O: TryFrom + + From + + super::num_consts::NumConsts + + core::cmp::PartialEq + + core::fmt::Debug, + String: From, + >::Error: core::fmt::Debug, +{ + if core::mem::size_of::() <= core::mem::size_of::() { + // if the input type is smaller than the output type, then `I::MAX` should fit into `O` + let v = I::MAX; + assert_eq!(O::try_from(v), Ok(O::try_from(v).unwrap()), "input::MAX value should fit"); + } else { + // if the input is bigger than the output, then `I::MAX` should not fit into `O` + let v = I::MAX; + assert_eq!( + O::try_from(v), + Err(crate::ConversionOverflowError::new(input_type, output_type)), + "input::MAX value should not fit" + ); + // but `O::MAX` should fit + let max = I::try_from(O::MAX).unwrap(); + assert_eq!(O::try_from(max), Ok(O::try_from(max).unwrap()), "output::MAX value should fit"); + // while `O::MAX + 1` should not + let v = max + I::ONE; + assert_eq!( + O::try_from(v), + Err(crate::ConversionOverflowError::new(input_type, output_type)), + "output::MAX + 1 should not fit" + ); + } + + // negative numbers should fail + let v = I::from(-42i32); + assert_eq!( + O::try_from(v), + Err(crate::ConversionOverflowError::new(input_type, output_type,)), + "negative numbers should not fit" + ); + + // zero should work + let v = I::ZERO; + assert_eq!(O::try_from(v), Ok(O::ZERO), "zero should fit"); + + // 42 should work + assert_eq!(O::try_from(I::from(42i32)), Ok(O::from(42u32)), "42 should fit") +} + +/// Helper macro to implement `TryFrom` for a conversion from a signed int to an unsigned int. +/// This is needed because `bnum` does not implement `TryFrom` for all of those conversions. +macro_rules! try_from_int_to_uint { + ($input: ty, $output: ty) => { + // statically assert that... + // input is signed + static_assertions::const_assert_eq!(stringify!($input).as_bytes()[0], b'I'); + // output is unsigned + static_assertions::const_assert_eq!(stringify!($output).as_bytes()[0], b'U'); + + impl TryFrom<$input> for $output { + type Error = ConversionOverflowError; + + fn try_from(value: $input) -> Result { + use bnum::prelude::As; + // if $input::MAX is smaller than $output::MAX, we only need to check the sign + if core::mem::size_of::<$input>() <= core::mem::size_of::<$output>() { + if value.is_negative() { + return Err(ConversionOverflowError::new( + stringify!($input), + stringify!($output), + )); + } + + // otherwise we can just cast it + Ok(Self(value.0.as_())) + } else { + // $output::MAX is smaller than $input::MAX. + // If it is negative or too big, we error. + // We can safely cast $output::MAX to $input size + if value.is_negative() || value.0 > <$output>::MAX.0.as_() { + return Err(ConversionOverflowError::new( + stringify!($input), + stringify!($output), + )); + } + + // at this point we know it fits + Ok(Self(value.0.as_())) + } + } + } + }; +} +pub(crate) use try_from_int_to_uint; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grow_be_int_works() { + // test against rust std's integers + let i32s = [i32::MIN, -1, 0, 1, 42, i32::MAX]; + for i in i32s { + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i64).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i128).to_be_bytes()); + } + let i8s = [i8::MIN, -1, 0, 1, 42, i8::MAX]; + for i in i8s { + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i16).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i32).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i64).to_be_bytes()); + assert_eq!(grow_be_int(i.to_be_bytes()), (i as i128).to_be_bytes()); + } + } + + #[test] + fn shrink_be_int_works() { + // test against rust std's integers + let i32s = [-42, -1, 0i32, 1, 42]; + for i in i32s { + assert_eq!(shrink_be_int(i.to_be_bytes()), Some((i as i16).to_be_bytes())); + assert_eq!(shrink_be_int(i.to_be_bytes()), Some((i as i8).to_be_bytes())); + } + // these should be too big to fit into an i16 or i8 + let oob = + [i32::MIN, i32::MIN + 10, i32::MIN + 1234, i32::MAX - 1234, i32::MAX - 10, i32::MAX]; + for i in oob { + // 32 -> 16 bit + assert_eq!(shrink_be_int::<4, 2>(i.to_be_bytes()), None); + // 32 -> 8 bit + assert_eq!(shrink_be_int::<4, 1>(i.to_be_bytes()), None); + } + + // compare against whole i16 range + for i in i16::MIN..=i16::MAX { + let cast = i as i8 as i16; + if i == cast { + // if the cast is lossless, `shrink_be_int` should get the same result + assert_eq!(shrink_be_int::<2, 1>(i.to_be_bytes()), Some((i as i8).to_be_bytes())); + } else { + // otherwise, we should get None + assert_eq!(shrink_be_int::<2, 1>(i.to_be_bytes()), None); + } + } + } +} diff --git a/cosmwasm/std/src/math/decimal.rs b/cosmwasm/std/src/math/decimal.rs new file mode 100644 index 00000000..30c885fc --- /dev/null +++ b/cosmwasm/std/src/math/decimal.rs @@ -0,0 +1,1965 @@ +use alloc::string::ToString; +use core::{ + cmp::Ordering, + fmt::{self, Write}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, RemAssign, Sub, SubAssign}, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{ + CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, RoundUpOverflowError, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Decimal256, SignedDecimal, SignedDecimal256, + __internal::forward_ref_partial_eq, +}; + +use super::{Fraction, Isqrt, Uint128, Uint256}; + +use crate::prelude::*; + +/// A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) +/// == 1.0 +/// +/// The greatest possible value that can be represented is 340282366920938463463.374607431768211455 +/// (which is (2^128 - 1) / 10^18) +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Decimal(#[schemars(with = "String")] Uint128); + +forward_ref_partial_eq!(Decimal, Decimal); + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Decimal range exceeded")] +pub struct DecimalRangeExceeded; + +impl Decimal { + const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 + const DECIMAL_FRACTIONAL_SQUARED: Uint128 = + Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000u128); // (1*10**18)**2 = 1*10**36 + + /// The number of decimal places. Since decimal types are fixed-point rather than + /// floating-point, this is a constant. + pub const DECIMAL_PLACES: u32 = 18; // This needs to be an even number. + /// The largest value that can be represented by this decimal type. + pub const MAX: Self = Self(Uint128::MAX); + /// The smallest value that can be represented by this decimal type. + pub const MIN: Self = Self(Uint128::MIN); + + /// Creates a Decimal(value) + /// This is equivalent to `Decimal::from_atomics(value, 18)` but usable in a const context. + pub const fn new(value: Uint128) -> Self { + Self(value) + } + + /// Creates a Decimal(Uint128(value)) + /// This is equivalent to `Decimal::from_atomics(value, 18)` but usable in a const context. + pub const fn raw(value: u128) -> Self { + Self(Uint128::new(value)) + } + + /// Create a 1.0 Decimal + #[inline] + pub const fn one() -> Self { + Self(Self::DECIMAL_FRACTIONAL) + } + + /// Create a 0.0 Decimal + #[inline] + pub const fn zero() -> Self { + Self(Uint128::zero()) + } + + /// Convert x% into Decimal + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal; + /// const HALF: Decimal = Decimal::percent(50); + /// + /// assert_eq!(HALF, Decimal::from_str("0.5").unwrap()); + /// ``` + pub const fn percent(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**16 is well in u128 range + let atomics = (x as u128) * 10_000_000_000_000_000; + Self(Uint128::new(atomics)) + } + + /// Convert permille (x/1000) into Decimal + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal; + /// const HALF: Decimal = Decimal::permille(500); + /// + /// assert_eq!(HALF, Decimal::from_str("0.5").unwrap()); + /// ``` + pub const fn permille(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**15 is well in u128 range + let atomics = (x as u128) * 1_000_000_000_000_000; + Self(Uint128::new(atomics)) + } + + /// Convert basis points (x/10000) into Decimal + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal; + /// const TWO_BPS: Decimal = Decimal::bps(2); + /// const HALF: Decimal = Decimal::bps(5000); + /// + /// assert_eq!(TWO_BPS, Decimal::from_str("0.0002").unwrap()); + /// assert_eq!(HALF, Decimal::from_str("0.5").unwrap()); + /// ``` + pub const fn bps(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**14 is well in u128 range + let atomics = (x as u128) * 100_000_000_000_000; + Self(Uint128::new(atomics)) + } + + /// Creates a decimal from a number of atomic units and the number + /// of decimal places. The inputs will be converted internally to form + /// a decimal with 18 decimal places. So the input 123 and 2 will create + /// the decimal 1.23. + /// + /// Using 18 decimal places is slightly more efficient than other values + /// as no internal conversion is necessary. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Decimal, Uint128}; + /// let a = Decimal::from_atomics(Uint128::new(1234), 3).unwrap(); + /// assert_eq!(a.to_string(), "1.234"); + /// + /// let a = Decimal::from_atomics(1234u128, 0).unwrap(); + /// assert_eq!(a.to_string(), "1234"); + /// + /// let a = Decimal::from_atomics(1u64, 18).unwrap(); + /// assert_eq!(a.to_string(), "0.000000000000000001"); + /// ``` + pub fn from_atomics( + atomics: impl Into, + decimal_places: u32, + ) -> Result { + let atomics = atomics.into(); + const TEN: Uint128 = Uint128::new(10); + Ok(match decimal_places.cmp(&Self::DECIMAL_PLACES) { + Ordering::Less => { + let digits = (Self::DECIMAL_PLACES) - decimal_places; // No overflow because decimal_places < DECIMAL_PLACES + let factor = TEN.checked_pow(digits).unwrap(); // Safe because digits <= 17 + Self(atomics.checked_mul(factor).map_err(|_| DecimalRangeExceeded)?) + }, + Ordering::Equal => Self(atomics), + Ordering::Greater => { + let digits = decimal_places - (Self::DECIMAL_PLACES); // No overflow because decimal_places > DECIMAL_PLACES + if let Ok(factor) = TEN.checked_pow(digits) { + Self(atomics.checked_div(factor).unwrap()) // Safe because factor cannot be zero + } else { + // In this case `factor` exceeds the Uint128 range. + // Any Uint128 `x` divided by `factor` with `factor > Uint128::MAX` is 0. + // Try e.g. Python3: `(2**128-1) // 2**128` + Self(Uint128::zero()) + } + }, + }) + } + + /// Returns the ratio (numerator / denominator) as a Decimal + pub fn from_ratio(numerator: impl Into, denominator: impl Into) -> Self { + match Decimal::checked_from_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns the ratio (numerator / denominator) as a Decimal + pub fn checked_from_ratio( + numerator: impl Into, + denominator: impl Into, + ) -> Result { + let numerator: Uint128 = numerator.into(); + let denominator: Uint128 = denominator.into(); + match numerator.checked_multiply_ratio(Self::DECIMAL_FRACTIONAL, denominator) { + Ok(ratio) => { + // numerator * DECIMAL_FRACTIONAL / denominator + Ok(Decimal(ratio)) + }, + Err(CheckedMultiplyRatioError::Overflow) => Err(CheckedFromRatioError::Overflow), + Err(CheckedMultiplyRatioError::DivideByZero) => + Err(CheckedFromRatioError::DivideByZero), + } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// A decimal is an integer of atomic units plus a number that specifies the + /// position of the decimal dot. So any decimal can be expressed as two numbers. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Decimal, Uint128}; + /// # use core::str::FromStr; + /// // Value with whole and fractional part + /// let a = Decimal::from_str("1.234").unwrap(); + /// assert_eq!(a.decimal_places(), 18); + /// assert_eq!(a.atomics(), Uint128::new(1234000000000000000)); + /// + /// // Smallest possible value + /// let b = Decimal::from_str("0.000000000000000001").unwrap(); + /// assert_eq!(b.decimal_places(), 18); + /// assert_eq!(b.atomics(), Uint128::new(1)); + /// ``` + #[must_use] + #[inline] + pub const fn atomics(&self) -> Uint128 { + self.0 + } + + /// The number of decimal places. This is a constant value for now + /// but this could potentially change as the type evolves. + /// + /// See also [`Decimal::atomics()`]. + #[must_use] + #[inline] + pub const fn decimal_places(&self) -> u32 { + Self::DECIMAL_PLACES + } + + /// Rounds value down after decimal places. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn floor(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value up after decimal places. Panics on overflow. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if floor == self { + Ok(floor) + } else { + floor.checked_add(Decimal::one()).map_err(|_| RoundUpOverflowError) + } + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Sub)) + } + + /// Multiplies one `Decimal` by another, returning an `OverflowError` if an overflow occurred. + pub fn checked_mul(self, other: Self) -> Result { + let result_as_uint256 = self.numerator().full_mul(other.numerator()) / + Uint256::from_uint128(Self::DECIMAL_FRACTIONAL); // from_uint128 is a const method and should be "free" + result_as_uint256 + .try_into() + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Mul)) + } + + /// Raises a value to the power of `exp`, panics if an overflow occurred. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => panic!("Multiplication overflow"), + } + } + + /// Raises a value to the power of `exp`, returning an `OverflowError` if an overflow occurred. + pub fn checked_pow(self, exp: u32) -> Result { + // This uses the exponentiation by squaring algorithm: + // https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Basic_method + + fn inner(mut x: Decimal, mut n: u32) -> Result { + if n == 0 { + return Ok(Decimal::one()); + } + + let mut y = Decimal::one(); + + while n > 1 { + if n % 2 == 0 { + x = x.checked_mul(x)?; + n /= 2; + } else { + y = x.checked_mul(y)?; + x = x.checked_mul(x)?; + n = (n - 1) / 2; + } + } + + Ok(x * y) + } + + inner(self, exp).map_err(|_| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + Decimal::checked_from_ratio(self.numerator(), other.numerator()) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).map_err(|_| DivideByZeroError) + } + + /// Returns the approximate square root as a Decimal. + /// + /// This should not overflow or panic. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn sqrt(&self) -> Self { + // Algorithm described in https://hackmd.io/@webmaster128/SJThlukj_ + // We start with the highest precision possible and lower it until + // there's no overflow. + // + // TODO: This could be made more efficient once log10 is in: + // https://github.com/rust-lang/rust/issues/70887 + // The max precision is something like `9 - log10(self.0) / 2`. + (0..=Self::DECIMAL_PLACES / 2) + .rev() + .find_map(|i| self.sqrt_with_precision(i)) + // The last step (i = 0) is guaranteed to succeed because `isqrt(u128::MAX) * 10^9` does + // not overflow + .unwrap() + } + + /// Lower precision means more aggressive rounding, but less risk of overflow. + /// Precision *must* be a number between 0 and 9 (inclusive). + /// + /// Returns `None` if the internal multiplication overflows. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn sqrt_with_precision(&self, precision: u32) -> Option { + let inner_mul = 100u128.pow(precision); + self.0.checked_mul(inner_mul.into()).ok().map(|inner| { + let outer_mul = 10u128.pow(Self::DECIMAL_PLACES / 2 - precision); + Decimal(inner.isqrt().checked_mul(Uint128::from(outer_mul)).unwrap()) + }) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Self { + Self(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + match self.checked_add(other) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + match self.checked_sub(other) { + Ok(value) => value, + Err(_) => Self::zero(), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + match self.checked_mul(other) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + /// Converts this decimal to an unsigned integer by truncating + /// the fractional part, e.g. 22.5 becomes 22. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{Decimal, Uint128}; + /// + /// let d = Decimal::from_str("12.345").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint128::new(12)); + /// + /// let d = Decimal::from_str("12.999").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint128::new(12)); + /// + /// let d = Decimal::from_str("75.0").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint128::new(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_uint_floor(self) -> Uint128 { + self.0 / Self::DECIMAL_FRACTIONAL + } + + /// Converts this decimal to an unsigned integer by rounting up + /// to the next integer, e.g. 22.3 becomes 23. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{Decimal, Uint128}; + /// + /// let d = Decimal::from_str("12.345").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint128::new(13)); + /// + /// let d = Decimal::from_str("12.999").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint128::new(13)); + /// + /// let d = Decimal::from_str("75.0").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint128::new(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_uint_ceil(self) -> Uint128 { + // Using `q = 1 + ((x - 1) / y); // if x != 0` with unsigned integers x, y, q + // from https://stackoverflow.com/a/2745086/2013738. We know `x + y` CAN overflow. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + if x.is_zero() { + Uint128::zero() + } else { + Uint128::one() + ((x - Uint128::one()) / y) + } + } +} + +impl Fraction for Decimal { + #[inline] + fn numerator(&self) -> Uint128 { + self.0 + } + + #[inline] + fn denominator(&self) -> Uint128 { + Self::DECIMAL_FRACTIONAL + } + + /// Returns the multiplicative inverse `1/d` for decimal `d`. + /// + /// If `d` is zero, none is returned. + fn inv(&self) -> Option { + if self.is_zero() { + None + } else { + // Let self be p/q with p = self.0 and q = DECIMAL_FRACTIONAL. + // Now we calculate the inverse a/b = q/p such that b = DECIMAL_FRACTIONAL. Then + // `a = DECIMAL_FRACTIONAL*DECIMAL_FRACTIONAL / self.0`. + Some(Decimal(Self::DECIMAL_FRACTIONAL_SQUARED / self.0)) + } + } +} + +impl TryFrom for Decimal { + type Error = DecimalRangeExceeded; + + fn try_from(value: Decimal256) -> Result { + value.atomics().try_into().map(Decimal).map_err(|_| DecimalRangeExceeded) + } +} + +impl TryFrom for Decimal { + type Error = DecimalRangeExceeded; + + fn try_from(value: SignedDecimal) -> Result { + value.atomics().try_into().map(Decimal).map_err(|_| DecimalRangeExceeded) + } +} + +impl TryFrom for Decimal { + type Error = DecimalRangeExceeded; + + fn try_from(value: SignedDecimal256) -> Result { + value.atomics().try_into().map(Decimal).map_err(|_| DecimalRangeExceeded) + } +} + +impl TryFrom for Decimal { + type Error = DecimalRangeExceeded; + + #[inline] + fn try_from(value: Uint128) -> Result { + Self::from_atomics(value, 0) + } +} + +impl FromStr for Decimal { + type Err = StdError; + + /// Converts the decimal string to a Decimal + /// Possible inputs: "1.23", "1", "000012", "1.123000000" + /// Disallowed: "", ".23" + /// + /// This never performs any kind of rounding. + /// More than DECIMAL_PLACES fractional digits, even zeros, result in an error. + fn from_str(input: &str) -> Result { + let mut parts_iter = input.split('.'); + + let whole_part = parts_iter.next().unwrap(); // split always returns at least one element + let whole = whole_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing whole"))?; + let mut atomics = whole + .checked_mul(Self::DECIMAL_FRACTIONAL) + .map_err(|_| StdError::generic_err("Value too big"))?; + + if let Some(fractional_part) = parts_iter.next() { + let fractional = fractional_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing fractional"))?; + let exp = (Self::DECIMAL_PLACES.checked_sub(fractional_part.len() as u32)).ok_or_else( + || { + StdError::generic_err(format!( + "Cannot parse more than {} fractional digits", + Self::DECIMAL_PLACES + )) + }, + )?; + debug_assert!(exp <= Self::DECIMAL_PLACES); + let fractional_factor = Uint128::from(10u128.pow(exp)); + atomics = atomics + .checked_add( + // The inner multiplication can't overflow because + // fractional < 10^DECIMAL_PLACES && fractional_factor <= 10^DECIMAL_PLACES + fractional.checked_mul(fractional_factor).unwrap(), + ) + .map_err(|_| StdError::generic_err("Value too big"))?; + } + + if parts_iter.next().is_some() { + return Err(StdError::generic_err("Unexpected number of dots")); + } + + Ok(Decimal(atomics)) + } +} + +impl fmt::Display for Decimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let whole = (self.0) / Self::DECIMAL_FRACTIONAL; + let fractional = (self.0).checked_rem(Self::DECIMAL_FRACTIONAL).unwrap(); + + if fractional.is_zero() { + write!(f, "{whole}") + } else { + let fractional_string = + format!("{:0>padding$}", fractional, padding = Self::DECIMAL_PLACES as usize); + f.write_str(&whole.to_string())?; + f.write_char('.')?; + f.write_str(fractional_string.trim_end_matches('0'))?; + Ok(()) + } + } +} + +impl fmt::Debug for Decimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Decimal({self})") + } +} + +impl Add for Decimal { + type Output = Self; + + fn add(self, other: Self) -> Self { + Decimal(self.0 + other.0) + } +} +forward_ref_binop!(impl Add, add for Decimal, Decimal); + +impl AddAssign for Decimal { + fn add_assign(&mut self, rhs: Decimal) { + *self = *self + rhs; + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Decimal, Decimal); + +impl Sub for Decimal { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Decimal(self.0 - other.0) + } +} +forward_ref_binop!(impl Sub, sub for Decimal, Decimal); + +impl SubAssign for Decimal { + fn sub_assign(&mut self, rhs: Decimal) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Decimal, Decimal); + +impl Mul for Decimal { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn mul(self, other: Self) -> Self { + // Decimals are fractions. We can multiply two decimals a and b + // via + // (a.numerator() * b.numerator()) / (a.denominator() * b.denominator()) + // = (a.numerator() * b.numerator()) / a.denominator() / b.denominator() + + let result_as_uint256 = self.numerator().full_mul(other.numerator()) / + Uint256::from_uint128(Self::DECIMAL_FRACTIONAL); // from_uint128 is a const method and should be "free" + match result_as_uint256.try_into() { + Ok(result) => Self(result), + Err(_) => panic!("attempt to multiply with overflow"), + } + } +} +forward_ref_binop!(impl Mul, mul for Decimal, Decimal); + +impl MulAssign for Decimal { + fn mul_assign(&mut self, rhs: Decimal) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Decimal, Decimal); + +impl Div for Decimal { + type Output = Self; + + fn div(self, other: Self) -> Self { + match Decimal::checked_from_ratio(self.numerator(), other.numerator()) { + Ok(ratio) => ratio, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Division failed - denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => { + panic!("Division failed - multiplication overflow") + }, + } + } +} +forward_ref_binop!(impl Div, div for Decimal, Decimal); + +impl DivAssign for Decimal { + fn div_assign(&mut self, rhs: Decimal) { + *self = *self / rhs; + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Decimal, Decimal); + +impl Div for Decimal { + type Output = Self; + + fn div(self, rhs: Uint128) -> Self::Output { + Decimal(self.0 / rhs) + } +} + +impl DivAssign for Decimal { + fn div_assign(&mut self, rhs: Uint128) { + self.0 /= rhs; + } +} + +impl Rem for Decimal { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Decimal, Decimal); + +impl RemAssign for Decimal { + fn rem_assign(&mut self, rhs: Decimal) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Decimal, Decimal); + +impl core::iter::Sum for Decimal +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +/// Serializes as a decimal string +impl Serialize for Decimal { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Deserializes as a base64 string +impl<'de> Deserialize<'de> for Decimal { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(DecimalVisitor) + } +} + +struct DecimalVisitor; + +impl<'de> de::Visitor<'de> for DecimalVisitor { + type Value = Decimal; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded decimal") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match Decimal::from_str(v) { + Ok(d) => Ok(d), + Err(e) => Err(E::custom(format_args!("Error parsing decimal '{v}': {e}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::vec::Vec; + + fn dec(input: &str) -> Decimal { + Decimal::from_str(input).unwrap() + } + + #[test] + fn decimal_new() { + let expected = Uint128::from(300u128); + assert_eq!(Decimal::new(expected).0, expected); + } + + #[test] + fn decimal_raw() { + let value = 300u128; + assert_eq!(Decimal::raw(value).0.u128(), value); + } + + #[test] + fn decimal_one() { + let value = Decimal::one(); + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL); + } + + #[test] + fn decimal_zero() { + let value = Decimal::zero(); + assert!(value.0.is_zero()); + } + + #[test] + fn decimal_percent() { + let value = Decimal::percent(50); + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL / Uint128::from(2u8)); + } + + #[test] + fn decimal_permille() { + let value = Decimal::permille(125); + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL / Uint128::from(8u8)); + } + + #[test] + fn decimal_bps() { + let value = Decimal::bps(125); + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL / Uint128::from(80u8)); + } + + #[test] + fn decimal_from_decimal256_works() { + let too_big = Decimal256::new(Uint256::from(Uint128::MAX) + Uint256::one()); + assert_eq!(Decimal::try_from(too_big), Err(DecimalRangeExceeded)); + + let just_right = Decimal256::new(Uint256::from(Uint128::MAX)); + assert_eq!(Decimal::try_from(just_right), Ok(Decimal::MAX)); + + assert_eq!(Decimal::try_from(Decimal256::zero()), Ok(Decimal::zero())); + assert_eq!(Decimal::try_from(Decimal256::one()), Ok(Decimal::one())); + assert_eq!(Decimal::try_from(Decimal256::percent(50)), Ok(Decimal::percent(50))); + } + + #[test] + fn decimal_try_from_integer() { + let int = Uint128::new(0xDEADBEEF); + let decimal = Decimal::try_from(int).unwrap(); + assert_eq!(int.to_string(), decimal.to_string()); + } + + #[test] + fn decimal_try_from_signed_works() { + assert_eq!( + Decimal::try_from(SignedDecimal::MAX).unwrap(), + Decimal::raw(SignedDecimal::MAX.atomics().i128() as u128) + ); + assert_eq!(Decimal::try_from(SignedDecimal::zero()).unwrap(), Decimal::zero()); + assert_eq!(Decimal::try_from(SignedDecimal::one()).unwrap(), Decimal::one()); + assert_eq!(Decimal::try_from(SignedDecimal::percent(50)).unwrap(), Decimal::percent(50)); + assert_eq!(Decimal::try_from(SignedDecimal::negative_one()), Err(DecimalRangeExceeded)); + assert_eq!(Decimal::try_from(SignedDecimal::MIN), Err(DecimalRangeExceeded)); + } + + #[test] + fn decimal_from_atomics_works() { + let one = Decimal::one(); + let two = one + one; + + assert_eq!(Decimal::from_atomics(1u128, 0).unwrap(), one); + assert_eq!(Decimal::from_atomics(10u128, 1).unwrap(), one); + assert_eq!(Decimal::from_atomics(100u128, 2).unwrap(), one); + assert_eq!(Decimal::from_atomics(1000u128, 3).unwrap(), one); + assert_eq!(Decimal::from_atomics(1000000000000000000u128, 18).unwrap(), one); + assert_eq!(Decimal::from_atomics(10000000000000000000u128, 19).unwrap(), one); + assert_eq!(Decimal::from_atomics(100000000000000000000u128, 20).unwrap(), one); + + assert_eq!(Decimal::from_atomics(2u128, 0).unwrap(), two); + assert_eq!(Decimal::from_atomics(20u128, 1).unwrap(), two); + assert_eq!(Decimal::from_atomics(200u128, 2).unwrap(), two); + assert_eq!(Decimal::from_atomics(2000u128, 3).unwrap(), two); + assert_eq!(Decimal::from_atomics(2000000000000000000u128, 18).unwrap(), two); + assert_eq!(Decimal::from_atomics(20000000000000000000u128, 19).unwrap(), two); + assert_eq!(Decimal::from_atomics(200000000000000000000u128, 20).unwrap(), two); + + // Cuts decimal digits (20 provided but only 18 can be stored) + assert_eq!( + Decimal::from_atomics(4321u128, 20).unwrap(), + Decimal::from_str("0.000000000000000043").unwrap() + ); + assert_eq!( + Decimal::from_atomics(6789u128, 20).unwrap(), + Decimal::from_str("0.000000000000000067").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 38).unwrap(), + Decimal::from_str("3.402823669209384634").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 39).unwrap(), + Decimal::from_str("0.340282366920938463").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 45).unwrap(), + Decimal::from_str("0.000000340282366920").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 51).unwrap(), + Decimal::from_str("0.000000000000340282").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 56).unwrap(), + Decimal::from_str("0.000000000000000003").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, 57).unwrap(), + Decimal::from_str("0.000000000000000000").unwrap() + ); + assert_eq!( + Decimal::from_atomics(u128::MAX, u32::MAX).unwrap(), + Decimal::from_str("0.000000000000000000").unwrap() + ); + + // Can be used with max value + let max = Decimal::MAX; + assert_eq!(Decimal::from_atomics(max.atomics(), max.decimal_places()).unwrap(), max); + + // Overflow is only possible with digits < 18 + let result = Decimal::from_atomics(u128::MAX, 17); + assert_eq!(result.unwrap_err(), DecimalRangeExceeded); + } + + #[test] + fn decimal_from_ratio_works() { + // 1.0 + assert_eq!(Decimal::from_ratio(1u128, 1u128), Decimal::one()); + assert_eq!(Decimal::from_ratio(53u128, 53u128), Decimal::one()); + assert_eq!(Decimal::from_ratio(125u128, 125u128), Decimal::one()); + + // 1.5 + assert_eq!(Decimal::from_ratio(3u128, 2u128), Decimal::percent(150)); + assert_eq!(Decimal::from_ratio(150u128, 100u128), Decimal::percent(150)); + assert_eq!(Decimal::from_ratio(333u128, 222u128), Decimal::percent(150)); + + // 0.125 + assert_eq!(Decimal::from_ratio(1u64, 8u64), Decimal::permille(125)); + assert_eq!(Decimal::from_ratio(125u64, 1000u64), Decimal::permille(125)); + + // 1/3 (result floored) + assert_eq!( + Decimal::from_ratio(1u64, 3u64), + Decimal(Uint128::from(333_333_333_333_333_333u128)) + ); + + // 2/3 (result floored) + assert_eq!( + Decimal::from_ratio(2u64, 3u64), + Decimal(Uint128::from(666_666_666_666_666_666u128)) + ); + + // large inputs + assert_eq!(Decimal::from_ratio(0u128, u128::MAX), Decimal::zero()); + assert_eq!(Decimal::from_ratio(u128::MAX, u128::MAX), Decimal::one()); + // 340282366920938463463 is the largest integer <= Decimal::MAX + assert_eq!( + Decimal::from_ratio(340282366920938463463u128, 1u128), + Decimal::from_str("340282366920938463463").unwrap() + ); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn decimal_from_ratio_panics_for_zero_denominator() { + Decimal::from_ratio(1u128, 0u128); + } + + #[test] + #[should_panic(expected = "Multiplication overflow")] + fn decimal_from_ratio_panics_for_mul_overflow() { + Decimal::from_ratio(u128::MAX, 1u128); + } + + #[test] + fn decimal_checked_from_ratio_does_not_panic() { + assert_eq!( + Decimal::checked_from_ratio(1u128, 0u128), + Err(CheckedFromRatioError::DivideByZero) + ); + + assert_eq!( + Decimal::checked_from_ratio(u128::MAX, 1u128), + Err(CheckedFromRatioError::Overflow) + ); + } + + #[test] + fn decimal_implements_fraction() { + let fraction = Decimal::from_str("1234.567").unwrap(); + assert_eq!(fraction.numerator(), Uint128::from(1_234_567_000_000_000_000_000u128)); + assert_eq!(fraction.denominator(), Uint128::from(1_000_000_000_000_000_000u128)); + } + + #[test] + fn decimal_from_str_works() { + // Integers + assert_eq!(Decimal::from_str("0").unwrap(), Decimal::percent(0)); + assert_eq!(Decimal::from_str("1").unwrap(), Decimal::percent(100)); + assert_eq!(Decimal::from_str("5").unwrap(), Decimal::percent(500)); + assert_eq!(Decimal::from_str("42").unwrap(), Decimal::percent(4200)); + assert_eq!(Decimal::from_str("000").unwrap(), Decimal::percent(0)); + assert_eq!(Decimal::from_str("001").unwrap(), Decimal::percent(100)); + assert_eq!(Decimal::from_str("005").unwrap(), Decimal::percent(500)); + assert_eq!(Decimal::from_str("0042").unwrap(), Decimal::percent(4200)); + + // Decimals + assert_eq!(Decimal::from_str("1.0").unwrap(), Decimal::percent(100)); + assert_eq!(Decimal::from_str("1.5").unwrap(), Decimal::percent(150)); + assert_eq!(Decimal::from_str("0.5").unwrap(), Decimal::percent(50)); + assert_eq!(Decimal::from_str("0.123").unwrap(), Decimal::permille(123)); + + assert_eq!(Decimal::from_str("40.00").unwrap(), Decimal::percent(4000)); + assert_eq!(Decimal::from_str("04.00").unwrap(), Decimal::percent(400)); + assert_eq!(Decimal::from_str("00.40").unwrap(), Decimal::percent(40)); + assert_eq!(Decimal::from_str("00.04").unwrap(), Decimal::percent(4)); + + // Can handle DECIMAL_PLACES fractional digits + assert_eq!( + Decimal::from_str("7.123456789012345678").unwrap(), + Decimal(Uint128::from(7123456789012345678u128)) + ); + assert_eq!( + Decimal::from_str("7.999999999999999999").unwrap(), + Decimal(Uint128::from(7999999999999999999u128)) + ); + + // Works for documented max value + assert_eq!( + Decimal::from_str("340282366920938463463.374607431768211455").unwrap(), + Decimal::MAX + ); + } + + #[test] + fn decimal_from_str_errors_for_broken_whole_part() { + match Decimal::from_str("").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str(" ").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str("-1").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal_from_str_errors_for_broken_fractional_part() { + match Decimal::from_str("1.").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str("1. ").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str("1.e").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str("1.2e3").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal_from_str_errors_for_more_than_18_fractional_digits() { + match Decimal::from_str("7.1234567890123456789").unwrap_err() { + StdError::GenericErr { msg, .. } => { + assert_eq!(msg, "Cannot parse more than 18 fractional digits",) + }, + e => panic!("Unexpected error: {e:?}"), + } + + // No special rules for trailing zeros. This could be changed but adds gas cost for the + // happy path. + match Decimal::from_str("7.1230000000000000000").unwrap_err() { + StdError::GenericErr { msg, .. } => { + assert_eq!(msg, "Cannot parse more than 18 fractional digits") + }, + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal_from_str_errors_for_invalid_number_of_dots() { + match Decimal::from_str("1.2.3").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Unexpected number of dots"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal::from_str("1.2.3.4").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Unexpected number of dots"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal_from_str_errors_for_more_than_max_value() { + // Integer + match Decimal::from_str("340282366920938463464").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + + // Decimal + match Decimal::from_str("340282366920938463464.0").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + match Decimal::from_str("340282366920938463463.374607431768211456").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal_atomics_works() { + let zero = Decimal::zero(); + let one = Decimal::one(); + let half = Decimal::percent(50); + let two = Decimal::percent(200); + let max = Decimal::MAX; + + assert_eq!(zero.atomics(), Uint128::new(0)); + assert_eq!(one.atomics(), Uint128::new(1000000000000000000)); + assert_eq!(half.atomics(), Uint128::new(500000000000000000)); + assert_eq!(two.atomics(), Uint128::new(2000000000000000000)); + assert_eq!(max.atomics(), Uint128::MAX); + } + + #[test] + fn decimal_decimal_places_works() { + let zero = Decimal::zero(); + let one = Decimal::one(); + let half = Decimal::percent(50); + let two = Decimal::percent(200); + let max = Decimal::MAX; + + assert_eq!(zero.decimal_places(), 18); + assert_eq!(one.decimal_places(), 18); + assert_eq!(half.decimal_places(), 18); + assert_eq!(two.decimal_places(), 18); + assert_eq!(max.decimal_places(), 18); + } + + #[test] + fn decimal_is_zero_works() { + assert!(Decimal::zero().is_zero()); + assert!(Decimal::percent(0).is_zero()); + assert!(Decimal::permille(0).is_zero()); + + assert!(!Decimal::one().is_zero()); + assert!(!Decimal::percent(123).is_zero()); + assert!(!Decimal::permille(1234).is_zero()); + } + + #[test] + fn decimal_inv_works() { + // d = 0 + assert_eq!(Decimal::zero().inv(), None); + + // d == 1 + assert_eq!(Decimal::one().inv(), Some(Decimal::one())); + + // d > 1 exact + assert_eq!(Decimal::from_str("2").unwrap().inv(), Some(Decimal::from_str("0.5").unwrap())); + assert_eq!( + Decimal::from_str("20").unwrap().inv(), + Some(Decimal::from_str("0.05").unwrap()) + ); + assert_eq!( + Decimal::from_str("200").unwrap().inv(), + Some(Decimal::from_str("0.005").unwrap()) + ); + assert_eq!( + Decimal::from_str("2000").unwrap().inv(), + Some(Decimal::from_str("0.0005").unwrap()) + ); + + // d > 1 rounded + assert_eq!( + Decimal::from_str("3").unwrap().inv(), + Some(Decimal::from_str("0.333333333333333333").unwrap()) + ); + assert_eq!( + Decimal::from_str("6").unwrap().inv(), + Some(Decimal::from_str("0.166666666666666666").unwrap()) + ); + + // d < 1 exact + assert_eq!(Decimal::from_str("0.5").unwrap().inv(), Some(Decimal::from_str("2").unwrap())); + assert_eq!( + Decimal::from_str("0.05").unwrap().inv(), + Some(Decimal::from_str("20").unwrap()) + ); + assert_eq!( + Decimal::from_str("0.005").unwrap().inv(), + Some(Decimal::from_str("200").unwrap()) + ); + assert_eq!( + Decimal::from_str("0.0005").unwrap().inv(), + Some(Decimal::from_str("2000").unwrap()) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal_add_works() { + let value = Decimal::one() + Decimal::percent(50); // 1.5 + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL * Uint128::from(3u8) / Uint128::from(2u8)); + + assert_eq!(Decimal::percent(5) + Decimal::percent(4), Decimal::percent(9)); + assert_eq!(Decimal::percent(5) + Decimal::zero(), Decimal::percent(5)); + assert_eq!(Decimal::zero() + Decimal::zero(), Decimal::zero()); + + // works for refs + let a = Decimal::percent(15); + let b = Decimal::percent(25); + let expected = Decimal::percent(40); + assert_eq!(a + b, expected); + assert_eq!(&a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn decimal_add_overflow_panics() { + let _value = Decimal::MAX + Decimal::percent(50); + } + + #[test] + fn decimal_add_assign_works() { + let mut a = Decimal::percent(30); + a += Decimal::percent(20); + assert_eq!(a, Decimal::percent(50)); + + // works for refs + let mut a = Decimal::percent(15); + let b = Decimal::percent(3); + let expected = Decimal::percent(18); + a += &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal_sub_works() { + let value = Decimal::one() - Decimal::percent(50); // 0.5 + assert_eq!(value.0, Decimal::DECIMAL_FRACTIONAL / Uint128::from(2u8)); + + assert_eq!(Decimal::percent(9) - Decimal::percent(4), Decimal::percent(5)); + assert_eq!(Decimal::percent(16) - Decimal::zero(), Decimal::percent(16)); + assert_eq!(Decimal::percent(16) - Decimal::percent(16), Decimal::zero()); + assert_eq!(Decimal::zero() - Decimal::zero(), Decimal::zero()); + + // works for refs + let a = Decimal::percent(13); + let b = Decimal::percent(6); + let expected = Decimal::percent(7); + assert_eq!(a - b, expected); + assert_eq!(&a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn decimal_sub_overflow_panics() { + let _value = Decimal::zero() - Decimal::percent(50); + } + + #[test] + fn decimal_sub_assign_works() { + let mut a = Decimal::percent(20); + a -= Decimal::percent(2); + assert_eq!(a, Decimal::percent(18)); + + // works for refs + let mut a = Decimal::percent(33); + let b = Decimal::percent(13); + let expected = Decimal::percent(20); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal_implements_mul() { + let one = Decimal::one(); + let two = one + one; + let half = Decimal::percent(50); + + // 1*x and x*1 + assert_eq!(one * Decimal::percent(0), Decimal::percent(0)); + assert_eq!(one * Decimal::percent(1), Decimal::percent(1)); + assert_eq!(one * Decimal::percent(10), Decimal::percent(10)); + assert_eq!(one * Decimal::percent(100), Decimal::percent(100)); + assert_eq!(one * Decimal::percent(1000), Decimal::percent(1000)); + assert_eq!(one * Decimal::MAX, Decimal::MAX); + assert_eq!(Decimal::percent(0) * one, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) * one, Decimal::percent(1)); + assert_eq!(Decimal::percent(10) * one, Decimal::percent(10)); + assert_eq!(Decimal::percent(100) * one, Decimal::percent(100)); + assert_eq!(Decimal::percent(1000) * one, Decimal::percent(1000)); + assert_eq!(Decimal::MAX * one, Decimal::MAX); + + // double + assert_eq!(two * Decimal::percent(0), Decimal::percent(0)); + assert_eq!(two * Decimal::percent(1), Decimal::percent(2)); + assert_eq!(two * Decimal::percent(10), Decimal::percent(20)); + assert_eq!(two * Decimal::percent(100), Decimal::percent(200)); + assert_eq!(two * Decimal::percent(1000), Decimal::percent(2000)); + assert_eq!(Decimal::percent(0) * two, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) * two, Decimal::percent(2)); + assert_eq!(Decimal::percent(10) * two, Decimal::percent(20)); + assert_eq!(Decimal::percent(100) * two, Decimal::percent(200)); + assert_eq!(Decimal::percent(1000) * two, Decimal::percent(2000)); + + // half + assert_eq!(half * Decimal::percent(0), Decimal::percent(0)); + assert_eq!(half * Decimal::percent(1), Decimal::permille(5)); + assert_eq!(half * Decimal::percent(10), Decimal::percent(5)); + assert_eq!(half * Decimal::percent(100), Decimal::percent(50)); + assert_eq!(half * Decimal::percent(1000), Decimal::percent(500)); + assert_eq!(Decimal::percent(0) * half, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) * half, Decimal::permille(5)); + assert_eq!(Decimal::percent(10) * half, Decimal::percent(5)); + assert_eq!(Decimal::percent(100) * half, Decimal::percent(50)); + assert_eq!(Decimal::percent(1000) * half, Decimal::percent(500)); + + // Move left + let a = dec("123.127726548762582"); + assert_eq!(a * dec("1"), dec("123.127726548762582")); + assert_eq!(a * dec("10"), dec("1231.27726548762582")); + assert_eq!(a * dec("100"), dec("12312.7726548762582")); + assert_eq!(a * dec("1000"), dec("123127.726548762582")); + assert_eq!(a * dec("1000000"), dec("123127726.548762582")); + assert_eq!(a * dec("1000000000"), dec("123127726548.762582")); + assert_eq!(a * dec("1000000000000"), dec("123127726548762.582")); + assert_eq!(a * dec("1000000000000000"), dec("123127726548762582")); + assert_eq!(a * dec("1000000000000000000"), dec("123127726548762582000")); + assert_eq!(dec("1") * a, dec("123.127726548762582")); + assert_eq!(dec("10") * a, dec("1231.27726548762582")); + assert_eq!(dec("100") * a, dec("12312.7726548762582")); + assert_eq!(dec("1000") * a, dec("123127.726548762582")); + assert_eq!(dec("1000000") * a, dec("123127726.548762582")); + assert_eq!(dec("1000000000") * a, dec("123127726548.762582")); + assert_eq!(dec("1000000000000") * a, dec("123127726548762.582")); + assert_eq!(dec("1000000000000000") * a, dec("123127726548762582")); + assert_eq!(dec("1000000000000000000") * a, dec("123127726548762582000")); + + // Move right + let max = Decimal::MAX; + assert_eq!(max * dec("1.0"), dec("340282366920938463463.374607431768211455")); + assert_eq!(max * dec("0.1"), dec("34028236692093846346.337460743176821145")); + assert_eq!(max * dec("0.01"), dec("3402823669209384634.633746074317682114")); + assert_eq!(max * dec("0.001"), dec("340282366920938463.463374607431768211")); + assert_eq!(max * dec("0.000001"), dec("340282366920938.463463374607431768")); + assert_eq!(max * dec("0.000000001"), dec("340282366920.938463463374607431")); + assert_eq!(max * dec("0.000000000001"), dec("340282366.920938463463374607")); + assert_eq!(max * dec("0.000000000000001"), dec("340282.366920938463463374")); + assert_eq!(max * dec("0.000000000000000001"), dec("340.282366920938463463")); + + // works for refs + let a = Decimal::percent(20); + let b = Decimal::percent(30); + let expected = Decimal::percent(6); + assert_eq!(a * b, expected); + assert_eq!(&a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn decimal_mul_assign_works() { + let mut a = Decimal::percent(15); + a *= Decimal::percent(60); + assert_eq!(a, Decimal::percent(9)); + + // works for refs + let mut a = Decimal::percent(50); + let b = Decimal::percent(20); + a *= &b; + assert_eq!(a, Decimal::percent(10)); + } + + #[test] + #[should_panic(expected = "attempt to multiply with overflow")] + fn decimal_mul_overflow_panics() { + let _value = Decimal::MAX * Decimal::percent(101); + } + + #[test] + fn decimal_checked_mul() { + let test_data = [ + (Decimal::zero(), Decimal::zero()), + (Decimal::zero(), Decimal::one()), + (Decimal::one(), Decimal::zero()), + (Decimal::percent(10), Decimal::zero()), + (Decimal::percent(10), Decimal::percent(5)), + (Decimal::MAX, Decimal::one()), + (Decimal::MAX / Uint128::new(2), Decimal::percent(200)), + (Decimal::permille(6), Decimal::permille(13)), + ]; + + // The regular core::ops::Mul is our source of truth for these tests. + for (x, y) in test_data.into_iter() { + assert_eq!(x * y, x.checked_mul(y).unwrap()); + } + } + + #[test] + fn decimal_checked_mul_overflow() { + assert_eq!( + Decimal::MAX.checked_mul(Decimal::percent(200)), + Err(OverflowError::new(OverflowOperation::Mul)) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal_implements_div() { + let one = Decimal::one(); + let two = one + one; + let half = Decimal::percent(50); + + // 1/x and x/1 + assert_eq!(one / Decimal::percent(1), Decimal::percent(10_000)); + assert_eq!(one / Decimal::percent(10), Decimal::percent(1_000)); + assert_eq!(one / Decimal::percent(100), Decimal::percent(100)); + assert_eq!(one / Decimal::percent(1000), Decimal::percent(10)); + assert_eq!(Decimal::percent(0) / one, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) / one, Decimal::percent(1)); + assert_eq!(Decimal::percent(10) / one, Decimal::percent(10)); + assert_eq!(Decimal::percent(100) / one, Decimal::percent(100)); + assert_eq!(Decimal::percent(1000) / one, Decimal::percent(1000)); + + // double + assert_eq!(two / Decimal::percent(1), Decimal::percent(20_000)); + assert_eq!(two / Decimal::percent(10), Decimal::percent(2_000)); + assert_eq!(two / Decimal::percent(100), Decimal::percent(200)); + assert_eq!(two / Decimal::percent(1000), Decimal::percent(20)); + assert_eq!(Decimal::percent(0) / two, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) / two, dec("0.005")); + assert_eq!(Decimal::percent(10) / two, Decimal::percent(5)); + assert_eq!(Decimal::percent(100) / two, Decimal::percent(50)); + assert_eq!(Decimal::percent(1000) / two, Decimal::percent(500)); + + // half + assert_eq!(half / Decimal::percent(1), Decimal::percent(5_000)); + assert_eq!(half / Decimal::percent(10), Decimal::percent(500)); + assert_eq!(half / Decimal::percent(100), Decimal::percent(50)); + assert_eq!(half / Decimal::percent(1000), Decimal::percent(5)); + assert_eq!(Decimal::percent(0) / half, Decimal::percent(0)); + assert_eq!(Decimal::percent(1) / half, Decimal::percent(2)); + assert_eq!(Decimal::percent(10) / half, Decimal::percent(20)); + assert_eq!(Decimal::percent(100) / half, Decimal::percent(200)); + assert_eq!(Decimal::percent(1000) / half, Decimal::percent(2000)); + + // Move right + let a = dec("123127726548762582"); + assert_eq!(a / dec("1"), dec("123127726548762582")); + assert_eq!(a / dec("10"), dec("12312772654876258.2")); + assert_eq!(a / dec("100"), dec("1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("0.123127726548762582")); + assert_eq!(dec("1") / a, dec("0.000000000000000008")); + assert_eq!(dec("10") / a, dec("0.000000000000000081")); + assert_eq!(dec("100") / a, dec("0.000000000000000812")); + assert_eq!(dec("1000") / a, dec("0.000000000000008121")); + assert_eq!(dec("1000000") / a, dec("0.000000000008121647")); + assert_eq!(dec("1000000000") / a, dec("0.000000008121647560")); + assert_eq!(dec("1000000000000") / a, dec("0.000008121647560868")); + assert_eq!(dec("1000000000000000") / a, dec("0.008121647560868164")); + assert_eq!(dec("1000000000000000000") / a, dec("8.121647560868164773")); + + // Move left + let a = dec("0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("123127726.548762582")); + assert_eq!(a / dec("0.000000000001"), dec("123127726548.762582")); + assert_eq!(a / dec("0.000000000000001"), dec("123127726548762.582")); + assert_eq!(a / dec("0.000000000000000001"), dec("123127726548762582")); + + assert_eq!(Decimal::percent(15) / Decimal::percent(60), Decimal::percent(25)); + + // works for refs + let a = Decimal::percent(100); + let b = Decimal::percent(20); + let expected = Decimal::percent(500); + assert_eq!(a / b, expected); + assert_eq!(&a / b, expected); + assert_eq!(a / &b, expected); + assert_eq!(&a / &b, expected); + } + + #[test] + fn decimal_div_assign_works() { + let mut a = Decimal::percent(15); + a /= Decimal::percent(20); + assert_eq!(a, Decimal::percent(75)); + + // works for refs + let mut a = Decimal::percent(50); + let b = Decimal::percent(20); + a /= &b; + assert_eq!(a, Decimal::percent(250)); + } + + #[test] + #[should_panic(expected = "Division failed - multiplication overflow")] + fn decimal_div_overflow_panics() { + let _value = Decimal::MAX / Decimal::percent(10); + } + + #[test] + #[should_panic(expected = "Division failed - denominator must not be zero")] + fn decimal_div_by_zero_panics() { + let _value = Decimal::one() / Decimal::zero(); + } + + #[test] + fn decimal_uint128_division() { + // a/b + let left = Decimal::percent(150); // 1.5 + let right = Uint128::new(3); + assert_eq!(left / right, Decimal::percent(50)); + + // 0/a + let left = Decimal::zero(); + let right = Uint128::new(300); + assert_eq!(left / right, Decimal::zero()); + } + + #[test] + #[should_panic(expected = "attempt to divide by zero")] + fn decimal_uint128_divide_by_zero() { + let left = Decimal::percent(150); // 1.5 + let right = Uint128::new(0); + let _result = left / right; + } + + #[test] + fn decimal_uint128_div_assign() { + // a/b + let mut dec = Decimal::percent(150); // 1.5 + dec /= Uint128::new(3); + assert_eq!(dec, Decimal::percent(50)); + + // 0/a + let mut dec = Decimal::zero(); + dec /= Uint128::new(300); + assert_eq!(dec, Decimal::zero()); + } + + #[test] + #[should_panic(expected = "attempt to divide by zero")] + fn decimal_uint128_div_assign_by_zero() { + // a/0 + let mut dec = Decimal::percent(50); + dec /= Uint128::new(0); + } + + #[test] + fn decimal_uint128_sqrt() { + assert_eq!(Decimal::percent(900).sqrt(), Decimal::percent(300)); + + assert!(Decimal::percent(316) < Decimal::percent(1000).sqrt()); + assert!(Decimal::percent(1000).sqrt() < Decimal::percent(317)); + } + + /// sqrt(2) is an irrational number, i.e. all 18 decimal places should be used. + #[test] + fn decimal_uint128_sqrt_is_precise() { + assert_eq!( + Decimal::from_str("2").unwrap().sqrt(), + Decimal::from_str("1.414213562373095048").unwrap() /* https://www.wolframalpha.com/input/?i=sqrt%282%29 */ + ); + } + + #[test] + fn decimal_uint128_sqrt_does_not_overflow() { + assert_eq!(Decimal::from_str("400").unwrap().sqrt(), Decimal::from_str("20").unwrap()); + } + + #[test] + fn decimal_uint128_sqrt_intermediate_precision_used() { + assert_eq!( + Decimal::from_str("400001").unwrap().sqrt(), + // The last two digits (27) are truncated below due to the algorithm + // we use. Larger numbers will cause less precision. + // https://www.wolframalpha.com/input/?i=sqrt%28400001%29 + Decimal::from_str("632.456322602596803200").unwrap() + ); + } + + #[test] + fn decimal_checked_pow() { + for exp in 0..10 { + assert_eq!(Decimal::one().checked_pow(exp).unwrap(), Decimal::one()); + } + + // This case is mathematically undefined but we ensure consistency with Rust standard types + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=20df6716048e77087acd40194b233494 + assert_eq!(Decimal::zero().checked_pow(0).unwrap(), Decimal::one()); + + for exp in 1..10 { + assert_eq!(Decimal::zero().checked_pow(exp).unwrap(), Decimal::zero()); + } + + for num in &[Decimal::percent(50), Decimal::percent(99), Decimal::percent(200)] { + assert_eq!(num.checked_pow(0).unwrap(), Decimal::one()) + } + + assert_eq!(Decimal::percent(20).checked_pow(2).unwrap(), Decimal::percent(4)); + + assert_eq!(Decimal::percent(20).checked_pow(3).unwrap(), Decimal::permille(8)); + + assert_eq!(Decimal::percent(200).checked_pow(4).unwrap(), Decimal::percent(1600)); + + assert_eq!(Decimal::percent(200).checked_pow(4).unwrap(), Decimal::percent(1600)); + + assert_eq!(Decimal::percent(700).checked_pow(5).unwrap(), Decimal::percent(1680700)); + + assert_eq!(Decimal::percent(700).checked_pow(8).unwrap(), Decimal::percent(576480100)); + + assert_eq!(Decimal::percent(700).checked_pow(10).unwrap(), Decimal::percent(28247524900)); + + assert_eq!( + Decimal::percent(120).checked_pow(123).unwrap(), + Decimal(5486473221892422150877397607u128.into()) + ); + + assert_eq!( + Decimal::percent(10).checked_pow(2).unwrap(), + Decimal(10000000000000000u128.into()) + ); + + assert_eq!(Decimal::percent(10).checked_pow(18).unwrap(), Decimal(1u128.into())); + } + + #[test] + fn decimal_checked_pow_overflow() { + assert_eq!(Decimal::MAX.checked_pow(2), Err(OverflowError::new(OverflowOperation::Pow))); + } + + #[test] + fn decimal_to_string() { + // Integers + assert_eq!(Decimal::zero().to_string(), "0"); + assert_eq!(Decimal::one().to_string(), "1"); + assert_eq!(Decimal::percent(500).to_string(), "5"); + + // Decimals + assert_eq!(Decimal::percent(125).to_string(), "1.25"); + assert_eq!(Decimal::percent(42638).to_string(), "426.38"); + assert_eq!(Decimal::percent(3).to_string(), "0.03"); + assert_eq!(Decimal::permille(987).to_string(), "0.987"); + + assert_eq!(Decimal(Uint128::from(1u128)).to_string(), "0.000000000000000001"); + assert_eq!(Decimal(Uint128::from(10u128)).to_string(), "0.00000000000000001"); + assert_eq!(Decimal(Uint128::from(100u128)).to_string(), "0.0000000000000001"); + assert_eq!(Decimal(Uint128::from(1000u128)).to_string(), "0.000000000000001"); + assert_eq!(Decimal(Uint128::from(10000u128)).to_string(), "0.00000000000001"); + assert_eq!(Decimal(Uint128::from(100000u128)).to_string(), "0.0000000000001"); + assert_eq!(Decimal(Uint128::from(1000000u128)).to_string(), "0.000000000001"); + assert_eq!(Decimal(Uint128::from(10000000u128)).to_string(), "0.00000000001"); + assert_eq!(Decimal(Uint128::from(100000000u128)).to_string(), "0.0000000001"); + assert_eq!(Decimal(Uint128::from(1000000000u128)).to_string(), "0.000000001"); + assert_eq!(Decimal(Uint128::from(10000000000u128)).to_string(), "0.00000001"); + assert_eq!(Decimal(Uint128::from(100000000000u128)).to_string(), "0.0000001"); + assert_eq!(Decimal(Uint128::from(10000000000000u128)).to_string(), "0.00001"); + assert_eq!(Decimal(Uint128::from(100000000000000u128)).to_string(), "0.0001"); + assert_eq!(Decimal(Uint128::from(1000000000000000u128)).to_string(), "0.001"); + assert_eq!(Decimal(Uint128::from(10000000000000000u128)).to_string(), "0.01"); + assert_eq!(Decimal(Uint128::from(100000000000000000u128)).to_string(), "0.1"); + } + + #[test] + fn decimal_iter_sum() { + let items = + vec![Decimal::zero(), Decimal(Uint128::from(2u128)), Decimal(Uint128::from(2u128))]; + assert_eq!(items.iter().sum::(), Decimal(Uint128::from(4u128))); + assert_eq!(items.into_iter().sum::(), Decimal(Uint128::from(4u128))); + + let empty: Vec = vec![]; + assert_eq!(Decimal::zero(), empty.iter().sum::()); + } + + #[test] + fn decimal_serialize() { + assert_eq!(serde_json::to_vec(&Decimal::zero()).unwrap(), br#""0""#); + assert_eq!(serde_json::to_vec(&Decimal::one()).unwrap(), br#""1""#); + assert_eq!(serde_json::to_vec(&Decimal::percent(8)).unwrap(), br#""0.08""#); + assert_eq!(serde_json::to_vec(&Decimal::percent(87)).unwrap(), br#""0.87""#); + assert_eq!(serde_json::to_vec(&Decimal::percent(876)).unwrap(), br#""8.76""#); + assert_eq!(serde_json::to_vec(&Decimal::percent(8765)).unwrap(), br#""87.65""#); + } + + #[test] + fn decimal_deserialize() { + assert_eq!(serde_json::from_slice::(br#""0""#).unwrap(), Decimal::zero()); + assert_eq!(serde_json::from_slice::(br#""1""#).unwrap(), Decimal::one()); + assert_eq!(serde_json::from_slice::(br#""000""#).unwrap(), Decimal::zero()); + assert_eq!(serde_json::from_slice::(br#""001""#).unwrap(), Decimal::one()); + + assert_eq!(serde_json::from_slice::(br#""0.08""#).unwrap(), Decimal::percent(8)); + assert_eq!(serde_json::from_slice::(br#""0.87""#).unwrap(), Decimal::percent(87)); + assert_eq!(serde_json::from_slice::(br#""8.76""#).unwrap(), Decimal::percent(876)); + assert_eq!( + serde_json::from_slice::(br#""87.65""#).unwrap(), + Decimal::percent(8765) + ); + } + + #[test] + fn decimal_abs_diff_works() { + let a = Decimal::percent(285); + let b = Decimal::percent(200); + let expected = Decimal::percent(85); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal_rem_works() { + // 4.02 % 1.11 = 0.69 + assert_eq!(Decimal::percent(402) % Decimal::percent(111), Decimal::percent(69)); + + // 15.25 % 4 = 3.25 + assert_eq!(Decimal::percent(1525) % Decimal::percent(400), Decimal::percent(325)); + + let a = Decimal::percent(318); + let b = Decimal::percent(317); + let expected = Decimal::percent(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn decimal_rem_assign_works() { + let mut a = Decimal::percent(17673); + a %= Decimal::percent(2362); + assert_eq!(a, Decimal::percent(1139)); // 176.73 % 23.62 = 11.39 + + let mut a = Decimal::percent(4262); + let b = Decimal::percent(1270); + a %= &b; + assert_eq!(a, Decimal::percent(452)); // 42.62 % 12.7 = 4.52 + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn decimal_rem_panics_for_zero() { + let _ = Decimal::percent(777) % Decimal::zero(); + } + + #[test] + fn decimal_checked_methods() { + // checked add + assert_eq!( + Decimal::percent(402).checked_add(Decimal::percent(111)).unwrap(), + Decimal::percent(513) + ); + assert!(matches!(Decimal::MAX.checked_add(Decimal::percent(1)), Err(OverflowError { .. }))); + + // checked sub + assert_eq!( + Decimal::percent(1111).checked_sub(Decimal::percent(111)).unwrap(), + Decimal::percent(1000) + ); + assert!(matches!( + Decimal::zero().checked_sub(Decimal::percent(1)), + Err(OverflowError { .. }) + )); + + // checked div + assert_eq!( + Decimal::percent(30).checked_div(Decimal::percent(200)).unwrap(), + Decimal::percent(15) + ); + assert_eq!( + Decimal::percent(88).checked_div(Decimal::percent(20)).unwrap(), + Decimal::percent(440) + ); + assert!(matches!( + Decimal::MAX.checked_div(Decimal::zero()), + Err(CheckedFromRatioError::DivideByZero {}) + )); + assert!(matches!( + Decimal::MAX.checked_div(Decimal::percent(1)), + Err(CheckedFromRatioError::Overflow {}) + )); + + // checked rem + assert_eq!( + Decimal::percent(402).checked_rem(Decimal::percent(111)).unwrap(), + Decimal::percent(69) + ); + assert_eq!( + Decimal::percent(1525).checked_rem(Decimal::percent(400)).unwrap(), + Decimal::percent(325) + ); + assert!(matches!(Decimal::MAX.checked_rem(Decimal::zero()), Err(DivideByZeroError { .. }))); + } + + #[test] + fn decimal_pow_works() { + assert_eq!(Decimal::percent(200).pow(2), Decimal::percent(400)); + assert_eq!(Decimal::percent(200).pow(10), Decimal::percent(102400)); + } + + #[test] + #[should_panic] + fn decimal_pow_overflow_panics() { + _ = Decimal::MAX.pow(2u32); + } + + #[test] + fn decimal_saturating_works() { + assert_eq!( + Decimal::percent(200).saturating_add(Decimal::percent(200)), + Decimal::percent(400) + ); + assert_eq!(Decimal::MAX.saturating_add(Decimal::percent(200)), Decimal::MAX); + assert_eq!( + Decimal::percent(200).saturating_sub(Decimal::percent(100)), + Decimal::percent(100) + ); + assert_eq!(Decimal::zero().saturating_sub(Decimal::percent(200)), Decimal::zero()); + assert_eq!( + Decimal::percent(200).saturating_mul(Decimal::percent(50)), + Decimal::percent(100) + ); + assert_eq!(Decimal::MAX.saturating_mul(Decimal::percent(200)), Decimal::MAX); + assert_eq!(Decimal::percent(400).saturating_pow(2u32), Decimal::percent(1600)); + assert_eq!(Decimal::MAX.saturating_pow(2u32), Decimal::MAX); + } + + #[test] + fn decimal_rounding() { + assert_eq!(Decimal::one().floor(), Decimal::one()); + assert_eq!(Decimal::percent(150).floor(), Decimal::one()); + assert_eq!(Decimal::percent(199).floor(), Decimal::one()); + assert_eq!(Decimal::percent(200).floor(), Decimal::percent(200)); + assert_eq!(Decimal::percent(99).floor(), Decimal::zero()); + + assert_eq!(Decimal::one().ceil(), Decimal::one()); + assert_eq!(Decimal::percent(150).ceil(), Decimal::percent(200)); + assert_eq!(Decimal::percent(199).ceil(), Decimal::percent(200)); + assert_eq!(Decimal::percent(99).ceil(), Decimal::one()); + assert_eq!(Decimal(Uint128::from(1u128)).ceil(), Decimal::one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn decimal_ceil_panics() { + let _ = Decimal::MAX.ceil(); + } + + #[test] + fn decimal_checked_ceil() { + assert_eq!(Decimal::percent(199).checked_ceil(), Ok(Decimal::percent(200))); + assert!(matches!(Decimal::MAX.checked_ceil(), Err(RoundUpOverflowError { .. }))); + } + + #[test] + fn decimal_to_uint_floor_works() { + let d = Decimal::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(12)); + let d = Decimal::from_str("12.345").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(12)); + let d = Decimal::from_str("12.999").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(12)); + let d = Decimal::from_str("0.98451384").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(0)); + + let d = Decimal::from_str("75.0").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(75)); + let d = Decimal::from_str("0.0").unwrap(); + assert_eq!(d.to_uint_floor(), Uint128::new(0)); + + let d = Decimal::MAX; + assert_eq!(d.to_uint_floor(), Uint128::new(340282366920938463463)); + + // Does the same as the old workaround `Uint128::one() * my_decimal`. + // This block can be deleted as part of https://github.com/CosmWasm/cosmwasm/issues/1485. + let tests = vec![ + (Decimal::from_str("12.345").unwrap(), 12u128), + (Decimal::from_str("0.98451384").unwrap(), 0u128), + (Decimal::from_str("178.0").unwrap(), 178u128), + (Decimal::MIN, 0u128), + (Decimal::MAX, u128::MAX / Decimal::DECIMAL_FRACTIONAL.u128()), + ]; + for (my_decimal, expected) in tests.into_iter() { + assert_eq!(my_decimal.to_uint_floor(), Uint128::new(expected)); + } + } + + #[test] + fn decimal_to_uint_ceil_works() { + let d = Decimal::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint128::new(13)); + let d = Decimal::from_str("12.345").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint128::new(13)); + let d = Decimal::from_str("12.999").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint128::new(13)); + + let d = Decimal::from_str("75.0").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint128::new(75)); + let d = Decimal::from_str("0.0").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint128::new(0)); + + let d = Decimal::MAX; + assert_eq!(d.to_uint_ceil(), Uint128::new(340282366920938463464)); + } + + #[test] + fn decimal_partial_eq() { + let test_cases = [ + ("1", "1", true), + ("0.5", "0.5", true), + ("0.5", "0.51", false), + ("0", "0.00000", true), + ] + .into_iter() + .map(|(lhs, rhs, expected)| (dec(lhs), dec(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn decimal_implements_debug() { + let decimal = Decimal::from_str("123.45").unwrap(); + assert_eq!(format!("{decimal:?}"), "Decimal(123.45)"); + + let test_cases = ["5", "5.01", "42", "0", "2"]; + for s in test_cases { + let decimal = Decimal::from_str(s).unwrap(); + let expected = format!("Decimal({s})"); + assert_eq!(format!("{decimal:?}"), expected); + } + } +} diff --git a/cosmwasm/std/src/math/decimal256.rs b/cosmwasm/std/src/math/decimal256.rs new file mode 100644 index 00000000..6abfc390 --- /dev/null +++ b/cosmwasm/std/src/math/decimal256.rs @@ -0,0 +1,2039 @@ +use alloc::string::ToString; +use core::{ + cmp::Ordering, + fmt::{self, Write}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, RemAssign, Sub, SubAssign}, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{ + CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, RoundUpOverflowError, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Decimal, SignedDecimal, SignedDecimal256, Uint512, + __internal::forward_ref_partial_eq, +}; + +use super::{Fraction, Isqrt, Uint256}; + +use crate::prelude::*; + +/// A fixed-point decimal value with 18 fractional digits, i.e. +/// Decimal256(1_000_000_000_000_000_000) == 1.0 +/// +/// The greatest possible value that can be represented is +/// 115792089237316195423570985008687907853269984665640564039457.584007913129639935 +/// (which is (2^256 - 1) / 10^18) +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Decimal256(#[schemars(with = "String")] Uint256); + +forward_ref_partial_eq!(Decimal256, Decimal256); + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("Decimal256 range exceeded")] +pub struct Decimal256RangeExceeded; + +impl Decimal256 { + const DECIMAL_FRACTIONAL: Uint256 = // 1*10**18 + Uint256::from_u128(1_000_000_000_000_000_000); + const DECIMAL_FRACTIONAL_SQUARED: Uint256 = // 1*10**36 + Uint256::from_u128(1_000_000_000_000_000_000_000_000_000_000_000_000); + + /// The number of decimal places. Since decimal types are fixed-point rather than + /// floating-point, this is a constant. + pub const DECIMAL_PLACES: u32 = 18; + /// The largest value that can be represented by this decimal type. + pub const MAX: Self = Self(Uint256::MAX); + /// The smallest value that can be represented by this decimal type. + pub const MIN: Self = Self(Uint256::MIN); + + /// Creates a Decimal256 from Uint256 + /// This is equivalent to `Decimal256::from_atomics(value, 18)` but usable in a const context. + pub const fn new(value: Uint256) -> Self { + Self(value) + } + + /// Creates a Decimal256 from u128 + /// This is equivalent to `Decimal256::from_atomics(value, 18)` but usable in a const context. + pub const fn raw(value: u128) -> Self { + Self(Uint256::from_u128(value)) + } + + /// Create a 1.0 Decimal256 + #[inline] + pub const fn one() -> Self { + Self(Self::DECIMAL_FRACTIONAL) + } + + /// Create a 0.0 Decimal256 + #[inline] + pub const fn zero() -> Self { + Self(Uint256::zero()) + } + + /// Convert x% into Decimal256 + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal256; + /// const HALF: Decimal256 = Decimal256::percent(50); + /// + /// assert_eq!(HALF, Decimal256::from_str("0.5").unwrap()); + /// ``` + pub const fn percent(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**16 is well in u128 range + let atomics = (x as u128) * 10_000_000_000_000_000; + Self(Uint256::from_u128(atomics)) + } + + /// Convert permille (x/1000) into Decimal256 + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal256; + /// const HALF: Decimal256 = Decimal256::permille(500); + /// + /// assert_eq!(HALF, Decimal256::from_str("0.5").unwrap()); + /// ``` + pub const fn permille(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**15 is well in u128 range + let atomics = (x as u128) * 1_000_000_000_000_000; + Self(Uint256::from_u128(atomics)) + } + + /// Convert basis points (x/10000) into Decimal256 + /// + /// ## Examples + /// + /// ``` + /// # use std::str::FromStr; + /// # use cosmwasm_std::Decimal256; + /// const TWO_BPS: Decimal256 = Decimal256::bps(2); + /// const HALF: Decimal256 = Decimal256::bps(5000); + /// + /// assert_eq!(TWO_BPS, Decimal256::from_str("0.0002").unwrap()); + /// assert_eq!(HALF, Decimal256::from_str("0.5").unwrap()); + /// ``` + pub const fn bps(x: u64) -> Self { + // multiplication does not overflow since `u64::MAX` * 10**14 is well in u128 range + let atomics = (x as u128) * 100_000_000_000_000; + Self(Uint256::from_u128(atomics)) + } + + /// Creates a decimal from a number of atomic units and the number + /// of decimal places. The inputs will be converted internally to form + /// a decimal with 18 decimal places. So the input 123 and 2 will create + /// the decimal 1.23. + /// + /// Using 18 decimal places is slightly more efficient than other values + /// as no internal conversion is necessary. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Decimal256, Uint256}; + /// let a = Decimal256::from_atomics(1234u64, 3).unwrap(); + /// assert_eq!(a.to_string(), "1.234"); + /// + /// let a = Decimal256::from_atomics(1234u128, 0).unwrap(); + /// assert_eq!(a.to_string(), "1234"); + /// + /// let a = Decimal256::from_atomics(1u64, 18).unwrap(); + /// assert_eq!(a.to_string(), "0.000000000000000001"); + /// + /// let a = Decimal256::from_atomics(Uint256::MAX, 18).unwrap(); + /// assert_eq!(a, Decimal256::MAX); + /// ``` + pub fn from_atomics( + atomics: impl Into, + decimal_places: u32, + ) -> Result { + let atomics = atomics.into(); + const TEN: Uint256 = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 10, + ]); + Ok(match decimal_places.cmp(&Self::DECIMAL_PLACES) { + Ordering::Less => { + let digits = (Self::DECIMAL_PLACES) - decimal_places; // No overflow because decimal_places < DECIMAL_PLACES + let factor = TEN.checked_pow(digits).unwrap(); // Safe because digits <= 17 + Self(atomics.checked_mul(factor).map_err(|_| Decimal256RangeExceeded)?) + }, + Ordering::Equal => Self(atomics), + Ordering::Greater => { + let digits = decimal_places - (Self::DECIMAL_PLACES); // No overflow because decimal_places > DECIMAL_PLACES + if let Ok(factor) = TEN.checked_pow(digits) { + Self(atomics.checked_div(factor).unwrap()) // Safe because factor cannot be zero + } else { + // In this case `factor` exceeds the Uint256 range. + // Any Uint256 `x` divided by `factor` with `factor > Uint256::MAX` is 0. + // Try e.g. Python3: `(2**256-1) // 2**256` + Self(Uint256::zero()) + } + }, + }) + } + + /// Returns the ratio (numerator / denominator) as a Decimal256 + pub fn from_ratio(numerator: impl Into, denominator: impl Into) -> Self { + match Decimal256::checked_from_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns the ratio (numerator / denominator) as a Decimal256 + pub fn checked_from_ratio( + numerator: impl Into, + denominator: impl Into, + ) -> Result { + let numerator: Uint256 = numerator.into(); + let denominator: Uint256 = denominator.into(); + match numerator.checked_multiply_ratio(Self::DECIMAL_FRACTIONAL, denominator) { + Ok(ratio) => { + // numerator * DECIMAL_FRACTIONAL / denominator + Ok(Self(ratio)) + }, + Err(CheckedMultiplyRatioError::Overflow) => Err(CheckedFromRatioError::Overflow), + Err(CheckedMultiplyRatioError::DivideByZero) => + Err(CheckedFromRatioError::DivideByZero), + } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// A decimal is an integer of atomic units plus a number that specifies the + /// position of the decimal dot. So any decimal can be expressed as two numbers. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Decimal256, Uint256}; + /// # use core::str::FromStr; + /// // Value with whole and fractional part + /// let a = Decimal256::from_str("1.234").unwrap(); + /// assert_eq!(a.decimal_places(), 18); + /// assert_eq!(a.atomics(), Uint256::from(1234000000000000000u128)); + /// + /// // Smallest possible value + /// let b = Decimal256::from_str("0.000000000000000001").unwrap(); + /// assert_eq!(b.decimal_places(), 18); + /// assert_eq!(b.atomics(), Uint256::from(1u128)); + /// ``` + #[must_use] + #[inline] + pub const fn atomics(&self) -> Uint256 { + self.0 + } + + /// The number of decimal places. This is a constant value for now + /// but this could potentially change as the type evolves. + /// + /// See also [`Decimal256::atomics()`]. + #[must_use] + #[inline] + pub const fn decimal_places(&self) -> u32 { + Self::DECIMAL_PLACES + } + + /// Rounds value down after decimal places. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn floor(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value up after decimal places. Panics on overflow. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if floor == self { + Ok(floor) + } else { + floor.checked_add(Decimal256::one()).map_err(|_| RoundUpOverflowError) + } + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Sub)) + } + + /// Multiplies one `Decimal256` by another, returning an `OverflowError` if an overflow + /// occurred. + pub fn checked_mul(self, other: Self) -> Result { + let result_as_uint512 = self.numerator().full_mul(other.numerator()) / + Uint512::from_uint256(Self::DECIMAL_FRACTIONAL); // from_uint128 is a const method and should be "free" + result_as_uint512 + .try_into() + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Mul)) + } + + /// Raises a value to the power of `exp`, panics if an overflow occurred. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => panic!("Multiplication overflow"), + } + } + + /// Raises a value to the power of `exp`, returning an `OverflowError` if an overflow occurred. + pub fn checked_pow(self, exp: u32) -> Result { + // This uses the exponentiation by squaring algorithm: + // https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Basic_method + + fn inner(mut x: Decimal256, mut n: u32) -> Result { + if n == 0 { + return Ok(Decimal256::one()); + } + + let mut y = Decimal256::one(); + + while n > 1 { + if n % 2 == 0 { + x = x.checked_mul(x)?; + n /= 2; + } else { + y = x.checked_mul(y)?; + x = x.checked_mul(x)?; + n = (n - 1) / 2; + } + } + + Ok(x * y) + } + + inner(self, exp).map_err(|_| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + Decimal256::checked_from_ratio(self.numerator(), other.numerator()) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).map_err(|_| DivideByZeroError) + } + + /// Returns the approximate square root as a Decimal256. + /// + /// This should not overflow or panic. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn sqrt(&self) -> Self { + // Algorithm described in https://hackmd.io/@webmaster128/SJThlukj_ + // We start with the highest precision possible and lower it until + // there's no overflow. + // + // TODO: This could be made more efficient once log10 is in: + // https://github.com/rust-lang/rust/issues/70887 + // The max precision is something like `18 - log10(self.0) / 2`. + (0..=Self::DECIMAL_PLACES / 2) + .rev() + .find_map(|i| self.sqrt_with_precision(i)) + // The last step (i = 0) is guaranteed to succeed because `isqrt(Uint256::MAX) * 10^9` + // does not overflow + .unwrap() + } + + /// Lower precision means more aggressive rounding, but less risk of overflow. + /// Precision *must* be a number between 0 and 9 (inclusive). + /// + /// Returns `None` if the internal multiplication overflows. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn sqrt_with_precision(&self, precision: u32) -> Option { + let inner_mul = Uint256::from(100u128).pow(precision); + self.0.checked_mul(inner_mul).ok().map(|inner| { + let outer_mul = Uint256::from(10u128).pow(Self::DECIMAL_PLACES / 2 - precision); + Self(inner.isqrt().checked_mul(outer_mul).unwrap()) + }) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn abs_diff(self, other: Self) -> Self { + if self < other { + other - self + } else { + self - other + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + match self.checked_add(other) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + match self.checked_sub(other) { + Ok(value) => value, + Err(_) => Self::zero(), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + match self.checked_mul(other) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => Self::MAX, + } + } + + /// Converts this decimal to an unsigned integer by truncating + /// the fractional part, e.g. 22.5 becomes 22. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{Decimal256, Uint256}; + /// + /// let d = Decimal256::from_str("12.345").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint256::from(12u64)); + /// + /// let d = Decimal256::from_str("12.999").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint256::from(12u64)); + /// + /// let d = Decimal256::from_str("75.0").unwrap(); + /// assert_eq!(d.to_uint_floor(), Uint256::from(75u64)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_uint_floor(self) -> Uint256 { + self.0 / Self::DECIMAL_FRACTIONAL + } + + /// Converts this decimal to an unsigned integer by rounting up + /// to the next integer, e.g. 22.3 becomes 23. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{Decimal256, Uint256}; + /// + /// let d = Decimal256::from_str("12.345").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint256::from(13u64)); + /// + /// let d = Decimal256::from_str("12.999").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint256::from(13u64)); + /// + /// let d = Decimal256::from_str("75.0").unwrap(); + /// assert_eq!(d.to_uint_ceil(), Uint256::from(75u64)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_uint_ceil(self) -> Uint256 { + // Using `q = 1 + ((x - 1) / y); // if x != 0` with unsigned integers x, y, q + // from https://stackoverflow.com/a/2745086/2013738. We know `x + y` CAN overflow. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + if x.is_zero() { + Uint256::zero() + } else { + Uint256::one() + ((x - Uint256::one()) / y) + } + } +} + +impl Fraction for Decimal256 { + #[inline] + fn numerator(&self) -> Uint256 { + self.0 + } + + #[inline] + fn denominator(&self) -> Uint256 { + Self::DECIMAL_FRACTIONAL + } + + /// Returns the multiplicative inverse `1/d` for decimal `d`. + /// + /// If `d` is zero, none is returned. + fn inv(&self) -> Option { + if self.is_zero() { + None + } else { + // Let self be p/q with p = self.0 and q = DECIMAL_FRACTIONAL. + // Now we calculate the inverse a/b = q/p such that b = DECIMAL_FRACTIONAL. Then + // `a = DECIMAL_FRACTIONAL*DECIMAL_FRACTIONAL / self.0`. + Some(Self(Self::DECIMAL_FRACTIONAL_SQUARED / self.0)) + } + } +} + +impl From for Decimal256 { + fn from(input: Decimal) -> Self { + // Unwrap is safe because Decimal256 and Decimal have the same decimal places. + // Every Decimal value can be stored in Decimal256. + Decimal256::from_atomics(input.atomics(), input.decimal_places()).unwrap() + } +} + +impl TryFrom for Decimal256 { + type Error = Decimal256RangeExceeded; + + fn try_from(value: SignedDecimal) -> Result { + value.atomics().try_into().map(Decimal256).map_err(|_| Decimal256RangeExceeded) + } +} + +impl TryFrom for Decimal256 { + type Error = Decimal256RangeExceeded; + + fn try_from(value: SignedDecimal256) -> Result { + value.atomics().try_into().map(Decimal256).map_err(|_| Decimal256RangeExceeded) + } +} + +impl FromStr for Decimal256 { + type Err = StdError; + + /// Converts the decimal string to a Decimal256 + /// Possible inputs: "1.23", "1", "000012", "1.123000000" + /// Disallowed: "", ".23" + /// + /// This never performs any kind of rounding. + /// More than DECIMAL_PLACES fractional digits, even zeros, result in an error. + fn from_str(input: &str) -> Result { + let mut parts_iter = input.split('.'); + + let whole_part = parts_iter.next().unwrap(); // split always returns at least one element + let whole = whole_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing whole"))?; + let mut atomics = whole + .checked_mul(Self::DECIMAL_FRACTIONAL) + .map_err(|_| StdError::generic_err("Value too big"))?; + + if let Some(fractional_part) = parts_iter.next() { + let fractional = fractional_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing fractional"))?; + let exp = (Self::DECIMAL_PLACES.checked_sub(fractional_part.len() as u32)).ok_or_else( + || { + StdError::generic_err(format!( + "Cannot parse more than {} fractional digits", + Self::DECIMAL_PLACES + )) + }, + )?; + debug_assert!(exp <= Self::DECIMAL_PLACES); + let fractional_factor = Uint256::from(10u128).pow(exp); + atomics = atomics + .checked_add( + // The inner multiplication can't overflow because + // fractional < 10^DECIMAL_PLACES && fractional_factor <= 10^DECIMAL_PLACES + fractional.checked_mul(fractional_factor).unwrap(), + ) + .map_err(|_| StdError::generic_err("Value too big"))?; + } + + if parts_iter.next().is_some() { + return Err(StdError::generic_err("Unexpected number of dots")); + } + + Ok(Self(atomics)) + } +} + +impl fmt::Display for Decimal256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let whole = (self.0) / Self::DECIMAL_FRACTIONAL; + let fractional = (self.0).checked_rem(Self::DECIMAL_FRACTIONAL).unwrap(); + + if fractional.is_zero() { + write!(f, "{whole}") + } else { + let fractional_string = + format!("{:0>padding$}", fractional, padding = Self::DECIMAL_PLACES as usize); + f.write_str(&whole.to_string())?; + f.write_char('.')?; + f.write_str(fractional_string.trim_end_matches('0'))?; + Ok(()) + } + } +} + +impl fmt::Debug for Decimal256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Decimal256({self})") + } +} + +impl Add for Decimal256 { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} +forward_ref_binop!(impl Add, add for Decimal256, Decimal256); + +impl AddAssign for Decimal256 { + fn add_assign(&mut self, rhs: Decimal256) { + *self = *self + rhs; + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Decimal256, Decimal256); + +impl Sub for Decimal256 { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Self(self.0 - other.0) + } +} +forward_ref_binop!(impl Sub, sub for Decimal256, Decimal256); + +impl SubAssign for Decimal256 { + fn sub_assign(&mut self, rhs: Decimal256) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Decimal256, Decimal256); + +impl Mul for Decimal256 { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn mul(self, other: Self) -> Self { + // Decimals are fractions. We can multiply two decimals a and b + // via + // (a.numerator() * b.numerator()) / (a.denominator() * b.denominator()) + // = (a.numerator() * b.numerator()) / a.denominator() / b.denominator() + + let result_as_uint512 = self.numerator().full_mul(other.numerator()) / + Uint512::from_uint256(Self::DECIMAL_FRACTIONAL); // from_uint256 is a const method and should be "free" + match result_as_uint512.try_into() { + Ok(result) => Self(result), + Err(_) => panic!("attempt to multiply with overflow"), + } + } +} +forward_ref_binop!(impl Mul, mul for Decimal256, Decimal256); + +impl MulAssign for Decimal256 { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Decimal256, Decimal256); + +impl Div for Decimal256 { + type Output = Self; + + fn div(self, other: Self) -> Self { + match Decimal256::checked_from_ratio(self.numerator(), other.numerator()) { + Ok(ratio) => ratio, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Division failed - denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => { + panic!("Division failed - multiplication overflow") + }, + } + } +} +forward_ref_binop!(impl Div, div for Decimal256, Decimal256); + +impl DivAssign for Decimal256 { + fn div_assign(&mut self, rhs: Decimal256) { + *self = *self / rhs; + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Decimal256, Decimal256); + +impl Div for Decimal256 { + type Output = Self; + + fn div(self, rhs: Uint256) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl DivAssign for Decimal256 { + fn div_assign(&mut self, rhs: Uint256) { + self.0 /= rhs; + } +} + +impl Rem for Decimal256 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Decimal256, Decimal256); + +impl RemAssign for Decimal256 { + fn rem_assign(&mut self, rhs: Decimal256) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Decimal256, Decimal256); + +impl core::iter::Sum for Decimal256 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +/// Serializes as a decimal string +impl Serialize for Decimal256 { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Deserializes as a base64 string +impl<'de> Deserialize<'de> for Decimal256 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Decimal256Visitor) + } +} + +struct Decimal256Visitor; + +impl<'de> de::Visitor<'de> for Decimal256Visitor { + type Value = Decimal256; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded decimal") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match Self::Value::from_str(v) { + Ok(d) => Ok(d), + Err(e) => Err(E::custom(format_args!("Error parsing decimal '{v}': {e}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::StdError; + + use alloc::vec::Vec; + + fn dec(input: &str) -> Decimal256 { + Decimal256::from_str(input).unwrap() + } + + #[test] + fn decimal256_new() { + let expected = Uint256::from(300u128); + assert_eq!(Decimal256::new(expected).0, expected); + } + + #[test] + fn decimal256_raw() { + let value = 300u128; + let expected = Uint256::from(value); + assert_eq!(Decimal256::raw(value).0, expected); + } + + #[test] + fn decimal256_one() { + let value = Decimal256::one(); + assert_eq!(value.0, Decimal256::DECIMAL_FRACTIONAL); + } + + #[test] + fn decimal256_zero() { + let value = Decimal256::zero(); + assert!(value.0.is_zero()); + } + + #[test] + fn decimal256_percent() { + let value = Decimal256::percent(50); + assert_eq!(value.0, Decimal256::DECIMAL_FRACTIONAL / Uint256::from(2u8)); + } + + #[test] + fn decimal256_permille() { + let value = Decimal256::permille(125); + assert_eq!(value.0, Decimal256::DECIMAL_FRACTIONAL / Uint256::from(8u8)); + } + + #[test] + fn decimal256_bps() { + let value = Decimal256::bps(125); + assert_eq!(value.0, Decimal256::DECIMAL_FRACTIONAL / Uint256::from(80u8)); + } + + #[test] + fn decimal256_from_atomics_works() { + let one = Decimal256::one(); + let two = one + one; + + assert_eq!(Decimal256::from_atomics(1u128, 0).unwrap(), one); + assert_eq!(Decimal256::from_atomics(10u128, 1).unwrap(), one); + assert_eq!(Decimal256::from_atomics(100u128, 2).unwrap(), one); + assert_eq!(Decimal256::from_atomics(1000u128, 3).unwrap(), one); + assert_eq!(Decimal256::from_atomics(1000000000000000000u128, 18).unwrap(), one); + assert_eq!(Decimal256::from_atomics(10000000000000000000u128, 19).unwrap(), one); + assert_eq!(Decimal256::from_atomics(100000000000000000000u128, 20).unwrap(), one); + + assert_eq!(Decimal256::from_atomics(2u128, 0).unwrap(), two); + assert_eq!(Decimal256::from_atomics(20u128, 1).unwrap(), two); + assert_eq!(Decimal256::from_atomics(200u128, 2).unwrap(), two); + assert_eq!(Decimal256::from_atomics(2000u128, 3).unwrap(), two); + assert_eq!(Decimal256::from_atomics(2000000000000000000u128, 18).unwrap(), two); + assert_eq!(Decimal256::from_atomics(20000000000000000000u128, 19).unwrap(), two); + assert_eq!(Decimal256::from_atomics(200000000000000000000u128, 20).unwrap(), two); + + // Cuts decimal digits (20 provided but only 18 can be stored) + assert_eq!( + Decimal256::from_atomics(4321u128, 20).unwrap(), + Decimal256::from_str("0.000000000000000043").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(6789u128, 20).unwrap(), + Decimal256::from_str("0.000000000000000067").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 38).unwrap(), + Decimal256::from_str("3.402823669209384634").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 39).unwrap(), + Decimal256::from_str("0.340282366920938463").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 45).unwrap(), + Decimal256::from_str("0.000000340282366920").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 51).unwrap(), + Decimal256::from_str("0.000000000000340282").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 56).unwrap(), + Decimal256::from_str("0.000000000000000003").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, 57).unwrap(), + Decimal256::from_str("0.000000000000000000").unwrap() + ); + assert_eq!( + Decimal256::from_atomics(u128::MAX, u32::MAX).unwrap(), + Decimal256::from_str("0.000000000000000000").unwrap() + ); + + // Can be used with max value + let max = Decimal256::MAX; + assert_eq!(Decimal256::from_atomics(max.atomics(), max.decimal_places()).unwrap(), max); + + // Overflow is only possible with digits < 18 + let result = Decimal256::from_atomics(Uint256::MAX, 17); + assert_eq!(result.unwrap_err(), Decimal256RangeExceeded); + } + + #[test] + fn decimal256_from_ratio_works() { + // 1.0 + assert_eq!(Decimal256::from_ratio(1u128, 1u128), Decimal256::one()); + assert_eq!(Decimal256::from_ratio(53u128, 53u128), Decimal256::one()); + assert_eq!(Decimal256::from_ratio(125u128, 125u128), Decimal256::one()); + + // 1.5 + assert_eq!(Decimal256::from_ratio(3u128, 2u128), Decimal256::percent(150)); + assert_eq!(Decimal256::from_ratio(150u128, 100u128), Decimal256::percent(150)); + assert_eq!(Decimal256::from_ratio(333u128, 222u128), Decimal256::percent(150)); + + // 0.125 + assert_eq!(Decimal256::from_ratio(1u64, 8u64), Decimal256::permille(125)); + assert_eq!(Decimal256::from_ratio(125u64, 1000u64), Decimal256::permille(125)); + + // 1/3 (result floored) + assert_eq!( + Decimal256::from_ratio(1u64, 3u64), + Decimal256(Uint256::from_str("333333333333333333").unwrap()) + ); + + // 2/3 (result floored) + assert_eq!( + Decimal256::from_ratio(2u64, 3u64), + Decimal256(Uint256::from_str("666666666666666666").unwrap()) + ); + + // large inputs + assert_eq!(Decimal256::from_ratio(0u128, u128::MAX), Decimal256::zero()); + assert_eq!(Decimal256::from_ratio(u128::MAX, u128::MAX), Decimal256::one()); + // 340282366920938463463 is the largest integer <= Decimal256::MAX + assert_eq!( + Decimal256::from_ratio(340282366920938463463u128, 1u128), + Decimal256::from_str("340282366920938463463").unwrap() + ); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn decimal256_from_ratio_panics_for_zero_denominator() { + Decimal256::from_ratio(1u128, 0u128); + } + + #[test] + #[should_panic(expected = "Multiplication overflow")] + fn decimal256_from_ratio_panics_for_mul_overflow() { + Decimal256::from_ratio(Uint256::MAX, 1u128); + } + + #[test] + fn decimal256_checked_from_ratio_does_not_panic() { + assert_eq!( + Decimal256::checked_from_ratio(1u128, 0u128), + Err(CheckedFromRatioError::DivideByZero) + ); + + assert_eq!( + Decimal256::checked_from_ratio(Uint256::MAX, 1u128), + Err(CheckedFromRatioError::Overflow) + ); + } + + #[test] + fn decimal256_implements_fraction() { + let fraction = Decimal256::from_str("1234.567").unwrap(); + assert_eq!(fraction.numerator(), Uint256::from_str("1234567000000000000000").unwrap()); + assert_eq!(fraction.denominator(), Uint256::from_str("1000000000000000000").unwrap()); + } + + #[test] + fn decimal256_implements_from_decimal() { + let a = Decimal::from_str("123.456").unwrap(); + let b = Decimal256::from(a); + assert_eq!(b.to_string(), "123.456"); + + let a = Decimal::from_str("0").unwrap(); + let b = Decimal256::from(a); + assert_eq!(b.to_string(), "0"); + + let a = Decimal::MAX; + let b = Decimal256::from(a); + assert_eq!(b.to_string(), "340282366920938463463.374607431768211455"); + } + + #[test] + fn decimal256_from_str_works() { + // Integers + assert_eq!(Decimal256::from_str("0").unwrap(), Decimal256::percent(0)); + assert_eq!(Decimal256::from_str("1").unwrap(), Decimal256::percent(100)); + assert_eq!(Decimal256::from_str("5").unwrap(), Decimal256::percent(500)); + assert_eq!(Decimal256::from_str("42").unwrap(), Decimal256::percent(4200)); + assert_eq!(Decimal256::from_str("000").unwrap(), Decimal256::percent(0)); + assert_eq!(Decimal256::from_str("001").unwrap(), Decimal256::percent(100)); + assert_eq!(Decimal256::from_str("005").unwrap(), Decimal256::percent(500)); + assert_eq!(Decimal256::from_str("0042").unwrap(), Decimal256::percent(4200)); + + // Decimals + assert_eq!(Decimal256::from_str("1.0").unwrap(), Decimal256::percent(100)); + assert_eq!(Decimal256::from_str("1.5").unwrap(), Decimal256::percent(150)); + assert_eq!(Decimal256::from_str("0.5").unwrap(), Decimal256::percent(50)); + assert_eq!(Decimal256::from_str("0.123").unwrap(), Decimal256::permille(123)); + + assert_eq!(Decimal256::from_str("40.00").unwrap(), Decimal256::percent(4000)); + assert_eq!(Decimal256::from_str("04.00").unwrap(), Decimal256::percent(400)); + assert_eq!(Decimal256::from_str("00.40").unwrap(), Decimal256::percent(40)); + assert_eq!(Decimal256::from_str("00.04").unwrap(), Decimal256::percent(4)); + + // Can handle 18 fractional digits + assert_eq!( + Decimal256::from_str("7.123456789012345678").unwrap(), + Decimal256(Uint256::from(7123456789012345678u128)) + ); + assert_eq!( + Decimal256::from_str("7.999999999999999999").unwrap(), + Decimal256(Uint256::from(7999999999999999999u128)) + ); + + // Works for documented max value + assert_eq!( + Decimal256::from_str( + "115792089237316195423570985008687907853269984665640564039457.584007913129639935" + ) + .unwrap(), + Decimal256::MAX + ); + } + + #[test] + fn decimal256_from_str_errors_for_broken_whole_part() { + match Decimal256::from_str("").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str(" ").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str("-1").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing whole"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal256_from_str_errors_for_broken_fractional_part() { + match Decimal256::from_str("1.").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str("1. ").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str("1.e").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str("1.2e3").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Error parsing fractional"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal256_from_str_errors_for_more_than_36_fractional_digits() { + match Decimal256::from_str("7.1234567890123456789").unwrap_err() { + StdError::GenericErr { msg, .. } => { + assert_eq!(msg, "Cannot parse more than 18 fractional digits") + }, + e => panic!("Unexpected error: {e:?}"), + } + + // No special rules for trailing zeros. This could be changed but adds gas cost for the + // happy path. + match Decimal256::from_str("7.1230000000000000000").unwrap_err() { + StdError::GenericErr { msg, .. } => { + assert_eq!(msg, "Cannot parse more than 18 fractional digits") + }, + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal256_from_str_errors_for_invalid_number_of_dots() { + match Decimal256::from_str("1.2.3").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Unexpected number of dots"), + e => panic!("Unexpected error: {e:?}"), + } + + match Decimal256::from_str("1.2.3.4").unwrap_err() { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Unexpected number of dots"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal256_from_str_errors_for_more_than_max_value() { + // Integer + match Decimal256::from_str("115792089237316195423570985008687907853269984665640564039458") + .unwrap_err() + { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + + // Decimal + match Decimal256::from_str("115792089237316195423570985008687907853269984665640564039458.0") + .unwrap_err() + { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + match Decimal256::from_str( + "115792089237316195423570985008687907853269984665640564039457.584007913129639936", + ) + .unwrap_err() + { + StdError::GenericErr { msg, .. } => assert_eq!(msg, "Value too big"), + e => panic!("Unexpected error: {e:?}"), + } + } + + #[test] + fn decimal256_atomics_works() { + let zero = Decimal256::zero(); + let one = Decimal256::one(); + let half = Decimal256::percent(50); + let two = Decimal256::percent(200); + let max = Decimal256::MAX; + + assert_eq!(zero.atomics(), Uint256::from(0u128)); + assert_eq!(one.atomics(), Uint256::from(1000000000000000000u128)); + assert_eq!(half.atomics(), Uint256::from(500000000000000000u128)); + assert_eq!(two.atomics(), Uint256::from(2000000000000000000u128)); + assert_eq!(max.atomics(), Uint256::MAX); + } + + #[test] + fn decimal256_decimal_places_works() { + let zero = Decimal256::zero(); + let one = Decimal256::one(); + let half = Decimal256::percent(50); + let two = Decimal256::percent(200); + let max = Decimal256::MAX; + + assert_eq!(zero.decimal_places(), 18); + assert_eq!(one.decimal_places(), 18); + assert_eq!(half.decimal_places(), 18); + assert_eq!(two.decimal_places(), 18); + assert_eq!(max.decimal_places(), 18); + } + + #[test] + fn decimal256_is_zero_works() { + assert!(Decimal256::zero().is_zero()); + assert!(Decimal256::percent(0).is_zero()); + assert!(Decimal256::permille(0).is_zero()); + + assert!(!Decimal256::one().is_zero()); + assert!(!Decimal256::percent(123).is_zero()); + assert!(!Decimal256::permille(1234).is_zero()); + } + + #[test] + fn decimal256_inv_works() { + // d = 0 + assert_eq!(Decimal256::zero().inv(), None); + + // d == 1 + assert_eq!(Decimal256::one().inv(), Some(Decimal256::one())); + + // d > 1 exact + assert_eq!( + Decimal256::from_str("2").unwrap().inv(), + Some(Decimal256::from_str("0.5").unwrap()) + ); + assert_eq!( + Decimal256::from_str("20").unwrap().inv(), + Some(Decimal256::from_str("0.05").unwrap()) + ); + assert_eq!( + Decimal256::from_str("200").unwrap().inv(), + Some(Decimal256::from_str("0.005").unwrap()) + ); + assert_eq!( + Decimal256::from_str("2000").unwrap().inv(), + Some(Decimal256::from_str("0.0005").unwrap()) + ); + + // d > 1 rounded + assert_eq!( + Decimal256::from_str("3").unwrap().inv(), + Some(Decimal256::from_str("0.333333333333333333").unwrap()) + ); + assert_eq!( + Decimal256::from_str("6").unwrap().inv(), + Some(Decimal256::from_str("0.166666666666666666").unwrap()) + ); + + // d < 1 exact + assert_eq!( + Decimal256::from_str("0.5").unwrap().inv(), + Some(Decimal256::from_str("2").unwrap()) + ); + assert_eq!( + Decimal256::from_str("0.05").unwrap().inv(), + Some(Decimal256::from_str("20").unwrap()) + ); + assert_eq!( + Decimal256::from_str("0.005").unwrap().inv(), + Some(Decimal256::from_str("200").unwrap()) + ); + assert_eq!( + Decimal256::from_str("0.0005").unwrap().inv(), + Some(Decimal256::from_str("2000").unwrap()) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal256_add_works() { + let value = Decimal256::one() + Decimal256::percent(50); // 1.5 + assert_eq!( + value.0, + Decimal256::DECIMAL_FRACTIONAL * Uint256::from(3u8) / Uint256::from(2u8) + ); + + assert_eq!(Decimal256::percent(5) + Decimal256::percent(4), Decimal256::percent(9)); + assert_eq!(Decimal256::percent(5) + Decimal256::zero(), Decimal256::percent(5)); + assert_eq!(Decimal256::zero() + Decimal256::zero(), Decimal256::zero()); + + // works for refs + let a = Decimal256::percent(15); + let b = Decimal256::percent(25); + let expected = Decimal256::percent(40); + assert_eq!(a + b, expected); + assert_eq!(&a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn decimal256_add_overflow_panics() { + let _value = Decimal256::MAX + Decimal256::percent(50); + } + + #[test] + fn decimal256_add_assign_works() { + let mut a = Decimal256::percent(30); + a += Decimal256::percent(20); + assert_eq!(a, Decimal256::percent(50)); + + // works for refs + let mut a = Decimal256::percent(15); + let b = Decimal256::percent(3); + let expected = Decimal256::percent(18); + a += &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal256_sub_works() { + let value = Decimal256::one() - Decimal256::percent(50); // 0.5 + assert_eq!(value.0, Decimal256::DECIMAL_FRACTIONAL / Uint256::from(2u8)); + + assert_eq!(Decimal256::percent(9) - Decimal256::percent(4), Decimal256::percent(5)); + assert_eq!(Decimal256::percent(16) - Decimal256::zero(), Decimal256::percent(16)); + assert_eq!(Decimal256::percent(16) - Decimal256::percent(16), Decimal256::zero()); + assert_eq!(Decimal256::zero() - Decimal256::zero(), Decimal256::zero()); + + // works for refs + let a = Decimal256::percent(13); + let b = Decimal256::percent(6); + let expected = Decimal256::percent(7); + assert_eq!(a - b, expected); + assert_eq!(&a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn decimal256_sub_overflow_panics() { + let _value = Decimal256::zero() - Decimal256::percent(50); + } + + #[test] + fn decimal256_sub_assign_works() { + let mut a = Decimal256::percent(20); + a -= Decimal256::percent(2); + assert_eq!(a, Decimal256::percent(18)); + + // works for refs + let mut a = Decimal256::percent(33); + let b = Decimal256::percent(13); + let expected = Decimal256::percent(20); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal256_implements_mul() { + let one = Decimal256::one(); + let two = one + one; + let half = Decimal256::percent(50); + + // 1*x and x*1 + assert_eq!(one * Decimal256::percent(0), Decimal256::percent(0)); + assert_eq!(one * Decimal256::percent(1), Decimal256::percent(1)); + assert_eq!(one * Decimal256::percent(10), Decimal256::percent(10)); + assert_eq!(one * Decimal256::percent(100), Decimal256::percent(100)); + assert_eq!(one * Decimal256::percent(1000), Decimal256::percent(1000)); + assert_eq!(one * Decimal256::MAX, Decimal256::MAX); + assert_eq!(Decimal256::percent(0) * one, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) * one, Decimal256::percent(1)); + assert_eq!(Decimal256::percent(10) * one, Decimal256::percent(10)); + assert_eq!(Decimal256::percent(100) * one, Decimal256::percent(100)); + assert_eq!(Decimal256::percent(1000) * one, Decimal256::percent(1000)); + assert_eq!(Decimal256::MAX * one, Decimal256::MAX); + + // double + assert_eq!(two * Decimal256::percent(0), Decimal256::percent(0)); + assert_eq!(two * Decimal256::percent(1), Decimal256::percent(2)); + assert_eq!(two * Decimal256::percent(10), Decimal256::percent(20)); + assert_eq!(two * Decimal256::percent(100), Decimal256::percent(200)); + assert_eq!(two * Decimal256::percent(1000), Decimal256::percent(2000)); + assert_eq!(Decimal256::percent(0) * two, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) * two, Decimal256::percent(2)); + assert_eq!(Decimal256::percent(10) * two, Decimal256::percent(20)); + assert_eq!(Decimal256::percent(100) * two, Decimal256::percent(200)); + assert_eq!(Decimal256::percent(1000) * two, Decimal256::percent(2000)); + + // half + assert_eq!(half * Decimal256::percent(0), Decimal256::percent(0)); + assert_eq!(half * Decimal256::percent(1), Decimal256::permille(5)); + assert_eq!(half * Decimal256::percent(10), Decimal256::percent(5)); + assert_eq!(half * Decimal256::percent(100), Decimal256::percent(50)); + assert_eq!(half * Decimal256::percent(1000), Decimal256::percent(500)); + assert_eq!(Decimal256::percent(0) * half, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) * half, Decimal256::permille(5)); + assert_eq!(Decimal256::percent(10) * half, Decimal256::percent(5)); + assert_eq!(Decimal256::percent(100) * half, Decimal256::percent(50)); + assert_eq!(Decimal256::percent(1000) * half, Decimal256::percent(500)); + + // Move left + let a = dec("123.127726548762582"); + assert_eq!(a * dec("1"), dec("123.127726548762582")); + assert_eq!(a * dec("10"), dec("1231.27726548762582")); + assert_eq!(a * dec("100"), dec("12312.7726548762582")); + assert_eq!(a * dec("1000"), dec("123127.726548762582")); + assert_eq!(a * dec("1000000"), dec("123127726.548762582")); + assert_eq!(a * dec("1000000000"), dec("123127726548.762582")); + assert_eq!(a * dec("1000000000000"), dec("123127726548762.582")); + assert_eq!(a * dec("1000000000000000"), dec("123127726548762582")); + assert_eq!(a * dec("1000000000000000000"), dec("123127726548762582000")); + assert_eq!(dec("1") * a, dec("123.127726548762582")); + assert_eq!(dec("10") * a, dec("1231.27726548762582")); + assert_eq!(dec("100") * a, dec("12312.7726548762582")); + assert_eq!(dec("1000") * a, dec("123127.726548762582")); + assert_eq!(dec("1000000") * a, dec("123127726.548762582")); + assert_eq!(dec("1000000000") * a, dec("123127726548.762582")); + assert_eq!(dec("1000000000000") * a, dec("123127726548762.582")); + assert_eq!(dec("1000000000000000") * a, dec("123127726548762582")); + assert_eq!(dec("1000000000000000000") * a, dec("123127726548762582000")); + + // Move right + let max = Decimal256::MAX; + assert_eq!( + max * dec("1.0"), + dec("115792089237316195423570985008687907853269984665640564039457.584007913129639935") + ); + assert_eq!( + max * dec("0.1"), + dec("11579208923731619542357098500868790785326998466564056403945.758400791312963993") + ); + assert_eq!( + max * dec("0.01"), + dec("1157920892373161954235709850086879078532699846656405640394.575840079131296399") + ); + assert_eq!( + max * dec("0.001"), + dec("115792089237316195423570985008687907853269984665640564039.457584007913129639") + ); + assert_eq!( + max * dec("0.000001"), + dec("115792089237316195423570985008687907853269984665640564.039457584007913129") + ); + assert_eq!( + max * dec("0.000000001"), + dec("115792089237316195423570985008687907853269984665640.564039457584007913") + ); + assert_eq!( + max * dec("0.000000000001"), + dec("115792089237316195423570985008687907853269984665.640564039457584007") + ); + assert_eq!( + max * dec("0.000000000000001"), + dec("115792089237316195423570985008687907853269984.665640564039457584") + ); + assert_eq!( + max * dec("0.000000000000000001"), + dec("115792089237316195423570985008687907853269.984665640564039457") + ); + + // works for refs + let a = Decimal256::percent(20); + let b = Decimal256::percent(30); + let expected = Decimal256::percent(6); + assert_eq!(a * b, expected); + assert_eq!(&a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn decimal256_mul_assign_works() { + let mut a = Decimal256::percent(15); + a *= Decimal256::percent(60); + assert_eq!(a, Decimal256::percent(9)); + + // works for refs + let mut a = Decimal256::percent(50); + let b = Decimal256::percent(20); + a *= &b; + assert_eq!(a, Decimal256::percent(10)); + } + + #[test] + #[should_panic(expected = "attempt to multiply with overflow")] + fn decimal256_mul_overflow_panics() { + let _value = Decimal256::MAX * Decimal256::percent(101); + } + + #[test] + fn decimal256_checked_mul() { + let test_data = [ + (Decimal256::zero(), Decimal256::zero()), + (Decimal256::zero(), Decimal256::one()), + (Decimal256::one(), Decimal256::zero()), + (Decimal256::percent(10), Decimal256::zero()), + (Decimal256::percent(10), Decimal256::percent(5)), + (Decimal256::MAX, Decimal256::one()), + (Decimal256::MAX / Uint256::from_uint128(2u128.into()), Decimal256::percent(200)), + (Decimal256::permille(6), Decimal256::permille(13)), + ]; + + // The regular core::ops::Mul is our source of truth for these tests. + for (x, y) in test_data.into_iter() { + assert_eq!(x * y, x.checked_mul(y).unwrap()); + } + } + + #[test] + fn decimal256_checked_mul_overflow() { + assert_eq!( + Decimal256::MAX.checked_mul(Decimal256::percent(200)), + Err(OverflowError::new(OverflowOperation::Mul)) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal256_implements_div() { + let one = Decimal256::one(); + let two = one + one; + let half = Decimal256::percent(50); + + // 1/x and x/1 + assert_eq!(one / Decimal256::percent(1), Decimal256::percent(10_000)); + assert_eq!(one / Decimal256::percent(10), Decimal256::percent(1_000)); + assert_eq!(one / Decimal256::percent(100), Decimal256::percent(100)); + assert_eq!(one / Decimal256::percent(1000), Decimal256::percent(10)); + assert_eq!(Decimal256::percent(0) / one, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) / one, Decimal256::percent(1)); + assert_eq!(Decimal256::percent(10) / one, Decimal256::percent(10)); + assert_eq!(Decimal256::percent(100) / one, Decimal256::percent(100)); + assert_eq!(Decimal256::percent(1000) / one, Decimal256::percent(1000)); + + // double + assert_eq!(two / Decimal256::percent(1), Decimal256::percent(20_000)); + assert_eq!(two / Decimal256::percent(10), Decimal256::percent(2_000)); + assert_eq!(two / Decimal256::percent(100), Decimal256::percent(200)); + assert_eq!(two / Decimal256::percent(1000), Decimal256::percent(20)); + assert_eq!(Decimal256::percent(0) / two, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) / two, dec("0.005")); + assert_eq!(Decimal256::percent(10) / two, Decimal256::percent(5)); + assert_eq!(Decimal256::percent(100) / two, Decimal256::percent(50)); + assert_eq!(Decimal256::percent(1000) / two, Decimal256::percent(500)); + + // half + assert_eq!(half / Decimal256::percent(1), Decimal256::percent(5_000)); + assert_eq!(half / Decimal256::percent(10), Decimal256::percent(500)); + assert_eq!(half / Decimal256::percent(100), Decimal256::percent(50)); + assert_eq!(half / Decimal256::percent(1000), Decimal256::percent(5)); + assert_eq!(Decimal256::percent(0) / half, Decimal256::percent(0)); + assert_eq!(Decimal256::percent(1) / half, Decimal256::percent(2)); + assert_eq!(Decimal256::percent(10) / half, Decimal256::percent(20)); + assert_eq!(Decimal256::percent(100) / half, Decimal256::percent(200)); + assert_eq!(Decimal256::percent(1000) / half, Decimal256::percent(2000)); + + // Move right + let a = dec("123127726548762582"); + assert_eq!(a / dec("1"), dec("123127726548762582")); + assert_eq!(a / dec("10"), dec("12312772654876258.2")); + assert_eq!(a / dec("100"), dec("1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("0.123127726548762582")); + assert_eq!(dec("1") / a, dec("0.000000000000000008")); + assert_eq!(dec("10") / a, dec("0.000000000000000081")); + assert_eq!(dec("100") / a, dec("0.000000000000000812")); + assert_eq!(dec("1000") / a, dec("0.000000000000008121")); + assert_eq!(dec("1000000") / a, dec("0.000000000008121647")); + assert_eq!(dec("1000000000") / a, dec("0.000000008121647560")); + assert_eq!(dec("1000000000000") / a, dec("0.000008121647560868")); + assert_eq!(dec("1000000000000000") / a, dec("0.008121647560868164")); + assert_eq!(dec("1000000000000000000") / a, dec("8.121647560868164773")); + + // Move left + let a = dec("0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("123127726.548762582")); + assert_eq!(a / dec("0.000000000001"), dec("123127726548.762582")); + assert_eq!(a / dec("0.000000000000001"), dec("123127726548762.582")); + assert_eq!(a / dec("0.000000000000000001"), dec("123127726548762582")); + + assert_eq!(Decimal256::percent(15) / Decimal256::percent(60), Decimal256::percent(25)); + + // works for refs + let a = Decimal256::percent(100); + let b = Decimal256::percent(20); + let expected = Decimal256::percent(500); + assert_eq!(a / b, expected); + assert_eq!(&a / b, expected); + assert_eq!(a / &b, expected); + assert_eq!(&a / &b, expected); + } + + #[test] + fn decimal256_div_assign_works() { + let mut a = Decimal256::percent(15); + a /= Decimal256::percent(20); + assert_eq!(a, Decimal256::percent(75)); + + // works for refs + let mut a = Decimal256::percent(50); + let b = Decimal256::percent(20); + a /= &b; + assert_eq!(a, Decimal256::percent(250)); + } + + #[test] + #[should_panic(expected = "Division failed - multiplication overflow")] + fn decimal256_div_overflow_panics() { + let _value = Decimal256::MAX / Decimal256::percent(10); + } + + #[test] + #[should_panic(expected = "Division failed - denominator must not be zero")] + fn decimal256_div_by_zero_panics() { + let _value = Decimal256::one() / Decimal256::zero(); + } + + #[test] + fn decimal256_uint128_division() { + // a/b + let left = Decimal256::percent(150); // 1.5 + let right = Uint256::from(3u128); + assert_eq!(left / right, Decimal256::percent(50)); + + // 0/a + let left = Decimal256::zero(); + let right = Uint256::from(300u128); + assert_eq!(left / right, Decimal256::zero()); + } + + #[test] + #[should_panic(expected = "attempt to divide by zero")] + fn decimal256_uint128_divide_by_zero() { + let left = Decimal256::percent(150); // 1.5 + let right = Uint256::from(0u128); + let _result = left / right; + } + + #[test] + fn decimal256_uint128_div_assign() { + // a/b + let mut dec = Decimal256::percent(150); // 1.5 + dec /= Uint256::from(3u128); + assert_eq!(dec, Decimal256::percent(50)); + + // 0/a + let mut dec = Decimal256::zero(); + dec /= Uint256::from(300u128); + assert_eq!(dec, Decimal256::zero()); + } + + #[test] + #[should_panic(expected = "attempt to divide by zero")] + fn decimal256_uint128_div_assign_by_zero() { + // a/0 + let mut dec = Decimal256::percent(50); + dec /= Uint256::from(0u128); + } + + #[test] + fn decimal256_uint128_sqrt() { + assert_eq!(Decimal256::percent(900).sqrt(), Decimal256::percent(300)); + + assert!(Decimal256::percent(316) < Decimal256::percent(1000).sqrt()); + assert!(Decimal256::percent(1000).sqrt() < Decimal256::percent(317)); + } + + /// sqrt(2) is an irrational number, i.e. all 36 decimal places should be used. + #[test] + fn decimal256_uint128_sqrt_is_precise() { + assert_eq!( + Decimal256::from_str("2").unwrap().sqrt(), + Decimal256::from_str("1.414213562373095048").unwrap() /* https://www.wolframalpha.com/input/?i=sqrt%282%29 */ + ); + } + + #[test] + fn decimal256_uint128_sqrt_does_not_overflow() { + assert_eq!( + Decimal256::from_str("40000000000000000000000000000000000000000000000000000000000") + .unwrap() + .sqrt(), + Decimal256::from_str("200000000000000000000000000000").unwrap() + ); + } + + #[test] + fn decimal256_uint128_sqrt_intermediate_precision_used() { + assert_eq!( + Decimal256::from_str("40000000000000000000000000000000000000000000000001") + .unwrap() + .sqrt(), + // The last few digits (39110) are truncated below due to the algorithm + // we use. Larger numbers will cause less precision. + // https://www.wolframalpha.com/input/?i=sqrt%2840000000000000000000000000000000000000000000000001%29 + Decimal256::from_str("6324555320336758663997787.088865437067400000").unwrap() + ); + } + + #[test] + fn decimal256_checked_pow() { + for exp in 0..10 { + assert_eq!(Decimal256::one().checked_pow(exp).unwrap(), Decimal256::one()); + } + + // This case is mathematically undefined but we ensure consistency with Rust standard types + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=20df6716048e77087acd40194b233494 + assert_eq!(Decimal256::zero().checked_pow(0).unwrap(), Decimal256::one()); + + for exp in 1..10 { + assert_eq!(Decimal256::zero().checked_pow(exp).unwrap(), Decimal256::zero()); + } + + for num in &[Decimal256::percent(50), Decimal256::percent(99), Decimal256::percent(200)] { + assert_eq!(num.checked_pow(0).unwrap(), Decimal256::one()) + } + + assert_eq!(Decimal256::percent(20).checked_pow(2).unwrap(), Decimal256::percent(4)); + + assert_eq!(Decimal256::percent(20).checked_pow(3).unwrap(), Decimal256::permille(8)); + + assert_eq!(Decimal256::percent(200).checked_pow(4).unwrap(), Decimal256::percent(1600)); + + assert_eq!(Decimal256::percent(200).checked_pow(4).unwrap(), Decimal256::percent(1600)); + + assert_eq!(Decimal256::percent(700).checked_pow(5).unwrap(), Decimal256::percent(1680700)); + + assert_eq!( + Decimal256::percent(700).checked_pow(8).unwrap(), + Decimal256::percent(576480100) + ); + + assert_eq!( + Decimal256::percent(700).checked_pow(10).unwrap(), + Decimal256::percent(28247524900) + ); + + assert_eq!( + Decimal256::percent(120).checked_pow(123).unwrap(), + Decimal256(5486473221892422150877397607u128.into()) + ); + + assert_eq!( + Decimal256::percent(10).checked_pow(2).unwrap(), + Decimal256(10000000000000000u128.into()) + ); + + assert_eq!(Decimal256::percent(10).checked_pow(18).unwrap(), Decimal256(1u128.into())); + } + + #[test] + fn decimal256_checked_pow_overflow() { + assert_eq!(Decimal256::MAX.checked_pow(2), Err(OverflowError::new(OverflowOperation::Pow))); + } + + #[test] + fn decimal256_to_string() { + // Integers + assert_eq!(Decimal256::zero().to_string(), "0"); + assert_eq!(Decimal256::one().to_string(), "1"); + assert_eq!(Decimal256::percent(500).to_string(), "5"); + + // Decimals + assert_eq!(Decimal256::percent(125).to_string(), "1.25"); + assert_eq!(Decimal256::percent(42638).to_string(), "426.38"); + assert_eq!(Decimal256::percent(3).to_string(), "0.03"); + assert_eq!(Decimal256::permille(987).to_string(), "0.987"); + + assert_eq!(Decimal256(Uint256::from(1u128)).to_string(), "0.000000000000000001"); + assert_eq!(Decimal256(Uint256::from(10u128)).to_string(), "0.00000000000000001"); + assert_eq!(Decimal256(Uint256::from(100u128)).to_string(), "0.0000000000000001"); + assert_eq!(Decimal256(Uint256::from(1000u128)).to_string(), "0.000000000000001"); + assert_eq!(Decimal256(Uint256::from(10000u128)).to_string(), "0.00000000000001"); + assert_eq!(Decimal256(Uint256::from(100000u128)).to_string(), "0.0000000000001"); + assert_eq!(Decimal256(Uint256::from(1000000u128)).to_string(), "0.000000000001"); + assert_eq!(Decimal256(Uint256::from(10000000u128)).to_string(), "0.00000000001"); + assert_eq!(Decimal256(Uint256::from(100000000u128)).to_string(), "0.0000000001"); + assert_eq!(Decimal256(Uint256::from(1000000000u128)).to_string(), "0.000000001"); + assert_eq!(Decimal256(Uint256::from(10000000000u128)).to_string(), "0.00000001"); + assert_eq!(Decimal256(Uint256::from(100000000000u128)).to_string(), "0.0000001"); + assert_eq!(Decimal256(Uint256::from(10000000000000u128)).to_string(), "0.00001"); + assert_eq!(Decimal256(Uint256::from(100000000000000u128)).to_string(), "0.0001"); + assert_eq!(Decimal256(Uint256::from(1000000000000000u128)).to_string(), "0.001"); + assert_eq!(Decimal256(Uint256::from(10000000000000000u128)).to_string(), "0.01"); + assert_eq!(Decimal256(Uint256::from(100000000000000000u128)).to_string(), "0.1"); + } + + #[test] + fn decimal256_iter_sum() { + let items = vec![ + Decimal256::zero(), + Decimal256::from_str("2").unwrap(), + Decimal256::from_str("2").unwrap(), + ]; + assert_eq!(items.iter().sum::(), Decimal256::from_str("4").unwrap()); + assert_eq!(items.into_iter().sum::(), Decimal256::from_str("4").unwrap()); + + let empty: Vec = vec![]; + assert_eq!(Decimal256::zero(), empty.iter().sum::()); + } + + #[test] + fn decimal256_serialize() { + assert_eq!(serde_json::to_vec(&Decimal256::zero()).unwrap(), br#""0""#); + assert_eq!(serde_json::to_vec(&Decimal256::one()).unwrap(), br#""1""#); + assert_eq!(serde_json::to_vec(&Decimal256::percent(8)).unwrap(), br#""0.08""#); + assert_eq!(serde_json::to_vec(&Decimal256::percent(87)).unwrap(), br#""0.87""#); + assert_eq!(serde_json::to_vec(&Decimal256::percent(876)).unwrap(), br#""8.76""#); + assert_eq!(serde_json::to_vec(&Decimal256::percent(8765)).unwrap(), br#""87.65""#); + } + + #[test] + fn decimal256_deserialize() { + assert_eq!(serde_json::from_slice::(br#""0""#).unwrap(), Decimal256::zero()); + assert_eq!(serde_json::from_slice::(br#""1""#).unwrap(), Decimal256::one()); + assert_eq!(serde_json::from_slice::(br#""000""#).unwrap(), Decimal256::zero()); + assert_eq!(serde_json::from_slice::(br#""001""#).unwrap(), Decimal256::one()); + + assert_eq!( + serde_json::from_slice::(br#""0.08""#).unwrap(), + Decimal256::percent(8) + ); + assert_eq!( + serde_json::from_slice::(br#""0.87""#).unwrap(), + Decimal256::percent(87) + ); + assert_eq!( + serde_json::from_slice::(br#""8.76""#).unwrap(), + Decimal256::percent(876) + ); + assert_eq!( + serde_json::from_slice::(br#""87.65""#).unwrap(), + Decimal256::percent(8765) + ); + } + + #[test] + fn decimal256_abs_diff_works() { + let a = Decimal256::percent(285); + let b = Decimal256::percent(200); + let expected = Decimal256::percent(85); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn decimal256_rem_works() { + // 4.02 % 1.11 = 0.69 + assert_eq!(Decimal256::percent(402) % Decimal256::percent(111), Decimal256::percent(69)); + + // 15.25 % 4 = 3.25 + assert_eq!(Decimal256::percent(1525) % Decimal256::percent(400), Decimal256::percent(325)); + + let a = Decimal256::percent(318); + let b = Decimal256::percent(317); + let expected = Decimal256::percent(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn decimal_rem_assign_works() { + let mut a = Decimal256::percent(17673); + a %= Decimal256::percent(2362); + assert_eq!(a, Decimal256::percent(1139)); // 176.73 % 23.62 = 11.39 + + let mut a = Decimal256::percent(4262); + let b = Decimal256::percent(1270); + a %= &b; + assert_eq!(a, Decimal256::percent(452)); // 42.62 % 12.7 = 4.52 + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn decimal256_rem_panics_for_zero() { + let _ = Decimal256::percent(777) % Decimal256::zero(); + } + + #[test] + fn decimal256_checked_methods() { + // checked add + assert_eq!( + Decimal256::percent(402).checked_add(Decimal256::percent(111)).unwrap(), + Decimal256::percent(513) + ); + assert!(matches!( + Decimal256::MAX.checked_add(Decimal256::percent(1)), + Err(OverflowError { .. }) + )); + + // checked sub + assert_eq!( + Decimal256::percent(1111).checked_sub(Decimal256::percent(111)).unwrap(), + Decimal256::percent(1000) + ); + assert!(matches!( + Decimal256::zero().checked_sub(Decimal256::percent(1)), + Err(OverflowError { .. }) + )); + + // checked div + assert_eq!( + Decimal256::percent(30).checked_div(Decimal256::percent(200)).unwrap(), + Decimal256::percent(15) + ); + assert_eq!( + Decimal256::percent(88).checked_div(Decimal256::percent(20)).unwrap(), + Decimal256::percent(440) + ); + assert!(matches!( + Decimal256::MAX.checked_div(Decimal256::zero()), + Err(CheckedFromRatioError::DivideByZero { .. }) + )); + assert!(matches!( + Decimal256::MAX.checked_div(Decimal256::percent(1)), + Err(CheckedFromRatioError::Overflow { .. }) + )); + + // checked rem + assert_eq!( + Decimal256::percent(402).checked_rem(Decimal256::percent(111)).unwrap(), + Decimal256::percent(69) + ); + assert_eq!( + Decimal256::percent(1525).checked_rem(Decimal256::percent(400)).unwrap(), + Decimal256::percent(325) + ); + assert!(matches!( + Decimal256::MAX.checked_rem(Decimal256::zero()), + Err(DivideByZeroError { .. }) + )); + } + + #[test] + fn decimal256_pow_works() { + assert_eq!(Decimal256::percent(200).pow(2), Decimal256::percent(400)); + assert_eq!(Decimal256::percent(200).pow(10), Decimal256::percent(102400)); + } + + #[test] + #[should_panic] + fn decimal256_pow_overflow_panics() { + _ = Decimal256::MAX.pow(2u32); + } + + #[test] + fn decimal256_saturating_works() { + assert_eq!( + Decimal256::percent(200).saturating_add(Decimal256::percent(200)), + Decimal256::percent(400) + ); + assert_eq!(Decimal256::MAX.saturating_add(Decimal256::percent(200)), Decimal256::MAX); + assert_eq!( + Decimal256::percent(200).saturating_sub(Decimal256::percent(100)), + Decimal256::percent(100) + ); + assert_eq!(Decimal256::zero().saturating_sub(Decimal256::percent(200)), Decimal256::zero()); + assert_eq!( + Decimal256::percent(200).saturating_mul(Decimal256::percent(50)), + Decimal256::percent(100) + ); + assert_eq!(Decimal256::MAX.saturating_mul(Decimal256::percent(200)), Decimal256::MAX); + assert_eq!(Decimal256::percent(400).saturating_pow(2u32), Decimal256::percent(1600)); + assert_eq!(Decimal256::MAX.saturating_pow(2u32), Decimal256::MAX); + } + + #[test] + fn decimal256_rounding() { + assert_eq!(Decimal256::one().floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(150).floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(199).floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(200).floor(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(99).floor(), Decimal256::zero()); + + assert_eq!(Decimal256::one().ceil(), Decimal256::one()); + assert_eq!(Decimal256::percent(150).ceil(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(199).ceil(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(99).ceil(), Decimal256::one()); + assert_eq!(Decimal256(Uint256::from(1u128)).ceil(), Decimal256::one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn decimal256_ceil_panics() { + let _ = Decimal256::MAX.ceil(); + } + + #[test] + fn decimal256_checked_ceil() { + assert_eq!(Decimal256::percent(199).checked_ceil(), Ok(Decimal256::percent(200))); + assert_eq!(Decimal256::MAX.checked_ceil(), Err(RoundUpOverflowError)); + } + + #[test] + fn decimal256_to_uint_floor_works() { + let d = Decimal256::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(12)); + let d = Decimal256::from_str("12.345").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(12)); + let d = Decimal256::from_str("12.999").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(12)); + let d = Decimal256::from_str("0.98451384").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(0)); + + let d = Decimal256::from_str("75.0").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(75)); + let d = Decimal256::from_str("0.0").unwrap(); + assert_eq!(d.to_uint_floor(), Uint256::from_u128(0)); + + let d = Decimal256::MAX; + assert_eq!( + d.to_uint_floor(), + Uint256::from_str("115792089237316195423570985008687907853269984665640564039457") + .unwrap() + ); + + // Does the same as the old workaround `Uint256::one() * my_decimal`. + // This block can be deleted as part of https://github.com/CosmWasm/cosmwasm/issues/1485. + let tests = vec![ + (Decimal256::from_str("12.345").unwrap(), Uint256::from(12u128)), + (Decimal256::from_str("0.98451384").unwrap(), Uint256::from(0u128)), + (Decimal256::from_str("178.0").unwrap(), Uint256::from(178u128)), + (Decimal256::MIN, Uint256::from(0u128)), + (Decimal256::MAX, Uint256::MAX / Decimal256::DECIMAL_FRACTIONAL), + ]; + for (my_decimal, expected) in tests.into_iter() { + assert_eq!(my_decimal.to_uint_floor(), expected); + } + } + + #[test] + fn decimal256_to_uint_ceil_works() { + let d = Decimal256::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint256::from_u128(13)); + let d = Decimal256::from_str("12.345").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint256::from_u128(13)); + let d = Decimal256::from_str("12.999").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint256::from_u128(13)); + + let d = Decimal256::from_str("75.0").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint256::from_u128(75)); + let d = Decimal256::from_str("0.0").unwrap(); + assert_eq!(d.to_uint_ceil(), Uint256::from_u128(0)); + + let d = Decimal256::MAX; + assert_eq!( + d.to_uint_ceil(), + Uint256::from_str("115792089237316195423570985008687907853269984665640564039458") + .unwrap() + ); + } + + #[test] + fn decimal256_partial_eq() { + let test_cases = [ + ("1", "1", true), + ("0.5", "0.5", true), + ("0.5", "0.51", false), + ("0", "0.00000", true), + ] + .into_iter() + .map(|(lhs, rhs, expected)| (dec(lhs), dec(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn decimal256_implements_debug() { + let decimal = Decimal256::from_str("123.45").unwrap(); + assert_eq!(format!("{decimal:?}"), "Decimal256(123.45)"); + + let test_cases = ["5", "5.01", "42", "0", "2"]; + for s in test_cases { + let decimal256 = Decimal256::from_str(s).unwrap(); + let expected = format!("Decimal256({s})"); + assert_eq!(format!("{decimal256:?}"), expected); + } + } +} diff --git a/cosmwasm/std/src/math/fraction.rs b/cosmwasm/std/src/math/fraction.rs new file mode 100644 index 00000000..e00e8d5a --- /dev/null +++ b/cosmwasm/std/src/math/fraction.rs @@ -0,0 +1,187 @@ +/// A fraction `p`/`q` with integers `p` and `q`. +/// +/// `p` is called the numerator and `q` is called the denominator. +pub trait Fraction: Sized { + /// Returns the numerator `p` + fn numerator(&self) -> T; + /// Returns the denominator `q` + fn denominator(&self) -> T; + + /// Returns the multiplicative inverse `q/p` for fraction `p/q`. + /// + /// If `p` is zero, None is returned. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn inv(&self) -> Option; +} + +impl + PartialEq> Fraction for (T, T) { + fn numerator(&self) -> T { + self.0 + } + + fn denominator(&self) -> T { + self.1 + } + + fn inv(&self) -> Option { + if self.numerator() == 0u8.into() { + None + } else { + Some((self.1, self.0)) + } + } +} + +#[macro_export] +macro_rules! impl_mul_fraction { + ($Uint:ident) => { + impl $Uint { + /// Multiply `self` with a struct implementing [`Fraction`] (e.g. [`crate::Decimal`]). + /// Result is rounded down. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::Uint128; + /// let fraction = (8u128, 21u128); + /// let res = Uint128::new(123456).checked_mul_floor(fraction).unwrap(); + /// assert_eq!(Uint128::new(47030), res); // 47030.8571 rounds down + /// ``` + pub fn checked_mul_floor, T: Into<$Uint>>( + self, + rhs: F, + ) -> Result { + let divisor = rhs.denominator().into(); + let res = self.full_mul(rhs.numerator().into()).checked_div(divisor.into())?; + Ok(res.try_into()?) + } + + /// Same operation as `checked_mul_floor` except unwrapped + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn mul_floor, T: Into<$Uint>>(self, rhs: F) -> Self { + self.checked_mul_floor(rhs).unwrap() + } + + /// Multiply `self` with a struct implementing [`Fraction`] (e.g. [`crate::Decimal`]). + /// Result is rounded up. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::Uint128; + /// let fraction = (8u128, 21u128); + /// let res = Uint128::new(123456).checked_mul_ceil(fraction).unwrap(); + /// assert_eq!(Uint128::new(47031), res); // 47030.8571 rounds up + /// ``` + pub fn checked_mul_ceil, T: Into<$Uint>>( + self, + rhs: F, + ) -> Result { + let dividend = self.full_mul(rhs.numerator().into()); + let divisor = rhs.denominator().into().into(); + let floor_result = dividend.checked_div(divisor)?.try_into()?; + let remainder = dividend.checked_rem(divisor)?; + if !remainder.is_zero() { + Ok($Uint::one().checked_add(floor_result)?) + } else { + Ok(floor_result) + } + } + + /// Same operation as `checked_mul_ceil` except unwrapped + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn mul_ceil, T: Into<$Uint>>(self, rhs: F) -> Self { + self.checked_mul_ceil(rhs).unwrap() + } + + /// Divide `self` with a struct implementing [`Fraction`] (e.g. [`crate::Decimal`]). + /// Result is rounded down. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::Uint128; + /// let fraction = (4u128, 5u128); + /// let res = Uint128::new(789).checked_div_floor(fraction).unwrap(); + /// assert_eq!(Uint128::new(986), res); // 986.25 rounds down + /// ``` + pub fn checked_div_floor, T: Into<$Uint>>( + self, + rhs: F, + ) -> Result + where + Self: Sized, + { + let divisor = rhs.numerator().into(); + let res = self.full_mul(rhs.denominator().into()).checked_div(divisor.into())?; + Ok(res.try_into()?) + } + + /// Same operation as `checked_div_floor` except unwrapped + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn div_floor, T: Into<$Uint>>(self, rhs: F) -> Self + where + Self: Sized, + { + self.checked_div_floor(rhs).unwrap() + } + + /// Divide `self` with a struct implementing [`Fraction`] (e.g. [`crate::Decimal`]). + /// Result is rounded up. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::Uint128; + /// let fraction = (4u128, 5u128); + /// let res = Uint128::new(789).checked_div_ceil(fraction).unwrap(); + /// assert_eq!(Uint128::new(987), res); // 986.25 rounds up + /// ``` + pub fn checked_div_ceil, T: Into<$Uint>>( + self, + rhs: F, + ) -> Result + where + Self: Sized, + { + let dividend = self.full_mul(rhs.denominator().into()); + let divisor = rhs.numerator().into().into(); + let floor_result = dividend.checked_div(divisor)?.try_into()?; + let remainder = dividend.checked_rem(divisor)?; + if !remainder.is_zero() { + Ok($Uint::one().checked_add(floor_result)?) + } else { + Ok(floor_result) + } + } + + /// Same operation as `checked_div_ceil` except unwrapped + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn div_ceil, T: Into<$Uint>>(self, rhs: F) -> Self + where + Self: Sized, + { + self.checked_div_ceil(rhs).unwrap() + } + } + }; +} + +#[cfg(test)] +mod tests { + use crate::{Fraction, Uint128, Uint64}; + + #[test] + fn fraction_tuple_methods() { + let fraction = (Uint64::one(), Uint64::new(2)); + assert_eq!(Uint64::one(), fraction.numerator()); + assert_eq!(Uint64::new(2), fraction.denominator()); + assert_eq!(Some((Uint64::new(2), Uint64::one())), fraction.inv()); + } + + #[test] + fn inverse_with_zero_denominator() { + let fraction = (Uint128::zero(), Uint128::one()); + assert_eq!(None, fraction.inv()); + } +} diff --git a/cosmwasm/std/src/math/int128.rs b/cosmwasm/std/src/math/int128.rs new file mode 100644 index 00000000..6d65e1ca --- /dev/null +++ b/cosmwasm/std/src/math/int128.rs @@ -0,0 +1,1201 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, + Shr, ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + CheckedMultiplyRatioError, Int256, Int512, Int64, Uint128, Uint256, Uint512, Uint64, + __internal::forward_ref_partial_eq, +}; + +use super::{ + conversion::{forward_try_from, try_from_int_to_int}, + num_consts::NumConsts, +}; + +/// An implementation of i128 that is using strings for JSON encoding/decoding, +/// such that the full i128 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances of this and `i128` to get the value out: +/// +/// ``` +/// # use cosmwasm_std::Int128; +/// let a = Int128::from(258i128); +/// assert_eq!(a.i128(), 258); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Int128(#[schemars(with = "String")] pub(crate) i128); + +forward_ref_partial_eq!(Int128, Int128); + +impl Int128 { + pub const MAX: Int128 = Int128(i128::MAX); + pub const MIN: Int128 = Int128(i128::MIN); + + /// Creates a Int128(value). + /// + /// This method is less flexible than `from` but can be called in a const context. + #[inline] + pub const fn new(value: i128) -> Self { + Self(value) + } + + /// Creates a Int128(0) + #[inline] + pub const fn zero() -> Self { + Int128(0) + } + + /// Creates a Int128(1) + #[inline] + pub const fn one() -> Self { + Self(1) + } + + /// Returns a copy of the internal data + pub const fn i128(&self) -> i128 { + self.0 + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 16]) -> Self { + Self(i128::from_be_bytes(data)) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 16]) -> Self { + Self(i128::from_le_bytes(data)) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 16] { + self.0.to_be_bytes() + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 16] { + self.0.to_le_bytes() + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0 == 0 + } + + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.is_negative() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator = numerator.into(); + let denominator = denominator.into(); + if denominator.is_zero() { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Int256::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two [`Int128`] values without overflow, producing an + /// [`Int256`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Int128; + /// + /// let a = Int128::MAX; + /// let result = a.full_mul(2i32); + /// assert_eq!(result.to_string(), "340282366920938463463374607431768211454"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Int256 { + Int256::from(self).checked_mul(Int256::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 128 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 128 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Uint128 { + Uint128(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs(self) -> Self { + match self.0.checked_abs() { + Some(val) => Self(val), + None => panic!("attempt to calculate absolute value with overflow"), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn unsigned_abs(self) -> Uint128 { + Uint128(self.0.unsigned_abs()) + } + + /// Strict negation. Computes -self, panicking if self == MIN. + /// + /// This is the same as [`Int128::neg`] but const. + pub const fn strict_neg(self) -> Self { + match self.0.checked_neg() { + Some(val) => Self(val), + None => panic!("attempt to negate with overflow"), + } + } +} + +impl NumConsts for Int128 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +// Uint to Int +impl From for Int128 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} +forward_try_from!(Uint128, Int128); +forward_try_from!(Uint256, Int128); +forward_try_from!(Uint512, Int128); + +// uint to Int +impl From for Int128 { + fn from(val: u64) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: u32) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: u16) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: u8) -> Self { + Int128(val.into()) + } +} + +// Int to Int +impl From for Int128 { + fn from(val: Int64) -> Self { + val.i64().into() + } +} + +try_from_int_to_int!(Int256, Int128); +try_from_int_to_int!(Int512, Int128); + +// int to Int +impl From for Int128 { + fn from(val: i128) -> Self { + Int128(val) + } +} + +impl From for Int128 { + fn from(val: i64) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: i32) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: i16) -> Self { + Int128(val.into()) + } +} + +impl From for Int128 { + fn from(val: i8) -> Self { + Int128(val.into()) + } +} + +impl TryFrom<&str> for Int128 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Int128 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(u) => Ok(Self(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing Int128: {e}"))), + } + } +} + +impl From for String { + fn from(original: Int128) -> Self { + original.to_string() + } +} + +impl fmt::Display for Int128 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Int128 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Int128(self.0.checked_add(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Add, add for Int128, Int128); + +impl Sub for Int128 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Int128(self.0.checked_sub(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Sub, sub for Int128, Int128); + +impl SubAssign for Int128 { + fn sub_assign(&mut self, rhs: Int128) { + self.0 = self.0.checked_sub(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Int128, Int128); + +impl Div for Int128 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Div, div for Int128, Int128); + +impl Rem for Int128 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Int128, Int128); + +impl Not for Int128 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl Neg for Int128 { + type Output = Self; + + fn neg(self) -> Self::Output { + self.strict_neg() + } +} + +impl RemAssign for Int128 { + fn rem_assign(&mut self, rhs: Int128) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Int128, Int128); + +impl Mul for Int128 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Mul, mul for Int128, Int128); + +impl MulAssign for Int128 { + fn mul_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_mul(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Int128, Int128); + +impl Shr for Int128 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Int128",) + }) + } +} +forward_ref_binop!(impl Shr, shr for Int128, u32); + +impl Shl for Int128 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).unwrap_or_else(|_| { + panic!("left shift error: {rhs} is larger or equal than the number of bits in Int128",) + }) + } +} +forward_ref_binop!(impl Shl, shl for Int128, u32); + +impl AddAssign for Int128 { + fn add_assign(&mut self, rhs: Int128) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Int128, Int128); + +impl DivAssign for Int128 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Int128, Int128); + +impl ShrAssign for Int128 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} +forward_ref_op_assign!(impl ShrAssign, shr_assign for Int128, u32); + +impl ShlAssign for Int128 { + fn shl_assign(&mut self, rhs: u32) { + *self = Shl::::shl(*self, rhs); + } +} +forward_ref_op_assign!(impl ShlAssign, shl_assign for Int128, u32); + +impl Serialize for Int128 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Int128 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Int128Visitor) + } +} + +struct Int128Visitor; + +impl<'de> de::Visitor<'de> for Int128Visitor { + type Value = Int128; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Int128::try_from(v).map_err(|e| E::custom(format_args!("invalid Int128 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Int128 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::conversion::test_try_from_uint_to_int; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 16); + } + + #[test] + fn int128_from_be_bytes_works() { + let num = Int128::from_be_bytes([1; 16]); + let a: [u8; 16] = num.to_be_bytes(); + assert_eq!(a, [1; 16]); + + let be_bytes = + [0u8, 222u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8]; + let num = Int128::from_be_bytes(be_bytes); + let resulting_bytes: [u8; 16] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn int128_new_works() { + let num = Int128::new(222); + assert_eq!(num.i128(), 222); + + let num = Int128::new(-222); + assert_eq!(num.i128(), -222); + + let num = Int128::new(i128::MAX); + assert_eq!(num.i128(), i128::MAX); + + let num = Int128::new(i128::MIN); + assert_eq!(num.i128(), i128::MIN); + } + + #[test] + fn int128_not_works() { + assert_eq!(!Int128::new(222), Int128::new(!222)); + assert_eq!(!Int128::new(-222), Int128::new(!-222)); + + assert_eq!(!Int128::MAX, Int128::new(!i128::MAX)); + assert_eq!(!Int128::MIN, Int128::new(!i128::MIN)); + } + + #[test] + fn int128_zero_works() { + let zero = Int128::zero(); + assert_eq!(zero.to_be_bytes(), [0; 16]); + } + + #[test] + fn uint128_one_works() { + let one = Int128::one(); + let mut one_be = [0; 16]; + one_be[15] = 1; + + assert_eq!(one.to_be_bytes(), one_be); + } + + #[test] + fn int128_endianness() { + let be_bytes = + [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8]; + let le_bytes = + [3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]; + + // These should all be the same. + let num1 = Int128::from_be_bytes(be_bytes); + let num2 = Int128::from_le_bytes(le_bytes); + assert_eq!(num1, Int128::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + } + + #[test] + fn int128_convert_from() { + let a = Int128::from(5i128); + assert_eq!(a.0, i128::from(5u32)); + + let a = Int128::from(5u64); + assert_eq!(a.0, i128::from(5u32)); + + let a = Int128::from(5u32); + assert_eq!(a.0, i128::from(5u32)); + + let a = Int128::from(5u16); + assert_eq!(a.0, i128::from(5u32)); + + let a = Int128::from(5u8); + assert_eq!(a.0, i128::from(5u32)); + + let a = Int128::from(-5i128); + assert_eq!(a.0, i128::from(-5i32)); + + let a = Int128::from(-5i64); + assert_eq!(a.0, i128::from(-5i32)); + + let a = Int128::from(-5i32); + assert_eq!(a.0, i128::from(-5i32)); + + let a = Int128::from(-5i16); + assert_eq!(a.0, i128::from(-5i32)); + + let a = Int128::from(-5i8); + assert_eq!(a.0, i128::from(-5i32)); + + let result = Int128::try_from("34567"); + assert_eq!(result.unwrap().0, "34567".parse::().unwrap()); + + let result = Int128::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn int128_try_from_unsigned_works() { + test_try_from_uint_to_int::("Uint128", "Int128"); + test_try_from_uint_to_int::("Uint256", "Int128"); + test_try_from_uint_to_int::("Uint512", "Int128"); + } + + #[test] + fn int128_implements_display() { + let a = Int128::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Int128::from(-12345i32); + assert_eq!(format!("Embedded: {a}"), "Embedded: -12345"); + assert_eq!(a.to_string(), "-12345"); + + let a = Int128::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn int128_display_padding_works() { + // width > natural representation + let a = Int128::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + let a = Int128::from(-123i64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: -0123"); + + // width < natural representation + let a = Int128::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + let a = Int128::from(-123i64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: -123"); + } + + #[test] + fn int128_to_be_bytes_works() { + assert_eq!(Int128::zero().to_be_bytes(), [0; 16]); + + let mut max = [0xff; 16]; + max[0] = 0x7f; + assert_eq!(Int128::MAX.to_be_bytes(), max); + + let mut one = [0; 16]; + one[15] = 1; + assert_eq!(Int128::from(1i128).to_be_bytes(), one); + // Python: `[b for b in (70141183460469231731687303715884018880).to_bytes(16, "big")]` + assert_eq!( + Int128::from(70141183460469231731687303715884018880i128).to_be_bytes(), + [52, 196, 179, 87, 165, 121, 59, 133, 246, 117, 221, 191, 255, 254, 172, 192] + ); + assert_eq!( + Int128::from_be_bytes([17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78]) + .to_be_bytes(), + [17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78] + ); + } + + #[test] + fn int128_to_le_bytes_works() { + assert_eq!(Int128::zero().to_le_bytes(), [0; 16]); + + let mut max = [0xff; 16]; + max[15] = 0x7f; + assert_eq!(Int128::MAX.to_le_bytes(), max); + + let mut one = [0; 16]; + one[0] = 1; + assert_eq!(Int128::from(1i128).to_le_bytes(), one); + // Python: `[b for b in (70141183460469231731687303715884018880).to_bytes(16, "little")]` + assert_eq!( + Int128::from(70141183460469231731687303715884018880i128).to_le_bytes(), + [192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 52] + ); + assert_eq!( + Int128::from_be_bytes([17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78]) + .to_le_bytes(), + [78, 67, 21, 33, 38, 0, 91, 58, 200, 123, 67, 87, 32, 23, 4, 17] + ); + } + + #[test] + fn int128_is_zero_works() { + assert!(Int128::zero().is_zero()); + assert!(Int128(i128::from(0u32)).is_zero()); + + assert!(!Int128::from(1u32).is_zero()); + assert!(!Int128::from(123u32).is_zero()); + assert!(!Int128::from(-123i32).is_zero()); + } + + #[test] + fn int128_is_negative_works() { + assert!(Int128::MIN.is_negative()); + assert!(Int128::from(-123i32).is_negative()); + + assert!(!Int128::MAX.is_negative()); + assert!(!Int128::zero().is_negative()); + assert!(!Int128::from(123u32).is_negative()); + } + + #[test] + fn int128_wrapping_methods() { + // wrapping_add + assert_eq!(Int128::from(2u32).wrapping_add(Int128::from(2u32)), Int128::from(4u32)); // non-wrapping + assert_eq!(Int128::MAX.wrapping_add(Int128::from(1u32)), Int128::MIN); // wrapping + + // wrapping_sub + assert_eq!(Int128::from(7u32).wrapping_sub(Int128::from(5u32)), Int128::from(2u32)); // non-wrapping + assert_eq!(Int128::MIN.wrapping_sub(Int128::from(1u32)), Int128::MAX); // wrapping + + // wrapping_mul + assert_eq!(Int128::from(3u32).wrapping_mul(Int128::from(2u32)), Int128::from(6u32)); // non-wrapping + assert_eq!(Int128::MAX.wrapping_mul(Int128::from(2u32)), Int128::from(-2i32)); // wrapping + + // wrapping_pow + assert_eq!(Int128::from(2u32).wrapping_pow(3), Int128::from(8u32)); // non-wrapping + assert_eq!(Int128::MAX.wrapping_pow(2), Int128::from(1u32)); // wrapping + } + + #[test] + fn int128_json() { + let orig = Int128::from(1234567890987654321i128); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Int128 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn int128_compare() { + let a = Int128::from(12345u32); + let b = Int128::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Int128::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn int128_math() { + let a = Int128::from(-12345i32); + let b = Int128::from(23456u32); + + // test + with owned and reference right hand side + assert_eq!(a + b, Int128::from(11111u32)); + assert_eq!(a + &b, Int128::from(11111u32)); + + // test - with owned and reference right hand side + assert_eq!(b - a, Int128::from(35801u32)); + assert_eq!(b - &a, Int128::from(35801u32)); + + // test += with owned and reference right hand side + let mut c = Int128::from(300000u32); + c += b; + assert_eq!(c, Int128::from(323456u32)); + let mut d = Int128::from(300000u32); + d += &b; + assert_eq!(d, Int128::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Int128::from(300000u32); + c -= b; + assert_eq!(c, Int128::from(276544u32)); + let mut d = Int128::from(300000u32); + d -= &b; + assert_eq!(d, Int128::from(276544u32)); + + // test - with negative result + assert_eq!(a - b, Int128::from(-35801i32)); + } + + #[test] + #[should_panic] + fn int128_add_overflow_panics() { + let _ = Int128::MAX + Int128::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn int128_sub_works() { + assert_eq!(Int128::from(2u32) - Int128::from(1u32), Int128::from(1u32)); + assert_eq!(Int128::from(2u32) - Int128::from(0u32), Int128::from(2u32)); + assert_eq!(Int128::from(2u32) - Int128::from(2u32), Int128::from(0u32)); + assert_eq!(Int128::from(2u32) - Int128::from(3u32), Int128::from(-1i32)); + + // works for refs + let a = Int128::from(10u32); + let b = Int128::from(3u32); + let expected = Int128::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn int128_sub_overflow_panics() { + let _ = Int128::MIN + Int128::one() - Int128::from(2u32); + } + + #[test] + fn int128_sub_assign_works() { + let mut a = Int128::from(14u32); + a -= Int128::from(2u32); + assert_eq!(a, Int128::from(12u32)); + + // works for refs + let mut a = Int128::from(10u32); + let b = Int128::from(3u32); + let expected = Int128::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn int128_mul_works() { + assert_eq!(Int128::from(2u32) * Int128::from(3u32), Int128::from(6u32)); + assert_eq!(Int128::from(2u32) * Int128::zero(), Int128::zero()); + + // works for refs + let a = Int128::from(11u32); + let b = Int128::from(3u32); + let expected = Int128::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn int128_mul_assign_works() { + let mut a = Int128::from(14u32); + a *= Int128::from(2u32); + assert_eq!(a, Int128::from(28u32)); + + // works for refs + let mut a = Int128::from(10u32); + let b = Int128::from(3u32); + a *= &b; + assert_eq!(a, Int128::from(30u32)); + } + + #[test] + fn int128_pow_works() { + assert_eq!(Int128::from(2u32).pow(2), Int128::from(4u32)); + assert_eq!(Int128::from(2u32).pow(10), Int128::from(1024u32)); + } + + #[test] + #[should_panic] + fn int128_pow_overflow_panics() { + _ = Int128::MAX.pow(2u32); + } + + #[test] + fn int128_checked_multiply_ratio_works() { + let base = Int128(500); + + // factor 1/1 + assert_eq!(base.checked_multiply_ratio(1i128, 1i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(3i128, 3i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(654321i128, 654321i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(i128::MAX, i128::MAX).unwrap(), base); + + // factor 3/2 + assert_eq!(base.checked_multiply_ratio(3i128, 2i128).unwrap(), Int128(750)); + assert_eq!(base.checked_multiply_ratio(333333i128, 222222i128).unwrap(), Int128(750)); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(2i128, 3i128).unwrap(), Int128(333)); + assert_eq!(base.checked_multiply_ratio(222222i128, 333333i128).unwrap(), Int128(333)); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(5i128, 6i128).unwrap(), Int128(416)); + assert_eq!(base.checked_multiply_ratio(100i128, 120i128).unwrap(), Int128(416)); + } + + #[test] + fn int128_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Int128(500i128).checked_multiply_ratio(1i128, 0i128), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Int128(500i128).checked_multiply_ratio(i128::MAX, 1i128), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn int128_shr_works() { + let original = Int128::from_be_bytes([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ]); + + let shifted = Int128::from_be_bytes([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn int128_shr_overflow_panics() { + let _ = Int128::from(1u32) >> 128u32; + } + + #[test] + fn sum_works() { + let nums = vec![ + Int128::from(17u32), + Int128::from(123u32), + Int128::from(540u32), + Int128::from(82u32), + ]; + let expected = Int128::from(762u32); + + let sum_as_ref: Int128 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Int128 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn int128_methods() { + // checked_* + assert!(matches!(Int128::MAX.checked_add(Int128::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int128::from(1u32).checked_add(Int128::from(1u32)), Ok(Int128::from(2u32)),); + assert!(matches!(Int128::MIN.checked_sub(Int128::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int128::from(2u32).checked_sub(Int128::from(1u32)), Ok(Int128::from(1u32)),); + assert!(matches!(Int128::MAX.checked_mul(Int128::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Int128::from(2u32).checked_mul(Int128::from(2u32)), Ok(Int128::from(4u32)),); + assert!(matches!(Int128::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Int128::from(2u32).checked_pow(3u32), Ok(Int128::from(8u32)),); + assert_eq!(Int128::MAX.checked_div(Int128::from(0u32)), Err(DivisionError::DivideByZero)); + assert_eq!(Int128::from(6u32).checked_div(Int128::from(2u32)), Ok(Int128::from(3u32)),); + assert_eq!( + Int128::MAX.checked_div_euclid(Int128::from(0u32)), + Err(DivisionError::DivideByZero) + ); + assert_eq!( + Int128::from(6u32).checked_div_euclid(Int128::from(2u32)), + Ok(Int128::from(3u32)), + ); + assert_eq!( + Int128::from(7u32).checked_div_euclid(Int128::from(2u32)), + Ok(Int128::from(3u32)), + ); + assert!(matches!( + Int128::MAX.checked_rem(Int128::from(0u32)), + Err(DivideByZeroError { .. }) + )); + // checked_* with negative numbers + assert_eq!(Int128::from(-12i32).checked_div(Int128::from(10i32)), Ok(Int128::from(-1i32)),); + assert_eq!(Int128::from(-2i32).checked_pow(3u32), Ok(Int128::from(-8i32)),); + assert_eq!(Int128::from(-6i32).checked_mul(Int128::from(-7i32)), Ok(Int128::from(42i32)),); + assert_eq!(Int128::from(-2i32).checked_add(Int128::from(3i32)), Ok(Int128::from(1i32)),); + assert_eq!( + Int128::from(-1i32).checked_div_euclid(Int128::from(-2i32)), + Ok(Int128::from(1u32)), + ); + + // saturating_* + assert_eq!(Int128::MAX.saturating_add(Int128::from(1u32)), Int128::MAX); + assert_eq!(Int128::MIN.saturating_sub(Int128::from(1u32)), Int128::MIN); + assert_eq!(Int128::MAX.saturating_mul(Int128::from(2u32)), Int128::MAX); + assert_eq!(Int128::from(4u32).saturating_pow(2u32), Int128::from(16u32)); + assert_eq!(Int128::MAX.saturating_pow(2u32), Int128::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn int128_implements_rem() { + let a = Int128::from(10u32); + assert_eq!(a % Int128::from(10u32), Int128::zero()); + assert_eq!(a % Int128::from(2u32), Int128::zero()); + assert_eq!(a % Int128::from(1u32), Int128::zero()); + assert_eq!(a % Int128::from(3u32), Int128::from(1u32)); + assert_eq!(a % Int128::from(4u32), Int128::from(2u32)); + + assert_eq!(Int128::from(-12i32) % Int128::from(10i32), Int128::from(-2i32)); + assert_eq!(Int128::from(12i32) % Int128::from(-10i32), Int128::from(2i32)); + assert_eq!(Int128::from(-12i32) % Int128::from(-10i32), Int128::from(-2i32)); + + // works for refs + let a = Int128::from(10u32); + let b = Int128::from(3u32); + let expected = Int128::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn int128_rem_panics_for_zero() { + let _ = Int128::from(10u32) % Int128::zero(); + } + + #[test] + fn int128_rem_assign_works() { + let mut a = Int128::from(30u32); + a %= Int128::from(4u32); + assert_eq!(a, Int128::from(2u32)); + + // works for refs + let mut a = Int128::from(25u32); + let b = Int128::from(6u32); + a %= &b; + assert_eq!(a, Int128::from(1u32)); + } + + #[test] + fn int128_shr() { + let x: Int128 = 0x4000_0000_0000_0000_0000_0000_0000_0000i128.into(); + assert_eq!(x >> 0, x); // right shift by 0 should be no-op + assert_eq!(x >> 1, Int128::from(0x2000_0000_0000_0000_0000_0000_0000_0000i128)); + assert_eq!(x >> 4, Int128::from(0x0400_0000_0000_0000_0000_0000_0000_0000i128)); + // right shift of MIN value by the maximum shift value should result in -1 (filled with 1s) + assert_eq!(Int128::MIN >> (core::mem::size_of::() as u32 * 8 - 1), -Int128::one()); + } + + #[test] + fn int128_shl() { + let x: Int128 = 0x0800_0000_0000_0000_0000_0000_0000_0000i128.into(); + assert_eq!(x << 0, x); // left shift by 0 should be no-op + assert_eq!(x << 1, Int128::from(0x1000_0000_0000_0000_0000_0000_0000_0000i128)); + assert_eq!(x << 4, Int128::from(0x0800_0000_0000_0000_0000_0000_0000_0000i128 << 4)); + // left shift by by the maximum shift value should result in MIN + assert_eq!(Int128::one() << (core::mem::size_of::() as u32 * 8 - 1), Int128::MIN); + } + + #[test] + fn int128_abs_diff_works() { + let a = Int128::from(42u32); + let b = Int128::from(5u32); + let expected = Uint128::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let c = Int128::from(-5i32); + assert_eq!(b.abs_diff(c), Uint128::from(10u32)); + assert_eq!(c.abs_diff(b), Uint128::from(10u32)); + } + + #[test] + fn int128_abs_works() { + let a = Int128::from(42i32); + assert_eq!(a.abs(), a); + + let b = Int128::from(-42i32); + assert_eq!(b.abs(), a); + + assert_eq!(Int128::zero().abs(), Int128::zero()); + assert_eq!((Int128::MIN + Int128::one()).abs(), Int128::MAX); + } + + #[test] + fn int128_unsigned_abs_works() { + assert_eq!(Int128::zero().unsigned_abs(), Uint128::zero()); + assert_eq!(Int128::one().unsigned_abs(), Uint128::one()); + assert_eq!( + Int128::MIN.unsigned_abs(), + Uint128::new(Int128::MAX.0 as u128) + Uint128::one() + ); + + let v = Int128::from(-42i32); + assert_eq!(v.unsigned_abs(), v.abs_diff(Int128::zero())); + } + + #[test] + #[should_panic = "attempt to calculate absolute value with overflow"] + fn int128_abs_min_panics() { + _ = Int128::MIN.abs(); + } + + #[test] + #[should_panic = "attempt to negate with overflow"] + fn int128_neg_min_panics() { + _ = -Int128::MIN; + } + + #[test] + fn int128_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (u64, u64, bool)| { + (Int128::from(lhs), Int128::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } +} diff --git a/cosmwasm/std/src/math/int256.rs b/cosmwasm/std/src/math/int256.rs new file mode 100644 index 00000000..f0ad0d49 --- /dev/null +++ b/cosmwasm/std/src/math/int256.rs @@ -0,0 +1,1310 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, + Shr, ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + CheckedMultiplyRatioError, Int128, Int512, Int64, Uint128, Uint256, Uint512, Uint64, + __internal::forward_ref_partial_eq, +}; + +/// Used internally - we don't want to leak this type since we might change +/// the implementation in the future. +use bnum::types::{I256, U256}; + +use super::{ + conversion::{grow_be_int, try_from_int_to_int, try_from_uint_to_int}, + num_consts::NumConsts, +}; + +/// An implementation of i256 that is using strings for JSON encoding/decoding, +/// such that the full i256 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances out of primitive uint types or `new` to provide big +/// endian bytes: +/// +/// ``` +/// # use cosmwasm_std::Int256; +/// let a = Int256::from(258u128); +/// let b = Int256::new([ +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, +/// ]); +/// assert_eq!(a, b); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Int256(#[schemars(with = "String")] pub(crate) I256); + +forward_ref_partial_eq!(Int256, Int256); + +impl Int256 { + pub const MAX: Int256 = Int256(I256::MAX); + pub const MIN: Int256 = Int256(I256::MIN); + + /// Creates a Int256(value) from a big endian representation. It's just an alias for + /// `from_be_bytes`. + #[inline] + pub const fn new(value: [u8; 32]) -> Self { + Self::from_be_bytes(value) + } + + /// Creates a Int256(0) + #[inline] + pub const fn zero() -> Self { + Int256(I256::ZERO) + } + + /// Creates a Int256(1) + #[inline] + pub const fn one() -> Self { + Self(I256::ONE) + } + + /// A conversion from `i128` that, unlike the one provided by the `From` trait, + /// can be used in a `const` context. + pub const fn from_i128(v: i128) -> Self { + Self::from_be_bytes(grow_be_int(v.to_be_bytes())) + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 32]) -> Self { + let words: [u64; 4] = [ + u64::from_le_bytes([ + data[31], data[30], data[29], data[28], data[27], data[26], data[25], data[24], + ]), + u64::from_le_bytes([ + data[23], data[22], data[21], data[20], data[19], data[18], data[17], data[16], + ]), + u64::from_le_bytes([ + data[15], data[14], data[13], data[12], data[11], data[10], data[9], data[8], + ]), + u64::from_le_bytes([ + data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], + ]), + ]; + Self(I256::from_bits(U256::from_digits(words))) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 32]) -> Self { + let words: [u64; 4] = [ + u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]), + u64::from_le_bytes([ + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], + ]), + u64::from_le_bytes([ + data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23], + ]), + u64::from_le_bytes([ + data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31], + ]), + ]; + Self(I256::from_bits(U256::from_digits(words))) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 32] { + let bits = self.0.to_bits(); + let words = bits.digits(); + let words = [ + words[3].to_be_bytes(), + words[2].to_be_bytes(), + words[1].to_be_bytes(), + words[0].to_be_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 4], [u8; 32]>(words) } + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 32] { + let bits = self.0.to_bits(); + let words = bits.digits(); + let words = [ + words[0].to_le_bytes(), + words[1].to_le_bytes(), + words[2].to_le_bytes(), + words[3].to_le_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 4], [u8; 32]>(words) } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.is_negative() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator = numerator.into(); + let denominator = denominator.into(); + if denominator.is_zero() { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Int512::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two [`Int256`] values without overflow, producing an + /// [`Int512`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Int256; + /// + /// let a = Int256::MAX; + /// let result = a.full_mul(2i32); + /// assert_eq!( + /// result.to_string(), + /// "115792089237316195423570985008687907853269984665640564039457584007913129639934" + /// ); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Int512 { + Int512::from(self).checked_mul(Int512::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 256 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 256 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Uint256 { + Uint256(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs(self) -> Self { + match self.0.checked_abs() { + Some(val) => Self(val), + None => panic!("attempt to calculate absolute value with overflow"), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn unsigned_abs(self) -> Uint256 { + Uint256(self.0.unsigned_abs()) + } + + /// Strict negation. Computes -self, panicking if self == MIN. + /// + /// This is the same as [`Int256::neg`] but const. + pub const fn strict_neg(self) -> Self { + match self.0.checked_neg() { + Some(val) => Self(val), + None => panic!("attempt to negate with overflow"), + } + } +} + +impl NumConsts for Int256 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +// Uint to Int +try_from_uint_to_int!(Uint512, Int256); +try_from_uint_to_int!(Uint256, Int256); + +impl From for Int256 { + fn from(val: Uint128) -> Self { + val.u128().into() + } +} + +impl From for Int256 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} + +// uint to Int +impl From for Int256 { + fn from(val: u128) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: u64) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: u32) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: u16) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: u8) -> Self { + Int256(val.into()) + } +} + +// Int to Int +try_from_int_to_int!(Int512, Int256); + +impl From for Int256 { + fn from(val: Int128) -> Self { + val.i128().into() + } +} + +impl From for Int256 { + fn from(val: Int64) -> Self { + val.i64().into() + } +} + +// int to Int +impl From for Int256 { + fn from(val: i128) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: i64) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: i32) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: i16) -> Self { + Int256(val.into()) + } +} + +impl From for Int256 { + fn from(val: i8) -> Self { + Int256(val.into()) + } +} + +impl TryFrom<&str> for Int256 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Int256 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match I256::from_str_radix(s, 10) { + Ok(u) => Ok(Self(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing Int256: {e}"))), + } + } +} + +impl From for String { + fn from(original: Int256) -> Self { + original.to_string() + } +} + +impl fmt::Display for Int256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Int256 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Int256(self.0.checked_add(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Add, add for Int256, Int256); + +impl Sub for Int256 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Int256(self.0.checked_sub(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Sub, sub for Int256, Int256); + +impl SubAssign for Int256 { + fn sub_assign(&mut self, rhs: Int256) { + self.0 = self.0.checked_sub(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Int256, Int256); + +impl Div for Int256 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Div, div for Int256, Int256); + +impl Rem for Int256 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Int256, Int256); + +impl Not for Int256 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl Neg for Int256 { + type Output = Self; + + fn neg(self) -> Self::Output { + self.strict_neg() + } +} + +impl RemAssign for Int256 { + fn rem_assign(&mut self, rhs: Int256) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Int256, Int256); + +impl Mul for Int256 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Mul, mul for Int256, Int256); + +impl MulAssign for Int256 { + fn mul_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_mul(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Int256, Int256); + +impl Shr for Int256 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Int256",) + }) + } +} +forward_ref_binop!(impl Shr, shr for Int256, u32); + +impl Shl for Int256 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).unwrap_or_else(|_| { + panic!("left shift error: {rhs} is larger or equal than the number of bits in Int256",) + }) + } +} +forward_ref_binop!(impl Shl, shl for Int256, u32); + +impl AddAssign for Int256 { + fn add_assign(&mut self, rhs: Int256) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Int256, Int256); + +impl DivAssign for Int256 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Int256, Int256); + +impl ShrAssign for Int256 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} +forward_ref_op_assign!(impl ShrAssign, shr_assign for Int256, u32); + +impl ShlAssign for Int256 { + fn shl_assign(&mut self, rhs: u32) { + *self = Shl::::shl(*self, rhs); + } +} +forward_ref_op_assign!(impl ShlAssign, shl_assign for Int256, u32); + +impl Serialize for Int256 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Int256 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Int256Visitor) + } +} + +struct Int256Visitor; + +impl<'de> de::Visitor<'de> for Int256Visitor { + type Value = Int256; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Int256::try_from(v).map_err(|e| E::custom(format_args!("invalid Int256 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Int256 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::conversion::test_try_from_uint_to_int; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 32); + } + + #[test] + fn int256_new_works() { + let num = Int256::new([1; 32]); + let a: [u8; 32] = num.to_be_bytes(); + assert_eq!(a, [1; 32]); + + let be_bytes = [ + 0u8, 222u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let num = Int256::new(be_bytes); + let resulting_bytes: [u8; 32] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn int256_not_works() { + let num = Int256::new([1; 32]); + let a = (!num).to_be_bytes(); + assert_eq!(a, [254; 32]); + + assert_eq!(!Int256::from(-1234806i128), Int256::from(!-1234806i128)); + + assert_eq!(!Int256::MAX, Int256::MIN); + assert_eq!(!Int256::MIN, Int256::MAX); + } + + #[test] + fn int256_zero_works() { + let zero = Int256::zero(); + assert_eq!(zero.to_be_bytes(), [0; 32]); + } + + #[test] + fn uint256_one_works() { + let one = Int256::one(); + let mut one_be = [0; 32]; + one_be[31] = 1; + + assert_eq!(one.to_be_bytes(), one_be); + } + + #[test] + fn int256_endianness() { + let be_bytes = [ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let le_bytes = [ + 3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]; + + // These should all be the same. + let num1 = Int256::new(be_bytes); + let num2 = Int256::from_be_bytes(be_bytes); + let num3 = Int256::from_le_bytes(le_bytes); + assert_eq!(num1, Int256::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + assert_eq!(num1, num3); + } + + #[test] + fn int256_convert_from() { + let a = Int256::from(5u128); + assert_eq!(a.0, I256::from(5u32)); + + let a = Int256::from(5u64); + assert_eq!(a.0, I256::from(5u32)); + + let a = Int256::from(5u32); + assert_eq!(a.0, I256::from(5u32)); + + let a = Int256::from(5u16); + assert_eq!(a.0, I256::from(5u32)); + + let a = Int256::from(5u8); + assert_eq!(a.0, I256::from(5u32)); + + let a = Int256::from(-5i128); + assert_eq!(a.0, I256::from(-5i32)); + + let a = Int256::from(-5i64); + assert_eq!(a.0, I256::from(-5i32)); + + let a = Int256::from(-5i32); + assert_eq!(a.0, I256::from(-5i32)); + + let a = Int256::from(-5i16); + assert_eq!(a.0, I256::from(-5i32)); + + let a = Int256::from(-5i8); + assert_eq!(a.0, I256::from(-5i32)); + + let result = Int256::try_from("34567"); + assert_eq!(result.unwrap().0, I256::from_str_radix("34567", 10).unwrap()); + + let result = Int256::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn int256_try_from_unsigned_works() { + test_try_from_uint_to_int::("Uint256", "Int256"); + test_try_from_uint_to_int::("Uint512", "Int256"); + } + + #[test] + fn int256_from_i128() { + assert_eq!(Int256::from_i128(123i128), Int256::from_str("123").unwrap()); + + assert_eq!( + Int256::from_i128(9785746283745i128), + Int256::from_str("9785746283745").unwrap() + ); + + assert_eq!(Int256::from_i128(i128::MAX).to_string(), i128::MAX.to_string()); + assert_eq!(Int256::from_i128(i128::MIN).to_string(), i128::MIN.to_string()); + } + + #[test] + fn int256_implements_display() { + let a = Int256::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Int256::from(-12345i32); + assert_eq!(format!("Embedded: {a}"), "Embedded: -12345"); + assert_eq!(a.to_string(), "-12345"); + + let a = Int256::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn int256_display_padding_works() { + // width > natural representation + let a = Int256::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + let a = Int256::from(-123i64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: -0123"); + + // width < natural representation + let a = Int256::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + let a = Int256::from(-123i64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: -123"); + } + + #[test] + fn int256_to_be_bytes_works() { + assert_eq!(Int256::zero().to_be_bytes(), [0; 32]); + + let mut max = [0xff; 32]; + max[0] = 0x7f; + assert_eq!(Int256::MAX.to_be_bytes(), max); + + let mut one = [0; 32]; + one[31] = 1; + assert_eq!(Int256::from(1u128).to_be_bytes(), one); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(32, "big")]` + assert_eq!( + Int256::from(240282366920938463463374607431768124608u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 196, 179, 87, 165, 121, 59, + 133, 246, 117, 221, 191, 255, 254, 172, 192 + ] + ); + assert_eq!( + Int256::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240 + ]) + .to_be_bytes(), + [ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240 + ] + ); + } + + #[test] + fn int256_to_le_bytes_works() { + assert_eq!(Int256::zero().to_le_bytes(), [0; 32]); + + let mut max = [0xff; 32]; + max[31] = 0x7f; + assert_eq!(Int256::MAX.to_le_bytes(), max); + + let mut one = [0; 32]; + one[0] = 1; + assert_eq!(Int256::from(1u128).to_le_bytes(), one); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(64, "little")]` + assert_eq!( + Int256::from(240282366920938463463374607431768124608u128).to_le_bytes(), + [ + 192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 180, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + assert_eq!( + Int256::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240 + ]) + .to_le_bytes(), + [ + 240, 150, 115, 200, 240, 2, 233, 42, 7, 192, 201, 211, 54, 65, 76, 87, 78, 67, 21, + 33, 38, 0, 91, 58, 200, 123, 67, 87, 32, 23, 4, 17 + ] + ); + } + + #[test] + fn int256_is_zero_works() { + assert!(Int256::zero().is_zero()); + assert!(Int256(I256::from(0u32)).is_zero()); + + assert!(!Int256::from(1u32).is_zero()); + assert!(!Int256::from(123u32).is_zero()); + assert!(!Int256::from(-123i32).is_zero()); + } + + #[test] + fn int256_is_negative_works() { + assert!(Int256::MIN.is_negative()); + assert!(Int256::from(-123i32).is_negative()); + + assert!(!Int256::MAX.is_negative()); + assert!(!Int256::zero().is_negative()); + assert!(!Int256::from(123u32).is_negative()); + } + + #[test] + fn int256_wrapping_methods() { + // wrapping_add + assert_eq!(Int256::from(2u32).wrapping_add(Int256::from(2u32)), Int256::from(4u32)); // non-wrapping + assert_eq!(Int256::MAX.wrapping_add(Int256::from(1u32)), Int256::MIN); // wrapping + + // wrapping_sub + assert_eq!(Int256::from(7u32).wrapping_sub(Int256::from(5u32)), Int256::from(2u32)); // non-wrapping + assert_eq!(Int256::MIN.wrapping_sub(Int256::from(1u32)), Int256::MAX); // wrapping + + // wrapping_mul + assert_eq!(Int256::from(3u32).wrapping_mul(Int256::from(2u32)), Int256::from(6u32)); // non-wrapping + assert_eq!(Int256::MAX.wrapping_mul(Int256::from(2u32)), Int256::from(-2i32)); // wrapping + + // wrapping_pow + assert_eq!(Int256::from(2u32).wrapping_pow(3), Int256::from(8u32)); // non-wrapping + assert_eq!(Int256::MAX.wrapping_pow(2), Int256::from(1u32)); // wrapping + } + + #[test] + fn int256_json() { + let orig = Int256::from(1234567890987654321u128); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Int256 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn int256_compare() { + let a = Int256::from(12345u32); + let b = Int256::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Int256::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn int256_math() { + let a = Int256::from(-12345i32); + let b = Int256::from(23456u32); + + // test + with owned and reference right hand side + assert_eq!(a + b, Int256::from(11111u32)); + assert_eq!(a + &b, Int256::from(11111u32)); + + // test - with owned and reference right hand side + assert_eq!(b - a, Int256::from(35801u32)); + assert_eq!(b - &a, Int256::from(35801u32)); + + // test += with owned and reference right hand side + let mut c = Int256::from(300000u32); + c += b; + assert_eq!(c, Int256::from(323456u32)); + let mut d = Int256::from(300000u32); + d += &b; + assert_eq!(d, Int256::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Int256::from(300000u32); + c -= b; + assert_eq!(c, Int256::from(276544u32)); + let mut d = Int256::from(300000u32); + d -= &b; + assert_eq!(d, Int256::from(276544u32)); + + // test - with negative result + assert_eq!(a - b, Int256::from(-35801i32)); + } + + #[test] + #[should_panic] + fn int256_add_overflow_panics() { + let _ = Int256::MAX + Int256::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn int256_sub_works() { + assert_eq!(Int256::from(2u32) - Int256::from(1u32), Int256::from(1u32)); + assert_eq!(Int256::from(2u32) - Int256::from(0u32), Int256::from(2u32)); + assert_eq!(Int256::from(2u32) - Int256::from(2u32), Int256::from(0u32)); + assert_eq!(Int256::from(2u32) - Int256::from(3u32), Int256::from(-1i32)); + + // works for refs + let a = Int256::from(10u32); + let b = Int256::from(3u32); + let expected = Int256::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn int256_sub_overflow_panics() { + let _ = Int256::MIN + Int256::one() - Int256::from(2u32); + } + + #[test] + fn int256_sub_assign_works() { + let mut a = Int256::from(14u32); + a -= Int256::from(2u32); + assert_eq!(a, Int256::from(12u32)); + + // works for refs + let mut a = Int256::from(10u32); + let b = Int256::from(3u32); + let expected = Int256::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn int256_mul_works() { + assert_eq!(Int256::from(2u32) * Int256::from(3u32), Int256::from(6u32)); + assert_eq!(Int256::from(2u32) * Int256::zero(), Int256::zero()); + + // works for refs + let a = Int256::from(11u32); + let b = Int256::from(3u32); + let expected = Int256::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn int256_mul_assign_works() { + let mut a = Int256::from(14u32); + a *= Int256::from(2u32); + assert_eq!(a, Int256::from(28u32)); + + // works for refs + let mut a = Int256::from(10u32); + let b = Int256::from(3u32); + a *= &b; + assert_eq!(a, Int256::from(30u32)); + } + + #[test] + fn int256_pow_works() { + assert_eq!(Int256::from(2u32).pow(2), Int256::from(4u32)); + assert_eq!(Int256::from(2u32).pow(10), Int256::from(1024u32)); + } + + #[test] + #[should_panic] + fn int256_pow_overflow_panics() { + _ = Int256::MAX.pow(2u32); + } + + #[test] + fn int256_checked_multiply_ratio_works() { + let base = Int256::from_i128(500); + + // factor 1/1 + assert_eq!(base.checked_multiply_ratio(1i128, 1i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(3i128, 3i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(654321i128, 654321i128).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(i128::MAX, i128::MAX).unwrap(), base); + + // factor 3/2 + assert_eq!(base.checked_multiply_ratio(3i128, 2i128).unwrap(), Int256::from_i128(750)); + assert_eq!( + base.checked_multiply_ratio(333333i128, 222222i128).unwrap(), + Int256::from_i128(750) + ); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(2i128, 3i128).unwrap(), Int256::from_i128(333)); + assert_eq!( + base.checked_multiply_ratio(222222i128, 333333i128).unwrap(), + Int256::from_i128(333) + ); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(5i128, 6i128).unwrap(), Int256::from_i128(416)); + assert_eq!(base.checked_multiply_ratio(100i128, 120i128).unwrap(), Int256::from_i128(416)); + } + + #[test] + fn int256_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Int256::from_i128(500i128).checked_multiply_ratio(1i128, 0i128), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Int256::MAX.checked_multiply_ratio(Int256::MAX, 1i128), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn int256_shr_works() { + let original = Int256::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ]); + + let shifted = Int256::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn int256_shr_overflow_panics() { + let _ = Int256::from(1u32) >> 256u32; + } + + #[test] + fn sum_works() { + let nums = vec![ + Int256::from(17u32), + Int256::from(123u32), + Int256::from(540u32), + Int256::from(82u32), + ]; + let expected = Int256::from(762u32); + + let sum_as_ref: Int256 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Int256 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn int256_methods() { + // checked_* + assert!(matches!(Int256::MAX.checked_add(Int256::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int256::from(1u32).checked_add(Int256::from(1u32)), Ok(Int256::from(2u32)),); + assert!(matches!(Int256::MIN.checked_sub(Int256::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int256::from(2u32).checked_sub(Int256::from(1u32)), Ok(Int256::from(1u32)),); + assert!(matches!(Int256::MAX.checked_mul(Int256::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Int256::from(2u32).checked_mul(Int256::from(2u32)), Ok(Int256::from(4u32)),); + assert!(matches!(Int256::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Int256::from(2u32).checked_pow(3u32), Ok(Int256::from(8u32)),); + assert_eq!(Int256::MAX.checked_div(Int256::from(0u32)), Err(DivisionError::DivideByZero)); + assert_eq!(Int256::from(6u32).checked_div(Int256::from(2u32)), Ok(Int256::from(3u32)),); + assert_eq!( + Int256::MAX.checked_div_euclid(Int256::from(0u32)), + Err(DivisionError::DivideByZero) + ); + assert_eq!( + Int256::from(6u32).checked_div_euclid(Int256::from(2u32)), + Ok(Int256::from(3u32)), + ); + assert_eq!( + Int256::from(7u32).checked_div_euclid(Int256::from(2u32)), + Ok(Int256::from(3u32)), + ); + assert!(matches!( + Int256::MAX.checked_rem(Int256::from(0u32)), + Err(DivideByZeroError { .. }) + )); + // checked_* with negative numbers + assert_eq!(Int256::from(-12i32).checked_div(Int256::from(10i32)), Ok(Int256::from(-1i32)),); + assert_eq!(Int256::from(-2i32).checked_pow(3u32), Ok(Int256::from(-8i32)),); + assert_eq!(Int256::from(-6i32).checked_mul(Int256::from(-7i32)), Ok(Int256::from(42i32)),); + assert_eq!(Int256::from(-2i32).checked_add(Int256::from(3i32)), Ok(Int256::from(1i32)),); + assert_eq!( + Int256::from(-1i32).checked_div_euclid(Int256::from(-2i32)), + Ok(Int256::from(1u32)), + ); + + // saturating_* + assert_eq!(Int256::MAX.saturating_add(Int256::from(1u32)), Int256::MAX); + assert_eq!(Int256::MIN.saturating_sub(Int256::from(1u32)), Int256::MIN); + assert_eq!(Int256::MAX.saturating_mul(Int256::from(2u32)), Int256::MAX); + assert_eq!(Int256::from(4u32).saturating_pow(2u32), Int256::from(16u32)); + assert_eq!(Int256::MAX.saturating_pow(2u32), Int256::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn int256_implements_rem() { + let a = Int256::from(10u32); + assert_eq!(a % Int256::from(10u32), Int256::zero()); + assert_eq!(a % Int256::from(2u32), Int256::zero()); + assert_eq!(a % Int256::from(1u32), Int256::zero()); + assert_eq!(a % Int256::from(3u32), Int256::from(1u32)); + assert_eq!(a % Int256::from(4u32), Int256::from(2u32)); + + assert_eq!(Int256::from(-12i32) % Int256::from(10i32), Int256::from(-2i32)); + assert_eq!(Int256::from(12i32) % Int256::from(-10i32), Int256::from(2i32)); + assert_eq!(Int256::from(-12i32) % Int256::from(-10i32), Int256::from(-2i32)); + + // works for refs + let a = Int256::from(10u32); + let b = Int256::from(3u32); + let expected = Int256::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn int256_rem_panics_for_zero() { + let _ = Int256::from(10u32) % Int256::zero(); + } + + #[test] + fn int256_rem_assign_works() { + let mut a = Int256::from(30u32); + a %= Int256::from(4u32); + assert_eq!(a, Int256::from(2u32)); + + // works for refs + let mut a = Int256::from(25u32); + let b = Int256::from(6u32); + a %= &b; + assert_eq!(a, Int256::from(1u32)); + } + + #[test] + fn int256_shr() { + let x: Int256 = 0x8000_0000_0000_0000_0000_0000_0000_0000u128.into(); + assert_eq!(x >> 0, x); // right shift by 0 should be no-op + assert_eq!(x >> 1, Int256::from(0x4000_0000_0000_0000_0000_0000_0000_0000u128)); + assert_eq!(x >> 4, Int256::from(0x0800_0000_0000_0000_0000_0000_0000_0000u128)); + // right shift of MIN value by the maximum shift value should result in -1 (filled with 1s) + assert_eq!(Int256::MIN >> (core::mem::size_of::() as u32 * 8 - 1), -Int256::one()); + } + + #[test] + fn int256_shl() { + let x: Int256 = 0x0800_0000_0000_0000_0000_0000_0000_0000u128.into(); + assert_eq!(x << 0, x); // left shift by 0 should be no-op + assert_eq!(x << 1, Int256::from(0x1000_0000_0000_0000_0000_0000_0000_0000u128)); + assert_eq!(x << 4, Int256::from(0x8000_0000_0000_0000_0000_0000_0000_0000u128)); + // left shift by by the maximum shift value should result in MIN + assert_eq!(Int256::one() << (core::mem::size_of::() as u32 * 8 - 1), Int256::MIN); + } + + #[test] + fn int256_abs_diff_works() { + let a = Int256::from(42u32); + let b = Int256::from(5u32); + let expected = Uint256::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let c = Int256::from(-5i32); + assert_eq!(b.abs_diff(c), Uint256::from(10u32)); + assert_eq!(c.abs_diff(b), Uint256::from(10u32)); + } + + #[test] + fn int256_abs_works() { + let a = Int256::from(42i32); + assert_eq!(a.abs(), a); + + let b = Int256::from(-42i32); + assert_eq!(b.abs(), a); + + assert_eq!(Int256::zero().abs(), Int256::zero()); + assert_eq!((Int256::MIN + Int256::one()).abs(), Int256::MAX); + } + + #[test] + fn int256_unsigned_abs_works() { + assert_eq!(Int256::zero().unsigned_abs(), Uint256::zero()); + assert_eq!(Int256::one().unsigned_abs(), Uint256::one()); + assert_eq!( + Int256::MIN.unsigned_abs(), + Uint256::from_be_bytes(Int256::MAX.to_be_bytes()) + Uint256::one() + ); + + let v = Int256::from(-42i32); + assert_eq!(v.unsigned_abs(), v.abs_diff(Int256::zero())); + } + + #[test] + #[should_panic = "attempt to calculate absolute value with overflow"] + fn int256_abs_min_panics() { + _ = Int256::MIN.abs(); + } + + #[test] + #[should_panic = "attempt to negate with overflow"] + fn int256_neg_min_panics() { + _ = -Int256::MIN; + } + + #[test] + fn int256_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (u64, u64, bool)| { + (Int256::from(lhs), Int256::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } +} diff --git a/cosmwasm/std/src/math/int512.rs b/cosmwasm/std/src/math/int512.rs new file mode 100644 index 00000000..d00ceb6e --- /dev/null +++ b/cosmwasm/std/src/math/int512.rs @@ -0,0 +1,1294 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, + Shr, ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Int128, Int256, Int64, Uint128, Uint256, Uint512, Uint64, + __internal::forward_ref_partial_eq, +}; + +/// Used internally - we don't want to leak this type since we might change +/// the implementation in the future. +use bnum::types::{I512, U512}; + +use super::{ + conversion::{grow_be_int, try_from_uint_to_int}, + num_consts::NumConsts, +}; + +/// An implementation of i512 that is using strings for JSON encoding/decoding, +/// such that the full i512 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances out of primitive uint types or `new` to provide big +/// endian bytes: +/// +/// ``` +/// # use cosmwasm_std::Int512; +/// let a = Int512::from(258u128); +/// let b = Int512::new([ +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, +/// ]); +/// assert_eq!(a, b); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Int512(#[schemars(with = "String")] pub(crate) I512); + +forward_ref_partial_eq!(Int512, Int512); + +impl Int512 { + pub const MAX: Int512 = Int512(I512::MAX); + pub const MIN: Int512 = Int512(I512::MIN); + + /// Creates a Int512(value) from a big endian representation. It's just an alias for + /// `from_be_bytes`. + #[inline] + pub const fn new(value: [u8; 64]) -> Self { + Self::from_be_bytes(value) + } + + /// Creates a Int512(0) + #[inline] + pub const fn zero() -> Self { + Int512(I512::ZERO) + } + + /// Creates a Int512(1) + #[inline] + pub const fn one() -> Self { + Self(I512::ONE) + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 64]) -> Self { + let words: [u64; 8] = [ + u64::from_le_bytes([ + data[63], data[62], data[61], data[60], data[59], data[58], data[57], data[56], + ]), + u64::from_le_bytes([ + data[55], data[54], data[53], data[52], data[51], data[50], data[49], data[48], + ]), + u64::from_le_bytes([ + data[47], data[46], data[45], data[44], data[43], data[42], data[41], data[40], + ]), + u64::from_le_bytes([ + data[39], data[38], data[37], data[36], data[35], data[34], data[33], data[32], + ]), + u64::from_le_bytes([ + data[31], data[30], data[29], data[28], data[27], data[26], data[25], data[24], + ]), + u64::from_le_bytes([ + data[23], data[22], data[21], data[20], data[19], data[18], data[17], data[16], + ]), + u64::from_le_bytes([ + data[15], data[14], data[13], data[12], data[11], data[10], data[9], data[8], + ]), + u64::from_le_bytes([ + data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], + ]), + ]; + Self(I512::from_bits(U512::from_digits(words))) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 64]) -> Self { + let words: [u64; 8] = [ + u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]), + u64::from_le_bytes([ + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], + ]), + u64::from_le_bytes([ + data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23], + ]), + u64::from_le_bytes([ + data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31], + ]), + u64::from_le_bytes([ + data[32], data[33], data[34], data[35], data[36], data[37], data[38], data[39], + ]), + u64::from_le_bytes([ + data[40], data[41], data[42], data[43], data[44], data[45], data[46], data[47], + ]), + u64::from_le_bytes([ + data[48], data[49], data[50], data[51], data[52], data[53], data[54], data[55], + ]), + u64::from_le_bytes([ + data[56], data[57], data[58], data[59], data[60], data[61], data[62], data[63], + ]), + ]; + Self(I512::from_bits(U512::from_digits(words))) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 64] { + let bits = self.0.to_bits(); + let words = bits.digits(); + let words = [ + words[7].to_be_bytes(), + words[6].to_be_bytes(), + words[5].to_be_bytes(), + words[4].to_be_bytes(), + words[3].to_be_bytes(), + words[2].to_be_bytes(), + words[1].to_be_bytes(), + words[0].to_be_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 8], [u8; 64]>(words) } + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 64] { + let bits = self.0.to_bits(); + let words = bits.digits(); + let words = [ + words[0].to_le_bytes(), + words[1].to_le_bytes(), + words[2].to_le_bytes(), + words[3].to_le_bytes(), + words[4].to_le_bytes(), + words[5].to_le_bytes(), + words[6].to_le_bytes(), + words[7].to_le_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 8], [u8; 64]>(words) } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.is_negative() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 512 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 512 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Uint512 { + Uint512(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs(self) -> Self { + match self.0.checked_abs() { + Some(val) => Self(val), + None => panic!("attempt to calculate absolute value with overflow"), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn unsigned_abs(self) -> Uint512 { + Uint512(self.0.unsigned_abs()) + } + + /// Strict negation. Computes -self, panicking if self == MIN. + /// + /// This is the same as [`Int512::neg`] but const. + pub const fn strict_neg(self) -> Self { + match self.0.checked_neg() { + Some(val) => Self(val), + None => panic!("attempt to negate with overflow"), + } + } +} + +impl NumConsts for Int512 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +// Uint to Int +try_from_uint_to_int!(Uint512, Int512); + +impl From for Int512 { + fn from(val: Uint256) -> Self { + let mut bytes = [0u8; 64]; + bytes[32..].copy_from_slice(&val.to_be_bytes()); + + Self::from_be_bytes(bytes) + } +} + +impl From for Int512 { + fn from(val: Uint128) -> Self { + val.u128().into() + } +} + +impl From for Int512 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} + +// uint to Int +impl From for Int512 { + fn from(val: u128) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: u64) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: u32) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: u16) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: u8) -> Self { + Int512(val.into()) + } +} + +// int to Int +impl From for Int512 { + fn from(val: i128) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: i64) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: i32) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: i16) -> Self { + Int512(val.into()) + } +} + +impl From for Int512 { + fn from(val: i8) -> Self { + Int512(val.into()) + } +} + +// Int to Int +impl From for Int512 { + fn from(val: Int64) -> Self { + Int512(val.i64().into()) + } +} + +impl From for Int512 { + fn from(val: Int128) -> Self { + Int512(val.i128().into()) + } +} + +impl From for Int512 { + fn from(val: Int256) -> Self { + Self::from_be_bytes(grow_be_int(val.to_be_bytes())) + } +} + +impl TryFrom<&str> for Int512 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Int512 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match I512::from_str_radix(s, 10) { + Ok(u) => Ok(Self(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing Int512: {e}"))), + } + } +} + +impl From for String { + fn from(original: Int512) -> Self { + original.to_string() + } +} + +impl fmt::Display for Int512 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Int512 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Int512(self.0.checked_add(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Add, add for Int512, Int512); + +impl Sub for Int512 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Int512(self.0.checked_sub(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Sub, sub for Int512, Int512); + +impl SubAssign for Int512 { + fn sub_assign(&mut self, rhs: Int512) { + self.0 = self.0.checked_sub(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Int512, Int512); + +impl Div for Int512 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Div, div for Int512, Int512); + +impl Rem for Int512 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Int512, Int512); + +impl Not for Int512 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl Neg for Int512 { + type Output = Self; + + fn neg(self) -> Self::Output { + self.strict_neg() + } +} + +impl RemAssign for Int512 { + fn rem_assign(&mut self, rhs: Int512) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Int512, Int512); + +impl Mul for Int512 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Mul, mul for Int512, Int512); + +impl MulAssign for Int512 { + fn mul_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_mul(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Int512, Int512); + +impl Shr for Int512 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Int512",) + }) + } +} +forward_ref_binop!(impl Shr, shr for Int512, u32); + +impl Shl for Int512 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).unwrap_or_else(|_| { + panic!("left shift error: {rhs} is larger or equal than the number of bits in Int512",) + }) + } +} +forward_ref_binop!(impl Shl, shl for Int512, u32); + +impl AddAssign for Int512 { + fn add_assign(&mut self, rhs: Int512) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Int512, Int512); + +impl DivAssign for Int512 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Int512, Int512); + +impl ShrAssign for Int512 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} +forward_ref_op_assign!(impl ShrAssign, shr_assign for Int512, u32); + +impl ShlAssign for Int512 { + fn shl_assign(&mut self, rhs: u32) { + *self = Shl::::shl(*self, rhs); + } +} +forward_ref_op_assign!(impl ShlAssign, shl_assign for Int512, u32); + +impl Serialize for Int512 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Int512 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Int512Visitor) + } +} + +struct Int512Visitor; + +impl<'de> de::Visitor<'de> for Int512Visitor { + type Value = Int512; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Int512::try_from(v).map_err(|e| E::custom(format!("invalid Int512 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Int512 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::conversion::test_try_from_uint_to_int; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 64); + } + + #[test] + fn int512_new_works() { + let num = Int512::new([1; 64]); + let a: [u8; 64] = num.to_be_bytes(); + assert_eq!(a, [1; 64]); + + let be_bytes = [ + 0u8, 222u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let num = Int512::new(be_bytes); + let resulting_bytes: [u8; 64] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn int512_not_works() { + let num = Int512::new([1; 64]); + let a = (!num).to_be_bytes(); + assert_eq!(a, [254; 64]); + + assert_eq!(!Int512::from(-1234806i128), Int512::from(!-1234806i128)); + + assert_eq!(!Int512::MAX, Int512::MIN); + assert_eq!(!Int512::MIN, Int512::MAX); + } + + #[test] + fn int512_zero_works() { + let zero = Int512::zero(); + assert_eq!(zero.to_be_bytes(), [0; 64]); + } + + #[test] + fn uint512_one_works() { + let one = Int512::one(); + let mut one_be = [0; 64]; + one_be[63] = 1; + + assert_eq!(one.to_be_bytes(), one_be); + } + + #[test] + fn int512_endianness() { + let be_bytes = [ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let le_bytes = [ + 3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]; + + // These should all be the same. + let num1 = Int512::new(be_bytes); + let num2 = Int512::from_be_bytes(be_bytes); + let num3 = Int512::from_le_bytes(le_bytes); + assert_eq!(num1, Int512::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + assert_eq!(num1, num3); + } + + #[test] + fn int512_convert_from() { + let a = Int512::from(5u128); + assert_eq!(a.0, I512::from(5u32)); + + let a = Int512::from(5u64); + assert_eq!(a.0, I512::from(5u32)); + + let a = Int512::from(5u32); + assert_eq!(a.0, I512::from(5u32)); + + let a = Int512::from(5u16); + assert_eq!(a.0, I512::from(5u32)); + + let a = Int512::from(5u8); + assert_eq!(a.0, I512::from(5u32)); + + let a = Int512::from(-5i128); + assert_eq!(a.0, I512::from(-5i32)); + + let a = Int512::from(-5i64); + assert_eq!(a.0, I512::from(-5i32)); + + let a = Int512::from(-5i32); + assert_eq!(a.0, I512::from(-5i32)); + + let a = Int512::from(-5i16); + assert_eq!(a.0, I512::from(-5i32)); + + let a = Int512::from(-5i8); + assert_eq!(a.0, I512::from(-5i32)); + + // other big signed integers + let values = [Int64::MAX, Int64::MIN, Int64::one(), -Int64::one(), Int64::zero()]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + + let values = [Int128::MAX, Int128::MIN, Int128::one(), -Int128::one(), Int128::zero()]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + + let values = [Int256::MAX, Int256::MIN, Int256::one(), -Int256::one(), Int256::zero()]; + for v in values { + assert_eq!(Int512::from(v).to_string(), v.to_string()); + } + + let result = Int512::try_from("34567"); + assert_eq!(result.unwrap().0, I512::from_str_radix("34567", 10).unwrap()); + + let result = Int512::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn int512_try_from_unsigned_works() { + test_try_from_uint_to_int::("Uint256", "Int256"); + test_try_from_uint_to_int::("Uint512", "Int256"); + } + + #[test] + fn int512_implements_display() { + let a = Int512::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Int512::from(-12345i32); + assert_eq!(format!("Embedded: {a}"), "Embedded: -12345"); + assert_eq!(a.to_string(), "-12345"); + + let a = Int512::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn int512_display_padding_works() { + // width > natural representation + let a = Int512::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + let a = Int512::from(-123i64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: -0123"); + + // width < natural representation + let a = Int512::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + let a = Int512::from(-123i64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: -123"); + } + + #[test] + fn int512_to_be_bytes_works() { + assert_eq!(Int512::zero().to_be_bytes(), [0; 64]); + + let mut max = [0xff; 64]; + max[0] = 0x7f; + assert_eq!(Int512::MAX.to_be_bytes(), max); + + let mut one = [0; 64]; + one[63] = 1; + assert_eq!(Int512::from(1u128).to_be_bytes(), one); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(64, "big")]` + assert_eq!( + Int512::from(240282366920938463463374607431768124608u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 196, 179, 87, 165, + 121, 59, 133, 246, 117, 221, 191, 255, 254, 172, 192 + ] + ); + assert_eq!( + Int512::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ]) + .to_be_bytes(), + [ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ] + ); + } + + #[test] + fn int512_to_le_bytes_works() { + assert_eq!(Int512::zero().to_le_bytes(), [0; 64]); + + let mut max = [0xff; 64]; + max[63] = 0x7f; + assert_eq!(Int512::MAX.to_le_bytes(), max); + + let mut one = [0; 64]; + one[0] = 1; + assert_eq!(Int512::from(1u128).to_le_bytes(), one); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(64, "little")]` + assert_eq!( + Int512::from(240282366920938463463374607431768124608u128).to_le_bytes(), + [ + 192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 180, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + Int512::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ]) + .to_le_bytes(), + [ + 29, 38, 187, 230, 96, 252, 8, 99, 12, 47, 22, 13, 59, 44, 10, 64, 73, 154, 195, + 166, 88, 14, 22, 85, 119, 238, 134, 208, 45, 106, 88, 218, 240, 150, 115, 200, 240, + 2, 233, 42, 7, 192, 201, 211, 54, 65, 76, 87, 78, 67, 21, 33, 38, 0, 91, 58, 200, + 123, 67, 87, 32, 23, 4, 17 + ] + ); + } + + #[test] + fn int512_is_zero_works() { + assert!(Int512::zero().is_zero()); + assert!(Int512(I512::from(0u32)).is_zero()); + + assert!(!Int512::from(1u32).is_zero()); + assert!(!Int512::from(123u32).is_zero()); + assert!(!Int512::from(-123i32).is_zero()); + } + + #[test] + fn int512_is_negative_works() { + assert!(Int512::MIN.is_negative()); + assert!(Int512::from(-123i32).is_negative()); + + assert!(!Int512::MAX.is_negative()); + assert!(!Int512::zero().is_negative()); + assert!(!Int512::from(123u32).is_negative()); + } + + #[test] + fn int512_wrapping_methods() { + // wrapping_add + assert_eq!(Int512::from(2u32).wrapping_add(Int512::from(2u32)), Int512::from(4u32)); // non-wrapping + assert_eq!(Int512::MAX.wrapping_add(Int512::from(1u32)), Int512::MIN); // wrapping + + // wrapping_sub + assert_eq!(Int512::from(7u32).wrapping_sub(Int512::from(5u32)), Int512::from(2u32)); // non-wrapping + assert_eq!(Int512::MIN.wrapping_sub(Int512::from(1u32)), Int512::MAX); // wrapping + + // wrapping_mul + assert_eq!(Int512::from(3u32).wrapping_mul(Int512::from(2u32)), Int512::from(6u32)); // non-wrapping + assert_eq!(Int512::MAX.wrapping_mul(Int512::from(2u32)), Int512::from(-2i32)); // wrapping + + // wrapping_pow + assert_eq!(Int512::from(2u32).wrapping_pow(3), Int512::from(8u32)); // non-wrapping + assert_eq!(Int512::MAX.wrapping_pow(2), Int512::from(1u32)); // wrapping + } + + #[test] + fn int512_json() { + let orig = Int512::from(1234567890987654321u128); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Int512 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn int512_compare() { + let a = Int512::from(12345u32); + let b = Int512::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Int512::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn int512_math() { + let a = Int512::from(-12345i32); + let b = Int512::from(23456u32); + + // test + with owned and reference right hand side + assert_eq!(a + b, Int512::from(11111u32)); + assert_eq!(a + &b, Int512::from(11111u32)); + + // test - with owned and reference right hand side + assert_eq!(b - a, Int512::from(35801u32)); + assert_eq!(b - &a, Int512::from(35801u32)); + + // test += with owned and reference right hand side + let mut c = Int512::from(300000u32); + c += b; + assert_eq!(c, Int512::from(323456u32)); + let mut d = Int512::from(300000u32); + d += &b; + assert_eq!(d, Int512::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Int512::from(300000u32); + c -= b; + assert_eq!(c, Int512::from(276544u32)); + let mut d = Int512::from(300000u32); + d -= &b; + assert_eq!(d, Int512::from(276544u32)); + + // test - with negative result + assert_eq!(a - b, Int512::from(-35801i32)); + } + + #[test] + #[should_panic] + fn int512_add_overflow_panics() { + let _ = Int512::MAX + Int512::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn int512_sub_works() { + assert_eq!(Int512::from(2u32) - Int512::from(1u32), Int512::from(1u32)); + assert_eq!(Int512::from(2u32) - Int512::from(0u32), Int512::from(2u32)); + assert_eq!(Int512::from(2u32) - Int512::from(2u32), Int512::from(0u32)); + assert_eq!(Int512::from(2u32) - Int512::from(3u32), Int512::from(-1i32)); + + // works for refs + let a = Int512::from(10u32); + let b = Int512::from(3u32); + let expected = Int512::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn int512_sub_overflow_panics() { + let _ = Int512::MIN + Int512::one() - Int512::from(2u32); + } + + #[test] + fn int512_sub_assign_works() { + let mut a = Int512::from(14u32); + a -= Int512::from(2u32); + assert_eq!(a, Int512::from(12u32)); + + // works for refs + let mut a = Int512::from(10u32); + let b = Int512::from(3u32); + let expected = Int512::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn int512_mul_works() { + assert_eq!(Int512::from(2u32) * Int512::from(3u32), Int512::from(6u32)); + assert_eq!(Int512::from(2u32) * Int512::zero(), Int512::zero()); + + // works for refs + let a = Int512::from(11u32); + let b = Int512::from(3u32); + let expected = Int512::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn int512_mul_assign_works() { + let mut a = Int512::from(14u32); + a *= Int512::from(2u32); + assert_eq!(a, Int512::from(28u32)); + + // works for refs + let mut a = Int512::from(10u32); + let b = Int512::from(3u32); + a *= &b; + assert_eq!(a, Int512::from(30u32)); + } + + #[test] + fn int512_pow_works() { + assert_eq!(Int512::from(2u32).pow(2), Int512::from(4u32)); + assert_eq!(Int512::from(2u32).pow(10), Int512::from(1024u32)); + } + + #[test] + #[should_panic] + fn int512_pow_overflow_panics() { + _ = Int512::MAX.pow(2u32); + } + + #[test] + fn int512_shr_works() { + let original = Int512::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ]); + + let shifted = Int512::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn int512_shr_overflow_panics() { + let _ = Int512::from(1u32) >> 512u32; + } + + #[test] + fn sum_works() { + let nums = vec![ + Int512::from(17u32), + Int512::from(123u32), + Int512::from(540u32), + Int512::from(82u32), + ]; + let expected = Int512::from(762u32); + + let sum_as_ref: Int512 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Int512 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn int512_methods() { + // checked_* + assert!(matches!(Int512::MAX.checked_add(Int512::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int512::from(1u32).checked_add(Int512::from(1u32)), Ok(Int512::from(2u32)),); + assert!(matches!(Int512::MIN.checked_sub(Int512::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int512::from(2u32).checked_sub(Int512::from(1u32)), Ok(Int512::from(1u32)),); + assert!(matches!(Int512::MAX.checked_mul(Int512::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Int512::from(2u32).checked_mul(Int512::from(2u32)), Ok(Int512::from(4u32)),); + assert!(matches!(Int512::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Int512::from(2u32).checked_pow(3u32), Ok(Int512::from(8u32)),); + assert_eq!(Int512::MAX.checked_div(Int512::from(0u32)), Err(DivisionError::DivideByZero)); + assert_eq!(Int512::from(6u32).checked_div(Int512::from(2u32)), Ok(Int512::from(3u32)),); + assert_eq!( + Int512::MAX.checked_div_euclid(Int512::from(0u32)), + Err(DivisionError::DivideByZero) + ); + assert_eq!( + Int512::from(6u32).checked_div_euclid(Int512::from(2u32)), + Ok(Int512::from(3u32)), + ); + assert_eq!( + Int512::from(7u32).checked_div_euclid(Int512::from(2u32)), + Ok(Int512::from(3u32)), + ); + assert!(matches!( + Int512::MAX.checked_rem(Int512::from(0u32)), + Err(DivideByZeroError { .. }) + )); + // checked_* with negative numbers + assert_eq!(Int512::from(-12i32).checked_div(Int512::from(10i32)), Ok(Int512::from(-1i32)),); + assert_eq!(Int512::from(-2i32).checked_pow(3u32), Ok(Int512::from(-8i32)),); + assert_eq!(Int512::from(-6i32).checked_mul(Int512::from(-7i32)), Ok(Int512::from(42i32)),); + assert_eq!(Int512::from(-2i32).checked_add(Int512::from(3i32)), Ok(Int512::from(1i32)),); + assert_eq!( + Int512::from(-1i32).checked_div_euclid(Int512::from(-2i32)), + Ok(Int512::from(1u32)), + ); + + // saturating_* + assert_eq!(Int512::MAX.saturating_add(Int512::from(1u32)), Int512::MAX); + assert_eq!(Int512::MIN.saturating_sub(Int512::from(1u32)), Int512::MIN); + assert_eq!(Int512::MAX.saturating_mul(Int512::from(2u32)), Int512::MAX); + assert_eq!(Int512::from(4u32).saturating_pow(2u32), Int512::from(16u32)); + assert_eq!(Int512::MAX.saturating_pow(2u32), Int512::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn int512_implements_rem() { + let a = Int512::from(10u32); + assert_eq!(a % Int512::from(10u32), Int512::zero()); + assert_eq!(a % Int512::from(2u32), Int512::zero()); + assert_eq!(a % Int512::from(1u32), Int512::zero()); + assert_eq!(a % Int512::from(3u32), Int512::from(1u32)); + assert_eq!(a % Int512::from(4u32), Int512::from(2u32)); + + assert_eq!(Int512::from(-12i32) % Int512::from(10i32), Int512::from(-2i32)); + assert_eq!(Int512::from(12i32) % Int512::from(-10i32), Int512::from(2i32)); + assert_eq!(Int512::from(-12i32) % Int512::from(-10i32), Int512::from(-2i32)); + + // works for refs + let a = Int512::from(10u32); + let b = Int512::from(3u32); + let expected = Int512::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn int512_rem_panics_for_zero() { + let _ = Int512::from(10u32) % Int512::zero(); + } + + #[test] + fn int512_rem_assign_works() { + let mut a = Int512::from(30u32); + a %= Int512::from(4u32); + assert_eq!(a, Int512::from(2u32)); + + // works for refs + let mut a = Int512::from(25u32); + let b = Int512::from(6u32); + a %= &b; + assert_eq!(a, Int512::from(1u32)); + } + + #[test] + fn int512_shr() { + let x: Int512 = 0x8000_0000_0000_0000_0000_0000_0000_0000u128.into(); + assert_eq!(x >> 0, x); // right shift by 0 should be no-op + assert_eq!(x >> 1, Int512::from(0x4000_0000_0000_0000_0000_0000_0000_0000u128)); + assert_eq!(x >> 4, Int512::from(0x0800_0000_0000_0000_0000_0000_0000_0000u128)); + // right shift of MIN value by the maximum shift value should result in -1 (filled with 1s) + assert_eq!(Int512::MIN >> (core::mem::size_of::() as u32 * 8 - 1), -Int512::one()); + } + + #[test] + fn int512_shl() { + let x: Int512 = 0x0800_0000_0000_0000_0000_0000_0000_0000u128.into(); + assert_eq!(x << 0, x); // left shift by 0 should be no-op + assert_eq!(x << 1, Int512::from(0x1000_0000_0000_0000_0000_0000_0000_0000u128)); + assert_eq!(x << 4, Int512::from(0x8000_0000_0000_0000_0000_0000_0000_0000u128)); + // left shift by by the maximum shift value should result in MIN + assert_eq!(Int512::one() << (core::mem::size_of::() as u32 * 8 - 1), Int512::MIN); + } + + #[test] + fn int512_abs_diff_works() { + let a = Int512::from(42u32); + let b = Int512::from(5u32); + let expected = Uint512::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let c = Int512::from(-5i32); + assert_eq!(b.abs_diff(c), Uint512::from(10u32)); + assert_eq!(c.abs_diff(b), Uint512::from(10u32)); + } + + #[test] + fn int512_abs_works() { + let a = Int512::from(42i32); + assert_eq!(a.abs(), a); + + let b = Int512::from(-42i32); + assert_eq!(b.abs(), a); + + assert_eq!(Int512::zero().abs(), Int512::zero()); + assert_eq!((Int512::MIN + Int512::one()).abs(), Int512::MAX); + } + + #[test] + fn int512_unsigned_abs_works() { + assert_eq!(Int512::zero().unsigned_abs(), Uint512::zero()); + assert_eq!(Int512::one().unsigned_abs(), Uint512::one()); + assert_eq!( + Int512::MIN.unsigned_abs(), + Uint512::from_be_bytes(Int512::MAX.to_be_bytes()) + Uint512::one() + ); + + let v = Int512::from(-42i32); + assert_eq!(v.unsigned_abs(), v.abs_diff(Int512::zero())); + } + + #[test] + #[should_panic = "attempt to calculate absolute value with overflow"] + fn int512_abs_min_panics() { + _ = Int512::MIN.abs(); + } + + #[test] + #[should_panic = "attempt to negate with overflow"] + fn int512_neg_min_panics() { + _ = -Int512::MIN; + } + + #[test] + fn int512_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (u64, u64, bool)| { + (Int512::from(lhs), Int512::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } +} diff --git a/cosmwasm/std/src/math/int64.rs b/cosmwasm/std/src/math/int64.rs new file mode 100644 index 00000000..f5051be4 --- /dev/null +++ b/cosmwasm/std/src/math/int64.rs @@ -0,0 +1,1156 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, + Shr, ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{DivideByZeroError, DivisionError, OverflowError, OverflowOperation, StdError}, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + CheckedMultiplyRatioError, Int128, Int256, Int512, Uint128, Uint256, Uint512, Uint64, + __internal::forward_ref_partial_eq, +}; + +use super::{ + conversion::{forward_try_from, try_from_int_to_int}, + num_consts::NumConsts, +}; + +/// An implementation of i64 that is using strings for JSON encoding/decoding, +/// such that the full i64 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances of this and `i64` to get the value out: +/// +/// ``` +/// # use cosmwasm_std::Int64; +/// let a = Int64::from(258i64); +/// assert_eq!(a.i64(), 258); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Int64(#[schemars(with = "String")] pub(crate) i64); + +forward_ref_partial_eq!(Int64, Int64); + +impl Int64 { + pub const MAX: Int64 = Int64(i64::MAX); + pub const MIN: Int64 = Int64(i64::MIN); + + /// Creates a Int64(value). + /// + /// This method is less flexible than `from` but can be called in a const context. + #[inline] + pub const fn new(value: i64) -> Self { + Self(value) + } + + /// Creates a Int64(0) + #[inline] + pub const fn zero() -> Self { + Int64(0) + } + + /// Creates a Int64(1) + #[inline] + pub const fn one() -> Self { + Self(1) + } + + /// Returns a copy of the internal data + pub const fn i64(&self) -> i64 { + self.0 + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 8]) -> Self { + Self(i64::from_be_bytes(data)) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 8]) -> Self { + Self(i64::from_le_bytes(data)) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0 == 0 + } + + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.is_negative() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator = numerator.into(); + let denominator = denominator.into(); + if denominator.is_zero() { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Int128::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two [`Int64`] values without overflow, producing an + /// [`Int128`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Int64; + /// + /// let a = Int64::MAX; + /// let result = a.full_mul(2i32); + /// assert_eq!(result.to_string(), "18446744073709551614"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Int128 { + Int128::from(self).checked_mul(Int128::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + if other.is_zero() { + return Err(DivisionError::DivideByZero); + } + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivisionError::Overflow) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 64 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 64 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Uint64 { + Uint64(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs(self) -> Self { + match self.0.checked_abs() { + Some(val) => Self(val), + None => panic!("attempt to calculate absolute value with overflow"), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn unsigned_abs(self) -> Uint64 { + Uint64(self.0.unsigned_abs()) + } + + /// Strict negation. Computes -self, panicking if self == MIN. + /// + /// This is the same as [`Int64::neg`] but const. + pub const fn strict_neg(self) -> Self { + match self.0.checked_neg() { + Some(val) => Self(val), + None => panic!("attempt to negate with overflow"), + } + } +} + +impl NumConsts for Int64 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +// uint to Int +impl From for Int64 { + fn from(val: u32) -> Self { + Int64(val.into()) + } +} + +impl From for Int64 { + fn from(val: u16) -> Self { + Int64(val.into()) + } +} + +impl From for Int64 { + fn from(val: u8) -> Self { + Int64(val.into()) + } +} + +// int to Int +impl From for Int64 { + fn from(val: i64) -> Self { + Int64(val) + } +} + +impl From for Int64 { + fn from(val: i32) -> Self { + Int64(val.into()) + } +} + +impl From for Int64 { + fn from(val: i16) -> Self { + Int64(val.into()) + } +} + +impl From for Int64 { + fn from(val: i8) -> Self { + Int64(val.into()) + } +} + +// Int to Int +try_from_int_to_int!(Int128, Int64); +try_from_int_to_int!(Int256, Int64); +try_from_int_to_int!(Int512, Int64); + +// Uint to Int +forward_try_from!(Uint64, Int64); +forward_try_from!(Uint128, Int64); +forward_try_from!(Uint256, Int64); +forward_try_from!(Uint512, Int64); + +impl TryFrom<&str> for Int64 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Int64 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(u) => Ok(Self(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing Int64: {e}"))), + } + } +} + +impl From for String { + fn from(original: Int64) -> Self { + original.to_string() + } +} + +impl fmt::Display for Int64 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Int64 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Int64(self.0.checked_add(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Add, add for Int64, Int64); + +impl Sub for Int64 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Int64(self.0.checked_sub(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Sub, sub for Int64, Int64); + +impl SubAssign for Int64 { + fn sub_assign(&mut self, rhs: Int64) { + self.0 = self.0.checked_sub(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Int64, Int64); + +impl Div for Int64 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Div, div for Int64, Int64); + +impl Rem for Int64 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Int64, Int64); + +impl Not for Int64 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl Neg for Int64 { + type Output = Self; + + fn neg(self) -> Self::Output { + self.strict_neg() + } +} + +impl RemAssign for Int64 { + fn rem_assign(&mut self, rhs: Int64) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Int64, Int64); + +impl Mul for Int64 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Mul, mul for Int64, Int64); + +impl MulAssign for Int64 { + fn mul_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_mul(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Int64, Int64); + +impl Shr for Int64 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Int64",) + }) + } +} +forward_ref_binop!(impl Shr, shr for Int64, u32); + +impl Shl for Int64 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).unwrap_or_else(|_| { + panic!("left shift error: {rhs} is larger or equal than the number of bits in Int64",) + }) + } +} +forward_ref_binop!(impl Shl, shl for Int64, u32); + +impl AddAssign for Int64 { + fn add_assign(&mut self, rhs: Int64) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for Int64, Int64); + +impl DivAssign for Int64 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for Int64, Int64); + +impl ShrAssign for Int64 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} +forward_ref_op_assign!(impl ShrAssign, shr_assign for Int64, u32); + +impl ShlAssign for Int64 { + fn shl_assign(&mut self, rhs: u32) { + *self = Shl::::shl(*self, rhs); + } +} +forward_ref_op_assign!(impl ShlAssign, shl_assign for Int64, u32); + +impl Serialize for Int64 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Int64 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Int64Visitor) + } +} + +struct Int64Visitor; + +impl<'de> de::Visitor<'de> for Int64Visitor { + type Value = Int64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Int64::try_from(v).map_err(|e| E::custom(format_args!("invalid Int64 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Int64 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::conversion::test_try_from_uint_to_int; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 8); + } + + #[test] + fn int64_from_be_bytes_works() { + let num = Int64::from_be_bytes([1; 8]); + let a: [u8; 8] = num.to_be_bytes(); + assert_eq!(a, [1; 8]); + + let be_bytes = [0u8, 222u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8]; + let num = Int64::from_be_bytes(be_bytes); + let resulting_bytes: [u8; 8] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn int64_new_works() { + let num = Int64::new(222); + assert_eq!(num.i64(), 222); + + let num = Int64::new(-222); + assert_eq!(num.i64(), -222); + + let num = Int64::new(i64::MAX); + assert_eq!(num.i64(), i64::MAX); + + let num = Int64::new(i64::MIN); + assert_eq!(num.i64(), i64::MIN); + } + + #[test] + fn int64_not_works() { + assert_eq!(!Int64::new(222), Int64::new(!222)); + assert_eq!(!Int64::new(-222), Int64::new(!-222)); + + assert_eq!(!Int64::MAX, Int64::new(!i64::MAX)); + assert_eq!(!Int64::MIN, Int64::new(!i64::MIN)); + } + + #[test] + fn int64_zero_works() { + let zero = Int64::zero(); + assert_eq!(zero.to_be_bytes(), [0; 8]); + } + + #[test] + fn uint64_one_works() { + let one = Int64::one(); + let mut one_be = [0; 8]; + one_be[7] = 1; + + assert_eq!(one.to_be_bytes(), one_be); + } + + #[test] + fn int64_endianness() { + let be_bytes = [0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8]; + let le_bytes = [3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8]; + + // These should all be the same. + let num1 = Int64::from_be_bytes(be_bytes); + let num2 = Int64::from_le_bytes(le_bytes); + assert_eq!(num1, Int64::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + } + + #[test] + fn int64_convert_from() { + let a = Int64::from(5i64); + assert_eq!(a.0, i64::from(5u32)); + + let a = Int64::from(5i64); + assert_eq!(a.0, i64::from(5u32)); + + let a = Int64::from(5u32); + assert_eq!(a.0, i64::from(5u32)); + + let a = Int64::from(5u16); + assert_eq!(a.0, i64::from(5u32)); + + let a = Int64::from(5u8); + assert_eq!(a.0, i64::from(5u32)); + + let a = Int64::from(-5i64); + assert_eq!(a.0, i64::from(-5i32)); + + let a = Int64::from(-5i64); + assert_eq!(a.0, i64::from(-5i32)); + + let a = Int64::from(-5i32); + assert_eq!(a.0, i64::from(-5i32)); + + let a = Int64::from(-5i16); + assert_eq!(a.0, i64::from(-5i32)); + + let a = Int64::from(-5i8); + assert_eq!(a.0, i64::from(-5i32)); + + let result = Int64::try_from("34567"); + assert_eq!(result.unwrap().0, "34567".parse::().unwrap()); + + let result = Int64::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn int64_try_from_unsigned_works() { + test_try_from_uint_to_int::("Uint64", "Int64"); + test_try_from_uint_to_int::("Uint128", "Int64"); + test_try_from_uint_to_int::("Uint256", "Int64"); + test_try_from_uint_to_int::("Uint512", "Int64"); + } + + #[test] + fn int64_implements_display() { + let a = Int64::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Int64::from(-12345i32); + assert_eq!(format!("Embedded: {a}"), "Embedded: -12345"); + assert_eq!(a.to_string(), "-12345"); + + let a = Int64::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn int64_display_padding_works() { + // width > natural representation + let a = Int64::from(123i64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + let a = Int64::from(-123i64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: -0123"); + + // width < natural representation + let a = Int64::from(123i64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + let a = Int64::from(-123i64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: -123"); + } + + #[test] + fn int64_to_be_bytes_works() { + assert_eq!(Int64::zero().to_be_bytes(), [0; 8]); + + let mut max = [0xff; 8]; + max[0] = 0x7f; + assert_eq!(Int64::MAX.to_be_bytes(), max); + + let mut one = [0; 8]; + one[7] = 1; + assert_eq!(Int64::from(1i64).to_be_bytes(), one); + // Python: `[b for b in (8535972485454015680).to_bytes(8, "big")]` + assert_eq!( + Int64::from(8535972485454015680i64).to_be_bytes(), + [118, 117, 221, 191, 255, 254, 172, 192] + ); + assert_eq!( + Int64::from_be_bytes([17, 4, 23, 32, 87, 67, 123, 200]).to_be_bytes(), + [17, 4, 23, 32, 87, 67, 123, 200] + ); + } + + #[test] + fn int64_to_le_bytes_works() { + assert_eq!(Int64::zero().to_le_bytes(), [0; 8]); + + let mut max = [0xff; 8]; + max[7] = 0x7f; + assert_eq!(Int64::MAX.to_le_bytes(), max); + + let mut one = [0; 8]; + one[0] = 1; + assert_eq!(Int64::from(1i64).to_le_bytes(), one); + // Python: `[b for b in (8535972485454015680).to_bytes(8, "little")]` + assert_eq!( + Int64::from(8535972485454015680i64).to_le_bytes(), + [192, 172, 254, 255, 191, 221, 117, 118] + ); + assert_eq!( + Int64::from_be_bytes([17, 4, 23, 32, 87, 67, 123, 200]).to_le_bytes(), + [200, 123, 67, 87, 32, 23, 4, 17] + ); + } + + #[test] + fn int64_is_zero_works() { + assert!(Int64::zero().is_zero()); + assert!(Int64(i64::from(0u32)).is_zero()); + + assert!(!Int64::from(1u32).is_zero()); + assert!(!Int64::from(123u32).is_zero()); + assert!(!Int64::from(-123i32).is_zero()); + } + + #[test] + fn int64_is_negative_works() { + assert!(Int64::MIN.is_negative()); + assert!(Int64::from(-123i32).is_negative()); + + assert!(!Int64::MAX.is_negative()); + assert!(!Int64::zero().is_negative()); + assert!(!Int64::from(123u32).is_negative()); + } + + #[test] + fn int64_wrapping_methods() { + // wrapping_add + assert_eq!(Int64::from(2u32).wrapping_add(Int64::from(2u32)), Int64::from(4u32)); // non-wrapping + assert_eq!(Int64::MAX.wrapping_add(Int64::from(1u32)), Int64::MIN); // wrapping + + // wrapping_sub + assert_eq!(Int64::from(7u32).wrapping_sub(Int64::from(5u32)), Int64::from(2u32)); // non-wrapping + assert_eq!(Int64::MIN.wrapping_sub(Int64::from(1u32)), Int64::MAX); // wrapping + + // wrapping_mul + assert_eq!(Int64::from(3u32).wrapping_mul(Int64::from(2u32)), Int64::from(6u32)); // non-wrapping + assert_eq!(Int64::MAX.wrapping_mul(Int64::from(2u32)), Int64::from(-2i32)); // wrapping + + // wrapping_pow + assert_eq!(Int64::from(2u32).wrapping_pow(3), Int64::from(8u32)); // non-wrapping + assert_eq!(Int64::MAX.wrapping_pow(2), Int64::from(1u32)); // wrapping + } + + #[test] + fn int64_json() { + let orig = Int64::from(1234567890987654321i64); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Int64 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn int64_compare() { + let a = Int64::from(12345u32); + let b = Int64::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Int64::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn int64_math() { + let a = Int64::from(-12345i32); + let b = Int64::from(23456u32); + + // test + with owned and reference right hand side + assert_eq!(a + b, Int64::from(11111u32)); + assert_eq!(a + &b, Int64::from(11111u32)); + + // test - with owned and reference right hand side + assert_eq!(b - a, Int64::from(35801u32)); + assert_eq!(b - &a, Int64::from(35801u32)); + + // test += with owned and reference right hand side + let mut c = Int64::from(300000u32); + c += b; + assert_eq!(c, Int64::from(323456u32)); + let mut d = Int64::from(300000u32); + d += &b; + assert_eq!(d, Int64::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Int64::from(300000u32); + c -= b; + assert_eq!(c, Int64::from(276544u32)); + let mut d = Int64::from(300000u32); + d -= &b; + assert_eq!(d, Int64::from(276544u32)); + + // test - with negative result + assert_eq!(a - b, Int64::from(-35801i32)); + } + + #[test] + #[should_panic] + fn int64_add_overflow_panics() { + let _ = Int64::MAX + Int64::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn int64_sub_works() { + assert_eq!(Int64::from(2u32) - Int64::from(1u32), Int64::from(1u32)); + assert_eq!(Int64::from(2u32) - Int64::from(0u32), Int64::from(2u32)); + assert_eq!(Int64::from(2u32) - Int64::from(2u32), Int64::from(0u32)); + assert_eq!(Int64::from(2u32) - Int64::from(3u32), Int64::from(-1i32)); + + // works for refs + let a = Int64::from(10u32); + let b = Int64::from(3u32); + let expected = Int64::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn int64_sub_overflow_panics() { + let _ = Int64::MIN + Int64::one() - Int64::from(2u32); + } + + #[test] + fn int64_sub_assign_works() { + let mut a = Int64::from(14u32); + a -= Int64::from(2u32); + assert_eq!(a, Int64::from(12u32)); + + // works for refs + let mut a = Int64::from(10u32); + let b = Int64::from(3u32); + let expected = Int64::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn int64_mul_works() { + assert_eq!(Int64::from(2u32) * Int64::from(3u32), Int64::from(6u32)); + assert_eq!(Int64::from(2u32) * Int64::zero(), Int64::zero()); + + // works for refs + let a = Int64::from(11u32); + let b = Int64::from(3u32); + let expected = Int64::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn int64_mul_assign_works() { + let mut a = Int64::from(14u32); + a *= Int64::from(2u32); + assert_eq!(a, Int64::from(28u32)); + + // works for refs + let mut a = Int64::from(10u32); + let b = Int64::from(3u32); + a *= &b; + assert_eq!(a, Int64::from(30u32)); + } + + #[test] + fn int64_pow_works() { + assert_eq!(Int64::from(2u32).pow(2), Int64::from(4u32)); + assert_eq!(Int64::from(2u32).pow(10), Int64::from(1024u32)); + } + + #[test] + #[should_panic] + fn int64_pow_overflow_panics() { + _ = Int64::MAX.pow(2u32); + } + + #[test] + fn int64_checked_multiply_ratio_works() { + let base = Int64(500); + + // factor 1/1 + assert_eq!(base.checked_multiply_ratio(1i64, 1i64).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(3i64, 3i64).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(654321i64, 654321i64).unwrap(), base); + assert_eq!(base.checked_multiply_ratio(i64::MAX, i64::MAX).unwrap(), base); + + // factor 3/2 + assert_eq!(base.checked_multiply_ratio(3i64, 2i64).unwrap(), Int64(750)); + assert_eq!(base.checked_multiply_ratio(333333i64, 222222i64).unwrap(), Int64(750)); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(2i64, 3i64).unwrap(), Int64(333)); + assert_eq!(base.checked_multiply_ratio(222222i64, 333333i64).unwrap(), Int64(333)); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.checked_multiply_ratio(5i64, 6i64).unwrap(), Int64(416)); + assert_eq!(base.checked_multiply_ratio(100i64, 120i64).unwrap(), Int64(416)); + } + + #[test] + fn int64_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Int64(500i64).checked_multiply_ratio(1i64, 0i64), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Int64(500i64).checked_multiply_ratio(i64::MAX, 1i64), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn int64_shr_works() { + let original = Int64::from_be_bytes([0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8]); + + let shifted = Int64::from_be_bytes([0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn int64_shr_overflow_panics() { + let _ = Int64::from(1u32) >> 64u32; + } + + #[test] + fn sum_works() { + let nums = + vec![Int64::from(17u32), Int64::from(123u32), Int64::from(540u32), Int64::from(82u32)]; + let expected = Int64::from(762u32); + + let sum_as_ref: Int64 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Int64 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn int64_methods() { + // checked_* + assert!(matches!(Int64::MAX.checked_add(Int64::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int64::from(1u32).checked_add(Int64::from(1u32)), Ok(Int64::from(2u32)),); + assert!(matches!(Int64::MIN.checked_sub(Int64::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Int64::from(2u32).checked_sub(Int64::from(1u32)), Ok(Int64::from(1u32)),); + assert!(matches!(Int64::MAX.checked_mul(Int64::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Int64::from(2u32).checked_mul(Int64::from(2u32)), Ok(Int64::from(4u32)),); + assert!(matches!(Int64::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Int64::from(2u32).checked_pow(3u32), Ok(Int64::from(8u32)),); + assert_eq!(Int64::MAX.checked_div(Int64::from(0u32)), Err(DivisionError::DivideByZero)); + assert_eq!(Int64::from(6u32).checked_div(Int64::from(2u32)), Ok(Int64::from(3u32)),); + assert_eq!( + Int64::MAX.checked_div_euclid(Int64::from(0u32)), + Err(DivisionError::DivideByZero) + ); + assert_eq!(Int64::from(6u32).checked_div_euclid(Int64::from(2u32)), Ok(Int64::from(3u32)),); + assert_eq!(Int64::from(7u32).checked_div_euclid(Int64::from(2u32)), Ok(Int64::from(3u32)),); + assert!(matches!(Int64::MAX.checked_rem(Int64::from(0u32)), Err(DivideByZeroError { .. }))); + // checked_* with negative numbers + assert_eq!(Int64::from(-12i32).checked_div(Int64::from(10i32)), Ok(Int64::from(-1i32)),); + assert_eq!(Int64::from(-2i32).checked_pow(3u32), Ok(Int64::from(-8i32)),); + assert_eq!(Int64::from(-6i32).checked_mul(Int64::from(-7i32)), Ok(Int64::from(42i32)),); + assert_eq!(Int64::from(-2i32).checked_add(Int64::from(3i32)), Ok(Int64::from(1i32)),); + assert_eq!( + Int64::from(-1i32).checked_div_euclid(Int64::from(-2i32)), + Ok(Int64::from(1u32)), + ); + + // saturating_* + assert_eq!(Int64::MAX.saturating_add(Int64::from(1u32)), Int64::MAX); + assert_eq!(Int64::MIN.saturating_sub(Int64::from(1u32)), Int64::MIN); + assert_eq!(Int64::MAX.saturating_mul(Int64::from(2u32)), Int64::MAX); + assert_eq!(Int64::from(4u32).saturating_pow(2u32), Int64::from(16u32)); + assert_eq!(Int64::MAX.saturating_pow(2u32), Int64::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn int64_implements_rem() { + let a = Int64::from(10u32); + assert_eq!(a % Int64::from(10u32), Int64::zero()); + assert_eq!(a % Int64::from(2u32), Int64::zero()); + assert_eq!(a % Int64::from(1u32), Int64::zero()); + assert_eq!(a % Int64::from(3u32), Int64::from(1u32)); + assert_eq!(a % Int64::from(4u32), Int64::from(2u32)); + + assert_eq!(Int64::from(-12i32) % Int64::from(10i32), Int64::from(-2i32)); + assert_eq!(Int64::from(12i32) % Int64::from(-10i32), Int64::from(2i32)); + assert_eq!(Int64::from(-12i32) % Int64::from(-10i32), Int64::from(-2i32)); + + // works for refs + let a = Int64::from(10u32); + let b = Int64::from(3u32); + let expected = Int64::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn int64_rem_panics_for_zero() { + let _ = Int64::from(10u32) % Int64::zero(); + } + + #[test] + fn int64_rem_assign_works() { + let mut a = Int64::from(30u32); + a %= Int64::from(4u32); + assert_eq!(a, Int64::from(2u32)); + + // works for refs + let mut a = Int64::from(25u32); + let b = Int64::from(6u32); + a %= &b; + assert_eq!(a, Int64::from(1u32)); + } + + #[test] + fn int64_shr() { + let x: Int64 = 0x4000_0000_0000_0000i64.into(); + assert_eq!(x >> 0, x); // right shift by 0 should be no-op + assert_eq!(x >> 1, Int64::from(0x2000_0000_0000_0000i64)); + assert_eq!(x >> 4, Int64::from(0x0400_0000_0000_0000i64)); + // right shift of MIN value by the maximum shift value should result in -1 (filled with 1s) + assert_eq!(Int64::MIN >> (core::mem::size_of::() as u32 * 8 - 1), -Int64::one()); + } + + #[test] + fn int64_shl() { + let x: Int64 = 0x0800_0000_0000_0000i64.into(); + assert_eq!(x << 0, x); // left shift by 0 should be no-op + assert_eq!(x << 1, Int64::from(0x1000_0000_0000_0000i64)); + assert_eq!(x << 4, Int64::from(0x0800_0000_0000_0000i64 << 4)); + // left shift by by the maximum shift value should result in MIN + assert_eq!(Int64::one() << (core::mem::size_of::() as u32 * 8 - 1), Int64::MIN); + } + + #[test] + fn int64_abs_diff_works() { + let a = Int64::from(42u32); + let b = Int64::from(5u32); + let expected = Uint64::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let c = Int64::from(-5i32); + assert_eq!(b.abs_diff(c), Uint64::from(10u32)); + assert_eq!(c.abs_diff(b), Uint64::from(10u32)); + } + + #[test] + fn int64_abs_works() { + let a = Int64::from(42i32); + assert_eq!(a.abs(), a); + + let b = Int64::from(-42i32); + assert_eq!(b.abs(), a); + + assert_eq!(Int64::zero().abs(), Int64::zero()); + assert_eq!((Int64::MIN + Int64::one()).abs(), Int64::MAX); + } + + #[test] + fn int64_unsigned_abs_works() { + assert_eq!(Int64::zero().unsigned_abs(), Uint64::zero()); + assert_eq!(Int64::one().unsigned_abs(), Uint64::one()); + assert_eq!(Int64::MIN.unsigned_abs(), Uint64::new(Int64::MAX.0 as u64) + Uint64::one()); + + let v = Int64::from(-42i32); + assert_eq!(v.unsigned_abs(), v.abs_diff(Int64::zero())); + } + + #[test] + #[should_panic = "attempt to calculate absolute value with overflow"] + fn int64_abs_min_panics() { + _ = Int64::MIN.abs(); + } + + #[test] + #[should_panic = "attempt to negate with overflow"] + fn int64_neg_min_panics() { + _ = -Int64::MIN; + } + + #[test] + fn int64_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (i64, i64, bool)| { + (Int64::from(lhs), Int64::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } +} diff --git a/cosmwasm/std/src/math/isqrt.rs b/cosmwasm/std/src/math/isqrt.rs new file mode 100644 index 00000000..ac39b865 --- /dev/null +++ b/cosmwasm/std/src/math/isqrt.rs @@ -0,0 +1,130 @@ +use core::{cmp, ops}; + +use crate::{Uint128, Uint256, Uint512, Uint64}; + +/// A trait for calculating the +/// [integer square root](https://en.wikipedia.org/wiki/Integer_square_root). +pub trait Isqrt { + /// The [integer square root](https://en.wikipedia.org/wiki/Integer_square_root). + #[must_use = "this returns the result of the operation, without modifying the original"] + fn isqrt(self) -> Self; +} + +impl Isqrt for I +where + I: Unsigned + + ops::Add + + ops::Div + + ops::Shl + + ops::Shr + + cmp::PartialOrd + + Copy, +{ + /// Algorithm adapted from + /// [Wikipedia](https://en.wikipedia.org/wiki/Integer_square_root#Example_implementation_in_C). + fn isqrt(self) -> Self { + // sqrt(0) = 0, sqrt(1) = 1 + if self <= Self::ONE { + return self; + } + + let mut x0 = Self::ONE << ((self.log_2() / 2) + 1); + + if x0 > Self::ZERO { + let mut x1 = (x0 + self / x0) >> 1; + + while x1 < x0 { + x0 = x1; + x1 = (x0 + self / x0) >> 1; + } + + return x0; + } + self + } +} + +/// Marker trait for types that represent unsigned integers. +pub trait Unsigned { + const ZERO: Self; + const ONE: Self; + + fn log_2(self) -> u32; +} + +macro_rules! impl_unsigned { + ($type:ty, $zero:expr, $one:expr) => { + impl Unsigned for $type { + const ZERO: Self = $zero; + const ONE: Self = $one; + + fn log_2(self) -> u32 { + self.ilog2() + } + } + }; +} +impl_unsigned!(u8, 0, 1); +impl_unsigned!(u16, 0, 1); +impl_unsigned!(u32, 0, 1); +impl_unsigned!(u64, 0, 1); +impl_unsigned!(u128, 0, 1); +impl_unsigned!(usize, 0, 1); +impl_unsigned!(Uint64, Self::zero(), Self::one()); +impl_unsigned!(Uint128, Self::zero(), Self::one()); +impl_unsigned!(Uint256, Self::zero(), Self::one()); +impl_unsigned!(Uint512, Self::zero(), Self::one()); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn isqrt_primitives() { + // Let's check correctness. + assert_eq!(super::Isqrt::isqrt(0u8), 0); + assert_eq!(super::Isqrt::isqrt(1u8), 1); + assert_eq!(super::Isqrt::isqrt(24u8), 4); + assert_eq!(super::Isqrt::isqrt(25u8), 5); + assert_eq!(super::Isqrt::isqrt(26u8), 5); + assert_eq!(super::Isqrt::isqrt(36u8), 6); + + // Let's also check different types. + assert_eq!(super::Isqrt::isqrt(26u8), 5); + assert_eq!(super::Isqrt::isqrt(26u16), 5); + assert_eq!(super::Isqrt::isqrt(26u32), 5); + assert_eq!(super::Isqrt::isqrt(26u64), 5); + assert_eq!(super::Isqrt::isqrt(26u128), 5); + } + + #[test] + fn isqrt_uint64() { + assert_eq!(Uint64::new(24).isqrt(), Uint64::new(4)); + } + + #[test] + fn isqrt_uint128() { + assert_eq!(Uint128::new(24).isqrt(), Uint128::new(4)); + } + + #[test] + fn isqrt_uint256() { + assert_eq!(Uint256::from(24u32).isqrt(), Uint256::from(4u32)); + assert_eq!( + (Uint256::from(u128::MAX) * Uint256::from(u128::MAX)).isqrt(), + Uint256::try_from("340282366920938463463374607431768211455").unwrap() + ); + } + + #[test] + fn isqrt_uint512() { + assert_eq!(Uint512::from(24u32).isqrt(), Uint512::from(4u32)); + assert_eq!( + (Uint512::from(Uint256::MAX) * Uint512::from(Uint256::MAX)).isqrt(), + Uint512::try_from( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ) + .unwrap() + ); + } +} diff --git a/cosmwasm/std/src/math/mod.rs b/cosmwasm/std/src/math/mod.rs new file mode 100644 index 00000000..af518ff8 --- /dev/null +++ b/cosmwasm/std/src/math/mod.rs @@ -0,0 +1,111 @@ +mod conversion; +mod decimal; +mod decimal256; +mod fraction; +mod int128; +mod int256; +mod int512; +mod int64; +mod isqrt; +mod num_consts; +mod signed_decimal; +mod signed_decimal_256; +mod uint128; +mod uint256; +mod uint512; +mod uint64; + +pub use decimal::{Decimal, DecimalRangeExceeded}; +pub use decimal256::{Decimal256, Decimal256RangeExceeded}; +pub use fraction::Fraction; +pub use int128::Int128; +pub use int256::Int256; +pub use int512::Int512; +pub use int64::Int64; +pub use isqrt::Isqrt; +pub use signed_decimal::{SignedDecimal, SignedDecimalRangeExceeded}; +pub use signed_decimal_256::{SignedDecimal256, SignedDecimal256RangeExceeded}; +pub use uint128::Uint128; +pub use uint256::Uint256; +pub use uint512::Uint512; +pub use uint64::Uint64; + +#[cfg(test)] +mod tests { + use super::*; + use core::ops::*; + + /// A trait that ensures other traits are implemented for our number types + trait AllImpl<'a>: + Add + + Add<&'a Self> + + AddAssign + + AddAssign<&'a Self> + + Sub + + Sub<&'a Self> + + SubAssign + + SubAssign<&'a Self> + + Mul + + Mul<&'a Self> + + MulAssign + + MulAssign<&'a Self> + + Div + + Div<&'a Self> + + DivAssign + + DivAssign<&'a Self> + + Rem + + Rem<&'a Self> + + RemAssign + + RemAssign<&'a Self> + + Sized + + Copy + where + Self: 'a, + { + } + + /// A trait that ensures other traits are implemented for our integer types + trait IntImpl<'a>: + AllImpl<'a> + + Shl + + Shl<&'a u32> + + ShlAssign + + ShlAssign<&'a u32> + + Shr + + Shr<&'a u32> + + ShrAssign + + ShrAssign<&'a u32> + + Not + + super::num_consts::NumConsts + { + } + + #[allow(dead_code)] // This is used to statically ensure all the integers have a shared set of traits + trait SignedImpl<'a>: IntImpl<'a> + Neg {} + + impl AllImpl<'_> for Uint64 {} + impl AllImpl<'_> for Uint128 {} + impl AllImpl<'_> for Uint256 {} + impl AllImpl<'_> for Uint512 {} + impl AllImpl<'_> for Int64 {} + impl AllImpl<'_> for Int128 {} + impl AllImpl<'_> for Int256 {} + impl AllImpl<'_> for Int512 {} + + impl IntImpl<'_> for Int64 {} + impl IntImpl<'_> for Int128 {} + impl IntImpl<'_> for Int256 {} + impl IntImpl<'_> for Int512 {} + impl IntImpl<'_> for Uint64 {} + impl IntImpl<'_> for Uint128 {} + impl IntImpl<'_> for Uint256 {} + impl IntImpl<'_> for Uint512 {} + + impl AllImpl<'_> for Decimal {} + impl AllImpl<'_> for Decimal256 {} + + impl SignedImpl<'_> for Int64 {} + impl SignedImpl<'_> for Int128 {} + impl SignedImpl<'_> for Int256 {} + impl SignedImpl<'_> for Int512 {} +} diff --git a/cosmwasm/std/src/math/num_consts.rs b/cosmwasm/std/src/math/num_consts.rs new file mode 100644 index 00000000..b3663e8b --- /dev/null +++ b/cosmwasm/std/src/math/num_consts.rs @@ -0,0 +1,7 @@ +/// Crate internal trait for all our signed and unsigned number types +pub(crate) trait NumConsts { + const MAX: Self; + const MIN: Self; + const ZERO: Self; + const ONE: Self; +} diff --git a/cosmwasm/std/src/math/signed_decimal.rs b/cosmwasm/std/src/math/signed_decimal.rs new file mode 100644 index 00000000..fb448d43 --- /dev/null +++ b/cosmwasm/std/src/math/signed_decimal.rs @@ -0,0 +1,2534 @@ +use alloc::string::{String, ToString}; +use core::{ + cmp::Ordering, + fmt::{self, Write}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign}, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{ + CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, RoundDownOverflowError, RoundUpOverflowError, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Decimal, Decimal256, Int256, SignedDecimal256, + __internal::forward_ref_partial_eq, +}; + +use super::{Fraction, Int128}; + +/// A signed fixed-point decimal value with 18 fractional digits, i.e. +/// SignedDecimal(1_000_000_000_000_000_000) == 1.0 +/// +/// The greatest possible value that can be represented is 170141183460469231731.687303715884105727 +/// (which is (2^127 - 1) / 10^18) and the smallest is -170141183460469231731.687303715884105728 +/// (which is -2^127 / 10^18). +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct SignedDecimal(#[schemars(with = "String")] Int128); + +forward_ref_partial_eq!(SignedDecimal, SignedDecimal); + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("SignedDecimal range exceeded")] +pub struct SignedDecimalRangeExceeded; + +impl SignedDecimal { + const DECIMAL_FRACTIONAL: Int128 = Int128::new(1_000_000_000_000_000_000i128); // 1*10**18 + const DECIMAL_FRACTIONAL_SQUARED: Int128 = + Int128::new(1_000_000_000_000_000_000_000_000_000_000_000_000i128); // (1*10**18)**2 = 1*10**36 + + /// The number of decimal places. Since decimal types are fixed-point rather than + /// floating-point, this is a constant. + pub const DECIMAL_PLACES: u32 = 18; // This needs to be an even number. + + /// The largest value that can be represented by this signed decimal type. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// assert_eq!(SignedDecimal::MAX.to_string(), "170141183460469231731.687303715884105727"); + /// ``` + pub const MAX: Self = Self(Int128::MAX); + + /// The smallest value that can be represented by this signed decimal type. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// assert_eq!(SignedDecimal::MIN.to_string(), "-170141183460469231731.687303715884105728"); + /// ``` + pub const MIN: Self = Self(Int128::MIN); + + /// Creates a SignedDecimal(value) + /// This is equivalent to `SignedDecimal::from_atomics(value, 18)` but usable in a const + /// context. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal, Int128}; + /// assert_eq!(SignedDecimal::new(Int128::one()).to_string(), "0.000000000000000001"); + /// ``` + pub const fn new(value: Int128) -> Self { + Self(value) + } + + /// Creates a SignedDecimal(Int128(value)) + /// This is equivalent to `SignedDecimal::from_atomics(value, 18)` but usable in a const + /// context. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// assert_eq!(SignedDecimal::raw(1234i128).to_string(), "0.000000000000001234"); + /// ``` + pub const fn raw(value: i128) -> Self { + Self(Int128::new(value)) + } + + /// Create a 1.0 SignedDecimal + #[inline] + pub const fn one() -> Self { + Self(Self::DECIMAL_FRACTIONAL) + } + + /// Create a -1.0 SignedDecimal + #[inline] + pub const fn negative_one() -> Self { + Self(Int128::new(-Self::DECIMAL_FRACTIONAL.i128())) + } + + /// Create a 0.0 SignedDecimal + #[inline] + pub const fn zero() -> Self { + Self(Int128::zero()) + } + + /// Convert x% into SignedDecimal + pub fn percent(x: i64) -> Self { + Self(((x as i128) * 10_000_000_000_000_000).into()) + } + + /// Convert permille (x/1000) into SignedDecimal + pub fn permille(x: i64) -> Self { + Self(((x as i128) * 1_000_000_000_000_000).into()) + } + + /// Convert basis points (x/10000) into SignedDecimal + pub fn bps(x: i64) -> Self { + Self(((x as i128) * 100_000_000_000_000).into()) + } + + /// Creates a signed decimal from a number of atomic units and the number + /// of decimal places. The inputs will be converted internally to form + /// a signed decimal with 18 decimal places. So the input 123 and 2 will create + /// the decimal 1.23. + /// + /// Using 18 decimal places is slightly more efficient than other values + /// as no internal conversion is necessary. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal, Int128}; + /// let a = SignedDecimal::from_atomics(Int128::new(1234), 3).unwrap(); + /// assert_eq!(a.to_string(), "1.234"); + /// + /// let a = SignedDecimal::from_atomics(1234i128, 0).unwrap(); + /// assert_eq!(a.to_string(), "1234"); + /// + /// let a = SignedDecimal::from_atomics(1i64, 18).unwrap(); + /// assert_eq!(a.to_string(), "0.000000000000000001"); + /// + /// let a = SignedDecimal::from_atomics(-1i64, 18).unwrap(); + /// assert_eq!(a.to_string(), "-0.000000000000000001"); + /// ``` + pub fn from_atomics( + atomics: impl Into, + decimal_places: u32, + ) -> Result { + let atomics = atomics.into(); + const TEN: Int128 = Int128::new(10); + Ok(match decimal_places.cmp(&(Self::DECIMAL_PLACES)) { + Ordering::Less => { + let digits = (Self::DECIMAL_PLACES) - decimal_places; // No overflow because decimal_places < DECIMAL_PLACES + let factor = TEN.checked_pow(digits).unwrap(); // Safe because digits <= 17 + Self(atomics.checked_mul(factor).map_err(|_| SignedDecimalRangeExceeded)?) + }, + Ordering::Equal => Self(atomics), + Ordering::Greater => { + let digits = decimal_places - (Self::DECIMAL_PLACES); // No overflow because decimal_places > DECIMAL_PLACES + if let Ok(factor) = TEN.checked_pow(digits) { + Self(atomics.checked_div(factor).unwrap()) // Safe because factor cannot be zero + } else { + // In this case `factor` exceeds the Int128 range. + // Any Int128 `x` divided by `factor` with `factor > Int128::MAX` is 0. + // Try e.g. Python3: `(2**128-1) // 2**128` + Self(Int128::zero()) + } + }, + }) + } + + /// Returns the ratio (numerator / denominator) as a SignedDecimal + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// assert_eq!( + /// SignedDecimal::from_ratio(1, 3).to_string(), + /// "0.333333333333333333" + /// ); + /// ``` + pub fn from_ratio(numerator: impl Into, denominator: impl Into) -> Self { + match SignedDecimal::checked_from_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns the ratio (numerator / denominator) as a SignedDecimal + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal, CheckedFromRatioError}; + /// assert_eq!( + /// SignedDecimal::checked_from_ratio(1, 3).unwrap().to_string(), + /// "0.333333333333333333" + /// ); + /// assert_eq!( + /// SignedDecimal::checked_from_ratio(1, 0), + /// Err(CheckedFromRatioError::DivideByZero) + /// ); + /// ``` + pub fn checked_from_ratio( + numerator: impl Into, + denominator: impl Into, + ) -> Result { + let numerator: Int128 = numerator.into(); + let denominator: Int128 = denominator.into(); + match numerator.checked_multiply_ratio(Self::DECIMAL_FRACTIONAL, denominator) { + Ok(ratio) => { + // numerator * DECIMAL_FRACTIONAL / denominator + Ok(SignedDecimal(ratio)) + }, + Err(CheckedMultiplyRatioError::Overflow) => Err(CheckedFromRatioError::Overflow), + Err(CheckedMultiplyRatioError::DivideByZero) => + Err(CheckedFromRatioError::DivideByZero), + } + } + + /// Returns `true` if the number is 0 + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// Returns `true` if the number is negative (< 0) + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.i128() < 0 + } + + /// A decimal is an integer of atomic units plus a number that specifies the + /// position of the decimal dot. So any decimal can be expressed as two numbers. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal, Int128}; + /// # use core::str::FromStr; + /// // Value with whole and fractional part + /// let a = SignedDecimal::from_str("1.234").unwrap(); + /// assert_eq!(a.decimal_places(), 18); + /// assert_eq!(a.atomics(), Int128::new(1234000000000000000)); + /// + /// // Smallest possible value + /// let b = SignedDecimal::from_str("0.000000000000000001").unwrap(); + /// assert_eq!(b.decimal_places(), 18); + /// assert_eq!(b.atomics(), Int128::new(1)); + /// ``` + #[must_use] + #[inline] + pub const fn atomics(&self) -> Int128 { + self.0 + } + + /// The number of decimal places. This is a constant value for now + /// but this could potentially change as the type evolves. + /// + /// See also [`SignedDecimal::atomics()`]. + #[must_use] + #[inline] + pub const fn decimal_places(&self) -> u32 { + Self::DECIMAL_PLACES + } + + /// Rounds value by truncating the decimal places. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// # use core::str::FromStr; + /// assert!(SignedDecimal::from_str("0.6").unwrap().trunc().is_zero()); + /// assert_eq!(SignedDecimal::from_str("-5.8").unwrap().trunc().to_string(), "-5"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn trunc(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value down after decimal places. Panics on overflow. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// # use core::str::FromStr; + /// assert!(SignedDecimal::from_str("0.6").unwrap().floor().is_zero()); + /// assert_eq!(SignedDecimal::from_str("-5.2").unwrap().floor().to_string(), "-6"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn floor(&self) -> Self { + match self.checked_floor() { + Ok(value) => value, + Err(_) => panic!("attempt to floor with overflow"), + } + } + + /// Rounds value down after decimal places. + pub fn checked_floor(&self) -> Result { + if self.is_negative() { + let truncated = self.trunc(); + + if truncated != self { + truncated.checked_sub(SignedDecimal::one()).map_err(|_| RoundDownOverflowError) + } else { + Ok(truncated) + } + } else { + Ok(self.trunc()) + } + } + + /// Rounds value up after decimal places. Panics on overflow. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal; + /// # use core::str::FromStr; + /// assert_eq!(SignedDecimal::from_str("0.2").unwrap().ceil(), SignedDecimal::one()); + /// assert_eq!(SignedDecimal::from_str("-5.8").unwrap().ceil().to_string(), "-5"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if floor == self { + Ok(floor) + } else { + floor.checked_add(SignedDecimal::one()).map_err(|_| RoundUpOverflowError) + } + } + + /// Computes `self + other`, returning an `OverflowError` if an overflow occurred. + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Add)) + } + + /// Computes `self - other`, returning an `OverflowError` if an overflow occurred. + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Sub)) + } + + /// Multiplies one `SignedDecimal` by another, returning an `OverflowError` if an overflow + /// occurred. + pub fn checked_mul(self, other: Self) -> Result { + let result_as_int256 = + self.numerator().full_mul(other.numerator()) / Int256::from(Self::DECIMAL_FRACTIONAL); + result_as_int256 + .try_into() + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Mul)) + } + + /// Raises a value to the power of `exp`, panics if an overflow occurred. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => panic!("Multiplication overflow"), + } + } + + /// Raises a value to the power of `exp`, returning an `OverflowError` if an overflow occurred. + pub fn checked_pow(self, exp: u32) -> Result { + // This uses the exponentiation by squaring algorithm: + // https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Basic_method + + fn inner(mut x: SignedDecimal, mut n: u32) -> Result { + if n == 0 { + return Ok(SignedDecimal::one()); + } + + let mut y = SignedDecimal::one(); + + while n > 1 { + if n % 2 == 0 { + x = x.checked_mul(x)?; + n /= 2; + } else { + y = x.checked_mul(y)?; + x = x.checked_mul(x)?; + n = (n - 1) / 2; + } + } + + Ok(x * y) + } + + inner(self, exp).map_err(|_| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + SignedDecimal::checked_from_ratio(self.numerator(), other.numerator()) + } + + /// Computes `self % other`, returning an `DivideByZeroError` if `other == 0`. + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).map_err(|_| DivideByZeroError) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Decimal { + Decimal::new(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + match self.checked_mul(other) { + Ok(value) => value, + Err(_) => { + // both negative or both positive results in positive number, otherwise negative + if self.is_negative() == other.is_negative() { + Self::MAX + } else { + Self::MIN + } + }, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => { + // odd exponent of negative number results in negative number + // everything else results in positive number + if self.is_negative() && exp % 2 == 1 { + Self::MIN + } else { + Self::MAX + } + }, + } + } + + /// Converts this decimal to a signed integer by rounding down + /// to the next integer, e.g. 22.5 becomes 22 and -1.2 becomes -2. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal, Int128}; + /// + /// let d = SignedDecimal::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_floor(), Int128::new(12)); + /// + /// let d = SignedDecimal::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_floor(), Int128::new(-13)); + /// + /// let d = SignedDecimal::from_str("-0.05").unwrap(); + /// assert_eq!(d.to_int_floor(), Int128::new(-1)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_floor(self) -> Int128 { + if self.is_negative() { + // Using `x.to_int_floor() = -(-x).to_int_ceil()` for a negative `x`, + // but avoiding overflow by implementing the formula from `to_int_ceil` directly. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + // making sure not to negate `x`, as this would overflow + -Int128::one() - ((-Int128::one() - x) / y) + } else { + self.to_int_trunc() + } + } + + /// Converts this decimal to a signed integer by truncating + /// the fractional part, e.g. 22.5 becomes 22. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal, Int128}; + /// + /// let d = SignedDecimal::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int128::new(12)); + /// + /// let d = SignedDecimal::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int128::new(-12)); + /// + /// let d = SignedDecimal::from_str("75.0").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int128::new(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_trunc(self) -> Int128 { + self.0 / Self::DECIMAL_FRACTIONAL + } + + /// Converts this decimal to a signed integer by rounding up + /// to the next integer, e.g. 22.3 becomes 23 and -1.2 becomes -1. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal, Int128}; + /// + /// let d = SignedDecimal::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int128::new(13)); + /// + /// let d = SignedDecimal::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int128::new(-12)); + /// + /// let d = SignedDecimal::from_str("75.0").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int128::new(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_ceil(self) -> Int128 { + if self.is_negative() { + self.to_int_trunc() + } else { + // Using `q = 1 + ((x - 1) / y); // if x != 0` with unsigned integers x, y, q + // from https://stackoverflow.com/a/2745086/2013738. We know `x + y` CAN overflow. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + if x.is_zero() { + Int128::zero() + } else { + Int128::one() + ((x - Int128::one()) / y) + } + } + } +} + +impl Fraction for SignedDecimal { + #[inline] + fn numerator(&self) -> Int128 { + self.0 + } + + #[inline] + fn denominator(&self) -> Int128 { + Self::DECIMAL_FRACTIONAL + } + + /// Returns the multiplicative inverse `1/d` for decimal `d`. + /// + /// If `d` is zero, none is returned. + fn inv(&self) -> Option { + if self.is_zero() { + None + } else { + // Let self be p/q with p = self.0 and q = DECIMAL_FRACTIONAL. + // Now we calculate the inverse a/b = q/p such that b = DECIMAL_FRACTIONAL. Then + // `a = DECIMAL_FRACTIONAL*DECIMAL_FRACTIONAL / self.0`. + Some(SignedDecimal(Self::DECIMAL_FRACTIONAL_SQUARED / self.0)) + } + } +} + +impl Neg for SignedDecimal { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +impl TryFrom for SignedDecimal { + type Error = SignedDecimalRangeExceeded; + + fn try_from(value: SignedDecimal256) -> Result { + value + .atomics() + .try_into() + .map(SignedDecimal) + .map_err(|_| SignedDecimalRangeExceeded) + } +} + +impl TryFrom for SignedDecimal { + type Error = SignedDecimalRangeExceeded; + + fn try_from(value: Decimal) -> Result { + value + .atomics() + .try_into() + .map(SignedDecimal) + .map_err(|_| SignedDecimalRangeExceeded) + } +} + +impl TryFrom for SignedDecimal { + type Error = SignedDecimalRangeExceeded; + + fn try_from(value: Decimal256) -> Result { + value + .atomics() + .try_into() + .map(SignedDecimal) + .map_err(|_| SignedDecimalRangeExceeded) + } +} + +impl TryFrom for SignedDecimal { + type Error = SignedDecimalRangeExceeded; + + #[inline] + fn try_from(value: Int128) -> Result { + Self::from_atomics(value, 0) + } +} + +impl FromStr for SignedDecimal { + type Err = StdError; + + /// Converts the decimal string to a SignedDecimal + /// Possible inputs: "1.23", "1", "000012", "1.123000000", "-1.12300" + /// Disallowed: "", ".23" + /// + /// This never performs any kind of rounding. + /// More than DECIMAL_PLACES fractional digits, even zeros, result in an error. + fn from_str(input: &str) -> Result { + let mut parts_iter = input.split('.'); + + let whole_part = parts_iter.next().unwrap(); // split always returns at least one element + let is_neg = whole_part.starts_with('-'); + + let whole = whole_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing whole"))?; + let mut atomics = whole + .checked_mul(Self::DECIMAL_FRACTIONAL) + .map_err(|_| StdError::generic_err("Value too big"))?; + + if let Some(fractional_part) = parts_iter.next() { + let fractional = fractional_part + .parse::() // u64 is enough for 18 decimal places + .map_err(|_| StdError::generic_err("Error parsing fractional"))?; + let exp = (Self::DECIMAL_PLACES.checked_sub(fractional_part.len() as u32)).ok_or_else( + || { + StdError::generic_err(format!( + "Cannot parse more than {} fractional digits", + Self::DECIMAL_PLACES + )) + }, + )?; + debug_assert!(exp <= Self::DECIMAL_PLACES); + let fractional_factor = Int128::from(10i128.pow(exp)); + + // This multiplication can't overflow because + // fractional < 10^DECIMAL_PLACES && fractional_factor <= 10^DECIMAL_PLACES + let fractional_part = Int128::from(fractional).checked_mul(fractional_factor).unwrap(); + + // for negative numbers, we need to subtract the fractional part + atomics = if is_neg { + atomics.checked_sub(fractional_part) + } else { + atomics.checked_add(fractional_part) + } + .map_err(|_| StdError::generic_err("Value too big"))?; + } + + if parts_iter.next().is_some() { + return Err(StdError::generic_err("Unexpected number of dots")); + } + + Ok(SignedDecimal(atomics)) + } +} + +impl fmt::Display for SignedDecimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let whole = (self.0) / Self::DECIMAL_FRACTIONAL; + let fractional = (self.0).checked_rem(Self::DECIMAL_FRACTIONAL).unwrap(); + + if fractional.is_zero() { + write!(f, "{whole}") + } else { + let fractional_string = format!( + "{:0>padding$}", + fractional.abs(), // fractional should always be printed as positive + padding = Self::DECIMAL_PLACES as usize + ); + if self.is_negative() { + f.write_char('-')?; + } + write!( + f, + "{whole}.{fractional}", + whole = whole.abs(), + fractional = fractional_string.trim_end_matches('0') + ) + } + } +} + +impl fmt::Debug for SignedDecimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SignedDecimal({self})") + } +} + +impl Add for SignedDecimal { + type Output = Self; + + fn add(self, other: Self) -> Self { + SignedDecimal(self.0 + other.0) + } +} +forward_ref_binop!(impl Add, add for SignedDecimal, SignedDecimal); + +impl AddAssign for SignedDecimal { + fn add_assign(&mut self, rhs: SignedDecimal) { + *self = *self + rhs; + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for SignedDecimal, SignedDecimal); + +impl Sub for SignedDecimal { + type Output = Self; + + fn sub(self, other: Self) -> Self { + SignedDecimal(self.0 - other.0) + } +} +forward_ref_binop!(impl Sub, sub for SignedDecimal, SignedDecimal); + +impl SubAssign for SignedDecimal { + fn sub_assign(&mut self, rhs: SignedDecimal) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for SignedDecimal, SignedDecimal); + +impl Mul for SignedDecimal { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn mul(self, other: Self) -> Self { + // SignedDecimals are fractions. We can multiply two decimals a and b + // via + // (a.numerator() * b.numerator()) / (a.denominator() * b.denominator()) + // = (a.numerator() * b.numerator()) / a.denominator() / b.denominator() + + let result_as_int256 = + self.numerator().full_mul(other.numerator()) / Int256::from(Self::DECIMAL_FRACTIONAL); + match result_as_int256.try_into() { + Ok(result) => Self(result), + Err(_) => panic!("attempt to multiply with overflow"), + } + } +} +forward_ref_binop!(impl Mul, mul for SignedDecimal, SignedDecimal); + +impl MulAssign for SignedDecimal { + fn mul_assign(&mut self, rhs: SignedDecimal) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for SignedDecimal, SignedDecimal); + +impl Div for SignedDecimal { + type Output = Self; + + fn div(self, other: Self) -> Self { + match SignedDecimal::checked_from_ratio(self.numerator(), other.numerator()) { + Ok(ratio) => ratio, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Division failed - denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => { + panic!("Division failed - multiplication overflow") + }, + } + } +} +forward_ref_binop!(impl Div, div for SignedDecimal, SignedDecimal); + +impl DivAssign for SignedDecimal { + fn div_assign(&mut self, rhs: SignedDecimal) { + *self = *self / rhs; + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for SignedDecimal, SignedDecimal); + +impl Div for SignedDecimal { + type Output = Self; + + fn div(self, rhs: Int128) -> Self::Output { + SignedDecimal(self.0 / rhs) + } +} + +impl DivAssign for SignedDecimal { + fn div_assign(&mut self, rhs: Int128) { + self.0 /= rhs; + } +} + +impl Rem for SignedDecimal { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for SignedDecimal, SignedDecimal); + +impl RemAssign for SignedDecimal { + fn rem_assign(&mut self, rhs: SignedDecimal) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for SignedDecimal, SignedDecimal); + +impl core::iter::Sum for SignedDecimal +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +/// Serializes as a decimal string +impl Serialize for SignedDecimal { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Deserializes as a base64 string +impl<'de> Deserialize<'de> for SignedDecimal { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(SignedDecimalVisitor) + } +} + +struct SignedDecimalVisitor; + +impl<'de> de::Visitor<'de> for SignedDecimalVisitor { + type Value = SignedDecimal; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded decimal") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match SignedDecimal::from_str(v) { + Ok(d) => Ok(d), + Err(e) => Err(E::custom(format_args!("Error parsing decimal '{v}': {e}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::vec::Vec; + + fn dec(input: &str) -> SignedDecimal { + SignedDecimal::from_str(input).unwrap() + } + + #[test] + fn signed_decimal_new() { + let expected = Int128::from(300i128); + assert_eq!(SignedDecimal::new(expected).0, expected); + + let expected = Int128::from(-300i128); + assert_eq!(SignedDecimal::new(expected).0, expected); + } + + #[test] + fn signed_decimal_raw() { + let value = 300i128; + assert_eq!(SignedDecimal::raw(value).0.i128(), value); + + let value = -300i128; + assert_eq!(SignedDecimal::raw(value).0.i128(), value); + } + + #[test] + fn signed_decimal_one() { + let value = SignedDecimal::one(); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL); + } + + #[test] + fn signed_decimal_zero() { + let value = SignedDecimal::zero(); + assert!(value.0.is_zero()); + } + + #[test] + fn signed_decimal_percent() { + let value = SignedDecimal::percent(50); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(2u8)); + + let value = SignedDecimal::percent(-50); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(-2i8)); + } + + #[test] + fn signed_decimal_permille() { + let value = SignedDecimal::permille(125); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(8u8)); + + let value = SignedDecimal::permille(-125); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(-8i8)); + } + + #[test] + fn signed_decimal_bps() { + let value = SignedDecimal::bps(125); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(80u8)); + + let value = SignedDecimal::bps(-125); + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(-80i8)); + } + + #[test] + fn try_from_integer() { + let int = Int128::new(0xDEADBEEF); + let decimal = SignedDecimal::try_from(int).unwrap(); + assert_eq!(int.to_string(), decimal.to_string()); + } + + #[test] + fn signed_decimal_from_atomics_works() { + let one = SignedDecimal::one(); + let two = one + one; + let neg_one = SignedDecimal::negative_one(); + + assert_eq!(SignedDecimal::from_atomics(1i128, 0).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(10i128, 1).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(100i128, 2).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(1000i128, 3).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(1000000000000000000i128, 18).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(10000000000000000000i128, 19).unwrap(), one); + assert_eq!(SignedDecimal::from_atomics(100000000000000000000i128, 20).unwrap(), one); + + assert_eq!(SignedDecimal::from_atomics(2i128, 0).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(20i128, 1).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(200i128, 2).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(2000i128, 3).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(2000000000000000000i128, 18).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(20000000000000000000i128, 19).unwrap(), two); + assert_eq!(SignedDecimal::from_atomics(200000000000000000000i128, 20).unwrap(), two); + + assert_eq!(SignedDecimal::from_atomics(-1i128, 0).unwrap(), neg_one); + assert_eq!(SignedDecimal::from_atomics(-10i128, 1).unwrap(), neg_one); + assert_eq!(SignedDecimal::from_atomics(-100000000000000000000i128, 20).unwrap(), neg_one); + + // Cuts decimal digits (20 provided but only 18 can be stored) + assert_eq!( + SignedDecimal::from_atomics(4321i128, 20).unwrap(), + SignedDecimal::from_str("0.000000000000000043").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(-4321i128, 20).unwrap(), + SignedDecimal::from_str("-0.000000000000000043").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(6789i128, 20).unwrap(), + SignedDecimal::from_str("0.000000000000000067").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 38).unwrap(), + SignedDecimal::from_str("1.701411834604692317").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 39).unwrap(), + SignedDecimal::from_str("0.170141183460469231").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 45).unwrap(), + SignedDecimal::from_str("0.000000170141183460").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 51).unwrap(), + SignedDecimal::from_str("0.000000000000170141").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 56).unwrap(), + SignedDecimal::from_str("0.000000000000000001").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, 57).unwrap(), + SignedDecimal::from_str("0.000000000000000000").unwrap() + ); + assert_eq!( + SignedDecimal::from_atomics(i128::MAX, u32::MAX).unwrap(), + SignedDecimal::from_str("0.000000000000000000").unwrap() + ); + + // Can be used with max value + let max = SignedDecimal::MAX; + assert_eq!(SignedDecimal::from_atomics(max.atomics(), max.decimal_places()).unwrap(), max); + + // Can be used with min value + let min = SignedDecimal::MIN; + assert_eq!(SignedDecimal::from_atomics(min.atomics(), min.decimal_places()).unwrap(), min); + + // Overflow is only possible with digits < 18 + let result = SignedDecimal::from_atomics(i128::MAX, 17); + assert_eq!(result.unwrap_err(), SignedDecimalRangeExceeded); + } + + #[test] + fn signed_decimal_from_ratio_works() { + // 1.0 + assert_eq!(SignedDecimal::from_ratio(1i128, 1i128), SignedDecimal::one()); + assert_eq!(SignedDecimal::from_ratio(53i128, 53i128), SignedDecimal::one()); + assert_eq!(SignedDecimal::from_ratio(125i128, 125i128), SignedDecimal::one()); + + // -1.0 + assert_eq!(SignedDecimal::from_ratio(-1i128, 1i128), SignedDecimal::negative_one()); + assert_eq!(SignedDecimal::from_ratio(-53i128, 53i128), SignedDecimal::negative_one()); + assert_eq!(SignedDecimal::from_ratio(125i128, -125i128), SignedDecimal::negative_one()); + + // 1.5 + assert_eq!(SignedDecimal::from_ratio(3i128, 2i128), SignedDecimal::percent(150)); + assert_eq!(SignedDecimal::from_ratio(150i128, 100i128), SignedDecimal::percent(150)); + assert_eq!(SignedDecimal::from_ratio(333i128, 222i128), SignedDecimal::percent(150)); + + // 0.125 + assert_eq!(SignedDecimal::from_ratio(1i64, 8i64), SignedDecimal::permille(125)); + assert_eq!(SignedDecimal::from_ratio(125i64, 1000i64), SignedDecimal::permille(125)); + + // -0.125 + assert_eq!(SignedDecimal::from_ratio(-1i64, 8i64), SignedDecimal::permille(-125)); + assert_eq!(SignedDecimal::from_ratio(125i64, -1000i64), SignedDecimal::permille(-125)); + + // 1/3 (result floored) + assert_eq!( + SignedDecimal::from_ratio(1i64, 3i64), + SignedDecimal(Int128::from(333_333_333_333_333_333i128)) + ); + + // 2/3 (result floored) + assert_eq!( + SignedDecimal::from_ratio(2i64, 3i64), + SignedDecimal(Int128::from(666_666_666_666_666_666i128)) + ); + + // large inputs + assert_eq!(SignedDecimal::from_ratio(0i128, i128::MAX), SignedDecimal::zero()); + assert_eq!(SignedDecimal::from_ratio(i128::MAX, i128::MAX), SignedDecimal::one()); + // 170141183460469231731 is the largest integer <= SignedDecimal::MAX + assert_eq!( + SignedDecimal::from_ratio(170141183460469231731i128, 1i128), + SignedDecimal::from_str("170141183460469231731").unwrap() + ); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn signed_decimal_from_ratio_panics_for_zero_denominator() { + SignedDecimal::from_ratio(1i128, 0i128); + } + + #[test] + #[should_panic(expected = "Multiplication overflow")] + fn signed_decimal_from_ratio_panics_for_mul_overflow() { + SignedDecimal::from_ratio(i128::MAX, 1i128); + } + + #[test] + fn signed_decimal_checked_from_ratio_does_not_panic() { + assert_eq!( + SignedDecimal::checked_from_ratio(1i128, 0i128), + Err(CheckedFromRatioError::DivideByZero) + ); + + assert_eq!( + SignedDecimal::checked_from_ratio(i128::MAX, 1i128), + Err(CheckedFromRatioError::Overflow) + ); + } + + #[test] + fn signed_decimal_implements_fraction() { + let fraction = SignedDecimal::from_str("1234.567").unwrap(); + assert_eq!(fraction.numerator(), Int128::from(1_234_567_000_000_000_000_000i128)); + assert_eq!(fraction.denominator(), Int128::from(1_000_000_000_000_000_000i128)); + + let fraction = SignedDecimal::from_str("-1234.567").unwrap(); + assert_eq!(fraction.numerator(), Int128::from(-1_234_567_000_000_000_000_000i128)); + assert_eq!(fraction.denominator(), Int128::from(1_000_000_000_000_000_000i128)); + } + + #[test] + fn signed_decimal_from_str_works() { + // Integers + assert_eq!(SignedDecimal::from_str("0").unwrap(), SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::from_str("1").unwrap(), SignedDecimal::percent(100)); + assert_eq!(SignedDecimal::from_str("5").unwrap(), SignedDecimal::percent(500)); + assert_eq!(SignedDecimal::from_str("42").unwrap(), SignedDecimal::percent(4200)); + assert_eq!(SignedDecimal::from_str("000").unwrap(), SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::from_str("001").unwrap(), SignedDecimal::percent(100)); + assert_eq!(SignedDecimal::from_str("005").unwrap(), SignedDecimal::percent(500)); + assert_eq!(SignedDecimal::from_str("0042").unwrap(), SignedDecimal::percent(4200)); + + // Positive decimals + assert_eq!(SignedDecimal::from_str("1.0").unwrap(), SignedDecimal::percent(100)); + assert_eq!(SignedDecimal::from_str("1.5").unwrap(), SignedDecimal::percent(150)); + assert_eq!(SignedDecimal::from_str("0.5").unwrap(), SignedDecimal::percent(50)); + assert_eq!(SignedDecimal::from_str("0.123").unwrap(), SignedDecimal::permille(123)); + + assert_eq!(SignedDecimal::from_str("40.00").unwrap(), SignedDecimal::percent(4000)); + assert_eq!(SignedDecimal::from_str("04.00").unwrap(), SignedDecimal::percent(400)); + assert_eq!(SignedDecimal::from_str("00.40").unwrap(), SignedDecimal::percent(40)); + assert_eq!(SignedDecimal::from_str("00.04").unwrap(), SignedDecimal::percent(4)); + // Negative decimals + assert_eq!(SignedDecimal::from_str("-00.04").unwrap(), SignedDecimal::percent(-4)); + assert_eq!(SignedDecimal::from_str("-00.40").unwrap(), SignedDecimal::percent(-40)); + assert_eq!(SignedDecimal::from_str("-04.00").unwrap(), SignedDecimal::percent(-400)); + + // Can handle DECIMAL_PLACES fractional digits + assert_eq!( + SignedDecimal::from_str("7.123456789012345678").unwrap(), + SignedDecimal(Int128::from(7123456789012345678i128)) + ); + assert_eq!( + SignedDecimal::from_str("7.999999999999999999").unwrap(), + SignedDecimal(Int128::from(7999999999999999999i128)) + ); + + // Works for documented max value + assert_eq!( + SignedDecimal::from_str("170141183460469231731.687303715884105727").unwrap(), + SignedDecimal::MAX + ); + // Works for documented min value + assert_eq!( + SignedDecimal::from_str("-170141183460469231731.687303715884105728").unwrap(), + SignedDecimal::MIN + ); + assert_eq!(SignedDecimal::from_str("-1").unwrap(), SignedDecimal::negative_one()); + } + + #[test] + fn signed_decimal_from_str_errors_for_broken_whole_part() { + let expected_err = StdError::generic_err("Error parsing whole"); + assert_eq!(SignedDecimal::from_str("").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str(" ").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("-").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_from_str_errors_for_broken_fractional_part() { + let expected_err = StdError::generic_err("Error parsing fractional"); + assert_eq!(SignedDecimal::from_str("1.").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("1. ").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("1.e").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("1.2e3").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("1.-2").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_from_str_errors_for_more_than_18_fractional_digits() { + let expected_err = StdError::generic_err("Cannot parse more than 18 fractional digits"); + assert_eq!(SignedDecimal::from_str("7.1234567890123456789").unwrap_err(), expected_err); + // No special rules for trailing zeros. This could be changed but adds gas cost for the + // happy path. + assert_eq!(SignedDecimal::from_str("7.1230000000000000000").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_from_str_errors_for_invalid_number_of_dots() { + let expected_err = StdError::generic_err("Unexpected number of dots"); + assert_eq!(SignedDecimal::from_str("1.2.3").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("1.2.3.4").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_from_str_errors_for_more_than_max_value() { + let expected_err = StdError::generic_err("Value too big"); + // Integer + assert_eq!(SignedDecimal::from_str("170141183460469231732").unwrap_err(), expected_err); + assert_eq!(SignedDecimal::from_str("-170141183460469231732").unwrap_err(), expected_err); + + // SignedDecimal + assert_eq!(SignedDecimal::from_str("170141183460469231732.0").unwrap_err(), expected_err); + assert_eq!( + SignedDecimal::from_str("170141183460469231731.687303715884105728").unwrap_err(), + expected_err + ); + assert_eq!( + SignedDecimal::from_str("-170141183460469231731.687303715884105729").unwrap_err(), + expected_err + ); + } + + #[test] + fn signed_decimal_conversions_work() { + // signed decimal to signed decimal + assert_eq!( + SignedDecimal::try_from(SignedDecimal256::MAX).unwrap_err(), + SignedDecimalRangeExceeded + ); + assert_eq!( + SignedDecimal::try_from(SignedDecimal256::MIN).unwrap_err(), + SignedDecimalRangeExceeded + ); + assert_eq!( + SignedDecimal::try_from(SignedDecimal256::zero()).unwrap(), + SignedDecimal::zero() + ); + assert_eq!(SignedDecimal::try_from(SignedDecimal256::one()).unwrap(), SignedDecimal::one()); + assert_eq!( + SignedDecimal::try_from(SignedDecimal256::percent(50)).unwrap(), + SignedDecimal::percent(50) + ); + assert_eq!( + SignedDecimal::try_from(SignedDecimal256::percent(-200)).unwrap(), + SignedDecimal::percent(-200) + ); + + // unsigned to signed decimal + assert_eq!(SignedDecimal::try_from(Decimal::MAX).unwrap_err(), SignedDecimalRangeExceeded); + let max = Decimal::raw(SignedDecimal::MAX.atomics().i128() as u128); + let too_big = max + Decimal::raw(1); + assert_eq!(SignedDecimal::try_from(too_big).unwrap_err(), SignedDecimalRangeExceeded); + assert_eq!(SignedDecimal::try_from(Decimal::zero()).unwrap(), SignedDecimal::zero()); + assert_eq!(SignedDecimal::try_from(max).unwrap(), SignedDecimal::MAX); + } + + #[test] + fn signed_decimal_atomics_works() { + let zero = SignedDecimal::zero(); + let one = SignedDecimal::one(); + let half = SignedDecimal::percent(50); + let two = SignedDecimal::percent(200); + let max = SignedDecimal::MAX; + let neg_half = SignedDecimal::percent(-50); + let neg_two = SignedDecimal::percent(-200); + let min = SignedDecimal::MIN; + + assert_eq!(zero.atomics(), Int128::new(0)); + assert_eq!(one.atomics(), Int128::new(1000000000000000000)); + assert_eq!(half.atomics(), Int128::new(500000000000000000)); + assert_eq!(two.atomics(), Int128::new(2000000000000000000)); + assert_eq!(max.atomics(), Int128::MAX); + assert_eq!(neg_half.atomics(), Int128::new(-500000000000000000)); + assert_eq!(neg_two.atomics(), Int128::new(-2000000000000000000)); + assert_eq!(min.atomics(), Int128::MIN); + } + + #[test] + fn signed_decimal_decimal_places_works() { + let zero = SignedDecimal::zero(); + let one = SignedDecimal::one(); + let half = SignedDecimal::percent(50); + let two = SignedDecimal::percent(200); + let max = SignedDecimal::MAX; + let neg_one = SignedDecimal::negative_one(); + + assert_eq!(zero.decimal_places(), 18); + assert_eq!(one.decimal_places(), 18); + assert_eq!(half.decimal_places(), 18); + assert_eq!(two.decimal_places(), 18); + assert_eq!(max.decimal_places(), 18); + assert_eq!(neg_one.decimal_places(), 18); + } + + #[test] + fn signed_decimal_is_zero_works() { + assert!(SignedDecimal::zero().is_zero()); + assert!(SignedDecimal::percent(0).is_zero()); + assert!(SignedDecimal::permille(0).is_zero()); + + assert!(!SignedDecimal::one().is_zero()); + assert!(!SignedDecimal::percent(123).is_zero()); + assert!(!SignedDecimal::permille(-1234).is_zero()); + } + + #[test] + fn signed_decimal_inv_works() { + // d = 0 + assert_eq!(SignedDecimal::zero().inv(), None); + + // d == 1 + assert_eq!(SignedDecimal::one().inv(), Some(SignedDecimal::one())); + + // d == -1 + assert_eq!(SignedDecimal::negative_one().inv(), Some(SignedDecimal::negative_one())); + + // d > 1 exact + assert_eq!( + SignedDecimal::from_str("2").unwrap().inv(), + Some(SignedDecimal::from_str("0.5").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("20").unwrap().inv(), + Some(SignedDecimal::from_str("0.05").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("200").unwrap().inv(), + Some(SignedDecimal::from_str("0.005").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("2000").unwrap().inv(), + Some(SignedDecimal::from_str("0.0005").unwrap()) + ); + + // d > 1 rounded + assert_eq!( + SignedDecimal::from_str("3").unwrap().inv(), + Some(SignedDecimal::from_str("0.333333333333333333").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("6").unwrap().inv(), + Some(SignedDecimal::from_str("0.166666666666666666").unwrap()) + ); + + // d < 1 exact + assert_eq!( + SignedDecimal::from_str("0.5").unwrap().inv(), + Some(SignedDecimal::from_str("2").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("0.05").unwrap().inv(), + Some(SignedDecimal::from_str("20").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("0.005").unwrap().inv(), + Some(SignedDecimal::from_str("200").unwrap()) + ); + assert_eq!( + SignedDecimal::from_str("0.0005").unwrap().inv(), + Some(SignedDecimal::from_str("2000").unwrap()) + ); + + // d < 0 + assert_eq!( + SignedDecimal::from_str("-0.5").unwrap().inv(), + Some(SignedDecimal::from_str("-2").unwrap()) + ); + // d < 0 rounded + assert_eq!( + SignedDecimal::from_str("-3").unwrap().inv(), + Some(SignedDecimal::from_str("-0.333333333333333333").unwrap()) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_add_works() { + let value = SignedDecimal::one() + SignedDecimal::percent(50); // 1.5 + assert_eq!( + value.0, + SignedDecimal::DECIMAL_FRACTIONAL * Int128::from(3u8) / Int128::from(2u8) + ); + + assert_eq!( + SignedDecimal::percent(5) + SignedDecimal::percent(4), + SignedDecimal::percent(9) + ); + assert_eq!(SignedDecimal::percent(5) + SignedDecimal::zero(), SignedDecimal::percent(5)); + assert_eq!(SignedDecimal::zero() + SignedDecimal::zero(), SignedDecimal::zero()); + // negative numbers + assert_eq!( + SignedDecimal::percent(-5) + SignedDecimal::percent(-4), + SignedDecimal::percent(-9) + ); + assert_eq!( + SignedDecimal::percent(-5) + SignedDecimal::percent(4), + SignedDecimal::percent(-1) + ); + assert_eq!( + SignedDecimal::percent(5) + SignedDecimal::percent(-4), + SignedDecimal::percent(1) + ); + + // works for refs + let a = SignedDecimal::percent(15); + let b = SignedDecimal::percent(25); + let expected = SignedDecimal::percent(40); + assert_eq!(a + b, expected); + assert_eq!(&a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic] + fn signed_decimal_add_overflow_panics() { + let _value = SignedDecimal::MAX + SignedDecimal::percent(50); + } + + #[test] + fn signed_decimal_add_assign_works() { + let mut a = SignedDecimal::percent(30); + a += SignedDecimal::percent(20); + assert_eq!(a, SignedDecimal::percent(50)); + + // works for refs + let mut a = SignedDecimal::percent(15); + let b = SignedDecimal::percent(3); + let expected = SignedDecimal::percent(18); + a += &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_sub_works() { + let value = SignedDecimal::one() - SignedDecimal::percent(50); // 0.5 + assert_eq!(value.0, SignedDecimal::DECIMAL_FRACTIONAL / Int128::from(2u8)); + + assert_eq!( + SignedDecimal::percent(9) - SignedDecimal::percent(4), + SignedDecimal::percent(5) + ); + assert_eq!(SignedDecimal::percent(16) - SignedDecimal::zero(), SignedDecimal::percent(16)); + assert_eq!(SignedDecimal::percent(16) - SignedDecimal::percent(16), SignedDecimal::zero()); + assert_eq!(SignedDecimal::zero() - SignedDecimal::zero(), SignedDecimal::zero()); + + // negative numbers + assert_eq!( + SignedDecimal::percent(-5) - SignedDecimal::percent(-4), + SignedDecimal::percent(-1) + ); + assert_eq!( + SignedDecimal::percent(-5) - SignedDecimal::percent(4), + SignedDecimal::percent(-9) + ); + assert_eq!( + SignedDecimal::percent(500) - SignedDecimal::percent(-4), + SignedDecimal::percent(504) + ); + + // works for refs + let a = SignedDecimal::percent(13); + let b = SignedDecimal::percent(6); + let expected = SignedDecimal::percent(7); + assert_eq!(a - b, expected); + assert_eq!(&a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn signed_decimal_sub_overflow_panics() { + let _value = SignedDecimal::MIN - SignedDecimal::percent(50); + } + + #[test] + fn signed_decimal_sub_assign_works() { + let mut a = SignedDecimal::percent(20); + a -= SignedDecimal::percent(2); + assert_eq!(a, SignedDecimal::percent(18)); + + // works for refs + let mut a = SignedDecimal::percent(33); + let b = SignedDecimal::percent(13); + let expected = SignedDecimal::percent(20); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_implements_mul() { + let one = SignedDecimal::one(); + let two = one + one; + let half = SignedDecimal::percent(50); + + // 1*x and x*1 + assert_eq!(one * SignedDecimal::percent(0), SignedDecimal::percent(0)); + assert_eq!(one * SignedDecimal::percent(1), SignedDecimal::percent(1)); + assert_eq!(one * SignedDecimal::percent(10), SignedDecimal::percent(10)); + assert_eq!(one * SignedDecimal::percent(100), SignedDecimal::percent(100)); + assert_eq!(one * SignedDecimal::percent(1000), SignedDecimal::percent(1000)); + assert_eq!(one * SignedDecimal::MAX, SignedDecimal::MAX); + assert_eq!(SignedDecimal::percent(0) * one, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) * one, SignedDecimal::percent(1)); + assert_eq!(SignedDecimal::percent(10) * one, SignedDecimal::percent(10)); + assert_eq!(SignedDecimal::percent(100) * one, SignedDecimal::percent(100)); + assert_eq!(SignedDecimal::percent(1000) * one, SignedDecimal::percent(1000)); + assert_eq!(SignedDecimal::MAX * one, SignedDecimal::MAX); + assert_eq!(SignedDecimal::percent(-1) * one, SignedDecimal::percent(-1)); + assert_eq!(one * SignedDecimal::percent(-10), SignedDecimal::percent(-10)); + + // double + assert_eq!(two * SignedDecimal::percent(0), SignedDecimal::percent(0)); + assert_eq!(two * SignedDecimal::percent(1), SignedDecimal::percent(2)); + assert_eq!(two * SignedDecimal::percent(10), SignedDecimal::percent(20)); + assert_eq!(two * SignedDecimal::percent(100), SignedDecimal::percent(200)); + assert_eq!(two * SignedDecimal::percent(1000), SignedDecimal::percent(2000)); + assert_eq!(SignedDecimal::percent(0) * two, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) * two, SignedDecimal::percent(2)); + assert_eq!(SignedDecimal::percent(10) * two, SignedDecimal::percent(20)); + assert_eq!(SignedDecimal::percent(100) * two, SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(1000) * two, SignedDecimal::percent(2000)); + assert_eq!(SignedDecimal::percent(-1) * two, SignedDecimal::percent(-2)); + assert_eq!(two * SignedDecimal::new(Int128::MIN / Int128::new(2)), SignedDecimal::MIN); + + // half + assert_eq!(half * SignedDecimal::percent(0), SignedDecimal::percent(0)); + assert_eq!(half * SignedDecimal::percent(1), SignedDecimal::permille(5)); + assert_eq!(half * SignedDecimal::percent(10), SignedDecimal::percent(5)); + assert_eq!(half * SignedDecimal::percent(100), SignedDecimal::percent(50)); + assert_eq!(half * SignedDecimal::percent(1000), SignedDecimal::percent(500)); + assert_eq!(SignedDecimal::percent(0) * half, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) * half, SignedDecimal::permille(5)); + assert_eq!(SignedDecimal::percent(10) * half, SignedDecimal::percent(5)); + assert_eq!(SignedDecimal::percent(100) * half, SignedDecimal::percent(50)); + assert_eq!(SignedDecimal::percent(1000) * half, SignedDecimal::percent(500)); + + // Move left + let a = dec("123.127726548762582"); + assert_eq!(a * dec("1"), dec("123.127726548762582")); + assert_eq!(a * dec("10"), dec("1231.27726548762582")); + assert_eq!(a * dec("100"), dec("12312.7726548762582")); + assert_eq!(a * dec("1000"), dec("123127.726548762582")); + assert_eq!(a * dec("1000000"), dec("123127726.548762582")); + assert_eq!(a * dec("1000000000"), dec("123127726548.762582")); + assert_eq!(a * dec("1000000000000"), dec("123127726548762.582")); + assert_eq!(a * dec("1000000000000000"), dec("123127726548762582")); + assert_eq!(a * dec("1000000000000000000"), dec("123127726548762582000")); + assert_eq!(dec("1") * a, dec("123.127726548762582")); + assert_eq!(dec("10") * a, dec("1231.27726548762582")); + assert_eq!(dec("100") * a, dec("12312.7726548762582")); + assert_eq!(dec("1000") * a, dec("123127.726548762582")); + assert_eq!(dec("1000000") * a, dec("123127726.548762582")); + assert_eq!(dec("1000000000") * a, dec("123127726548.762582")); + assert_eq!(dec("1000000000000") * a, dec("123127726548762.582")); + assert_eq!(dec("1000000000000000") * a, dec("123127726548762582")); + assert_eq!(dec("1000000000000000000") * a, dec("123127726548762582000")); + assert_eq!(dec("-1000000000000000000") * a, dec("-123127726548762582000")); + + // Move right + let max = SignedDecimal::MAX; + assert_eq!(max * dec("1.0"), dec("170141183460469231731.687303715884105727")); + assert_eq!(max * dec("0.1"), dec("17014118346046923173.168730371588410572")); + assert_eq!(max * dec("0.01"), dec("1701411834604692317.316873037158841057")); + assert_eq!(max * dec("0.001"), dec("170141183460469231.731687303715884105")); + assert_eq!(max * dec("0.000001"), dec("170141183460469.231731687303715884")); + assert_eq!(max * dec("0.000000001"), dec("170141183460.469231731687303715")); + assert_eq!(max * dec("0.000000000001"), dec("170141183.460469231731687303")); + assert_eq!(max * dec("0.000000000000001"), dec("170141.183460469231731687")); + assert_eq!(max * dec("0.000000000000000001"), dec("170.141183460469231731")); + + // works for refs + let a = SignedDecimal::percent(20); + let b = SignedDecimal::percent(30); + let expected = SignedDecimal::percent(6); + assert_eq!(a * b, expected); + assert_eq!(&a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn signed_decimal_mul_assign_works() { + let mut a = SignedDecimal::percent(15); + a *= SignedDecimal::percent(60); + assert_eq!(a, SignedDecimal::percent(9)); + + // works for refs + let mut a = SignedDecimal::percent(50); + let b = SignedDecimal::percent(20); + a *= &b; + assert_eq!(a, SignedDecimal::percent(10)); + } + + #[test] + #[should_panic(expected = "attempt to multiply with overflow")] + fn signed_decimal_mul_overflow_panics() { + let _value = SignedDecimal::MAX * SignedDecimal::percent(101); + } + + #[test] + fn signed_decimal_checked_mul() { + let test_data = [ + (SignedDecimal::zero(), SignedDecimal::zero()), + (SignedDecimal::zero(), SignedDecimal::one()), + (SignedDecimal::one(), SignedDecimal::zero()), + (SignedDecimal::percent(10), SignedDecimal::zero()), + (SignedDecimal::percent(10), SignedDecimal::percent(5)), + (SignedDecimal::MAX, SignedDecimal::one()), + (SignedDecimal::MAX / Int128::new(2), SignedDecimal::percent(200)), + (SignedDecimal::permille(6), SignedDecimal::permille(13)), + (SignedDecimal::permille(-6), SignedDecimal::permille(0)), + (SignedDecimal::MAX, SignedDecimal::negative_one()), + ]; + + // The regular core::ops::Mul is our source of truth for these tests. + for (x, y) in test_data.into_iter() { + assert_eq!(x * y, x.checked_mul(y).unwrap()); + } + } + + #[test] + fn signed_decimal_checked_mul_overflow() { + assert_eq!( + SignedDecimal::MAX.checked_mul(SignedDecimal::percent(200)), + Err(OverflowError::new(OverflowOperation::Mul)) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_implements_div() { + let one = SignedDecimal::one(); + let two = one + one; + let half = SignedDecimal::percent(50); + + // 1/x and x/1 + assert_eq!(one / SignedDecimal::percent(1), SignedDecimal::percent(10_000)); + assert_eq!(one / SignedDecimal::percent(10), SignedDecimal::percent(1_000)); + assert_eq!(one / SignedDecimal::percent(100), SignedDecimal::percent(100)); + assert_eq!(one / SignedDecimal::percent(1000), SignedDecimal::percent(10)); + assert_eq!(SignedDecimal::percent(0) / one, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) / one, SignedDecimal::percent(1)); + assert_eq!(SignedDecimal::percent(10) / one, SignedDecimal::percent(10)); + assert_eq!(SignedDecimal::percent(100) / one, SignedDecimal::percent(100)); + assert_eq!(SignedDecimal::percent(1000) / one, SignedDecimal::percent(1000)); + assert_eq!(one / SignedDecimal::percent(-1), SignedDecimal::percent(-10_000)); + assert_eq!(one / SignedDecimal::percent(-10), SignedDecimal::percent(-1_000)); + + // double + assert_eq!(two / SignedDecimal::percent(1), SignedDecimal::percent(20_000)); + assert_eq!(two / SignedDecimal::percent(10), SignedDecimal::percent(2_000)); + assert_eq!(two / SignedDecimal::percent(100), SignedDecimal::percent(200)); + assert_eq!(two / SignedDecimal::percent(1000), SignedDecimal::percent(20)); + assert_eq!(SignedDecimal::percent(0) / two, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) / two, dec("0.005")); + assert_eq!(SignedDecimal::percent(10) / two, SignedDecimal::percent(5)); + assert_eq!(SignedDecimal::percent(100) / two, SignedDecimal::percent(50)); + assert_eq!(SignedDecimal::percent(1000) / two, SignedDecimal::percent(500)); + assert_eq!(two / SignedDecimal::percent(-1), SignedDecimal::percent(-20_000)); + assert_eq!(SignedDecimal::percent(-10000) / two, SignedDecimal::percent(-5000)); + + // half + assert_eq!(half / SignedDecimal::percent(1), SignedDecimal::percent(5_000)); + assert_eq!(half / SignedDecimal::percent(10), SignedDecimal::percent(500)); + assert_eq!(half / SignedDecimal::percent(100), SignedDecimal::percent(50)); + assert_eq!(half / SignedDecimal::percent(1000), SignedDecimal::percent(5)); + assert_eq!(SignedDecimal::percent(0) / half, SignedDecimal::percent(0)); + assert_eq!(SignedDecimal::percent(1) / half, SignedDecimal::percent(2)); + assert_eq!(SignedDecimal::percent(10) / half, SignedDecimal::percent(20)); + assert_eq!(SignedDecimal::percent(100) / half, SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(1000) / half, SignedDecimal::percent(2000)); + + // Move right + let a = dec("123127726548762582"); + assert_eq!(a / dec("1"), dec("123127726548762582")); + assert_eq!(a / dec("10"), dec("12312772654876258.2")); + assert_eq!(a / dec("100"), dec("1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("0.123127726548762582")); + assert_eq!(dec("1") / a, dec("0.000000000000000008")); + assert_eq!(dec("10") / a, dec("0.000000000000000081")); + assert_eq!(dec("100") / a, dec("0.000000000000000812")); + assert_eq!(dec("1000") / a, dec("0.000000000000008121")); + assert_eq!(dec("1000000") / a, dec("0.000000000008121647")); + assert_eq!(dec("1000000000") / a, dec("0.000000008121647560")); + assert_eq!(dec("1000000000000") / a, dec("0.000008121647560868")); + assert_eq!(dec("1000000000000000") / a, dec("0.008121647560868164")); + assert_eq!(dec("1000000000000000000") / a, dec("8.121647560868164773")); + // negative + let a = dec("-123127726548762582"); + assert_eq!(a / dec("1"), dec("-123127726548762582")); + assert_eq!(a / dec("10"), dec("-12312772654876258.2")); + assert_eq!(a / dec("100"), dec("-1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("-123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("-123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("-123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("-123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("-123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("-0.123127726548762582")); + assert_eq!(dec("1") / a, dec("-0.000000000000000008")); + + // Move left + let a = dec("0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("123127726.548762582")); + assert_eq!(a / dec("0.000000000001"), dec("123127726548.762582")); + assert_eq!(a / dec("0.000000000000001"), dec("123127726548762.582")); + assert_eq!(a / dec("0.000000000000000001"), dec("123127726548762582")); + // negative + let a = dec("-0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("-0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("-1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("-12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("-123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("-123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("-123127726.548762582")); + + assert_eq!( + SignedDecimal::percent(15) / SignedDecimal::percent(60), + SignedDecimal::percent(25) + ); + + // works for refs + let a = SignedDecimal::percent(100); + let b = SignedDecimal::percent(20); + let expected = SignedDecimal::percent(500); + assert_eq!(a / b, expected); + assert_eq!(&a / b, expected); + assert_eq!(a / &b, expected); + assert_eq!(&a / &b, expected); + } + + #[test] + fn signed_decimal_div_assign_works() { + let mut a = SignedDecimal::percent(15); + a /= SignedDecimal::percent(20); + assert_eq!(a, SignedDecimal::percent(75)); + + // works for refs + let mut a = SignedDecimal::percent(50); + let b = SignedDecimal::percent(20); + a /= &b; + assert_eq!(a, SignedDecimal::percent(250)); + } + + #[test] + #[should_panic(expected = "Division failed - multiplication overflow")] + fn signed_decimal_div_overflow_panics() { + let _value = SignedDecimal::MAX / SignedDecimal::percent(10); + } + + #[test] + #[should_panic(expected = "Division failed - denominator must not be zero")] + fn signed_decimal_div_by_zero_panics() { + let _value = SignedDecimal::one() / SignedDecimal::zero(); + } + + #[test] + fn signed_decimal_int128_division() { + // a/b + let left = SignedDecimal::percent(150); // 1.5 + let right = Int128::new(3); + assert_eq!(left / right, SignedDecimal::percent(50)); + + // negative + let left = SignedDecimal::percent(-150); // -1.5 + let right = Int128::new(3); + assert_eq!(left / right, SignedDecimal::percent(-50)); + + // 0/a + let left = SignedDecimal::zero(); + let right = Int128::new(300); + assert_eq!(left / right, SignedDecimal::zero()); + } + + #[test] + #[should_panic] + fn signed_decimal_int128_divide_by_zero() { + let left = SignedDecimal::percent(150); // 1.5 + let right = Int128::new(0); + let _result = left / right; + } + + #[test] + fn signed_decimal_int128_div_assign() { + // a/b + let mut dec = SignedDecimal::percent(150); // 1.5 + dec /= Int128::new(3); + assert_eq!(dec, SignedDecimal::percent(50)); + + // 0/a + let mut dec = SignedDecimal::zero(); + dec /= Int128::new(300); + assert_eq!(dec, SignedDecimal::zero()); + } + + #[test] + #[should_panic] + fn signed_decimal_int128_div_assign_by_zero() { + // a/0 + let mut dec = SignedDecimal::percent(50); + dec /= Int128::new(0); + } + + #[test] + fn signed_decimal_checked_pow() { + for exp in 0..10 { + assert_eq!(SignedDecimal::one().checked_pow(exp).unwrap(), SignedDecimal::one()); + } + + // This case is mathematically undefined but we ensure consistency with Rust standard types + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=20df6716048e77087acd40194b233494 + assert_eq!(SignedDecimal::zero().checked_pow(0).unwrap(), SignedDecimal::one()); + + for exp in 1..10 { + assert_eq!(SignedDecimal::zero().checked_pow(exp).unwrap(), SignedDecimal::zero()); + } + + for exp in 1..10 { + assert_eq!( + SignedDecimal::negative_one().checked_pow(exp).unwrap(), + // alternates between 1 and -1 + if exp % 2 == 0 { SignedDecimal::one() } else { SignedDecimal::negative_one() } + ) + } + + for num in + &[SignedDecimal::percent(50), SignedDecimal::percent(99), SignedDecimal::percent(200)] + { + assert_eq!(num.checked_pow(0).unwrap(), SignedDecimal::one()) + } + + assert_eq!(SignedDecimal::percent(20).checked_pow(2).unwrap(), SignedDecimal::percent(4)); + + assert_eq!(SignedDecimal::percent(20).checked_pow(3).unwrap(), SignedDecimal::permille(8)); + + assert_eq!( + SignedDecimal::percent(200).checked_pow(4).unwrap(), + SignedDecimal::percent(1600) + ); + + assert_eq!( + SignedDecimal::percent(200).checked_pow(4).unwrap(), + SignedDecimal::percent(1600) + ); + + assert_eq!( + SignedDecimal::percent(700).checked_pow(5).unwrap(), + SignedDecimal::percent(1680700) + ); + + assert_eq!( + SignedDecimal::percent(700).checked_pow(8).unwrap(), + SignedDecimal::percent(576480100) + ); + + assert_eq!( + SignedDecimal::percent(700).checked_pow(10).unwrap(), + SignedDecimal::percent(28247524900) + ); + + assert_eq!( + SignedDecimal::percent(120).checked_pow(123).unwrap(), + SignedDecimal(5486473221892422150877397607i128.into()) + ); + + assert_eq!( + SignedDecimal::percent(10).checked_pow(2).unwrap(), + SignedDecimal(10000000000000000i128.into()) + ); + + assert_eq!( + SignedDecimal::percent(10).checked_pow(18).unwrap(), + SignedDecimal(1i128.into()) + ); + + let decimals = [ + SignedDecimal::percent(-50), + SignedDecimal::percent(-99), + SignedDecimal::percent(-200), + ]; + let exponents = [1, 2, 3, 4, 5, 8, 10]; + + for d in decimals { + for e in exponents { + // use multiplication as source of truth + let mut mul = Ok(d); + for _ in 1..e { + mul = mul.and_then(|mul| mul.checked_mul(d)); + } + assert_eq!(mul, d.checked_pow(e)); + } + } + } + + #[test] + fn signed_decimal_checked_pow_overflow() { + assert_eq!( + SignedDecimal::MAX.checked_pow(2), + Err(OverflowError::new(OverflowOperation::Pow)) + ); + } + + #[test] + fn signed_decimal_to_string() { + // Integers + assert_eq!(SignedDecimal::zero().to_string(), "0"); + assert_eq!(SignedDecimal::one().to_string(), "1"); + assert_eq!(SignedDecimal::percent(500).to_string(), "5"); + assert_eq!(SignedDecimal::percent(-500).to_string(), "-5"); + + // SignedDecimals + assert_eq!(SignedDecimal::percent(125).to_string(), "1.25"); + assert_eq!(SignedDecimal::percent(42638).to_string(), "426.38"); + assert_eq!(SignedDecimal::percent(3).to_string(), "0.03"); + assert_eq!(SignedDecimal::permille(987).to_string(), "0.987"); + assert_eq!(SignedDecimal::percent(-125).to_string(), "-1.25"); + assert_eq!(SignedDecimal::percent(-42638).to_string(), "-426.38"); + assert_eq!(SignedDecimal::percent(-3).to_string(), "-0.03"); + assert_eq!(SignedDecimal::permille(-987).to_string(), "-0.987"); + + assert_eq!(SignedDecimal(Int128::from(1i128)).to_string(), "0.000000000000000001"); + assert_eq!(SignedDecimal(Int128::from(10i128)).to_string(), "0.00000000000000001"); + assert_eq!(SignedDecimal(Int128::from(100i128)).to_string(), "0.0000000000000001"); + assert_eq!(SignedDecimal(Int128::from(1000i128)).to_string(), "0.000000000000001"); + assert_eq!(SignedDecimal(Int128::from(10000i128)).to_string(), "0.00000000000001"); + assert_eq!(SignedDecimal(Int128::from(100000i128)).to_string(), "0.0000000000001"); + assert_eq!(SignedDecimal(Int128::from(1000000i128)).to_string(), "0.000000000001"); + assert_eq!(SignedDecimal(Int128::from(10000000i128)).to_string(), "0.00000000001"); + assert_eq!(SignedDecimal(Int128::from(100000000i128)).to_string(), "0.0000000001"); + assert_eq!(SignedDecimal(Int128::from(1000000000i128)).to_string(), "0.000000001"); + assert_eq!(SignedDecimal(Int128::from(10000000000i128)).to_string(), "0.00000001"); + assert_eq!(SignedDecimal(Int128::from(100000000000i128)).to_string(), "0.0000001"); + assert_eq!(SignedDecimal(Int128::from(10000000000000i128)).to_string(), "0.00001"); + assert_eq!(SignedDecimal(Int128::from(100000000000000i128)).to_string(), "0.0001"); + assert_eq!(SignedDecimal(Int128::from(1000000000000000i128)).to_string(), "0.001"); + assert_eq!(SignedDecimal(Int128::from(10000000000000000i128)).to_string(), "0.01"); + assert_eq!(SignedDecimal(Int128::from(100000000000000000i128)).to_string(), "0.1"); + assert_eq!(SignedDecimal(Int128::from(-1i128)).to_string(), "-0.000000000000000001"); + assert_eq!(SignedDecimal(Int128::from(-100000000000000i128)).to_string(), "-0.0001"); + assert_eq!(SignedDecimal(Int128::from(-100000000000000000i128)).to_string(), "-0.1"); + } + + #[test] + fn signed_decimal_iter_sum() { + let items = vec![ + SignedDecimal::zero(), + SignedDecimal(Int128::from(2i128)), + SignedDecimal(Int128::from(2i128)), + SignedDecimal(Int128::from(-2i128)), + ]; + assert_eq!(items.iter().sum::(), SignedDecimal(Int128::from(2i128))); + assert_eq!(items.into_iter().sum::(), SignedDecimal(Int128::from(2i128))); + + let empty: Vec = vec![]; + assert_eq!(SignedDecimal::zero(), empty.iter().sum::()); + } + + #[test] + fn signed_decimal_serialize() { + assert_eq!(serde_json::to_vec(&SignedDecimal::zero()).unwrap(), br#""0""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::one()).unwrap(), br#""1""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::percent(8)).unwrap(), br#""0.08""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::percent(87)).unwrap(), br#""0.87""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::percent(876)).unwrap(), br#""8.76""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::percent(8765)).unwrap(), br#""87.65""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::percent(-87654)).unwrap(), br#""-876.54""#); + assert_eq!(serde_json::to_vec(&SignedDecimal::negative_one()).unwrap(), br#""-1""#); + assert_eq!(serde_json::to_vec(&-SignedDecimal::percent(8)).unwrap(), br#""-0.08""#); + } + + #[test] + fn signed_decimal_deserialize() { + assert_eq!( + serde_json::from_slice::(br#""0""#).unwrap(), + SignedDecimal::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""1""#).unwrap(), + SignedDecimal::one() + ); + assert_eq!( + serde_json::from_slice::(br#""000""#).unwrap(), + SignedDecimal::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""001""#).unwrap(), + SignedDecimal::one() + ); + + assert_eq!( + serde_json::from_slice::(br#""0.08""#).unwrap(), + SignedDecimal::percent(8) + ); + assert_eq!( + serde_json::from_slice::(br#""0.87""#).unwrap(), + SignedDecimal::percent(87) + ); + assert_eq!( + serde_json::from_slice::(br#""8.76""#).unwrap(), + SignedDecimal::percent(876) + ); + assert_eq!( + serde_json::from_slice::(br#""87.65""#).unwrap(), + SignedDecimal::percent(8765) + ); + + // negative numbers + assert_eq!( + serde_json::from_slice::(br#""-0""#).unwrap(), + SignedDecimal::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""-1""#).unwrap(), + SignedDecimal::negative_one() + ); + assert_eq!( + serde_json::from_slice::(br#""-001""#).unwrap(), + SignedDecimal::negative_one() + ); + assert_eq!( + serde_json::from_slice::(br#""-0.08""#).unwrap(), + SignedDecimal::percent(-8) + ); + } + + #[test] + fn signed_decimal_abs_diff_works() { + let a = SignedDecimal::percent(285); + let b = SignedDecimal::percent(200); + let expected = Decimal::percent(85); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let a = SignedDecimal::percent(-200); + let b = SignedDecimal::percent(200); + let expected = Decimal::percent(400); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let a = SignedDecimal::percent(-200); + let b = SignedDecimal::percent(-240); + let expected = Decimal::percent(40); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_rem_works() { + // 4.02 % 1.11 = 0.69 + assert_eq!( + SignedDecimal::percent(402) % SignedDecimal::percent(111), + SignedDecimal::percent(69) + ); + + // 15.25 % 4 = 3.25 + assert_eq!( + SignedDecimal::percent(1525) % SignedDecimal::percent(400), + SignedDecimal::percent(325) + ); + + // -20.25 % 5 = -25 + assert_eq!( + SignedDecimal::percent(-2025) % SignedDecimal::percent(500), + SignedDecimal::percent(-25) + ); + + let a = SignedDecimal::percent(318); + let b = SignedDecimal::percent(317); + let expected = SignedDecimal::percent(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn signed_decimal_rem_assign_works() { + let mut a = SignedDecimal::percent(17673); + a %= SignedDecimal::percent(2362); + assert_eq!(a, SignedDecimal::percent(1139)); // 176.73 % 23.62 = 11.39 + + let mut a = SignedDecimal::percent(4262); + let b = SignedDecimal::percent(1270); + a %= &b; + assert_eq!(a, SignedDecimal::percent(452)); // 42.62 % 12.7 = 4.52 + + let mut a = SignedDecimal::percent(-4262); + let b = SignedDecimal::percent(1270); + a %= &b; + assert_eq!(a, SignedDecimal::percent(-452)); // -42.62 % 12.7 = -4.52 + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn signed_decimal_rem_panics_for_zero() { + let _ = SignedDecimal::percent(777) % SignedDecimal::zero(); + } + + #[test] + fn signed_decimal_checked_methods() { + // checked add + assert_eq!( + SignedDecimal::percent(402).checked_add(SignedDecimal::percent(111)).unwrap(), + SignedDecimal::percent(513) + ); + assert!(matches!( + SignedDecimal::MAX.checked_add(SignedDecimal::percent(1)), + Err(OverflowError { .. }) + )); + assert!(matches!( + SignedDecimal::MIN.checked_add(SignedDecimal::percent(-1)), + Err(OverflowError { .. }) + )); + + // checked sub + assert_eq!( + SignedDecimal::percent(1111).checked_sub(SignedDecimal::percent(111)).unwrap(), + SignedDecimal::percent(1000) + ); + assert_eq!( + SignedDecimal::zero().checked_sub(SignedDecimal::percent(1)).unwrap(), + SignedDecimal::percent(-1) + ); + assert!(matches!( + SignedDecimal::MIN.checked_sub(SignedDecimal::percent(1)), + Err(OverflowError { .. }) + )); + assert!(matches!( + SignedDecimal::MAX.checked_sub(SignedDecimal::percent(-1)), + Err(OverflowError { .. }) + )); + + // checked div + assert_eq!( + SignedDecimal::percent(30).checked_div(SignedDecimal::percent(200)).unwrap(), + SignedDecimal::percent(15) + ); + assert_eq!( + SignedDecimal::percent(88).checked_div(SignedDecimal::percent(20)).unwrap(), + SignedDecimal::percent(440) + ); + assert!(matches!( + SignedDecimal::MAX.checked_div(SignedDecimal::zero()), + Err(CheckedFromRatioError::DivideByZero {}) + )); + assert!(matches!( + SignedDecimal::MAX.checked_div(SignedDecimal::percent(1)), + Err(CheckedFromRatioError::Overflow {}) + )); + assert_eq!( + SignedDecimal::percent(-88).checked_div(SignedDecimal::percent(20)).unwrap(), + SignedDecimal::percent(-440) + ); + assert_eq!( + SignedDecimal::percent(-88).checked_div(SignedDecimal::percent(-20)).unwrap(), + SignedDecimal::percent(440) + ); + + // checked rem + assert_eq!( + SignedDecimal::percent(402).checked_rem(SignedDecimal::percent(111)).unwrap(), + SignedDecimal::percent(69) + ); + assert_eq!( + SignedDecimal::percent(1525).checked_rem(SignedDecimal::percent(400)).unwrap(), + SignedDecimal::percent(325) + ); + assert_eq!( + SignedDecimal::percent(-1525).checked_rem(SignedDecimal::percent(400)).unwrap(), + SignedDecimal::percent(-325) + ); + assert_eq!( + SignedDecimal::percent(-1525).checked_rem(SignedDecimal::percent(-400)).unwrap(), + SignedDecimal::percent(-325) + ); + assert!(matches!( + SignedDecimal::MAX.checked_rem(SignedDecimal::zero()), + Err(DivideByZeroError { .. }) + )); + } + + #[test] + fn signed_decimal_pow_works() { + assert_eq!(SignedDecimal::percent(200).pow(2), SignedDecimal::percent(400)); + assert_eq!(SignedDecimal::percent(-200).pow(2), SignedDecimal::percent(400)); + assert_eq!(SignedDecimal::percent(-200).pow(3), SignedDecimal::percent(-800)); + assert_eq!(SignedDecimal::percent(200).pow(10), SignedDecimal::percent(102400)); + } + + #[test] + #[should_panic] + fn signed_decimal_pow_overflow_panics() { + _ = SignedDecimal::MAX.pow(2u32); + } + + #[test] + fn signed_decimal_saturating_works() { + assert_eq!( + SignedDecimal::percent(200).saturating_add(SignedDecimal::percent(200)), + SignedDecimal::percent(400) + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_add(SignedDecimal::percent(200)), + SignedDecimal::zero() + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_add(SignedDecimal::percent(-200)), + SignedDecimal::percent(-400) + ); + assert_eq!( + SignedDecimal::MAX.saturating_add(SignedDecimal::percent(200)), + SignedDecimal::MAX + ); + assert_eq!( + SignedDecimal::MIN.saturating_add(SignedDecimal::percent(-1)), + SignedDecimal::MIN + ); + assert_eq!( + SignedDecimal::percent(200).saturating_sub(SignedDecimal::percent(100)), + SignedDecimal::percent(100) + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_sub(SignedDecimal::percent(100)), + SignedDecimal::percent(-300) + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_sub(SignedDecimal::percent(-100)), + SignedDecimal::percent(-100) + ); + assert_eq!( + SignedDecimal::zero().saturating_sub(SignedDecimal::percent(200)), + SignedDecimal::from_str("-2").unwrap() + ); + assert_eq!( + SignedDecimal::MIN.saturating_sub(SignedDecimal::percent(200)), + SignedDecimal::MIN + ); + assert_eq!( + SignedDecimal::MAX.saturating_sub(SignedDecimal::percent(-200)), + SignedDecimal::MAX + ); + assert_eq!( + SignedDecimal::percent(200).saturating_mul(SignedDecimal::percent(50)), + SignedDecimal::percent(100) + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_mul(SignedDecimal::percent(50)), + SignedDecimal::percent(-100) + ); + assert_eq!( + SignedDecimal::percent(-200).saturating_mul(SignedDecimal::percent(-50)), + SignedDecimal::percent(100) + ); + assert_eq!( + SignedDecimal::MAX.saturating_mul(SignedDecimal::percent(200)), + SignedDecimal::MAX + ); + assert_eq!( + SignedDecimal::MIN.saturating_mul(SignedDecimal::percent(200)), + SignedDecimal::MIN + ); + assert_eq!( + SignedDecimal::MIN.saturating_mul(SignedDecimal::percent(-200)), + SignedDecimal::MAX + ); + assert_eq!(SignedDecimal::percent(400).saturating_pow(2u32), SignedDecimal::percent(1600)); + assert_eq!(SignedDecimal::MAX.saturating_pow(2u32), SignedDecimal::MAX); + assert_eq!(SignedDecimal::MAX.saturating_pow(3u32), SignedDecimal::MAX); + assert_eq!(SignedDecimal::MIN.saturating_pow(2u32), SignedDecimal::MAX); + assert_eq!(SignedDecimal::MIN.saturating_pow(3u32), SignedDecimal::MIN); + } + + #[test] + fn signed_decimal_rounding() { + assert_eq!(SignedDecimal::one().floor(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(150).floor(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(199).floor(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(200).floor(), SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(99).floor(), SignedDecimal::zero()); + assert_eq!(SignedDecimal(Int128::from(1i128)).floor(), SignedDecimal::zero()); + assert_eq!(SignedDecimal(Int128::from(-1i128)).floor(), SignedDecimal::negative_one()); + assert_eq!(SignedDecimal::permille(-1234).floor(), SignedDecimal::percent(-200)); + + assert_eq!(SignedDecimal::one().ceil(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(150).ceil(), SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(199).ceil(), SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(99).ceil(), SignedDecimal::one()); + assert_eq!(SignedDecimal(Int128::from(1i128)).ceil(), SignedDecimal::one()); + assert_eq!(SignedDecimal(Int128::from(-1i128)).ceil(), SignedDecimal::zero()); + assert_eq!(SignedDecimal::permille(-1234).ceil(), SignedDecimal::negative_one()); + + assert_eq!(SignedDecimal::one().trunc(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(150).trunc(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(199).trunc(), SignedDecimal::one()); + assert_eq!(SignedDecimal::percent(200).trunc(), SignedDecimal::percent(200)); + assert_eq!(SignedDecimal::percent(99).trunc(), SignedDecimal::zero()); + assert_eq!(SignedDecimal(Int128::from(1i128)).trunc(), SignedDecimal::zero()); + assert_eq!(SignedDecimal(Int128::from(-1i128)).trunc(), SignedDecimal::zero()); + assert_eq!(SignedDecimal::permille(-1234).trunc(), SignedDecimal::negative_one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn signed_decimal_ceil_panics() { + let _ = SignedDecimal::MAX.ceil(); + } + + #[test] + #[should_panic(expected = "attempt to floor with overflow")] + fn signed_decimal_floor_panics() { + let _ = SignedDecimal::MIN.floor(); + } + + #[test] + fn signed_decimal_checked_ceil() { + assert_eq!(SignedDecimal::percent(199).checked_ceil(), Ok(SignedDecimal::percent(200))); + assert_eq!(SignedDecimal::MAX.checked_ceil(), Err(RoundUpOverflowError)); + } + + #[test] + fn signed_decimal_checked_floor() { + assert_eq!(SignedDecimal::percent(199).checked_floor(), Ok(SignedDecimal::one())); + assert_eq!(SignedDecimal::percent(-199).checked_floor(), Ok(SignedDecimal::percent(-200))); + assert_eq!(SignedDecimal::MIN.checked_floor(), Err(RoundDownOverflowError)); + assert_eq!( + SignedDecimal::negative_one().checked_floor(), + Ok(SignedDecimal::negative_one()) + ); + } + + #[test] + fn signed_decimal_to_int_floor_works() { + let d = SignedDecimal::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(12)); + let d = SignedDecimal::from_str("12.345").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(12)); + let d = SignedDecimal::from_str("12.999").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(12)); + let d = SignedDecimal::from_str("0.98451384").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(0)); + let d = SignedDecimal::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(-13)); + let d = SignedDecimal::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(-13)); + let d = SignedDecimal::from_str("75.0").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(75)); + let d = SignedDecimal::from_str("0.0001").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(0)); + let d = SignedDecimal::from_str("0.0").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(0)); + let d = SignedDecimal::from_str("-0.0").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(0)); + let d = SignedDecimal::from_str("-0.0001").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(-1)); + let d = SignedDecimal::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_floor(), Int128::new(-75)); + let d = SignedDecimal::MAX; + assert_eq!(d.to_int_floor(), Int128::new(170141183460469231731)); + let d = SignedDecimal::MIN; + assert_eq!(d.to_int_floor(), Int128::new(-170141183460469231732)); + } + + #[test] + fn signed_decimal_to_int_ceil_works() { + let d = SignedDecimal::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(13)); + let d = SignedDecimal::from_str("12.345").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(13)); + let d = SignedDecimal::from_str("12.999").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(13)); + let d = SignedDecimal::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(-12)); + let d = SignedDecimal::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(-12)); + + let d = SignedDecimal::from_str("75.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(75)); + let d = SignedDecimal::from_str("0.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(0)); + let d = SignedDecimal::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int128::new(-75)); + + let d = SignedDecimal::MAX; + assert_eq!(d.to_int_ceil(), Int128::new(170141183460469231732)); + let d = SignedDecimal::MIN; + assert_eq!(d.to_int_ceil(), Int128::new(-170141183460469231731)); + } + + #[test] + fn signed_decimal_to_int_trunc_works() { + let d = SignedDecimal::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(12)); + let d = SignedDecimal::from_str("12.345").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(12)); + let d = SignedDecimal::from_str("12.999").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(12)); + let d = SignedDecimal::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(-12)); + let d = SignedDecimal::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(-12)); + + let d = SignedDecimal::from_str("75.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(75)); + let d = SignedDecimal::from_str("0.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(0)); + let d = SignedDecimal::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int128::new(-75)); + + let d = SignedDecimal::MAX; + assert_eq!(d.to_int_trunc(), Int128::new(170141183460469231731)); + let d = SignedDecimal::MIN; + assert_eq!(d.to_int_trunc(), Int128::new(-170141183460469231731)); + } + + #[test] + fn signed_decimal_neg_works() { + assert_eq!(-SignedDecimal::percent(50), SignedDecimal::percent(-50)); + assert_eq!(-SignedDecimal::one(), SignedDecimal::negative_one()); + } + + #[test] + fn signed_decimal_partial_eq() { + let test_cases = [ + ("1", "1", true), + ("0.5", "0.5", true), + ("0.5", "0.51", false), + ("0", "0.00000", true), + ("-1", "-1", true), + ("-0.5", "-0.5", true), + ("-0.5", "0.5", false), + ("-0.5", "-0.51", false), + ("-0", "-0.00000", true), + ] + .into_iter() + .map(|(lhs, rhs, expected)| (dec(lhs), dec(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn signed_decimal_implements_debug() { + let decimal = SignedDecimal::from_str("123.45").unwrap(); + assert_eq!(format!("{decimal:?}"), "SignedDecimal(123.45)"); + + let test_cases = ["5", "5.01", "42", "0", "2", "-0.000001"]; + for s in test_cases { + let decimal = SignedDecimal::from_str(s).unwrap(); + let expected = format!("SignedDecimal({s})"); + assert_eq!(format!("{decimal:?}"), expected); + } + } + + #[test] + fn signed_decimal_can_be_instantiated_from_decimal256() { + let d: SignedDecimal = Decimal256::zero().try_into().unwrap(); + assert_eq!(d, SignedDecimal::zero()); + } + + #[test] + fn signed_decimal_may_fail_when_instantiated_from_decimal256() { + let err = >::try_into(Decimal256::MAX).unwrap_err(); + assert_eq!("SignedDecimalRangeExceeded", format!("{err:?}")); + assert_eq!("SignedDecimal range exceeded", format!("{err}")); + } + + #[test] + fn signed_decimal_can_be_serialized_and_deserialized() { + // properly deserialized + let value: SignedDecimal = serde_json::from_str(r#""123""#).unwrap(); + assert_eq!(SignedDecimal::from_str("123").unwrap(), value); + + // properly serialized + let value = SignedDecimal::from_str("456").unwrap(); + assert_eq!(r#""456""#, serde_json::to_string(&value).unwrap()); + + // invalid: not a string encoded decimal + assert_eq!( + "invalid type: integer `123`, expected string-encoded decimal at line 1 column 3", + serde_json::from_str::("123").err().unwrap().to_string() + ); + + // invalid: not properly defined signed decimal value + assert_eq!( + "Error parsing decimal '1.e': Generic error: Error parsing fractional at line 1 column 5", + serde_json::from_str::(r#""1.e""#) + .err() + .unwrap() + .to_string() + ); + } + + // #[test] + // fn signed_decimal_has_defined_json_schema() { + // let schema = schemars::schema_for!(SignedDecimal); + // assert_eq!("SignedDecimal", schema.schema.metadata.unwrap().title.unwrap()); + // } +} diff --git a/cosmwasm/std/src/math/signed_decimal_256.rs b/cosmwasm/std/src/math/signed_decimal_256.rs new file mode 100644 index 00000000..f655e0d1 --- /dev/null +++ b/cosmwasm/std/src/math/signed_decimal_256.rs @@ -0,0 +1,2691 @@ +use crate::prelude::*; +use core::{ + cmp::Ordering, + fmt::{self, Write}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign}, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{ + CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, RoundDownOverflowError, RoundUpOverflowError, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Decimal, Decimal256, Int512, SignedDecimal, + __internal::forward_ref_partial_eq, +}; + +use super::{Fraction, Int256}; + +/// A signed fixed-point decimal value with 18 fractional digits, +/// i.e. SignedDecimal256(1_000_000_000_000_000_000) == 1.0 +/// +/// The greatest possible value that can be represented is +/// 57896044618658097711785492504343953926634992332820282019728.792003956564819967 +/// (which is (2^255 - 1) / 10^18) +/// and the smallest is +/// -57896044618658097711785492504343953926634992332820282019728.792003956564819968 +/// (which is -2^255 / 10^18). +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct SignedDecimal256(#[schemars(with = "String")] Int256); + +forward_ref_partial_eq!(SignedDecimal256, SignedDecimal256); + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[error("SignedDecimal256 range exceeded")] +pub struct SignedDecimal256RangeExceeded; + +impl SignedDecimal256 { + const DECIMAL_FRACTIONAL: Int256 = // 1*10**18 + Int256::from_i128(1_000_000_000_000_000_000); + const DECIMAL_FRACTIONAL_SQUARED: Int256 = // 1*10**36 + Int256::from_i128(1_000_000_000_000_000_000_000_000_000_000_000_000); + + /// The number of decimal places. Since decimal types are fixed-point rather than + /// floating-point, this is a constant. + pub const DECIMAL_PLACES: u32 = 18; // This needs to be an even number. + + /// The largest value that can be represented by this signed decimal type. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// assert_eq!( + /// SignedDecimal256::MAX.to_string(), + /// "57896044618658097711785492504343953926634992332820282019728.792003956564819967" + /// ); + /// ``` + pub const MAX: Self = Self(Int256::MAX); + + /// The smallest value that can be represented by this signed decimal type. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// assert_eq!( + /// SignedDecimal256::MIN.to_string(), + /// "-57896044618658097711785492504343953926634992332820282019728.792003956564819968" + /// ); + /// ``` + pub const MIN: Self = Self(Int256::MIN); + + /// Creates a SignedDecimal256(value) + /// This is equivalent to `SignedDecimal256::from_atomics(value, 18)` but usable in a const + /// context. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal256, Int256}; + /// assert_eq!(SignedDecimal256::new(Int256::one()).to_string(), "0.000000000000000001"); + /// ``` + pub const fn new(value: Int256) -> Self { + Self(value) + } + + /// Creates a SignedDecimal256(Int256(value)) + /// This is equivalent to `SignedDecimal256::from_atomics(value, 18)` but usable in a const + /// context. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// assert_eq!(SignedDecimal256::raw(1234i128).to_string(), "0.000000000000001234"); + /// ``` + pub const fn raw(value: i128) -> Self { + Self(Int256::from_i128(value)) + } + + /// Create a 1.0 SignedDecimal256 + #[inline] + pub const fn one() -> Self { + Self(Self::DECIMAL_FRACTIONAL) + } + + /// Create a -1.0 SignedDecimal256 + #[inline] + pub const fn negative_one() -> Self { + // -DECIMAL_FRATIONAL + Self(Int256::from_i128(-1_000_000_000_000_000_000)) + } + + /// Create a 0.0 SignedDecimal256 + #[inline] + pub const fn zero() -> Self { + Self(Int256::zero()) + } + + /// Convert x% into SignedDecimal256 + pub fn percent(x: i64) -> Self { + Self(((x as i128) * 10_000_000_000_000_000).into()) + } + + /// Convert permille (x/1000) into SignedDecimal256 + pub fn permille(x: i64) -> Self { + Self(((x as i128) * 1_000_000_000_000_000).into()) + } + + /// Convert basis points (x/10000) into SignedDecimal256 + pub fn bps(x: i64) -> Self { + Self(((x as i128) * 100_000_000_000_000).into()) + } + + /// Creates a signed decimal from a number of atomic units and the number + /// of decimal places. The inputs will be converted internally to form + /// a signed decimal with 18 decimal places. So the input 123 and 2 will create + /// the decimal 1.23. + /// + /// Using 18 decimal places is slightly more efficient than other values + /// as no internal conversion is necessary. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal256, Int256}; + /// let a = SignedDecimal256::from_atomics(Int256::from(1234), 3).unwrap(); + /// assert_eq!(a.to_string(), "1.234"); + /// + /// let a = SignedDecimal256::from_atomics(1234i128, 0).unwrap(); + /// assert_eq!(a.to_string(), "1234"); + /// + /// let a = SignedDecimal256::from_atomics(1i64, 18).unwrap(); + /// assert_eq!(a.to_string(), "0.000000000000000001"); + /// + /// let a = SignedDecimal256::from_atomics(-1i64, 18).unwrap(); + /// assert_eq!(a.to_string(), "-0.000000000000000001"); + /// ``` + pub fn from_atomics( + atomics: impl Into, + decimal_places: u32, + ) -> Result { + let atomics = atomics.into(); + let ten = Int256::from(10u64); + Ok(match decimal_places.cmp(&(Self::DECIMAL_PLACES)) { + Ordering::Less => { + let digits = (Self::DECIMAL_PLACES) - decimal_places; // No overflow because decimal_places < DECIMAL_PLACES + let factor = ten.checked_pow(digits).unwrap(); // Safe because digits <= 17 + Self(atomics.checked_mul(factor).map_err(|_| SignedDecimal256RangeExceeded)?) + }, + Ordering::Equal => Self(atomics), + Ordering::Greater => { + let digits = decimal_places - (Self::DECIMAL_PLACES); // No overflow because decimal_places > DECIMAL_PLACES + if let Ok(factor) = ten.checked_pow(digits) { + Self(atomics.checked_div(factor).unwrap()) // Safe because factor cannot be zero + } else { + // In this case `factor` exceeds the Int256 range. + // Any Int256 `x` divided by `factor` with `factor > Int256::MAX` is 0. + // Try e.g. Python3: `(2**128-1) // 2**128` + Self(Int256::zero()) + } + }, + }) + } + + /// Returns the ratio (numerator / denominator) as a SignedDecimal256 + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// assert_eq!( + /// SignedDecimal256::from_ratio(1, 3).to_string(), + /// "0.333333333333333333" + /// ); + /// ``` + pub fn from_ratio(numerator: impl Into, denominator: impl Into) -> Self { + match SignedDecimal256::checked_from_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns the ratio (numerator / denominator) as a SignedDecimal256 + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal256, CheckedFromRatioError}; + /// assert_eq!( + /// SignedDecimal256::checked_from_ratio(1, 3).unwrap().to_string(), + /// "0.333333333333333333" + /// ); + /// assert_eq!( + /// SignedDecimal256::checked_from_ratio(1, 0), + /// Err(CheckedFromRatioError::DivideByZero) + /// ); + /// ``` + pub fn checked_from_ratio( + numerator: impl Into, + denominator: impl Into, + ) -> Result { + let numerator: Int256 = numerator.into(); + let denominator: Int256 = denominator.into(); + match numerator.checked_multiply_ratio(Self::DECIMAL_FRACTIONAL, denominator) { + Ok(ratio) => { + // numerator * DECIMAL_FRACTIONAL / denominator + Ok(SignedDecimal256(ratio)) + }, + Err(CheckedMultiplyRatioError::Overflow) => Err(CheckedFromRatioError::Overflow), + Err(CheckedMultiplyRatioError::DivideByZero) => + Err(CheckedFromRatioError::DivideByZero), + } + } + + /// Returns `true` if the number is 0 + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// Returns `true` if the number is negative (< 0) + #[must_use] + pub const fn is_negative(&self) -> bool { + self.0.is_negative() + } + + /// A decimal is an integer of atomic units plus a number that specifies the + /// position of the decimal dot. So any decimal can be expressed as two numbers. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{SignedDecimal256, Int256}; + /// # use core::str::FromStr; + /// // Value with whole and fractional part + /// let a = SignedDecimal256::from_str("1.234").unwrap(); + /// assert_eq!(a.decimal_places(), 18); + /// assert_eq!(a.atomics(), Int256::from(1234000000000000000i128)); + /// + /// // Smallest possible value + /// let b = SignedDecimal256::from_str("0.000000000000000001").unwrap(); + /// assert_eq!(b.decimal_places(), 18); + /// assert_eq!(b.atomics(), Int256::from(1)); + /// ``` + #[must_use] + #[inline] + pub const fn atomics(&self) -> Int256 { + self.0 + } + + /// The number of decimal places. This is a constant value for now + /// but this could potentially change as the type evolves. + /// + /// See also [`SignedDecimal256::atomics()`]. + #[must_use] + #[inline] + pub const fn decimal_places(&self) -> u32 { + Self::DECIMAL_PLACES + } + + /// Rounds value by truncating the decimal places. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// # use core::str::FromStr; + /// assert!(SignedDecimal256::from_str("0.6").unwrap().trunc().is_zero()); + /// assert_eq!(SignedDecimal256::from_str("-5.8").unwrap().trunc().to_string(), "-5"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn trunc(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value down after decimal places. Panics on overflow. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// # use core::str::FromStr; + /// assert!(SignedDecimal256::from_str("0.6").unwrap().floor().is_zero()); + /// assert_eq!(SignedDecimal256::from_str("-5.2").unwrap().floor().to_string(), "-6"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn floor(&self) -> Self { + match self.checked_floor() { + Ok(value) => value, + Err(_) => panic!("attempt to floor with overflow"), + } + } + + /// Rounds value down after decimal places. + pub fn checked_floor(&self) -> Result { + if self.is_negative() { + let truncated = self.trunc(); + + if truncated != self { + truncated + .checked_sub(SignedDecimal256::one()) + .map_err(|_| RoundDownOverflowError) + } else { + Ok(truncated) + } + } else { + Ok(self.trunc()) + } + } + + /// Rounds value up after decimal places. Panics on overflow. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::SignedDecimal256; + /// # use core::str::FromStr; + /// assert_eq!(SignedDecimal256::from_str("0.2").unwrap().ceil(), SignedDecimal256::one()); + /// assert_eq!(SignedDecimal256::from_str("-5.8").unwrap().ceil().to_string(), "-5"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if floor == self { + Ok(floor) + } else { + floor.checked_add(SignedDecimal256::one()).map_err(|_| RoundUpOverflowError) + } + } + + /// Computes `self + other`, returning an `OverflowError` if an overflow occurred. + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Add)) + } + + /// Computes `self - other`, returning an `OverflowError` if an overflow occurred. + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Sub)) + } + + /// Multiplies one `SignedDecimal256` by another, returning an `OverflowError` if an overflow + /// occurred. + pub fn checked_mul(self, other: Self) -> Result { + let result_as_int512 = + self.numerator().full_mul(other.numerator()) / Int512::from(Self::DECIMAL_FRACTIONAL); + result_as_int512 + .try_into() + .map(Self) + .map_err(|_| OverflowError::new(OverflowOperation::Mul)) + } + + /// Raises a value to the power of `exp`, panics if an overflow occurred. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => panic!("Multiplication overflow"), + } + } + + /// Raises a value to the power of `exp`, returning an `OverflowError` if an overflow occurred. + pub fn checked_pow(self, exp: u32) -> Result { + // This uses the exponentiation by squaring algorithm: + // https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Basic_method + + fn inner(mut x: SignedDecimal256, mut n: u32) -> Result { + if n == 0 { + return Ok(SignedDecimal256::one()); + } + + let mut y = SignedDecimal256::one(); + + while n > 1 { + if n % 2 == 0 { + x = x.checked_mul(x)?; + n /= 2; + } else { + y = x.checked_mul(y)?; + x = x.checked_mul(x)?; + n = (n - 1) / 2; + } + } + + Ok(x * y) + } + + inner(self, exp).map_err(|_| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + SignedDecimal256::checked_from_ratio(self.numerator(), other.numerator()) + } + + /// Computes `self % other`, returning an `DivideByZeroError` if `other == 0`. + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).map_err(|_| DivideByZeroError) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Decimal256 { + Decimal256::new(self.0.abs_diff(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + match self.checked_mul(other) { + Ok(value) => value, + Err(_) => { + // both negative or both positive results in positive number, otherwise negative + if self.is_negative() == other.is_negative() { + Self::MAX + } else { + Self::MIN + } + }, + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + match self.checked_pow(exp) { + Ok(value) => value, + Err(_) => { + // odd exponent of negative number results in negative number + // everything else results in positive number + if self.is_negative() && exp % 2 == 1 { + Self::MIN + } else { + Self::MAX + } + }, + } + } + + /// Converts this decimal to a signed integer by rounding down + /// to the next integer, e.g. 22.5 becomes 22 and -1.2 becomes -2. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal256, Int256}; + /// + /// let d = SignedDecimal256::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_floor(), Int256::from(12)); + /// + /// let d = SignedDecimal256::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_floor(), Int256::from(-13)); + /// + /// let d = SignedDecimal256::from_str("-0.05").unwrap(); + /// assert_eq!(d.to_int_floor(), Int256::from(-1)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_floor(self) -> Int256 { + if self.is_negative() { + // Using `x.to_int_floor() = -(-x).to_int_ceil()` for a negative `x`, + // but avoiding overflow by implementing the formula from `to_int_ceil` directly. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + // making sure not to negate `x`, as this would overflow + -Int256::one() - ((-Int256::one() - x) / y) + } else { + self.to_int_trunc() + } + } + + /// Converts this decimal to a signed integer by truncating + /// the fractional part, e.g. 22.5 becomes 22. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal256, Int256}; + /// + /// let d = SignedDecimal256::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int256::from(12)); + /// + /// let d = SignedDecimal256::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int256::from(-12)); + /// + /// let d = SignedDecimal256::from_str("75.0").unwrap(); + /// assert_eq!(d.to_int_trunc(), Int256::from(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_trunc(self) -> Int256 { + self.0 / Self::DECIMAL_FRACTIONAL + } + + /// Converts this decimal to a signed integer by rounding up + /// to the next integer, e.g. 22.3 becomes 23 and -1.2 becomes -1. + /// + /// ## Examples + /// + /// ``` + /// use core::str::FromStr; + /// use cosmwasm_std::{SignedDecimal256, Int256}; + /// + /// let d = SignedDecimal256::from_str("12.345").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int256::from(13)); + /// + /// let d = SignedDecimal256::from_str("-12.999").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int256::from(-12)); + /// + /// let d = SignedDecimal256::from_str("75.0").unwrap(); + /// assert_eq!(d.to_int_ceil(), Int256::from(75)); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_int_ceil(self) -> Int256 { + if self.is_negative() { + self.to_int_trunc() + } else { + // Using `q = 1 + ((x - 1) / y); // if x != 0` with unsigned integers x, y, q + // from https://stackoverflow.com/a/2745086/2013738. We know `x + y` CAN overflow. + let x = self.0; + let y = Self::DECIMAL_FRACTIONAL; + if x.is_zero() { + Int256::zero() + } else { + Int256::one() + ((x - Int256::one()) / y) + } + } + } +} + +impl Fraction for SignedDecimal256 { + #[inline] + fn numerator(&self) -> Int256 { + self.0 + } + + #[inline] + fn denominator(&self) -> Int256 { + Self::DECIMAL_FRACTIONAL + } + + /// Returns the multiplicative inverse `1/d` for decimal `d`. + /// + /// If `d` is zero, none is returned. + fn inv(&self) -> Option { + if self.is_zero() { + None + } else { + // Let self be p/q with p = self.0 and q = DECIMAL_FRACTIONAL. + // Now we calculate the inverse a/b = q/p such that b = DECIMAL_FRACTIONAL. Then + // `a = DECIMAL_FRACTIONAL*DECIMAL_FRACTIONAL / self.0`. + Some(SignedDecimal256(Self::DECIMAL_FRACTIONAL_SQUARED / self.0)) + } + } +} + +impl Neg for SignedDecimal256 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +impl From for SignedDecimal256 { + fn from(value: SignedDecimal) -> Self { + Self::new(value.atomics().into()) + } +} + +impl From for SignedDecimal256 { + fn from(value: Decimal) -> Self { + Self::new(value.atomics().into()) + } +} + +impl TryFrom for SignedDecimal256 { + type Error = SignedDecimal256RangeExceeded; + + fn try_from(value: Decimal256) -> Result { + value + .atomics() + .try_into() + .map(SignedDecimal256) + .map_err(|_| SignedDecimal256RangeExceeded) + } +} + +impl TryFrom for SignedDecimal256 { + type Error = SignedDecimal256RangeExceeded; + + #[inline] + fn try_from(value: Int256) -> Result { + Self::from_atomics(value, 0) + } +} + +impl FromStr for SignedDecimal256 { + type Err = StdError; + + /// Converts the decimal string to a SignedDecimal256 + /// Possible inputs: "1.23", "1", "000012", "1.123000000", "-1.12300" + /// Disallowed: "", ".23" + /// + /// This never performs any kind of rounding. + /// More than DECIMAL_PLACES fractional digits, even zeros, result in an error. + fn from_str(input: &str) -> Result { + let mut parts_iter = input.split('.'); + + let whole_part = parts_iter.next().unwrap(); // split always returns at least one element + let is_neg = whole_part.starts_with('-'); + + let whole = whole_part + .parse::() + .map_err(|_| StdError::generic_err("Error parsing whole"))?; + let mut atomics = whole + .checked_mul(Self::DECIMAL_FRACTIONAL) + .map_err(|_| StdError::generic_err("Value too big"))?; + + if let Some(fractional_part) = parts_iter.next() { + let fractional = fractional_part + .parse::() // u64 is enough for 18 decimal places + .map_err(|_| StdError::generic_err("Error parsing fractional"))?; + let exp = (Self::DECIMAL_PLACES.checked_sub(fractional_part.len() as u32)).ok_or_else( + || { + StdError::generic_err(format!( + "Cannot parse more than {} fractional digits", + Self::DECIMAL_PLACES + )) + }, + )?; + debug_assert!(exp <= Self::DECIMAL_PLACES); + let fractional_factor = Int256::from(10i128.pow(exp)); + + // This multiplication can't overflow because + // fractional < 10^DECIMAL_PLACES && fractional_factor <= 10^DECIMAL_PLACES + let fractional_part = Int256::from(fractional).checked_mul(fractional_factor).unwrap(); + + // for negative numbers, we need to subtract the fractional part + atomics = if is_neg { + atomics.checked_sub(fractional_part) + } else { + atomics.checked_add(fractional_part) + } + .map_err(|_| StdError::generic_err("Value too big"))?; + } + + if parts_iter.next().is_some() { + return Err(StdError::generic_err("Unexpected number of dots")); + } + + Ok(SignedDecimal256(atomics)) + } +} + +impl fmt::Display for SignedDecimal256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let whole = (self.0) / Self::DECIMAL_FRACTIONAL; + let fractional = (self.0).checked_rem(Self::DECIMAL_FRACTIONAL).unwrap(); + + if fractional.is_zero() { + write!(f, "{whole}") + } else { + let fractional_string = format!( + "{:0>padding$}", + fractional.abs(), // fractional should always be printed as positive + padding = Self::DECIMAL_PLACES as usize + ); + if self.is_negative() { + f.write_char('-')?; + } + write!( + f, + "{whole}.{fractional}", + whole = whole.abs(), + fractional = fractional_string.trim_end_matches('0') + ) + } + } +} + +impl fmt::Debug for SignedDecimal256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SignedDecimal256({self})") + } +} + +impl Add for SignedDecimal256 { + type Output = Self; + + fn add(self, other: Self) -> Self { + SignedDecimal256(self.0 + other.0) + } +} +forward_ref_binop!(impl Add, add for SignedDecimal256, SignedDecimal256); + +impl AddAssign for SignedDecimal256 { + fn add_assign(&mut self, rhs: SignedDecimal256) { + *self = *self + rhs; + } +} +forward_ref_op_assign!(impl AddAssign, add_assign for SignedDecimal256, SignedDecimal256); + +impl Sub for SignedDecimal256 { + type Output = Self; + + fn sub(self, other: Self) -> Self { + SignedDecimal256(self.0 - other.0) + } +} +forward_ref_binop!(impl Sub, sub for SignedDecimal256, SignedDecimal256); + +impl SubAssign for SignedDecimal256 { + fn sub_assign(&mut self, rhs: SignedDecimal256) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for SignedDecimal256, SignedDecimal256); + +impl Mul for SignedDecimal256 { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn mul(self, other: Self) -> Self { + // SignedDecimal256s are fractions. We can multiply two decimals a and b + // via + // (a.numerator() * b.numerator()) / (a.denominator() * b.denominator()) + // = (a.numerator() * b.numerator()) / a.denominator() / b.denominator() + + let result_as_int512 = + self.numerator().full_mul(other.numerator()) / Int512::from(Self::DECIMAL_FRACTIONAL); + match result_as_int512.try_into() { + Ok(result) => Self(result), + Err(_) => panic!("attempt to multiply with overflow"), + } + } +} +forward_ref_binop!(impl Mul, mul for SignedDecimal256, SignedDecimal256); + +impl MulAssign for SignedDecimal256 { + fn mul_assign(&mut self, rhs: SignedDecimal256) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for SignedDecimal256, SignedDecimal256); + +impl Div for SignedDecimal256 { + type Output = Self; + + fn div(self, other: Self) -> Self { + match SignedDecimal256::checked_from_ratio(self.numerator(), other.numerator()) { + Ok(ratio) => ratio, + Err(CheckedFromRatioError::DivideByZero) => { + panic!("Division failed - denominator must not be zero") + }, + Err(CheckedFromRatioError::Overflow) => { + panic!("Division failed - multiplication overflow") + }, + } + } +} +forward_ref_binop!(impl Div, div for SignedDecimal256, SignedDecimal256); + +impl DivAssign for SignedDecimal256 { + fn div_assign(&mut self, rhs: SignedDecimal256) { + *self = *self / rhs; + } +} +forward_ref_op_assign!(impl DivAssign, div_assign for SignedDecimal256, SignedDecimal256); + +impl Div for SignedDecimal256 { + type Output = Self; + + fn div(self, rhs: Int256) -> Self::Output { + SignedDecimal256(self.0 / rhs) + } +} + +impl DivAssign for SignedDecimal256 { + fn div_assign(&mut self, rhs: Int256) { + self.0 /= rhs; + } +} + +impl Rem for SignedDecimal256 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for SignedDecimal256, SignedDecimal256); + +impl RemAssign for SignedDecimal256 { + fn rem_assign(&mut self, rhs: SignedDecimal256) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for SignedDecimal256, SignedDecimal256); + +impl core::iter::Sum for SignedDecimal256 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +/// Serializes as a decimal string +impl Serialize for SignedDecimal256 { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Deserializes as a base64 string +impl<'de> Deserialize<'de> for SignedDecimal256 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(SignedDecimal256Visitor) + } +} + +struct SignedDecimal256Visitor; + +impl<'de> de::Visitor<'de> for SignedDecimal256Visitor { + type Value = SignedDecimal256; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded decimal") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match SignedDecimal256::from_str(v) { + Ok(d) => Ok(d), + Err(e) => Err(E::custom(format_args!("Error parsing decimal '{v}': {e}"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::vec::Vec; + + fn dec(input: &str) -> SignedDecimal256 { + SignedDecimal256::from_str(input).unwrap() + } + + #[test] + fn try_from_integer() { + let int = Int256::from_i128(0xDEADBEEF); + let decimal = SignedDecimal256::try_from(int).unwrap(); + assert_eq!(int.to_string(), decimal.to_string()); + } + + #[test] + fn signed_decimal_256_new() { + let expected = Int256::from(300i128); + assert_eq!(SignedDecimal256::new(expected).0, expected); + + let expected = Int256::from(-300i128); + assert_eq!(SignedDecimal256::new(expected).0, expected); + } + + #[test] + fn signed_decimal_256_raw() { + let value = 300i128; + assert_eq!(SignedDecimal256::raw(value).0, Int256::from(value)); + + let value = -300i128; + assert_eq!(SignedDecimal256::raw(value).0, Int256::from(value)); + } + + #[test] + fn signed_decimal_256_one() { + let value = SignedDecimal256::one(); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL); + } + + #[test] + fn signed_decimal_256_zero() { + let value = SignedDecimal256::zero(); + assert!(value.0.is_zero()); + } + + #[test] + fn signed_decimal_256_percent() { + let value = SignedDecimal256::percent(50); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(2u8)); + + let value = SignedDecimal256::percent(-50); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(-2i8)); + } + + #[test] + fn signed_decimal_256_permille() { + let value = SignedDecimal256::permille(125); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(8u8)); + + let value = SignedDecimal256::permille(-125); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(-8i8)); + } + + #[test] + fn signed_decimal_256_bps() { + let value = SignedDecimal256::bps(125); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(80u8)); + + let value = SignedDecimal256::bps(-125); + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(-80i8)); + } + + #[test] + fn signed_decimal_256_from_atomics_works() { + let one = SignedDecimal256::one(); + let two = one + one; + let neg_one = SignedDecimal256::negative_one(); + + assert_eq!(SignedDecimal256::from_atomics(1i128, 0).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(10i128, 1).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(100i128, 2).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(1000i128, 3).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(1000000000000000000i128, 18).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(10000000000000000000i128, 19).unwrap(), one); + assert_eq!(SignedDecimal256::from_atomics(100000000000000000000i128, 20).unwrap(), one); + + assert_eq!(SignedDecimal256::from_atomics(2i128, 0).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(20i128, 1).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(200i128, 2).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(2000i128, 3).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(2000000000000000000i128, 18).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(20000000000000000000i128, 19).unwrap(), two); + assert_eq!(SignedDecimal256::from_atomics(200000000000000000000i128, 20).unwrap(), two); + + assert_eq!(SignedDecimal256::from_atomics(-1i128, 0).unwrap(), neg_one); + assert_eq!(SignedDecimal256::from_atomics(-10i128, 1).unwrap(), neg_one); + assert_eq!( + SignedDecimal256::from_atomics(-100000000000000000000i128, 20).unwrap(), + neg_one + ); + + // Cuts decimal digits (20 provided but only 18 can be stored) + assert_eq!( + SignedDecimal256::from_atomics(4321i128, 20).unwrap(), + SignedDecimal256::from_str("0.000000000000000043").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(-4321i128, 20).unwrap(), + SignedDecimal256::from_str("-0.000000000000000043").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(6789i128, 20).unwrap(), + SignedDecimal256::from_str("0.000000000000000067").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 38).unwrap(), + SignedDecimal256::from_str("1.701411834604692317").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 39).unwrap(), + SignedDecimal256::from_str("0.170141183460469231").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 45).unwrap(), + SignedDecimal256::from_str("0.000000170141183460").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 51).unwrap(), + SignedDecimal256::from_str("0.000000000000170141").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 56).unwrap(), + SignedDecimal256::from_str("0.000000000000000001").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, 57).unwrap(), + SignedDecimal256::from_str("0.000000000000000000").unwrap() + ); + assert_eq!( + SignedDecimal256::from_atomics(i128::MAX, u32::MAX).unwrap(), + SignedDecimal256::from_str("0.000000000000000000").unwrap() + ); + + // Can be used with max value + let max = SignedDecimal256::MAX; + assert_eq!( + SignedDecimal256::from_atomics(max.atomics(), max.decimal_places()).unwrap(), + max + ); + + // Can be used with min value + let min = SignedDecimal256::MIN; + assert_eq!( + SignedDecimal256::from_atomics(min.atomics(), min.decimal_places()).unwrap(), + min + ); + + // Overflow is only possible with digits < 18 + let result = SignedDecimal256::from_atomics(Int256::MAX, 17); + assert_eq!(result.unwrap_err(), SignedDecimal256RangeExceeded); + } + + #[test] + fn signed_decimal_256_from_ratio_works() { + // 1.0 + assert_eq!(SignedDecimal256::from_ratio(1i128, 1i128), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::from_ratio(53i128, 53i128), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::from_ratio(125i128, 125i128), SignedDecimal256::one()); + + // -1.0 + assert_eq!(SignedDecimal256::from_ratio(-1i128, 1i128), SignedDecimal256::negative_one()); + assert_eq!(SignedDecimal256::from_ratio(-53i128, 53i128), SignedDecimal256::negative_one()); + assert_eq!( + SignedDecimal256::from_ratio(125i128, -125i128), + SignedDecimal256::negative_one() + ); + + // 1.5 + assert_eq!(SignedDecimal256::from_ratio(3i128, 2i128), SignedDecimal256::percent(150)); + assert_eq!(SignedDecimal256::from_ratio(150i128, 100i128), SignedDecimal256::percent(150)); + assert_eq!(SignedDecimal256::from_ratio(333i128, 222i128), SignedDecimal256::percent(150)); + + // 0.125 + assert_eq!(SignedDecimal256::from_ratio(1i64, 8i64), SignedDecimal256::permille(125)); + assert_eq!(SignedDecimal256::from_ratio(125i64, 1000i64), SignedDecimal256::permille(125)); + + // -0.125 + assert_eq!(SignedDecimal256::from_ratio(-1i64, 8i64), SignedDecimal256::permille(-125)); + assert_eq!( + SignedDecimal256::from_ratio(125i64, -1000i64), + SignedDecimal256::permille(-125) + ); + + // 1/3 (result floored) + assert_eq!( + SignedDecimal256::from_ratio(1i64, 3i64), + SignedDecimal256(Int256::from(333_333_333_333_333_333i128)) + ); + + // 2/3 (result floored) + assert_eq!( + SignedDecimal256::from_ratio(2i64, 3i64), + SignedDecimal256(Int256::from(666_666_666_666_666_666i128)) + ); + + // large inputs + assert_eq!(SignedDecimal256::from_ratio(0i128, i128::MAX), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256::from_ratio(i128::MAX, i128::MAX), SignedDecimal256::one()); + // 170141183460469231731 is the largest integer <= SignedDecimal256::MAX + assert_eq!( + SignedDecimal256::from_ratio(170141183460469231731i128, 1i128), + SignedDecimal256::from_str("170141183460469231731").unwrap() + ); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn signed_decimal_256_from_ratio_panics_for_zero_denominator() { + SignedDecimal256::from_ratio(1i128, 0i128); + } + + #[test] + #[should_panic(expected = "Multiplication overflow")] + fn signed_decimal_256_from_ratio_panics_for_mul_overflow() { + SignedDecimal256::from_ratio(Int256::MAX, 1i128); + } + + #[test] + fn signed_decimal_256_checked_from_ratio_does_not_panic() { + assert_eq!( + SignedDecimal256::checked_from_ratio(1i128, 0i128), + Err(CheckedFromRatioError::DivideByZero) + ); + + assert_eq!( + SignedDecimal256::checked_from_ratio(Int256::MAX, 1i128), + Err(CheckedFromRatioError::Overflow) + ); + } + + #[test] + fn signed_decimal_256_implements_fraction() { + let fraction = SignedDecimal256::from_str("1234.567").unwrap(); + assert_eq!(fraction.numerator(), Int256::from(1_234_567_000_000_000_000_000i128)); + assert_eq!(fraction.denominator(), Int256::from(1_000_000_000_000_000_000i128)); + + let fraction = SignedDecimal256::from_str("-1234.567").unwrap(); + assert_eq!(fraction.numerator(), Int256::from(-1_234_567_000_000_000_000_000i128)); + assert_eq!(fraction.denominator(), Int256::from(1_000_000_000_000_000_000i128)); + } + + #[test] + fn signed_decimal_256_from_str_works() { + // Integers + assert_eq!(SignedDecimal256::from_str("0").unwrap(), SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::from_str("1").unwrap(), SignedDecimal256::percent(100)); + assert_eq!(SignedDecimal256::from_str("5").unwrap(), SignedDecimal256::percent(500)); + assert_eq!(SignedDecimal256::from_str("42").unwrap(), SignedDecimal256::percent(4200)); + assert_eq!(SignedDecimal256::from_str("000").unwrap(), SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::from_str("001").unwrap(), SignedDecimal256::percent(100)); + assert_eq!(SignedDecimal256::from_str("005").unwrap(), SignedDecimal256::percent(500)); + assert_eq!(SignedDecimal256::from_str("0042").unwrap(), SignedDecimal256::percent(4200)); + + // Positive decimals + assert_eq!(SignedDecimal256::from_str("1.0").unwrap(), SignedDecimal256::percent(100)); + assert_eq!(SignedDecimal256::from_str("1.5").unwrap(), SignedDecimal256::percent(150)); + assert_eq!(SignedDecimal256::from_str("0.5").unwrap(), SignedDecimal256::percent(50)); + assert_eq!(SignedDecimal256::from_str("0.123").unwrap(), SignedDecimal256::permille(123)); + + assert_eq!(SignedDecimal256::from_str("40.00").unwrap(), SignedDecimal256::percent(4000)); + assert_eq!(SignedDecimal256::from_str("04.00").unwrap(), SignedDecimal256::percent(400)); + assert_eq!(SignedDecimal256::from_str("00.40").unwrap(), SignedDecimal256::percent(40)); + assert_eq!(SignedDecimal256::from_str("00.04").unwrap(), SignedDecimal256::percent(4)); + // Negative decimals + assert_eq!(SignedDecimal256::from_str("-00.04").unwrap(), SignedDecimal256::percent(-4)); + assert_eq!(SignedDecimal256::from_str("-00.40").unwrap(), SignedDecimal256::percent(-40)); + assert_eq!(SignedDecimal256::from_str("-04.00").unwrap(), SignedDecimal256::percent(-400)); + + // Can handle DECIMAL_PLACES fractional digits + assert_eq!( + SignedDecimal256::from_str("7.123456789012345678").unwrap(), + SignedDecimal256(Int256::from(7123456789012345678i128)) + ); + assert_eq!( + SignedDecimal256::from_str("7.999999999999999999").unwrap(), + SignedDecimal256(Int256::from(7999999999999999999i128)) + ); + + // Works for documented max value + assert_eq!( + SignedDecimal256::from_str( + "57896044618658097711785492504343953926634992332820282019728.792003956564819967" + ) + .unwrap(), + SignedDecimal256::MAX + ); + // Works for documented min value + assert_eq!( + SignedDecimal256::from_str( + "-57896044618658097711785492504343953926634992332820282019728.792003956564819968" + ) + .unwrap(), + SignedDecimal256::MIN + ); + assert_eq!(SignedDecimal256::from_str("-1").unwrap(), SignedDecimal256::negative_one()); + } + + #[test] + fn signed_decimal_256_from_str_errors_for_broken_whole_part() { + let expected_err = StdError::generic_err("Error parsing whole"); + assert_eq!(SignedDecimal256::from_str("").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str(" ").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("-").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_256_from_str_errors_for_broken_fractional_part() { + let expected_err = StdError::generic_err("Error parsing fractional"); + assert_eq!(SignedDecimal256::from_str("1.").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("1. ").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("1.e").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("1.2e3").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("1.-2").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_256_from_str_errors_for_more_than_18_fractional_digits() { + let expected_err = StdError::generic_err("Cannot parse more than 18 fractional digits"); + assert_eq!(SignedDecimal256::from_str("7.1234567890123456789").unwrap_err(), expected_err); + // No special rules for trailing zeros. This could be changed but adds gas cost for the + // happy path. + assert_eq!(SignedDecimal256::from_str("7.1230000000000000000").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_256_from_str_errors_for_invalid_number_of_dots() { + let expected_err = StdError::generic_err("Unexpected number of dots"); + assert_eq!(SignedDecimal256::from_str("1.2.3").unwrap_err(), expected_err); + assert_eq!(SignedDecimal256::from_str("1.2.3.4").unwrap_err(), expected_err); + } + + #[test] + fn signed_decimal_256_from_str_errors_for_more_than_max_value() { + let expected_err = StdError::generic_err("Value too big"); + // Integer + assert_eq!( + SignedDecimal256::from_str( + "57896044618658097711785492504343953926634992332820282019729", + ) + .unwrap_err(), + expected_err + ); + assert_eq!( + SignedDecimal256::from_str( + "-57896044618658097711785492504343953926634992332820282019729", + ) + .unwrap_err(), + expected_err + ); + + // SignedDecimal256 + assert_eq!( + SignedDecimal256::from_str( + "57896044618658097711785492504343953926634992332820282019729.0", + ) + .unwrap_err(), + expected_err + ); + assert_eq!( + SignedDecimal256::from_str( + "57896044618658097711785492504343953926634992332820282019728.792003956564819968", + ) + .unwrap_err(), + expected_err + ); + assert_eq!( + SignedDecimal256::from_str( + "-57896044618658097711785492504343953926634992332820282019728.792003956564819969", + ) + .unwrap_err(), + expected_err + ); + } + + #[test] + fn signed_decimal_256_conversions_work() { + assert_eq!(SignedDecimal256::from(SignedDecimal::zero()), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256::from(SignedDecimal::one()), SignedDecimal256::one()); + assert_eq!( + SignedDecimal256::from(SignedDecimal::percent(50)), + SignedDecimal256::percent(50) + ); + assert_eq!( + SignedDecimal256::from(SignedDecimal::MAX), + SignedDecimal256::new(Int256::from_i128(i128::MAX)) + ); + assert_eq!( + SignedDecimal256::from(SignedDecimal::percent(-50)), + SignedDecimal256::percent(-50) + ); + assert_eq!( + SignedDecimal256::from(SignedDecimal::MIN), + SignedDecimal256::new(Int256::from_i128(i128::MIN)) + ); + } + + #[test] + fn signed_decimal_256_atomics_works() { + let zero = SignedDecimal256::zero(); + let one = SignedDecimal256::one(); + let half = SignedDecimal256::percent(50); + let two = SignedDecimal256::percent(200); + let max = SignedDecimal256::MAX; + let neg_half = SignedDecimal256::percent(-50); + let neg_two = SignedDecimal256::percent(-200); + let min = SignedDecimal256::MIN; + + assert_eq!(zero.atomics(), Int256::from(0)); + assert_eq!(one.atomics(), Int256::from(1000000000000000000i128)); + assert_eq!(half.atomics(), Int256::from(500000000000000000i128)); + assert_eq!(two.atomics(), Int256::from(2000000000000000000i128)); + assert_eq!(max.atomics(), Int256::MAX); + assert_eq!(neg_half.atomics(), Int256::from(-500000000000000000i128)); + assert_eq!(neg_two.atomics(), Int256::from(-2000000000000000000i128)); + assert_eq!(min.atomics(), Int256::MIN); + } + + #[test] + fn signed_decimal_256_decimal_places_works() { + let zero = SignedDecimal256::zero(); + let one = SignedDecimal256::one(); + let half = SignedDecimal256::percent(50); + let two = SignedDecimal256::percent(200); + let max = SignedDecimal256::MAX; + let neg_one = SignedDecimal256::negative_one(); + + assert_eq!(zero.decimal_places(), 18); + assert_eq!(one.decimal_places(), 18); + assert_eq!(half.decimal_places(), 18); + assert_eq!(two.decimal_places(), 18); + assert_eq!(max.decimal_places(), 18); + assert_eq!(neg_one.decimal_places(), 18); + } + + #[test] + fn signed_decimal_256_is_zero_works() { + assert!(SignedDecimal256::zero().is_zero()); + assert!(SignedDecimal256::percent(0).is_zero()); + assert!(SignedDecimal256::permille(0).is_zero()); + + assert!(!SignedDecimal256::one().is_zero()); + assert!(!SignedDecimal256::percent(123).is_zero()); + assert!(!SignedDecimal256::permille(-1234).is_zero()); + } + + #[test] + fn signed_decimal_256_inv_works() { + // d = 0 + assert_eq!(SignedDecimal256::zero().inv(), None); + + // d == 1 + assert_eq!(SignedDecimal256::one().inv(), Some(SignedDecimal256::one())); + + // d == -1 + assert_eq!(SignedDecimal256::negative_one().inv(), Some(SignedDecimal256::negative_one())); + + // d > 1 exact + assert_eq!( + SignedDecimal256::from_str("2").unwrap().inv(), + Some(SignedDecimal256::from_str("0.5").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("20").unwrap().inv(), + Some(SignedDecimal256::from_str("0.05").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("200").unwrap().inv(), + Some(SignedDecimal256::from_str("0.005").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("2000").unwrap().inv(), + Some(SignedDecimal256::from_str("0.0005").unwrap()) + ); + + // d > 1 rounded + assert_eq!( + SignedDecimal256::from_str("3").unwrap().inv(), + Some(SignedDecimal256::from_str("0.333333333333333333").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("6").unwrap().inv(), + Some(SignedDecimal256::from_str("0.166666666666666666").unwrap()) + ); + + // d < 1 exact + assert_eq!( + SignedDecimal256::from_str("0.5").unwrap().inv(), + Some(SignedDecimal256::from_str("2").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("0.05").unwrap().inv(), + Some(SignedDecimal256::from_str("20").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("0.005").unwrap().inv(), + Some(SignedDecimal256::from_str("200").unwrap()) + ); + assert_eq!( + SignedDecimal256::from_str("0.0005").unwrap().inv(), + Some(SignedDecimal256::from_str("2000").unwrap()) + ); + + // d < 0 + assert_eq!( + SignedDecimal256::from_str("-0.5").unwrap().inv(), + Some(SignedDecimal256::from_str("-2").unwrap()) + ); + // d < 0 rounded + assert_eq!( + SignedDecimal256::from_str("-3").unwrap().inv(), + Some(SignedDecimal256::from_str("-0.333333333333333333").unwrap()) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_256_add_works() { + let value = SignedDecimal256::one() + SignedDecimal256::percent(50); // 1.5 + assert_eq!( + value.0, + SignedDecimal256::DECIMAL_FRACTIONAL * Int256::from(3u8) / Int256::from(2u8) + ); + + assert_eq!( + SignedDecimal256::percent(5) + SignedDecimal256::percent(4), + SignedDecimal256::percent(9) + ); + assert_eq!( + SignedDecimal256::percent(5) + SignedDecimal256::zero(), + SignedDecimal256::percent(5) + ); + assert_eq!(SignedDecimal256::zero() + SignedDecimal256::zero(), SignedDecimal256::zero()); + // negative numbers + assert_eq!( + SignedDecimal256::percent(-5) + SignedDecimal256::percent(-4), + SignedDecimal256::percent(-9) + ); + assert_eq!( + SignedDecimal256::percent(-5) + SignedDecimal256::percent(4), + SignedDecimal256::percent(-1) + ); + assert_eq!( + SignedDecimal256::percent(5) + SignedDecimal256::percent(-4), + SignedDecimal256::percent(1) + ); + + // works for refs + let a = SignedDecimal256::percent(15); + let b = SignedDecimal256::percent(25); + let expected = SignedDecimal256::percent(40); + assert_eq!(a + b, expected); + assert_eq!(&a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic] + fn signed_decimal_256_add_overflow_panics() { + let _value = SignedDecimal256::MAX + SignedDecimal256::percent(50); + } + + #[test] + fn signed_decimal_256_add_assign_works() { + let mut a = SignedDecimal256::percent(30); + a += SignedDecimal256::percent(20); + assert_eq!(a, SignedDecimal256::percent(50)); + + // works for refs + let mut a = SignedDecimal256::percent(15); + let b = SignedDecimal256::percent(3); + let expected = SignedDecimal256::percent(18); + a += &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_256_sub_works() { + let value = SignedDecimal256::one() - SignedDecimal256::percent(50); // 0.5 + assert_eq!(value.0, SignedDecimal256::DECIMAL_FRACTIONAL / Int256::from(2u8)); + + assert_eq!( + SignedDecimal256::percent(9) - SignedDecimal256::percent(4), + SignedDecimal256::percent(5) + ); + assert_eq!( + SignedDecimal256::percent(16) - SignedDecimal256::zero(), + SignedDecimal256::percent(16) + ); + assert_eq!( + SignedDecimal256::percent(16) - SignedDecimal256::percent(16), + SignedDecimal256::zero() + ); + assert_eq!(SignedDecimal256::zero() - SignedDecimal256::zero(), SignedDecimal256::zero()); + + // negative numbers + assert_eq!( + SignedDecimal256::percent(-5) - SignedDecimal256::percent(-4), + SignedDecimal256::percent(-1) + ); + assert_eq!( + SignedDecimal256::percent(-5) - SignedDecimal256::percent(4), + SignedDecimal256::percent(-9) + ); + assert_eq!( + SignedDecimal256::percent(500) - SignedDecimal256::percent(-4), + SignedDecimal256::percent(504) + ); + + // works for refs + let a = SignedDecimal256::percent(13); + let b = SignedDecimal256::percent(6); + let expected = SignedDecimal256::percent(7); + assert_eq!(a - b, expected); + assert_eq!(&a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn signed_decimal_256_sub_overflow_panics() { + let _value = SignedDecimal256::MIN - SignedDecimal256::percent(50); + } + + #[test] + fn signed_decimal_256_sub_assign_works() { + let mut a = SignedDecimal256::percent(20); + a -= SignedDecimal256::percent(2); + assert_eq!(a, SignedDecimal256::percent(18)); + + // works for refs + let mut a = SignedDecimal256::percent(33); + let b = SignedDecimal256::percent(13); + let expected = SignedDecimal256::percent(20); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_256_implements_mul() { + let one = SignedDecimal256::one(); + let two = one + one; + let half = SignedDecimal256::percent(50); + + // 1*x and x*1 + assert_eq!(one * SignedDecimal256::percent(0), SignedDecimal256::percent(0)); + assert_eq!(one * SignedDecimal256::percent(1), SignedDecimal256::percent(1)); + assert_eq!(one * SignedDecimal256::percent(10), SignedDecimal256::percent(10)); + assert_eq!(one * SignedDecimal256::percent(100), SignedDecimal256::percent(100)); + assert_eq!(one * SignedDecimal256::percent(1000), SignedDecimal256::percent(1000)); + assert_eq!(one * SignedDecimal256::MAX, SignedDecimal256::MAX); + assert_eq!(SignedDecimal256::percent(0) * one, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) * one, SignedDecimal256::percent(1)); + assert_eq!(SignedDecimal256::percent(10) * one, SignedDecimal256::percent(10)); + assert_eq!(SignedDecimal256::percent(100) * one, SignedDecimal256::percent(100)); + assert_eq!(SignedDecimal256::percent(1000) * one, SignedDecimal256::percent(1000)); + assert_eq!(SignedDecimal256::MAX * one, SignedDecimal256::MAX); + assert_eq!(SignedDecimal256::percent(-1) * one, SignedDecimal256::percent(-1)); + assert_eq!(one * SignedDecimal256::percent(-10), SignedDecimal256::percent(-10)); + + // double + assert_eq!(two * SignedDecimal256::percent(0), SignedDecimal256::percent(0)); + assert_eq!(two * SignedDecimal256::percent(1), SignedDecimal256::percent(2)); + assert_eq!(two * SignedDecimal256::percent(10), SignedDecimal256::percent(20)); + assert_eq!(two * SignedDecimal256::percent(100), SignedDecimal256::percent(200)); + assert_eq!(two * SignedDecimal256::percent(1000), SignedDecimal256::percent(2000)); + assert_eq!(SignedDecimal256::percent(0) * two, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) * two, SignedDecimal256::percent(2)); + assert_eq!(SignedDecimal256::percent(10) * two, SignedDecimal256::percent(20)); + assert_eq!(SignedDecimal256::percent(100) * two, SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(1000) * two, SignedDecimal256::percent(2000)); + assert_eq!(SignedDecimal256::percent(-1) * two, SignedDecimal256::percent(-2)); + assert_eq!( + two * SignedDecimal256::new(Int256::MIN / Int256::from(2)), + SignedDecimal256::MIN + ); + + // half + assert_eq!(half * SignedDecimal256::percent(0), SignedDecimal256::percent(0)); + assert_eq!(half * SignedDecimal256::percent(1), SignedDecimal256::permille(5)); + assert_eq!(half * SignedDecimal256::percent(10), SignedDecimal256::percent(5)); + assert_eq!(half * SignedDecimal256::percent(100), SignedDecimal256::percent(50)); + assert_eq!(half * SignedDecimal256::percent(1000), SignedDecimal256::percent(500)); + assert_eq!(SignedDecimal256::percent(0) * half, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) * half, SignedDecimal256::permille(5)); + assert_eq!(SignedDecimal256::percent(10) * half, SignedDecimal256::percent(5)); + assert_eq!(SignedDecimal256::percent(100) * half, SignedDecimal256::percent(50)); + assert_eq!(SignedDecimal256::percent(1000) * half, SignedDecimal256::percent(500)); + + // Move left + let a = dec("123.127726548762582"); + assert_eq!(a * dec("1"), dec("123.127726548762582")); + assert_eq!(a * dec("10"), dec("1231.27726548762582")); + assert_eq!(a * dec("100"), dec("12312.7726548762582")); + assert_eq!(a * dec("1000"), dec("123127.726548762582")); + assert_eq!(a * dec("1000000"), dec("123127726.548762582")); + assert_eq!(a * dec("1000000000"), dec("123127726548.762582")); + assert_eq!(a * dec("1000000000000"), dec("123127726548762.582")); + assert_eq!(a * dec("1000000000000000"), dec("123127726548762582")); + assert_eq!(a * dec("1000000000000000000"), dec("123127726548762582000")); + assert_eq!(dec("1") * a, dec("123.127726548762582")); + assert_eq!(dec("10") * a, dec("1231.27726548762582")); + assert_eq!(dec("100") * a, dec("12312.7726548762582")); + assert_eq!(dec("1000") * a, dec("123127.726548762582")); + assert_eq!(dec("1000000") * a, dec("123127726.548762582")); + assert_eq!(dec("1000000000") * a, dec("123127726548.762582")); + assert_eq!(dec("1000000000000") * a, dec("123127726548762.582")); + assert_eq!(dec("1000000000000000") * a, dec("123127726548762582")); + assert_eq!(dec("1000000000000000000") * a, dec("123127726548762582000")); + assert_eq!(dec("-1000000000000000000") * a, dec("-123127726548762582000")); + + // Move right + let max = SignedDecimal256::MAX; + assert_eq!( + max * dec("1.0"), + dec("57896044618658097711785492504343953926634992332820282019728.792003956564819967") + ); + assert_eq!( + max * dec("0.1"), + dec("5789604461865809771178549250434395392663499233282028201972.879200395656481996") + ); + assert_eq!( + max * dec("0.01"), + dec("578960446186580977117854925043439539266349923328202820197.287920039565648199") + ); + assert_eq!( + max * dec("0.001"), + dec("57896044618658097711785492504343953926634992332820282019.728792003956564819") + ); + assert_eq!( + max * dec("0.000001"), + dec("57896044618658097711785492504343953926634992332820282.019728792003956564") + ); + assert_eq!( + max * dec("0.000000001"), + dec("57896044618658097711785492504343953926634992332820.282019728792003956") + ); + assert_eq!( + max * dec("0.000000000001"), + dec("57896044618658097711785492504343953926634992332.820282019728792003") + ); + assert_eq!( + max * dec("0.000000000000001"), + dec("57896044618658097711785492504343953926634992.332820282019728792") + ); + assert_eq!( + max * dec("0.000000000000000001"), + dec("57896044618658097711785492504343953926634.992332820282019728") + ); + + // works for refs + let a = SignedDecimal256::percent(20); + let b = SignedDecimal256::percent(30); + let expected = SignedDecimal256::percent(6); + assert_eq!(a * b, expected); + assert_eq!(&a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn signed_decimal_256_mul_assign_works() { + let mut a = SignedDecimal256::percent(15); + a *= SignedDecimal256::percent(60); + assert_eq!(a, SignedDecimal256::percent(9)); + + // works for refs + let mut a = SignedDecimal256::percent(50); + let b = SignedDecimal256::percent(20); + a *= &b; + assert_eq!(a, SignedDecimal256::percent(10)); + } + + #[test] + #[should_panic(expected = "attempt to multiply with overflow")] + fn signed_decimal_256_mul_overflow_panics() { + let _value = SignedDecimal256::MAX * SignedDecimal256::percent(101); + } + + #[test] + fn signed_decimal_256_checked_mul() { + let test_data = [ + (SignedDecimal256::zero(), SignedDecimal256::zero()), + (SignedDecimal256::zero(), SignedDecimal256::one()), + (SignedDecimal256::one(), SignedDecimal256::zero()), + (SignedDecimal256::percent(10), SignedDecimal256::zero()), + (SignedDecimal256::percent(10), SignedDecimal256::percent(5)), + (SignedDecimal256::MAX, SignedDecimal256::one()), + (SignedDecimal256::MAX / Int256::from(2), SignedDecimal256::percent(200)), + (SignedDecimal256::permille(6), SignedDecimal256::permille(13)), + (SignedDecimal256::permille(-6), SignedDecimal256::permille(0)), + (SignedDecimal256::MAX, SignedDecimal256::negative_one()), + ]; + + // The regular core::ops::Mul is our source of truth for these tests. + for (x, y) in test_data.into_iter() { + assert_eq!(x * y, x.checked_mul(y).unwrap()); + } + } + + #[test] + fn signed_decimal_256_checked_mul_overflow() { + assert_eq!( + SignedDecimal256::MAX.checked_mul(SignedDecimal256::percent(200)), + Err(OverflowError::new(OverflowOperation::Mul)) + ); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_256_implements_div() { + let one = SignedDecimal256::one(); + let two = one + one; + let half = SignedDecimal256::percent(50); + + // 1/x and x/1 + assert_eq!(one / SignedDecimal256::percent(1), SignedDecimal256::percent(10_000)); + assert_eq!(one / SignedDecimal256::percent(10), SignedDecimal256::percent(1_000)); + assert_eq!(one / SignedDecimal256::percent(100), SignedDecimal256::percent(100)); + assert_eq!(one / SignedDecimal256::percent(1000), SignedDecimal256::percent(10)); + assert_eq!(SignedDecimal256::percent(0) / one, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) / one, SignedDecimal256::percent(1)); + assert_eq!(SignedDecimal256::percent(10) / one, SignedDecimal256::percent(10)); + assert_eq!(SignedDecimal256::percent(100) / one, SignedDecimal256::percent(100)); + assert_eq!(SignedDecimal256::percent(1000) / one, SignedDecimal256::percent(1000)); + assert_eq!(one / SignedDecimal256::percent(-1), SignedDecimal256::percent(-10_000)); + assert_eq!(one / SignedDecimal256::percent(-10), SignedDecimal256::percent(-1_000)); + + // double + assert_eq!(two / SignedDecimal256::percent(1), SignedDecimal256::percent(20_000)); + assert_eq!(two / SignedDecimal256::percent(10), SignedDecimal256::percent(2_000)); + assert_eq!(two / SignedDecimal256::percent(100), SignedDecimal256::percent(200)); + assert_eq!(two / SignedDecimal256::percent(1000), SignedDecimal256::percent(20)); + assert_eq!(SignedDecimal256::percent(0) / two, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) / two, dec("0.005")); + assert_eq!(SignedDecimal256::percent(10) / two, SignedDecimal256::percent(5)); + assert_eq!(SignedDecimal256::percent(100) / two, SignedDecimal256::percent(50)); + assert_eq!(SignedDecimal256::percent(1000) / two, SignedDecimal256::percent(500)); + assert_eq!(two / SignedDecimal256::percent(-1), SignedDecimal256::percent(-20_000)); + assert_eq!(SignedDecimal256::percent(-10000) / two, SignedDecimal256::percent(-5000)); + + // half + assert_eq!(half / SignedDecimal256::percent(1), SignedDecimal256::percent(5_000)); + assert_eq!(half / SignedDecimal256::percent(10), SignedDecimal256::percent(500)); + assert_eq!(half / SignedDecimal256::percent(100), SignedDecimal256::percent(50)); + assert_eq!(half / SignedDecimal256::percent(1000), SignedDecimal256::percent(5)); + assert_eq!(SignedDecimal256::percent(0) / half, SignedDecimal256::percent(0)); + assert_eq!(SignedDecimal256::percent(1) / half, SignedDecimal256::percent(2)); + assert_eq!(SignedDecimal256::percent(10) / half, SignedDecimal256::percent(20)); + assert_eq!(SignedDecimal256::percent(100) / half, SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(1000) / half, SignedDecimal256::percent(2000)); + + // Move right + let a = dec("123127726548762582"); + assert_eq!(a / dec("1"), dec("123127726548762582")); + assert_eq!(a / dec("10"), dec("12312772654876258.2")); + assert_eq!(a / dec("100"), dec("1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("0.123127726548762582")); + assert_eq!(dec("1") / a, dec("0.000000000000000008")); + assert_eq!(dec("10") / a, dec("0.000000000000000081")); + assert_eq!(dec("100") / a, dec("0.000000000000000812")); + assert_eq!(dec("1000") / a, dec("0.000000000000008121")); + assert_eq!(dec("1000000") / a, dec("0.000000000008121647")); + assert_eq!(dec("1000000000") / a, dec("0.000000008121647560")); + assert_eq!(dec("1000000000000") / a, dec("0.000008121647560868")); + assert_eq!(dec("1000000000000000") / a, dec("0.008121647560868164")); + assert_eq!(dec("1000000000000000000") / a, dec("8.121647560868164773")); + // negative + let a = dec("-123127726548762582"); + assert_eq!(a / dec("1"), dec("-123127726548762582")); + assert_eq!(a / dec("10"), dec("-12312772654876258.2")); + assert_eq!(a / dec("100"), dec("-1231277265487625.82")); + assert_eq!(a / dec("1000"), dec("-123127726548762.582")); + assert_eq!(a / dec("1000000"), dec("-123127726548.762582")); + assert_eq!(a / dec("1000000000"), dec("-123127726.548762582")); + assert_eq!(a / dec("1000000000000"), dec("-123127.726548762582")); + assert_eq!(a / dec("1000000000000000"), dec("-123.127726548762582")); + assert_eq!(a / dec("1000000000000000000"), dec("-0.123127726548762582")); + assert_eq!(dec("1") / a, dec("-0.000000000000000008")); + + // Move left + let a = dec("0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("123127726.548762582")); + assert_eq!(a / dec("0.000000000001"), dec("123127726548.762582")); + assert_eq!(a / dec("0.000000000000001"), dec("123127726548762.582")); + assert_eq!(a / dec("0.000000000000000001"), dec("123127726548762582")); + // negative + let a = dec("-0.123127726548762582"); + assert_eq!(a / dec("1.0"), dec("-0.123127726548762582")); + assert_eq!(a / dec("0.1"), dec("-1.23127726548762582")); + assert_eq!(a / dec("0.01"), dec("-12.3127726548762582")); + assert_eq!(a / dec("0.001"), dec("-123.127726548762582")); + assert_eq!(a / dec("0.000001"), dec("-123127.726548762582")); + assert_eq!(a / dec("0.000000001"), dec("-123127726.548762582")); + + assert_eq!( + SignedDecimal256::percent(15) / SignedDecimal256::percent(60), + SignedDecimal256::percent(25) + ); + + // works for refs + let a = SignedDecimal256::percent(100); + let b = SignedDecimal256::percent(20); + let expected = SignedDecimal256::percent(500); + assert_eq!(a / b, expected); + assert_eq!(&a / b, expected); + assert_eq!(a / &b, expected); + assert_eq!(&a / &b, expected); + } + + #[test] + fn signed_decimal_256_div_assign_works() { + let mut a = SignedDecimal256::percent(15); + a /= SignedDecimal256::percent(20); + assert_eq!(a, SignedDecimal256::percent(75)); + + // works for refs + let mut a = SignedDecimal256::percent(50); + let b = SignedDecimal256::percent(20); + a /= &b; + assert_eq!(a, SignedDecimal256::percent(250)); + } + + #[test] + #[should_panic(expected = "Division failed - multiplication overflow")] + fn signed_decimal_256_div_overflow_panics() { + let _value = SignedDecimal256::MAX / SignedDecimal256::percent(10); + } + + #[test] + #[should_panic(expected = "Division failed - denominator must not be zero")] + fn signed_decimal_256_div_by_zero_panics() { + let _value = SignedDecimal256::one() / SignedDecimal256::zero(); + } + + #[test] + fn signed_decimal_256_int128_division() { + // a/b + let left = SignedDecimal256::percent(150); // 1.5 + let right = Int256::from(3); + assert_eq!(left / right, SignedDecimal256::percent(50)); + + // negative + let left = SignedDecimal256::percent(-150); // -1.5 + let right = Int256::from(3); + assert_eq!(left / right, SignedDecimal256::percent(-50)); + + // 0/a + let left = SignedDecimal256::zero(); + let right = Int256::from(300); + assert_eq!(left / right, SignedDecimal256::zero()); + } + + #[test] + #[should_panic] + fn signed_decimal_256_int128_divide_by_zero() { + let left = SignedDecimal256::percent(150); // 1.5 + let right = Int256::from(0); + let _result = left / right; + } + + #[test] + fn signed_decimal_256_int128_div_assign() { + // a/b + let mut dec = SignedDecimal256::percent(150); // 1.5 + dec /= Int256::from(3); + assert_eq!(dec, SignedDecimal256::percent(50)); + + // 0/a + let mut dec = SignedDecimal256::zero(); + dec /= Int256::from(300); + assert_eq!(dec, SignedDecimal256::zero()); + } + + #[test] + #[should_panic] + fn signed_decimal_256_int128_div_assign_by_zero() { + // a/0 + let mut dec = SignedDecimal256::percent(50); + dec /= Int256::from(0); + } + + #[test] + fn signed_decimal_256_checked_pow() { + for exp in 0..10 { + assert_eq!(SignedDecimal256::one().checked_pow(exp).unwrap(), SignedDecimal256::one()); + } + + // This case is mathematically undefined but we ensure consistency with Rust standard types + // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=20df6716048e77087acd40194b233494 + assert_eq!(SignedDecimal256::zero().checked_pow(0).unwrap(), SignedDecimal256::one()); + + for exp in 1..10 { + assert_eq!( + SignedDecimal256::zero().checked_pow(exp).unwrap(), + SignedDecimal256::zero() + ); + } + + for exp in 1..10 { + assert_eq!( + SignedDecimal256::negative_one().checked_pow(exp).unwrap(), + // alternates between 1 and -1 + if exp % 2 == 0 { + SignedDecimal256::one() + } else { + SignedDecimal256::negative_one() + } + ) + } + + for num in &[ + SignedDecimal256::percent(50), + SignedDecimal256::percent(99), + SignedDecimal256::percent(200), + ] { + assert_eq!(num.checked_pow(0).unwrap(), SignedDecimal256::one()) + } + + assert_eq!( + SignedDecimal256::percent(20).checked_pow(2).unwrap(), + SignedDecimal256::percent(4) + ); + + assert_eq!( + SignedDecimal256::percent(20).checked_pow(3).unwrap(), + SignedDecimal256::permille(8) + ); + + assert_eq!( + SignedDecimal256::percent(200).checked_pow(4).unwrap(), + SignedDecimal256::percent(1600) + ); + + assert_eq!( + SignedDecimal256::percent(200).checked_pow(4).unwrap(), + SignedDecimal256::percent(1600) + ); + + assert_eq!( + SignedDecimal256::percent(700).checked_pow(5).unwrap(), + SignedDecimal256::percent(1680700) + ); + + assert_eq!( + SignedDecimal256::percent(700).checked_pow(8).unwrap(), + SignedDecimal256::percent(576480100) + ); + + assert_eq!( + SignedDecimal256::percent(700).checked_pow(10).unwrap(), + SignedDecimal256::percent(28247524900) + ); + + assert_eq!( + SignedDecimal256::percent(120).checked_pow(123).unwrap(), + SignedDecimal256(5486473221892422150877397607i128.into()) + ); + + assert_eq!( + SignedDecimal256::percent(10).checked_pow(2).unwrap(), + SignedDecimal256(10000000000000000i128.into()) + ); + + assert_eq!( + SignedDecimal256::percent(10).checked_pow(18).unwrap(), + SignedDecimal256(1i128.into()) + ); + + let decimals = [ + SignedDecimal256::percent(-50), + SignedDecimal256::percent(-99), + SignedDecimal256::percent(-200), + ]; + let exponents = [1, 2, 3, 4, 5, 8, 10]; + + for d in decimals { + for e in exponents { + // use multiplication as source of truth + let mut mul = Ok(d); + for _ in 1..e { + mul = mul.and_then(|mul| mul.checked_mul(d)); + } + assert_eq!(mul, d.checked_pow(e)); + } + } + } + + #[test] + fn signed_decimal_256_checked_pow_overflow() { + assert_eq!( + SignedDecimal256::MAX.checked_pow(2), + Err(OverflowError::new(OverflowOperation::Pow)) + ); + } + + #[test] + fn signed_decimal_256_to_string() { + // Integers + assert_eq!(SignedDecimal256::zero().to_string(), "0"); + assert_eq!(SignedDecimal256::one().to_string(), "1"); + assert_eq!(SignedDecimal256::percent(500).to_string(), "5"); + assert_eq!(SignedDecimal256::percent(-500).to_string(), "-5"); + + // SignedDecimal256s + assert_eq!(SignedDecimal256::percent(125).to_string(), "1.25"); + assert_eq!(SignedDecimal256::percent(42638).to_string(), "426.38"); + assert_eq!(SignedDecimal256::percent(3).to_string(), "0.03"); + assert_eq!(SignedDecimal256::permille(987).to_string(), "0.987"); + assert_eq!(SignedDecimal256::percent(-125).to_string(), "-1.25"); + assert_eq!(SignedDecimal256::percent(-42638).to_string(), "-426.38"); + assert_eq!(SignedDecimal256::percent(-3).to_string(), "-0.03"); + assert_eq!(SignedDecimal256::permille(-987).to_string(), "-0.987"); + + assert_eq!(SignedDecimal256(Int256::from(1i128)).to_string(), "0.000000000000000001"); + assert_eq!(SignedDecimal256(Int256::from(10i128)).to_string(), "0.00000000000000001"); + assert_eq!(SignedDecimal256(Int256::from(100i128)).to_string(), "0.0000000000000001"); + assert_eq!(SignedDecimal256(Int256::from(1000i128)).to_string(), "0.000000000000001"); + assert_eq!(SignedDecimal256(Int256::from(10000i128)).to_string(), "0.00000000000001"); + assert_eq!(SignedDecimal256(Int256::from(100000i128)).to_string(), "0.0000000000001"); + assert_eq!(SignedDecimal256(Int256::from(1000000i128)).to_string(), "0.000000000001"); + assert_eq!(SignedDecimal256(Int256::from(10000000i128)).to_string(), "0.00000000001"); + assert_eq!(SignedDecimal256(Int256::from(100000000i128)).to_string(), "0.0000000001"); + assert_eq!(SignedDecimal256(Int256::from(1000000000i128)).to_string(), "0.000000001"); + assert_eq!(SignedDecimal256(Int256::from(10000000000i128)).to_string(), "0.00000001"); + assert_eq!(SignedDecimal256(Int256::from(100000000000i128)).to_string(), "0.0000001"); + assert_eq!(SignedDecimal256(Int256::from(10000000000000i128)).to_string(), "0.00001"); + assert_eq!(SignedDecimal256(Int256::from(100000000000000i128)).to_string(), "0.0001"); + assert_eq!(SignedDecimal256(Int256::from(1000000000000000i128)).to_string(), "0.001"); + assert_eq!(SignedDecimal256(Int256::from(10000000000000000i128)).to_string(), "0.01"); + assert_eq!(SignedDecimal256(Int256::from(100000000000000000i128)).to_string(), "0.1"); + assert_eq!(SignedDecimal256(Int256::from(-1i128)).to_string(), "-0.000000000000000001"); + assert_eq!(SignedDecimal256(Int256::from(-100000000000000i128)).to_string(), "-0.0001"); + assert_eq!(SignedDecimal256(Int256::from(-100000000000000000i128)).to_string(), "-0.1"); + } + + #[test] + fn signed_decimal_256_iter_sum() { + let items = vec![ + SignedDecimal256::zero(), + SignedDecimal256(Int256::from(2i128)), + SignedDecimal256(Int256::from(2i128)), + SignedDecimal256(Int256::from(-2i128)), + ]; + assert_eq!(items.iter().sum::(), SignedDecimal256(Int256::from(2i128))); + assert_eq!( + items.into_iter().sum::(), + SignedDecimal256(Int256::from(2i128)) + ); + + let empty: Vec = vec![]; + assert_eq!(SignedDecimal256::zero(), empty.iter().sum::()); + } + + #[test] + fn signed_decimal_256_serialize() { + assert_eq!(serde_json::to_vec(&SignedDecimal256::zero()).unwrap(), br#""0""#); + assert_eq!(serde_json::to_vec(&SignedDecimal256::one()).unwrap(), br#""1""#); + assert_eq!(serde_json::to_vec(&SignedDecimal256::percent(8)).unwrap(), br#""0.08""#); + assert_eq!(serde_json::to_vec(&SignedDecimal256::percent(87)).unwrap(), br#""0.87""#); + assert_eq!(serde_json::to_vec(&SignedDecimal256::percent(876)).unwrap(), br#""8.76""#); + assert_eq!(serde_json::to_vec(&SignedDecimal256::percent(8765)).unwrap(), br#""87.65""#); + assert_eq!( + serde_json::to_vec(&SignedDecimal256::percent(-87654)).unwrap(), + br#""-876.54""# + ); + assert_eq!(serde_json::to_vec(&SignedDecimal256::negative_one()).unwrap(), br#""-1""#); + assert_eq!(serde_json::to_vec(&-SignedDecimal256::percent(8)).unwrap(), br#""-0.08""#); + } + + #[test] + fn signed_decimal_256_deserialize() { + assert_eq!( + serde_json::from_slice::(br#""0""#).unwrap(), + SignedDecimal256::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""1""#).unwrap(), + SignedDecimal256::one() + ); + assert_eq!( + serde_json::from_slice::(br#""000""#).unwrap(), + SignedDecimal256::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""001""#).unwrap(), + SignedDecimal256::one() + ); + + assert_eq!( + serde_json::from_slice::(br#""0.08""#).unwrap(), + SignedDecimal256::percent(8) + ); + assert_eq!( + serde_json::from_slice::(br#""0.87""#).unwrap(), + SignedDecimal256::percent(87) + ); + assert_eq!( + serde_json::from_slice::(br#""8.76""#).unwrap(), + SignedDecimal256::percent(876) + ); + assert_eq!( + serde_json::from_slice::(br#""87.65""#).unwrap(), + SignedDecimal256::percent(8765) + ); + + // negative numbers + assert_eq!( + serde_json::from_slice::(br#""-0""#).unwrap(), + SignedDecimal256::zero() + ); + assert_eq!( + serde_json::from_slice::(br#""-1""#).unwrap(), + SignedDecimal256::negative_one() + ); + assert_eq!( + serde_json::from_slice::(br#""-001""#).unwrap(), + SignedDecimal256::negative_one() + ); + assert_eq!( + serde_json::from_slice::(br#""-0.08""#).unwrap(), + SignedDecimal256::percent(-8) + ); + } + + #[test] + fn signed_decimal_256_abs_diff_works() { + let a = SignedDecimal256::percent(285); + let b = SignedDecimal256::percent(200); + let expected = Decimal256::percent(85); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let a = SignedDecimal256::percent(-200); + let b = SignedDecimal256::percent(200); + let expected = Decimal256::percent(400); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + + let a = SignedDecimal256::percent(-200); + let b = SignedDecimal256::percent(-240); + let expected = Decimal256::percent(40); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn signed_decimal_256_rem_works() { + // 4.02 % 1.11 = 0.69 + assert_eq!( + SignedDecimal256::percent(402) % SignedDecimal256::percent(111), + SignedDecimal256::percent(69) + ); + + // 15.25 % 4 = 3.25 + assert_eq!( + SignedDecimal256::percent(1525) % SignedDecimal256::percent(400), + SignedDecimal256::percent(325) + ); + + // -20.25 % 5 = -25 + assert_eq!( + SignedDecimal256::percent(-2025) % SignedDecimal256::percent(500), + SignedDecimal256::percent(-25) + ); + + let a = SignedDecimal256::percent(318); + let b = SignedDecimal256::percent(317); + let expected = SignedDecimal256::percent(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn signed_decimal_256_rem_assign_works() { + let mut a = SignedDecimal256::percent(17673); + a %= SignedDecimal256::percent(2362); + assert_eq!(a, SignedDecimal256::percent(1139)); // 176.73 % 23.62 = 11.39 + + let mut a = SignedDecimal256::percent(4262); + let b = SignedDecimal256::percent(1270); + a %= &b; + assert_eq!(a, SignedDecimal256::percent(452)); // 42.62 % 12.7 = 4.52 + + let mut a = SignedDecimal256::percent(-4262); + let b = SignedDecimal256::percent(1270); + a %= &b; + assert_eq!(a, SignedDecimal256::percent(-452)); // -42.62 % 12.7 = -4.52 + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn signed_decimal_256_rem_panics_for_zero() { + let _ = SignedDecimal256::percent(777) % SignedDecimal256::zero(); + } + + #[test] + fn signed_decimal_256_checked_methods() { + // checked add + assert_eq!( + SignedDecimal256::percent(402) + .checked_add(SignedDecimal256::percent(111)) + .unwrap(), + SignedDecimal256::percent(513) + ); + assert!(matches!( + SignedDecimal256::MAX.checked_add(SignedDecimal256::percent(1)), + Err(OverflowError { .. }) + )); + assert!(matches!( + SignedDecimal256::MIN.checked_add(SignedDecimal256::percent(-1)), + Err(OverflowError { .. }) + )); + + // checked sub + assert_eq!( + SignedDecimal256::percent(1111) + .checked_sub(SignedDecimal256::percent(111)) + .unwrap(), + SignedDecimal256::percent(1000) + ); + assert_eq!( + SignedDecimal256::zero().checked_sub(SignedDecimal256::percent(1)).unwrap(), + SignedDecimal256::percent(-1) + ); + assert!(matches!( + SignedDecimal256::MIN.checked_sub(SignedDecimal256::percent(1)), + Err(OverflowError { .. }) + )); + assert!(matches!( + SignedDecimal256::MAX.checked_sub(SignedDecimal256::percent(-1)), + Err(OverflowError { .. }) + )); + + // checked div + assert_eq!( + SignedDecimal256::percent(30) + .checked_div(SignedDecimal256::percent(200)) + .unwrap(), + SignedDecimal256::percent(15) + ); + assert_eq!( + SignedDecimal256::percent(88) + .checked_div(SignedDecimal256::percent(20)) + .unwrap(), + SignedDecimal256::percent(440) + ); + assert!(matches!( + SignedDecimal256::MAX.checked_div(SignedDecimal256::zero()), + Err(CheckedFromRatioError::DivideByZero {}) + )); + assert!(matches!( + SignedDecimal256::MAX.checked_div(SignedDecimal256::percent(1)), + Err(CheckedFromRatioError::Overflow {}) + )); + assert_eq!( + SignedDecimal256::percent(-88) + .checked_div(SignedDecimal256::percent(20)) + .unwrap(), + SignedDecimal256::percent(-440) + ); + assert_eq!( + SignedDecimal256::percent(-88) + .checked_div(SignedDecimal256::percent(-20)) + .unwrap(), + SignedDecimal256::percent(440) + ); + + // checked rem + assert_eq!( + SignedDecimal256::percent(402) + .checked_rem(SignedDecimal256::percent(111)) + .unwrap(), + SignedDecimal256::percent(69) + ); + assert_eq!( + SignedDecimal256::percent(1525) + .checked_rem(SignedDecimal256::percent(400)) + .unwrap(), + SignedDecimal256::percent(325) + ); + assert_eq!( + SignedDecimal256::percent(-1525) + .checked_rem(SignedDecimal256::percent(400)) + .unwrap(), + SignedDecimal256::percent(-325) + ); + assert_eq!( + SignedDecimal256::percent(-1525) + .checked_rem(SignedDecimal256::percent(-400)) + .unwrap(), + SignedDecimal256::percent(-325) + ); + assert!(matches!( + SignedDecimal256::MAX.checked_rem(SignedDecimal256::zero()), + Err(DivideByZeroError { .. }) + )); + } + + #[test] + fn signed_decimal_256_pow_works() { + assert_eq!(SignedDecimal256::percent(200).pow(2), SignedDecimal256::percent(400)); + assert_eq!(SignedDecimal256::percent(-200).pow(2), SignedDecimal256::percent(400)); + assert_eq!(SignedDecimal256::percent(-200).pow(3), SignedDecimal256::percent(-800)); + assert_eq!(SignedDecimal256::percent(200).pow(10), SignedDecimal256::percent(102400)); + } + + #[test] + #[should_panic] + fn signed_decimal_256_pow_overflow_panics() { + _ = SignedDecimal256::MAX.pow(2u32); + } + + #[test] + fn signed_decimal_256_saturating_works() { + assert_eq!( + SignedDecimal256::percent(200).saturating_add(SignedDecimal256::percent(200)), + SignedDecimal256::percent(400) + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_add(SignedDecimal256::percent(200)), + SignedDecimal256::zero() + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_add(SignedDecimal256::percent(-200)), + SignedDecimal256::percent(-400) + ); + assert_eq!( + SignedDecimal256::MAX.saturating_add(SignedDecimal256::percent(200)), + SignedDecimal256::MAX + ); + assert_eq!( + SignedDecimal256::MIN.saturating_add(SignedDecimal256::percent(-1)), + SignedDecimal256::MIN + ); + assert_eq!( + SignedDecimal256::percent(200).saturating_sub(SignedDecimal256::percent(100)), + SignedDecimal256::percent(100) + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_sub(SignedDecimal256::percent(100)), + SignedDecimal256::percent(-300) + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_sub(SignedDecimal256::percent(-100)), + SignedDecimal256::percent(-100) + ); + assert_eq!( + SignedDecimal256::zero().saturating_sub(SignedDecimal256::percent(200)), + SignedDecimal256::from_str("-2").unwrap() + ); + assert_eq!( + SignedDecimal256::MIN.saturating_sub(SignedDecimal256::percent(200)), + SignedDecimal256::MIN + ); + assert_eq!( + SignedDecimal256::MAX.saturating_sub(SignedDecimal256::percent(-200)), + SignedDecimal256::MAX + ); + assert_eq!( + SignedDecimal256::percent(200).saturating_mul(SignedDecimal256::percent(50)), + SignedDecimal256::percent(100) + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_mul(SignedDecimal256::percent(50)), + SignedDecimal256::percent(-100) + ); + assert_eq!( + SignedDecimal256::percent(-200).saturating_mul(SignedDecimal256::percent(-50)), + SignedDecimal256::percent(100) + ); + assert_eq!( + SignedDecimal256::MAX.saturating_mul(SignedDecimal256::percent(200)), + SignedDecimal256::MAX + ); + assert_eq!( + SignedDecimal256::MIN.saturating_mul(SignedDecimal256::percent(200)), + SignedDecimal256::MIN + ); + assert_eq!( + SignedDecimal256::MIN.saturating_mul(SignedDecimal256::percent(-200)), + SignedDecimal256::MAX + ); + assert_eq!( + SignedDecimal256::percent(400).saturating_pow(2u32), + SignedDecimal256::percent(1600) + ); + assert_eq!(SignedDecimal256::MAX.saturating_pow(2u32), SignedDecimal256::MAX); + assert_eq!(SignedDecimal256::MAX.saturating_pow(3u32), SignedDecimal256::MAX); + assert_eq!(SignedDecimal256::MIN.saturating_pow(2u32), SignedDecimal256::MAX); + assert_eq!(SignedDecimal256::MIN.saturating_pow(3u32), SignedDecimal256::MIN); + } + + #[test] + fn signed_decimal_256_rounding() { + assert_eq!(SignedDecimal256::one().floor(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(150).floor(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(199).floor(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(200).floor(), SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(99).floor(), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256(Int256::from(1i128)).floor(), SignedDecimal256::zero()); + assert_eq!( + SignedDecimal256(Int256::from(-1i128)).floor(), + SignedDecimal256::negative_one() + ); + assert_eq!(SignedDecimal256::permille(-1234).floor(), SignedDecimal256::percent(-200)); + + assert_eq!(SignedDecimal256::one().ceil(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(150).ceil(), SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(199).ceil(), SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(99).ceil(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256(Int256::from(1i128)).ceil(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256(Int256::from(-1i128)).ceil(), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256::permille(-1234).ceil(), SignedDecimal256::negative_one()); + + assert_eq!(SignedDecimal256::one().trunc(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(150).trunc(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(199).trunc(), SignedDecimal256::one()); + assert_eq!(SignedDecimal256::percent(200).trunc(), SignedDecimal256::percent(200)); + assert_eq!(SignedDecimal256::percent(99).trunc(), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256(Int256::from(1i128)).trunc(), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256(Int256::from(-1i128)).trunc(), SignedDecimal256::zero()); + assert_eq!(SignedDecimal256::permille(-1234).trunc(), SignedDecimal256::negative_one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn signed_decimal_256_ceil_panics() { + let _ = SignedDecimal256::MAX.ceil(); + } + + #[test] + #[should_panic(expected = "attempt to floor with overflow")] + fn signed_decimal_256_floor_panics() { + let _ = SignedDecimal256::MIN.floor(); + } + + #[test] + fn signed_decimal_256_checked_ceil() { + assert_eq!( + SignedDecimal256::percent(199).checked_ceil(), + Ok(SignedDecimal256::percent(200)) + ); + assert_eq!(SignedDecimal256::MAX.checked_ceil(), Err(RoundUpOverflowError)); + } + + #[test] + fn signed_decimal_256_checked_floor() { + assert_eq!(SignedDecimal256::percent(199).checked_floor(), Ok(SignedDecimal256::one())); + assert_eq!( + SignedDecimal256::percent(-199).checked_floor(), + Ok(SignedDecimal256::percent(-200)) + ); + assert_eq!(SignedDecimal256::MIN.checked_floor(), Err(RoundDownOverflowError)); + assert_eq!( + SignedDecimal256::negative_one().checked_floor(), + Ok(SignedDecimal256::negative_one()) + ); + } + + #[test] + fn signed_decimal_256_to_int_floor_works() { + let d = SignedDecimal256::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(12)); + let d = SignedDecimal256::from_str("12.345").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(12)); + let d = SignedDecimal256::from_str("12.999").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(12)); + let d = SignedDecimal256::from_str("0.98451384").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(0)); + let d = SignedDecimal256::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(-13)); + let d = SignedDecimal256::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(-13)); + let d = SignedDecimal256::from_str("0.0001").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(0)); + let d = SignedDecimal256::from_str("75.0").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(75)); + let d = SignedDecimal256::from_str("0.0").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(0)); + let d = SignedDecimal256::from_str("-0.0").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(0)); + let d = SignedDecimal256::from_str("-0.0001").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(-1)); + let d = SignedDecimal256::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_floor(), Int256::from(-75)); + + let d = SignedDecimal256::MAX; + assert_eq!( + d.to_int_floor(), + Int256::from_str("57896044618658097711785492504343953926634992332820282019728") + .unwrap() + ); + let d = SignedDecimal256::MIN; + assert_eq!( + d.to_int_floor(), + Int256::from_str("-57896044618658097711785492504343953926634992332820282019729") + .unwrap() + ); + } + + #[test] + fn signed_decimal_256_to_int_ceil_works() { + let d = SignedDecimal256::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(13)); + let d = SignedDecimal256::from_str("12.345").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(13)); + let d = SignedDecimal256::from_str("12.999").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(13)); + let d = SignedDecimal256::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(-12)); + let d = SignedDecimal256::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(-12)); + + let d = SignedDecimal256::from_str("75.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(75)); + let d = SignedDecimal256::from_str("0.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(0)); + let d = SignedDecimal256::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_ceil(), Int256::from(-75)); + + let d = SignedDecimal256::MAX; + assert_eq!( + d.to_int_ceil(), + Int256::from_str("57896044618658097711785492504343953926634992332820282019729") + .unwrap() + ); + let d = SignedDecimal256::MIN; + assert_eq!( + d.to_int_ceil(), + Int256::from_str("-57896044618658097711785492504343953926634992332820282019728") + .unwrap() + ); + } + + #[test] + fn signed_decimal_256_to_int_trunc_works() { + let d = SignedDecimal256::from_str("12.000000000000000001").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(12)); + let d = SignedDecimal256::from_str("12.345").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(12)); + let d = SignedDecimal256::from_str("12.999").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(12)); + let d = SignedDecimal256::from_str("-12.000000000000000001").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(-12)); + let d = SignedDecimal256::from_str("-12.345").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(-12)); + + let d = SignedDecimal256::from_str("75.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(75)); + let d = SignedDecimal256::from_str("0.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(0)); + let d = SignedDecimal256::from_str("-75.0").unwrap(); + assert_eq!(d.to_int_trunc(), Int256::from(-75)); + + let d = SignedDecimal256::MAX; + assert_eq!( + d.to_int_trunc(), + Int256::from_str("57896044618658097711785492504343953926634992332820282019728") + .unwrap() + ); + let d = SignedDecimal256::MIN; + assert_eq!( + d.to_int_trunc(), + Int256::from_str("-57896044618658097711785492504343953926634992332820282019728") + .unwrap() + ); + } + + #[test] + fn signed_decimal_256_neg_works() { + assert_eq!(-SignedDecimal256::percent(50), SignedDecimal256::percent(-50)); + assert_eq!(-SignedDecimal256::one(), SignedDecimal256::negative_one()); + } + + #[test] + fn signed_decimal_256_partial_eq() { + let test_cases = [ + ("1", "1", true), + ("0.5", "0.5", true), + ("0.5", "0.51", false), + ("0", "0.00000", true), + ("-1", "-1", true), + ("-0.5", "-0.5", true), + ("-0.5", "0.5", false), + ("-0.5", "-0.51", false), + ("-0", "-0.00000", true), + ] + .into_iter() + .map(|(lhs, rhs, expected)| (dec(lhs), dec(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn signed_decimal_256_implements_debug() { + let decimal = SignedDecimal256::from_str("123.45").unwrap(); + assert_eq!(format!("{decimal:?}"), "SignedDecimal256(123.45)"); + + let test_cases = ["5", "5.01", "42", "0", "2", "-0.000001"]; + for s in test_cases { + let decimal = SignedDecimal256::from_str(s).unwrap(); + let expected = format!("SignedDecimal256({s})"); + assert_eq!(format!("{decimal:?}"), expected); + } + } + + #[test] + fn signed_decimal_256_can_be_instantiated_from_decimal() { + let d: SignedDecimal256 = Decimal::one().into(); + assert_eq!(d, SignedDecimal256::one()); + } + + #[test] + fn signed_decimal_256_can_be_instantiated_from_decimal_256() { + let d: SignedDecimal256 = Decimal256::zero().try_into().unwrap(); + assert_eq!(d, SignedDecimal256::zero()); + } + + #[test] + fn signed_decimal_256_may_fail_when_instantiated_from_decimal_256() { + let err = >::try_into(Decimal256::MAX).unwrap_err(); + assert_eq!("SignedDecimal256RangeExceeded", format!("{err:?}")); + assert_eq!("SignedDecimal256 range exceeded", format!("{err}")); + } + + #[test] + fn signed_decimal_256_can_be_serialized_and_deserialized() { + // properly deserialized + let value: SignedDecimal256 = serde_json::from_str(r#""123""#).unwrap(); + assert_eq!(SignedDecimal256::from_str("123").unwrap(), value); + + // properly serialized + let value = SignedDecimal256::from_str("456").unwrap(); + assert_eq!(r#""456""#, serde_json::to_string(&value).unwrap()); + + // invalid: not a string encoded decimal + assert_eq!( + "invalid type: integer `123`, expected string-encoded decimal at line 1 column 3", + serde_json::from_str::("123").err().unwrap().to_string() + ); + + // invalid: not properly defined signed decimal value + assert_eq!( + "Error parsing decimal '1.e': Generic error: Error parsing fractional at line 1 column 5", + serde_json::from_str::(r#""1.e""#) + .err() + .unwrap() + .to_string() + ); + } + + // #[test] + // fn signed_decimal_256_has_defined_json_schema() { + // let schema = schemars::schema_for!(SignedDecimal256); + // assert_eq!("SignedDecimal256", schema.schema.metadata.unwrap().title.unwrap()); + // } +} diff --git a/cosmwasm/std/src/math/uint128.rs b/cosmwasm/std/src/math/uint128.rs new file mode 100644 index 00000000..d42fbefe --- /dev/null +++ b/cosmwasm/std/src/math/uint128.rs @@ -0,0 +1,1462 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, Shl, ShlAssign, Shr, + ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; + +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + __internal::forward_ref_partial_eq, + errors::{ + CheckedMultiplyFractionError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + impl_mul_fraction, Fraction, Int128, Int256, Int512, Int64, Uint256, Uint64, +}; + +use super::{conversion::forward_try_from, num_consts::NumConsts}; + +/// A thin wrapper around u128 that is using strings for JSON encoding/decoding, +/// such that the full u128 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances of this and `u128` to get the value out: +/// +/// ``` +/// # use cosmwasm_std::Uint128; +/// let a = Uint128::from(123u128); +/// assert_eq!(a.u128(), 123); +/// +/// let b = Uint128::from(42u64); +/// assert_eq!(b.u128(), 42); +/// +/// let c = Uint128::from(70u32); +/// assert_eq!(c.u128(), 70); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Uint128(#[schemars(with = "String")] pub(crate) u128); + +forward_ref_partial_eq!(Uint128, Uint128); + +impl Uint128 { + pub const MAX: Self = Self(u128::MAX); + pub const MIN: Self = Self(u128::MIN); + + /// Creates a Uint128(value). + /// + /// This method is less flexible than `from` but can be called in a const context. + pub const fn new(value: u128) -> Self { + Uint128(value) + } + + /// Creates a Uint128(0) + #[inline] + pub const fn zero() -> Self { + Uint128(0) + } + + /// Creates a Uint128(1) + #[inline] + pub const fn one() -> Self { + Self(1) + } + + /// Returns a copy of the internal data + pub const fn u128(&self) -> u128 { + self.0 + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 16] { + self.0.to_be_bytes() + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 16] { + self.0.to_le_bytes() + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0 == 0 + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns the base 2 logarithm of the number, rounded down. + /// + /// # Panics + /// + /// This function will panic if `self` is zero. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ilog2(self) -> u32 { + self.0.checked_ilog2().unwrap() + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Uint128 { + match self.checked_multiply_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedMultiplyRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedMultiplyRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator: u128 = numerator.into(); + let denominator: u128 = denominator.into(); + if denominator == 0 { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Uint256::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two u128 values without overflow, producing an + /// [`Uint256`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Uint128; + /// + /// let a = Uint128::MAX; + /// let result = a.full_mul(2u32); + /// assert_eq!(result.to_string(), "680564733841876926926749214863536422910"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Uint256 { + Uint256::from(self).checked_mul(Uint256::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + self.0.checked_div(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 128 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 128 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + /// Strict integer addition. Computes `self + rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint128::add`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_add(self, rhs: Self) -> Self { + match self.0.checked_add(rhs.u128()) { + None => panic!("attempt to add with overflow"), + Some(sum) => Self(sum), + } + } + + /// Strict integer subtraction. Computes `self - rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint128::sub`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_sub(self, other: Self) -> Self { + match self.0.checked_sub(other.u128()) { + None => panic!("attempt to subtract with overflow"), + Some(diff) => Self(diff), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Self { + Self(if self.0 < other.0 { other.0 - self.0 } else { self.0 - other.0 }) + } +} + +impl NumConsts for Uint128 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +impl_mul_fraction!(Uint128); + +// `From` is implemented manually instead of +// using `impl> From for Uint128` because +// of the conflict with `TryFrom<&str>` as described here +// https://stackoverflow.com/questions/63136970/how-do-i-work-around-the-upstream-crates-may-add-a-new-impl-of-trait-error + +impl From for Uint128 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} + +impl From for Uint128 { + fn from(val: u128) -> Self { + Uint128(val) + } +} + +impl From for Uint128 { + fn from(val: u64) -> Self { + Uint128(val.into()) + } +} + +impl From for Uint128 { + fn from(val: u32) -> Self { + Uint128(val.into()) + } +} + +impl From for Uint128 { + fn from(val: u16) -> Self { + Uint128(val.into()) + } +} + +impl From for Uint128 { + fn from(val: u8) -> Self { + Uint128(val.into()) + } +} + +forward_try_from!(Uint128, Uint64); + +// Int to Uint +forward_try_from!(Int64, Uint128); +forward_try_from!(Int128, Uint128); +forward_try_from!(Int256, Uint128); +forward_try_from!(Int512, Uint128); + +impl TryFrom<&str> for Uint128 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Uint128 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(u) => Ok(Uint128(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing u128: {e}"))), + } + } +} + +impl From for String { + fn from(original: Uint128) -> Self { + original.to_string() + } +} + +impl From for u128 { + fn from(original: Uint128) -> Self { + original.0 + } +} + +impl fmt::Display for Uint128 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Uint128 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + self.strict_add(rhs) + } +} +forward_ref_binop!(impl Add, add for Uint128, Uint128); + +impl Sub for Uint128 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + self.strict_sub(rhs) + } +} +forward_ref_binop!(impl Sub, sub for Uint128, Uint128); + +impl SubAssign for Uint128 { + fn sub_assign(&mut self, rhs: Uint128) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Uint128, Uint128); + +impl Mul for Uint128 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.u128().checked_mul(rhs.u128()).expect("attempt to multiply with overflow")) + } +} +forward_ref_binop!(impl Mul, mul for Uint128, Uint128); + +impl MulAssign for Uint128 { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Uint128, Uint128); + +impl Div for Uint128 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.u128().checked_div(rhs.u128()).expect("attempt to divide by zero")) + } +} + +impl<'a> Div<&'a Uint128> for Uint128 { + type Output = Self; + + fn div(self, rhs: &'a Uint128) -> Self::Output { + self / *rhs + } +} + +impl Shr for Uint128 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + Self(self.u128().checked_shr(rhs).expect("attempt to shift right with overflow")) + } +} + +impl<'a> Shr<&'a u32> for Uint128 { + type Output = Self; + + fn shr(self, rhs: &'a u32) -> Self::Output { + self >> *rhs + } +} + +impl Shl for Uint128 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + Self(self.u128().checked_shl(rhs).expect("attempt to shift left with overflow")) + } +} + +impl<'a> Shl<&'a u32> for Uint128 { + type Output = Self; + + fn shl(self, rhs: &'a u32) -> Self::Output { + self.shl(*rhs) + } +} + +impl AddAssign for Uint128 { + fn add_assign(&mut self, rhs: Uint128) { + *self = *self + rhs; + } +} + +impl<'a> AddAssign<&'a Uint128> for Uint128 { + fn add_assign(&mut self, rhs: &'a Uint128) { + *self = *self + rhs; + } +} + +impl DivAssign for Uint128 { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl<'a> DivAssign<&'a Uint128> for Uint128 { + fn div_assign(&mut self, rhs: &'a Uint128) { + *self = *self / rhs; + } +} + +impl Rem for Uint128 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Uint128, Uint128); + +impl Not for Uint128 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl RemAssign for Uint128 { + fn rem_assign(&mut self, rhs: Uint128) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Uint128, Uint128); + +impl ShrAssign for Uint128 { + fn shr_assign(&mut self, rhs: u32) { + *self = *self >> rhs; + } +} + +impl<'a> ShrAssign<&'a u32> for Uint128 { + fn shr_assign(&mut self, rhs: &'a u32) { + *self = *self >> rhs; + } +} + +impl ShlAssign for Uint128 { + fn shl_assign(&mut self, rhs: u32) { + *self = Shl::::shl(*self, rhs); + } +} + +impl<'a> ShlAssign<&'a u32> for Uint128 { + fn shl_assign(&mut self, rhs: &'a u32) { + *self = Shl::::shl(*self, *rhs); + } +} + +impl Serialize for Uint128 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Uint128 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Uint128Visitor) + } +} + +struct Uint128Visitor; + +impl<'de> de::Visitor<'de> for Uint128Visitor { + type Value = Uint128; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v.parse::() { + Ok(u) => Ok(Uint128(u)), + Err(e) => Err(E::custom(format_args!("invalid Uint128 '{v}' - {e}"))), + } + } +} + +impl core::iter::Sum for Uint128 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + errors::CheckedMultiplyFractionError::{ConversionOverflow, DivideByZero}, + math::conversion::test_try_from_int_to_uint, + ConversionOverflowError, Decimal, + }; + + use super::*; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 16); + } + + #[test] + fn uint128_not_works() { + assert_eq!(!Uint128::new(1234806), Uint128::new(!1234806)); + + assert_eq!(!Uint128::MAX, Uint128::new(!u128::MAX)); + assert_eq!(!Uint128::MIN, Uint128::new(!u128::MIN)); + } + + #[test] + fn uint128_zero_works() { + let zero = Uint128::zero(); + assert_eq!(zero.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + } + + #[test] + fn uint128_one_works() { + let one = Uint128::one(); + assert_eq!(one.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + } + + #[test] + fn uint128_convert_into() { + let original = Uint128(12345); + let a = u128::from(original); + assert_eq!(a, 12345); + + let original = Uint128(12345); + let a = String::from(original); + assert_eq!(a, "12345"); + } + + #[test] + fn uint128_convert_from() { + let a = Uint128::from(5u128); + assert_eq!(a.0, 5); + + let a = Uint128::from(5u64); + assert_eq!(a.0, 5); + + let a = Uint128::from(5u32); + assert_eq!(a.0, 5); + + let a = Uint128::from(5u16); + assert_eq!(a.0, 5); + + let a = Uint128::from(5u8); + assert_eq!(a.0, 5); + + let result = Uint128::try_from("34567"); + assert_eq!(result.unwrap().0, 34567); + + let result = Uint128::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn uint128_try_from_signed_works() { + test_try_from_int_to_uint::("Int64", "Uint128"); + test_try_from_int_to_uint::("Int128", "Uint128"); + test_try_from_int_to_uint::("Int256", "Uint128"); + test_try_from_int_to_uint::("Int512", "Uint128"); + } + + #[test] + fn uint128_try_into() { + assert!(Uint64::try_from(Uint128::MAX).is_err()); + + assert_eq!(Uint64::try_from(Uint128::zero()), Ok(Uint64::zero())); + + assert_eq!(Uint64::try_from(Uint128::from(42u64)), Ok(Uint64::from(42u64))); + } + + #[test] + fn uint128_implements_display() { + let a = Uint128(12345); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Uint128(0); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn uint128_display_padding_works() { + // width > natural representation + let a = Uint128::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + + // width < natural representation + let a = Uint128::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + } + + #[test] + fn uint128_to_be_bytes_works() { + assert_eq!(Uint128::zero().to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + Uint128::MAX.to_be_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff + ] + ); + assert_eq!(Uint128::new(1).to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(16, "big")]` + assert_eq!( + Uint128::new(240282366920938463463374607431768124608).to_be_bytes(), + [180, 196, 179, 87, 165, 121, 59, 133, 246, 117, 221, 191, 255, 254, 172, 192] + ); + } + + #[test] + fn uint128_to_le_bytes_works() { + assert_eq!(Uint128::zero().to_le_bytes(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + Uint128::MAX.to_le_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff + ] + ); + assert_eq!(Uint128::new(1).to_le_bytes(), [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(16, "little")]` + assert_eq!( + Uint128::new(240282366920938463463374607431768124608).to_le_bytes(), + [192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 180] + ); + } + + #[test] + fn uint128_is_zero_works() { + assert!(Uint128::zero().is_zero()); + assert!(Uint128(0).is_zero()); + + assert!(!Uint128(1).is_zero()); + assert!(!Uint128(123).is_zero()); + } + + #[test] + fn uint128_json() { + let orig = Uint128(1234567890987654321); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Uint128 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn uint128_compare() { + let a = Uint128(12345); + let b = Uint128(23456); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Uint128(12345)); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_math() { + let a = Uint128(12345); + let b = Uint128(23456); + + // test - with owned and reference right hand side + assert_eq!(b - a, Uint128(11111)); + assert_eq!(b - &a, Uint128(11111)); + + // test += with owned and reference right hand side + let mut c = Uint128(300000); + c += b; + assert_eq!(c, Uint128(323456)); + let mut d = Uint128(300000); + d += &b; + assert_eq!(d, Uint128(323456)); + + // test -= with owned and reference right hand side + let mut c = Uint128(300000); + c -= b; + assert_eq!(c, Uint128(276544)); + let mut d = Uint128(300000); + d -= &b; + assert_eq!(d, Uint128(276544)); + + // error result on underflow (- would produce negative result) + let underflow_result = a.checked_sub(b); + let OverflowError { operation } = underflow_result.unwrap_err(); + assert_eq!(operation, OverflowOperation::Sub); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_add_works() { + assert_eq!(Uint128::from(2u32) + Uint128::from(1u32), Uint128::from(3u32)); + assert_eq!(Uint128::from(2u32) + Uint128::from(0u32), Uint128::from(2u32)); + + // works for refs + let a = Uint128::from(10u32); + let b = Uint128::from(3u32); + let expected = Uint128::from(13u32); + assert_eq!(a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint128_add_overflow_panics() { + let max = Uint128::MAX; + let _ = max + Uint128(12); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_sub_works() { + assert_eq!(Uint128(2) - Uint128(1), Uint128(1)); + assert_eq!(Uint128(2) - Uint128(0), Uint128(2)); + assert_eq!(Uint128(2) - Uint128(2), Uint128(0)); + + // works for refs + let a = Uint128::new(10); + let b = Uint128::new(3); + let expected = Uint128::new(7); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn uint128_sub_overflow_panics() { + let _ = Uint128(1) - Uint128(2); + } + + #[test] + fn uint128_sub_assign_works() { + let mut a = Uint128(14); + a -= Uint128(2); + assert_eq!(a, Uint128(12)); + + // works for refs + let mut a = Uint128::new(10); + let b = Uint128::new(3); + let expected = Uint128::new(7); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_mul_works() { + assert_eq!(Uint128::from(2u32) * Uint128::from(3u32), Uint128::from(6u32)); + assert_eq!(Uint128::from(2u32) * Uint128::zero(), Uint128::zero()); + + // works for refs + let a = Uint128::from(11u32); + let b = Uint128::from(3u32); + let expected = Uint128::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn uint128_mul_assign_works() { + let mut a = Uint128::from(14u32); + a *= Uint128::from(2u32); + assert_eq!(a, Uint128::from(28u32)); + + // works for refs + let mut a = Uint128::from(10u32); + let b = Uint128::from(3u32); + a *= &b; + assert_eq!(a, Uint128::from(30u32)); + } + + #[test] + fn uint128_pow_works() { + assert_eq!(Uint128::from(2u32).pow(2), Uint128::from(4u32)); + assert_eq!(Uint128::from(2u32).pow(10), Uint128::from(1024u32)); + } + + #[test] + #[should_panic] + fn uint128_pow_overflow_panics() { + _ = Uint128::MAX.pow(2u32); + } + + #[test] + fn uint128_multiply_ratio_works() { + let base = Uint128(500); + + // factor 1/1 + assert_eq!(base.multiply_ratio(1u128, 1u128), base); + assert_eq!(base.multiply_ratio(3u128, 3u128), base); + assert_eq!(base.multiply_ratio(654321u128, 654321u128), base); + assert_eq!(base.multiply_ratio(u128::MAX, u128::MAX), base); + + // factor 3/2 + assert_eq!(base.multiply_ratio(3u128, 2u128), Uint128(750)); + assert_eq!(base.multiply_ratio(333333u128, 222222u128), Uint128(750)); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(2u128, 3u128), Uint128(333)); + assert_eq!(base.multiply_ratio(222222u128, 333333u128), Uint128(333)); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(5u128, 6u128), Uint128(416)); + assert_eq!(base.multiply_ratio(100u128, 120u128), Uint128(416)); + } + + #[test] + fn uint128_multiply_ratio_does_not_overflow_when_result_fits() { + // Almost max value for Uint128. + let base = Uint128(u128::MAX - 9); + + assert_eq!(base.multiply_ratio(2u128, 2u128), base); + } + + #[test] + #[should_panic] + fn uint128_multiply_ratio_panicks_on_overflow() { + // Almost max value for Uint128. + let base = Uint128(u128::MAX - 9); + + assert_eq!(base.multiply_ratio(2u128, 1u128), base); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn uint128_multiply_ratio_panics_for_zero_denominator() { + _ = Uint128(500).multiply_ratio(1u128, 0u128); + } + + #[test] + fn uint128_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Uint128(500u128).checked_multiply_ratio(1u128, 0u128), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Uint128(500u128).checked_multiply_ratio(u128::MAX, 1u128), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn uint128_shr_works() { + let original = Uint128::new(u128::from_be_bytes([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ])); + + let shifted = Uint128::new(u128::from_be_bytes([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ])); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint128_shr_overflow_panics() { + let _ = Uint128::from(1u32) >> 128u32; + } + + #[test] + fn uint128_shl_works() { + let original = Uint128::new(u128::from_be_bytes([ + 64u8, 128u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ])); + + let shifted = Uint128::new(u128::from_be_bytes([ + 2u8, 0u8, 4u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ])); + + assert_eq!(original << 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint128_shl_overflow_panics() { + let _ = Uint128::from(1u32) << 128u32; + } + + #[test] + fn sum_works() { + let nums = vec![Uint128(17), Uint128(123), Uint128(540), Uint128(82)]; + let expected = Uint128(762); + + let sum_as_ref: Uint128 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Uint128 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn uint128_methods() { + // checked_* + assert!(matches!(Uint128::MAX.checked_add(Uint128(1)), Err(OverflowError { .. }))); + assert!(matches!(Uint128(1).checked_add(Uint128(1)), Ok(Uint128(2)))); + assert!(matches!(Uint128(0).checked_sub(Uint128(1)), Err(OverflowError { .. }))); + assert!(matches!(Uint128(2).checked_sub(Uint128(1)), Ok(Uint128(1)))); + assert!(matches!(Uint128::MAX.checked_mul(Uint128(2)), Err(OverflowError { .. }))); + assert!(matches!(Uint128(2).checked_mul(Uint128(2)), Ok(Uint128(4)))); + assert!(matches!(Uint128::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert!(matches!(Uint128(2).checked_pow(3), Ok(Uint128(8)))); + assert!(matches!(Uint128::MAX.checked_div(Uint128(0)), Err(DivideByZeroError { .. }))); + assert!(matches!(Uint128(6).checked_div(Uint128(2)), Ok(Uint128(3)))); + assert!(matches!( + Uint128::MAX.checked_div_euclid(Uint128(0)), + Err(DivideByZeroError { .. }) + )); + assert!(matches!(Uint128(6).checked_div_euclid(Uint128(2)), Ok(Uint128(3)),)); + assert!(matches!(Uint128::MAX.checked_rem(Uint128(0)), Err(DivideByZeroError { .. }))); + + // saturating_* + assert_eq!(Uint128::MAX.saturating_add(Uint128(1)), Uint128::MAX); + assert_eq!(Uint128(0).saturating_sub(Uint128(1)), Uint128(0)); + assert_eq!(Uint128::MAX.saturating_mul(Uint128(2)), Uint128::MAX); + assert_eq!(Uint128::MAX.saturating_pow(2), Uint128::MAX); + } + + #[test] + fn uint128_wrapping_methods() { + // wrapping_add + assert_eq!(Uint128(2).wrapping_add(Uint128(2)), Uint128(4)); // non-wrapping + assert_eq!(Uint128::MAX.wrapping_add(Uint128(1)), Uint128(0)); // wrapping + + // wrapping_sub + assert_eq!(Uint128(7).wrapping_sub(Uint128(5)), Uint128(2)); // non-wrapping + assert_eq!(Uint128(0).wrapping_sub(Uint128(1)), Uint128::MAX); // wrapping + + // wrapping_mul + assert_eq!(Uint128(3).wrapping_mul(Uint128(2)), Uint128(6)); // non-wrapping + assert_eq!(Uint128::MAX.wrapping_mul(Uint128(2)), Uint128::MAX - Uint128::one()); // wrapping + + // wrapping_pow + assert_eq!(Uint128(2).wrapping_pow(3), Uint128(8)); // non-wrapping + assert_eq!(Uint128::MAX.wrapping_pow(2), Uint128(1)); // wrapping + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_implements_rem() { + let a = Uint128::new(10); + assert_eq!(a % Uint128::new(10), Uint128::zero()); + assert_eq!(a % Uint128::new(2), Uint128::zero()); + assert_eq!(a % Uint128::new(1), Uint128::zero()); + assert_eq!(a % Uint128::new(3), Uint128::new(1)); + assert_eq!(a % Uint128::new(4), Uint128::new(2)); + + // works for refs + let a = Uint128::new(10); + let b = Uint128::new(3); + let expected = Uint128::new(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn uint128_rem_panics_for_zero() { + let _ = Uint128::new(10) % Uint128::zero(); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint128_rem_works() { + assert_eq!(Uint128::from(12u32) % Uint128::from(10u32), Uint128::from(2u32)); + assert_eq!(Uint128::from(50u32) % Uint128::from(5u32), Uint128::zero()); + + // works for refs + let a = Uint128::from(42u32); + let b = Uint128::from(5u32); + let expected = Uint128::from(2u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn uint128_rem_assign_works() { + let mut a = Uint128::from(30u32); + a %= Uint128::from(4u32); + assert_eq!(a, Uint128::from(2u32)); + + // works for refs + let mut a = Uint128::from(25u32); + let b = Uint128::from(6u32); + a %= &b; + assert_eq!(a, Uint128::from(1u32)); + } + + #[test] + fn uint128_strict_add_works() { + let a = Uint128::new(5); + let b = Uint128::new(3); + assert_eq!(a.strict_add(b), Uint128::new(8)); + assert_eq!(b.strict_add(a), Uint128::new(8)); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint128_strict_add_panics_on_overflow() { + let a = Uint128::MAX; + let b = Uint128::ONE; + let _ = a.strict_add(b); + } + + #[test] + fn uint128_strict_sub_works() { + let a = Uint128::new(5); + let b = Uint128::new(3); + assert_eq!(a.strict_sub(b), Uint128::new(2)); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn uint128_strict_sub_panics_on_overflow() { + let a = Uint128::ZERO; + let b = Uint128::ONE; + let _ = a.strict_sub(b); + } + + #[test] + fn uint128_abs_diff_works() { + let a = Uint128::from(42u32); + let b = Uint128::from(5u32); + let expected = Uint128::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + fn uint128_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected)| (Uint128::new(lhs), Uint128::new(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn mul_floor_works_with_zero() { + let fraction = (Uint128::zero(), Uint128::new(21)); + let res = Uint128::new(123456).mul_floor(fraction); + assert_eq!(Uint128::zero(), res) + } + + #[test] + fn mul_floor_does_nothing_with_one() { + let fraction = (Uint128::one(), Uint128::one()); + let res = Uint128::new(123456).mul_floor(fraction); + assert_eq!(Uint128::new(123456), res) + } + + #[test] + fn mul_floor_rounds_down_with_normal_case() { + let fraction = (8u128, 21u128); + let res = Uint128::new(123456).mul_floor(fraction); // 47030.8571 + assert_eq!(Uint128::new(47030), res) + } + + #[test] + fn mul_floor_does_not_round_on_even_divide() { + let fraction = (2u128, 5u128); + let res = Uint128::new(25).mul_floor(fraction); + assert_eq!(Uint128::new(10), res) + } + + #[test] + fn mul_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u128, 21u128); + let res = Uint128::MAX.mul_floor(fraction); // 129_631_377_874_643_224_176_523_659_974_006_937_697.14285 + assert_eq!(Uint128::new(129_631_377_874_643_224_176_523_659_974_006_937_697), res) + } + + #[test] + fn mul_floor_works_with_decimal() { + let decimal = Decimal::from_ratio(8u128, 21u128); + let res = Uint128::new(123456).mul_floor(decimal); // 47030.8571 + assert_eq!(Uint128::new(47030), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_floor_panics_on_overflow() { + let fraction = (21u128, 8u128); + _ = Uint128::MAX.mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_overflow() { + let fraction = (21u128, 8u128); + assert_eq!( + Uint128::MAX.checked_mul_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint256", + target_type: "Uint128", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_floor_panics_on_zero_div() { + let fraction = (21u128, 0u128); + _ = Uint128::new(123456).mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_zero_div() { + let fraction = (21u128, 0u128); + assert_eq!( + Uint128::new(123456).checked_mul_floor(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + fn mul_ceil_works_with_zero() { + let fraction = (Uint128::zero(), Uint128::new(21)); + let res = Uint128::new(123456).mul_ceil(fraction); + assert_eq!(Uint128::zero(), res) + } + + #[test] + fn mul_ceil_does_nothing_with_one() { + let fraction = (Uint128::one(), Uint128::one()); + let res = Uint128::new(123456).mul_ceil(fraction); + assert_eq!(Uint128::new(123456), res) + } + + #[test] + fn mul_ceil_rounds_up_with_normal_case() { + let fraction = (8u128, 21u128); + let res = Uint128::new(123456).mul_ceil(fraction); // 47030.8571 + assert_eq!(Uint128::new(47031), res) + } + + #[test] + fn mul_ceil_does_not_round_on_even_divide() { + let fraction = (2u128, 5u128); + let res = Uint128::new(25).mul_ceil(fraction); + assert_eq!(Uint128::new(10), res) + } + + #[test] + fn mul_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u128, 21u128); + let res = Uint128::MAX.mul_ceil(fraction); // 129_631_377_874_643_224_176_523_659_974_006_937_697.14285 + assert_eq!(Uint128::new(129_631_377_874_643_224_176_523_659_974_006_937_698), res) + } + + #[test] + fn mul_ceil_works_with_decimal() { + let decimal = Decimal::from_ratio(8u128, 21u128); + let res = Uint128::new(123456).mul_ceil(decimal); // 47030.8571 + assert_eq!(Uint128::new(47031), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_ceil_panics_on_overflow() { + let fraction = (21u128, 8u128); + _ = Uint128::MAX.mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_overflow() { + let fraction = (21u128, 8u128); + assert_eq!( + Uint128::MAX.checked_mul_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint256", + target_type: "Uint128", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_ceil_panics_on_zero_div() { + let fraction = (21u128, 0u128); + _ = Uint128::new(123456).mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_zero_div() { + let fraction = (21u128, 0u128); + assert_eq!( + Uint128::new(123456).checked_mul_ceil(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_floor_raises_with_zero() { + let fraction = (Uint128::zero(), Uint128::new(21)); + _ = Uint128::new(123456).div_floor(fraction); + } + + #[test] + fn div_floor_does_nothing_with_one() { + let fraction = (Uint128::one(), Uint128::one()); + let res = Uint128::new(123456).div_floor(fraction); + assert_eq!(Uint128::new(123456), res) + } + + #[test] + fn div_floor_rounds_down_with_normal_case() { + let fraction = (5u128, 21u128); + let res = Uint128::new(123456).div_floor(fraction); // 518515.2 + assert_eq!(Uint128::new(518515), res) + } + + #[test] + fn div_floor_does_not_round_on_even_divide() { + let fraction = (5u128, 2u128); + let res = Uint128::new(25).div_floor(fraction); + assert_eq!(Uint128::new(10), res) + } + + #[test] + fn div_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u128, 8u128); + let res = Uint128::MAX.div_floor(fraction); // 129_631_377_874_643_224_176_523_659_974_006_937_697.1428 + assert_eq!(Uint128::new(129_631_377_874_643_224_176_523_659_974_006_937_697), res) + } + + #[test] + fn div_floor_works_with_decimal() { + let decimal = Decimal::from_ratio(21u128, 8u128); + let res = Uint128::new(123456).div_floor(decimal); // 47030.8571 + assert_eq!(Uint128::new(47030), res) + } + + #[test] + fn div_floor_works_with_decimal_evenly() { + let res = Uint128::new(60).div_floor(Decimal::from_atomics(6u128, 0).unwrap()); + assert_eq!(res, Uint128::new(10)); + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_floor_panics_on_overflow() { + let fraction = (8u128, 21u128); + _ = Uint128::MAX.div_floor(fraction); + } + + #[test] + fn div_floor_does_not_panic_on_overflow() { + let fraction = (8u128, 21u128); + assert_eq!( + Uint128::MAX.checked_div_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint256", + target_type: "Uint128", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_ceil_raises_with_zero() { + let fraction = (Uint128::zero(), Uint128::new(21)); + _ = Uint128::new(123456).div_ceil(fraction); + } + + #[test] + fn div_ceil_does_nothing_with_one() { + let fraction = (Uint128::one(), Uint128::one()); + let res = Uint128::new(123456).div_ceil(fraction); + assert_eq!(Uint128::new(123456), res) + } + + #[test] + fn div_ceil_rounds_up_with_normal_case() { + let fraction = (5u128, 21u128); + let res = Uint128::new(123456).div_ceil(fraction); // 518515.2 + assert_eq!(Uint128::new(518516), res) + } + + #[test] + fn div_ceil_does_not_round_on_even_divide() { + let fraction = (5u128, 2u128); + let res = Uint128::new(25).div_ceil(fraction); + assert_eq!(Uint128::new(10), res) + } + + #[test] + fn div_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u128, 8u128); + let res = Uint128::MAX.div_ceil(fraction); // 129_631_377_874_643_224_176_523_659_974_006_937_697.1428 + assert_eq!(Uint128::new(129_631_377_874_643_224_176_523_659_974_006_937_698), res) + } + + #[test] + fn div_ceil_works_with_decimal() { + let decimal = Decimal::from_ratio(21u128, 8u128); + let res = Uint128::new(123456).div_ceil(decimal); // 47030.8571 + assert_eq!(Uint128::new(47031), res) + } + + #[test] + fn div_ceil_works_with_decimal_evenly() { + let res = Uint128::new(60).div_ceil(Decimal::from_atomics(6u128, 0).unwrap()); + assert_eq!(res, Uint128::new(10)); + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_ceil_panics_on_overflow() { + let fraction = (8u128, 21u128); + _ = Uint128::MAX.div_ceil(fraction); + } + + #[test] + fn div_ceil_does_not_panic_on_overflow() { + let fraction = (8u128, 21u128); + assert_eq!( + Uint128::MAX.checked_div_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint256", + target_type: "Uint128", + })), + ); + } +} diff --git a/cosmwasm/std/src/math/uint256.rs b/cosmwasm/std/src/math/uint256.rs new file mode 100644 index 00000000..5cdc2226 --- /dev/null +++ b/cosmwasm/std/src/math/uint256.rs @@ -0,0 +1,2022 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, Shl, ShlAssign, Shr, + ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + __internal::forward_ref_partial_eq, + errors::{ + CheckedMultiplyFractionError, CheckedMultiplyRatioError, ConversionOverflowError, + DivideByZeroError, OverflowError, OverflowOperation, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + impl_mul_fraction, Fraction, Int128, Int256, Int512, Int64, Uint128, Uint512, Uint64, +}; + +/// Used internally - we don't want to leak this type since we might change +/// the implementation in the future. +use bnum::types::U256; + +use super::{ + conversion::{forward_try_from, try_from_int_to_uint}, + num_consts::NumConsts, +}; + +/// An implementation of u256 that is using strings for JSON encoding/decoding, +/// such that the full u256 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances out of primitive uint types or `new` to provide big +/// endian bytes: +/// +/// ``` +/// # use cosmwasm_std::Uint256; +/// let a = Uint256::from(258u128); +/// let b = Uint256::new([ +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, +/// ]); +/// assert_eq!(a, b); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Uint256(#[schemars(with = "String")] pub(crate) U256); + +forward_ref_partial_eq!(Uint256, Uint256); + +impl Uint256 { + pub const MAX: Uint256 = Uint256(U256::MAX); + pub const MIN: Uint256 = Uint256(U256::ZERO); + + /// Creates a Uint256(value) from a big endian representation. It's just an alias for + /// [`Uint256::from_be_bytes`]. + /// + /// This method is less flexible than `from` but can be called in a const context. + pub const fn new(value: [u8; 32]) -> Self { + Self::from_be_bytes(value) + } + + /// Creates a Uint256(0) + #[inline] + pub const fn zero() -> Self { + Self(U256::ZERO) + } + + /// Creates a Uint256(1) + #[inline] + pub const fn one() -> Self { + Self(U256::ONE) + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 32]) -> Self { + let words: [u64; 4] = [ + u64::from_le_bytes([ + data[31], data[30], data[29], data[28], data[27], data[26], data[25], data[24], + ]), + u64::from_le_bytes([ + data[23], data[22], data[21], data[20], data[19], data[18], data[17], data[16], + ]), + u64::from_le_bytes([ + data[15], data[14], data[13], data[12], data[11], data[10], data[9], data[8], + ]), + u64::from_le_bytes([ + data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], + ]), + ]; + Self(U256::from_digits(words)) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 32]) -> Self { + let words: [u64; 4] = [ + u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]), + u64::from_le_bytes([ + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], + ]), + u64::from_le_bytes([ + data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23], + ]), + u64::from_le_bytes([ + data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31], + ]), + ]; + Self(U256::from_digits(words)) + } + + /// A conversion from `u128` that, unlike the one provided by the `From` trait, + /// can be used in a `const` context. + #[must_use] + pub const fn from_u128(num: u128) -> Self { + let bytes = num.to_le_bytes(); + + Self::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]) + } + + /// A conversion from `Uint128` that, unlike the one provided by the `From` trait, + /// can be used in a `const` context. + #[must_use] + pub const fn from_uint128(num: Uint128) -> Self { + Self::from_u128(num.u128()) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 32] { + let words = self.0.digits(); + let words = [ + words[3].to_be_bytes(), + words[2].to_be_bytes(), + words[1].to_be_bytes(), + words[0].to_be_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 4], [u8; 32]>(words) } + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 32] { + let words = self.0.digits(); + let words = [ + words[0].to_le_bytes(), + words[1].to_le_bytes(), + words[2].to_le_bytes(), + words[3].to_le_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 4], [u8; 32]>(words) } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns the base 2 logarithm of the number, rounded down. + /// + /// # Panics + /// + /// This function will panic if `self` is zero. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ilog2(self) -> u32 { + self.0.checked_ilog2().unwrap() + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Uint256 { + match self.checked_multiply_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedMultiplyRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedMultiplyRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator: Uint256 = numerator.into(); + let denominator: Uint256 = denominator.into(); + if denominator.is_zero() { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Uint512::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two u256 values without overflow, producing an + /// [`Uint512`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Uint256; + /// + /// let a = Uint256::MAX; + /// let result = a.full_mul(2u32); + /// assert_eq!( + /// result.to_string(), + /// "231584178474632390847141970017375815706539969331281128078915168015826259279870", + /// ); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Uint512 { + Uint512::from(self).checked_mul(Uint512::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + self.0.checked_div(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + self.checked_div(other) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 256 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 256 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + /// Strict integer addition. Computes `self + rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint256::add`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_add(self, rhs: Self) -> Self { + match self.0.checked_add(rhs.0) { + None => panic!("attempt to add with overflow"), + Some(sum) => Self(sum), + } + } + + /// Strict integer subtraction. Computes `self - rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint256::sub`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_sub(self, other: Self) -> Self { + match self.0.checked_sub(other.0) { + None => panic!("attempt to subtract with overflow"), + Some(diff) => Self(diff), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Self { + Self(self.0.abs_diff(other.0)) + } +} + +impl NumConsts for Uint256 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +impl_mul_fraction!(Uint256); + +impl From for Uint256 { + fn from(val: Uint128) -> Self { + val.u128().into() + } +} + +impl From for Uint256 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} + +impl From for Uint256 { + fn from(val: u128) -> Self { + Uint256(val.into()) + } +} + +impl From for Uint256 { + fn from(val: u64) -> Self { + Uint256(val.into()) + } +} + +impl From for Uint256 { + fn from(val: u32) -> Self { + Uint256(val.into()) + } +} + +impl From for Uint256 { + fn from(val: u16) -> Self { + Uint256(val.into()) + } +} + +impl From for Uint256 { + fn from(val: u8) -> Self { + Uint256(val.into()) + } +} + +forward_try_from!(Uint256, Uint128); +forward_try_from!(Uint256, Uint64); + +// Int to Uint +try_from_int_to_uint!(Int64, Uint256); +try_from_int_to_uint!(Int128, Uint256); +try_from_int_to_uint!(Int256, Uint256); +try_from_int_to_uint!(Int512, Uint256); + +impl TryFrom<&str> for Uint256 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Uint256 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(StdError::generic_err("Parsing u256: received empty string")); + } + + match U256::from_str_radix(s, 10) { + Ok(u) => Ok(Uint256(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing u256: {e}"))), + } + } +} + +impl From for String { + fn from(original: Uint256) -> Self { + original.to_string() + } +} + +impl fmt::Display for Uint256 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Uint256 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + self.strict_add(rhs) + } +} +forward_ref_binop!(impl Add, add for Uint256, Uint256); + +impl Sub for Uint256 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + self.strict_sub(rhs) + } +} +forward_ref_binop!(impl Sub, sub for Uint256, Uint256); + +impl SubAssign for Uint256 { + fn sub_assign(&mut self, rhs: Uint256) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Uint256, Uint256); + +impl Div for Uint256 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).expect("attempt to divide by zero")) + } +} + +impl<'a> Div<&'a Uint256> for Uint256 { + type Output = Self; + + fn div(self, rhs: &'a Uint256) -> Self::Output { + self / *rhs + } +} + +impl Rem for Uint256 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Uint256, Uint256); + +impl Not for Uint256 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl RemAssign for Uint256 { + fn rem_assign(&mut self, rhs: Uint256) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Uint256, Uint256); + +impl Mul for Uint256 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).expect("attempt to multiply with overflow")) + } +} +forward_ref_binop!(impl Mul, mul for Uint256, Uint256); + +impl MulAssign for Uint256 { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Uint256, Uint256); + +impl Shr for Uint256 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Uint256",) + }) + } +} + +impl<'a> Shr<&'a u32> for Uint256 { + type Output = Self; + + fn shr(self, rhs: &'a u32) -> Self::Output { + self.shr(*rhs) + } +} + +impl Shl for Uint256 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).expect("attempt to shift left with overflow") + } +} + +impl<'a> Shl<&'a u32> for Uint256 { + type Output = Self; + + fn shl(self, rhs: &'a u32) -> Self::Output { + self.shl(*rhs) + } +} + +impl AddAssign for Uint256 { + fn add_assign(&mut self, rhs: Uint256) { + *self = *self + rhs; + } +} + +impl<'a> AddAssign<&'a Uint256> for Uint256 { + fn add_assign(&mut self, rhs: &'a Uint256) { + *self = *self + rhs; + } +} + +impl DivAssign for Uint256 { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl<'a> DivAssign<&'a Uint256> for Uint256 { + fn div_assign(&mut self, rhs: &'a Uint256) { + *self = *self / rhs; + } +} + +impl ShrAssign for Uint256 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} + +impl<'a> ShrAssign<&'a u32> for Uint256 { + fn shr_assign(&mut self, rhs: &'a u32) { + *self = Shr::::shr(*self, *rhs); + } +} + +impl ShlAssign for Uint256 { + fn shl_assign(&mut self, rhs: u32) { + *self = self.shl(rhs); + } +} + +impl<'a> ShlAssign<&'a u32> for Uint256 { + fn shl_assign(&mut self, rhs: &'a u32) { + *self = self.shl(*rhs); + } +} + +impl Serialize for Uint256 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Uint256 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Uint256Visitor) + } +} + +struct Uint256Visitor; + +impl<'de> de::Visitor<'de> for Uint256Visitor { + type Value = Uint256; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Uint256::try_from(v).map_err(|e| E::custom(format_args!("invalid Uint256 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Uint256 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::CheckedMultiplyFractionError::{ConversionOverflow, DivideByZero}, + math::conversion::test_try_from_int_to_uint, + Decimal, Decimal256, + }; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 32); + } + + #[test] + fn uint256_new_works() { + let num = Uint256::new([1; 32]); + let a: [u8; 32] = num.to_be_bytes(); + assert_eq!(a, [1; 32]); + + let be_bytes = [ + 0u8, 222u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let num = Uint256::new(be_bytes); + let resulting_bytes: [u8; 32] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn uint256_not_works() { + let num = Uint256::new([1; 32]); + let a = (!num).to_be_bytes(); + assert_eq!(a, [254; 32]); + + assert_eq!(!Uint256::MAX, Uint256::MIN); + assert_eq!(!Uint256::MIN, Uint256::MAX); + } + + #[test] + fn uint256_zero_works() { + let zero = Uint256::zero(); + assert_eq!( + zero.to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + ] + ); + } + + #[test] + fn uin256_one_works() { + let one = Uint256::one(); + assert_eq!( + one.to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ] + ); + } + + #[test] + fn uint256_from_be_bytes() { + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(0u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 42, + ]); + assert_eq!(a, Uint256::from(42u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, + ]); + assert_eq!(a, Uint256::from(1u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, + ]); + assert_eq!(a, Uint256::from(256u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, + ]); + assert_eq!(a, Uint256::from(65536u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(16777216u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(4294967296u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1099511627776u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(281474976710656u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(72057594037927936u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(18446744073709551616u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(4722366482869645213696u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1208925819614629174706176u128)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1329227995784915872903807060280344576u128)); + + // Values > u128::MAX + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 16)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 17)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 18)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 19)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 20)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 21)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 22)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 23)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 24)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 25)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 26)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 27)); + let a = Uint256::from_be_bytes([ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 28)); + let a = Uint256::from_be_bytes([ + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 29)); + let a = Uint256::from_be_bytes([ + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 30)); + let a = Uint256::from_be_bytes([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 31)); + } + + #[test] + fn uint256_from_le_bytes() { + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(0u128)); + let a = Uint256::from_le_bytes([ + 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(42u128)); + let a = Uint256::from_le_bytes([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128)); + let a = Uint256::from_le_bytes([ + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(256u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(65536u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(16777216u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(4294967296u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(72057594037927936u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(18446744073709551616u128)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1329227995784915872903807060280344576u128)); + + // Values > u128::MAX + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 16)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 17)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 18)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 19)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 20)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 21)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 22)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 23)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 24)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 25)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 26)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 27)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 28)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 29)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 30)); + let a = Uint256::from_le_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, + ]); + assert_eq!(a, Uint256::from(1u128) << (8 * 31)); + } + + #[test] + fn uint256_endianness() { + let be_bytes = [ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let le_bytes = [ + 3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]; + + // These should all be the same. + let num1 = Uint256::new(be_bytes); + let num2 = Uint256::from_be_bytes(be_bytes); + let num3 = Uint256::from_le_bytes(le_bytes); + assert_eq!(num1, Uint256::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + assert_eq!(num1, num3); + } + + #[test] + fn uint256_convert_from() { + let a = Uint256::from(5u128); + assert_eq!(a.0, U256::from(5u32)); + + let a = Uint256::from(5u64); + assert_eq!(a.0, U256::from(5u32)); + + let a = Uint256::from(5u32); + assert_eq!(a.0, U256::from(5u32)); + + let a = Uint256::from(5u16); + assert_eq!(a.0, U256::from(5u32)); + + let a = Uint256::from(5u8); + assert_eq!(a.0, U256::from(5u32)); + + let result = Uint256::try_from("34567"); + assert_eq!(result.unwrap().0, U256::from_str_radix("34567", 10).unwrap()); + + let result = Uint256::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn uint256_try_from_signed_works() { + test_try_from_int_to_uint::("Int64", "Uint256"); + test_try_from_int_to_uint::("Int128", "Uint256"); + test_try_from_int_to_uint::("Int256", "Uint256"); + test_try_from_int_to_uint::("Int512", "Uint256"); + } + + #[test] + fn uint256_try_into() { + assert!(Uint64::try_from(Uint256::MAX).is_err()); + assert!(Uint128::try_from(Uint256::MAX).is_err()); + + assert_eq!(Uint64::try_from(Uint256::zero()), Ok(Uint64::zero())); + assert_eq!(Uint128::try_from(Uint256::zero()), Ok(Uint128::zero())); + + assert_eq!(Uint64::try_from(Uint256::from(42u64)), Ok(Uint64::from(42u64))); + assert_eq!(Uint128::try_from(Uint256::from(42u128)), Ok(Uint128::from(42u128))); + } + + #[test] + fn uint256_convert_to_uint128() { + let source = Uint256::from(42u128); + let target = Uint128::try_from(source); + assert_eq!(target, Ok(Uint128::new(42u128))); + + let source = Uint256::MAX; + let target = Uint128::try_from(source); + assert_eq!(target, Err(ConversionOverflowError::new("Uint256", "Uint128"))); + } + + #[test] + fn uint256_from_u128() { + assert_eq!(Uint256::from_u128(123u128), Uint256::from_str("123").unwrap()); + + assert_eq!( + Uint256::from_u128(9785746283745u128), + Uint256::from_str("9785746283745").unwrap() + ); + } + + #[test] + fn uint256_from_uint128() { + assert_eq!(Uint256::from_uint128(Uint128::new(123)), Uint256::from_str("123").unwrap()); + + assert_eq!( + Uint256::from_uint128(Uint128::new(9785746283745)), + Uint256::from_str("9785746283745").unwrap() + ); + } + + #[test] + fn uint256_implements_display() { + let a = Uint256::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Uint256::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn uint256_display_padding_works() { + // width > natural representation + let a = Uint256::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + + // width < natural representation + let a = Uint256::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + } + + #[test] + fn uint256_to_be_bytes_works() { + assert_eq!( + Uint256::zero().to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ] + ); + assert_eq!( + Uint256::MAX.to_be_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + ] + ); + assert_eq!( + Uint256::from(1u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1 + ] + ); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(32, "big")]` + assert_eq!( + Uint256::from(240282366920938463463374607431768124608u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 196, 179, 87, 165, 121, 59, + 133, 246, 117, 221, 191, 255, 254, 172, 192 + ] + ); + assert_eq!( + Uint256::from_be_bytes([ + 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, 238, 119, 85, 22, 14, + 88, 166, 195, 154, 73, 64, 10, 44, 252, 96, 230, 187, 38, 29 + ]) + .to_be_bytes(), + [ + 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, 238, 119, 85, 22, 14, + 88, 166, 195, 154, 73, 64, 10, 44, 252, 96, 230, 187, 38, 29 + ] + ); + } + + #[test] + fn uint256_to_le_bytes_works() { + assert_eq!( + Uint256::zero().to_le_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + ] + ); + assert_eq!( + Uint256::MAX.to_le_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff + ] + ); + assert_eq!( + Uint256::from(1u128).to_le_bytes(), + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + ] + ); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(32, "little")]` + assert_eq!( + Uint256::from(240282366920938463463374607431768124608u128).to_le_bytes(), + [ + 192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 180, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + Uint256::from_be_bytes([ + 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, 238, 119, 85, 22, 14, + 88, 166, 195, 154, 73, 64, 10, 44, 252, 96, 230, 187, 38, 29 + ]) + .to_le_bytes(), + [ + 29, 38, 187, 230, 96, 252, 44, 10, 64, 73, 154, 195, 166, 88, 14, 22, 85, 119, 238, + 134, 208, 45, 106, 88, 218, 240, 150, 115, 200, 240, 2, 233 + ] + ); + } + + #[test] + fn uint256_is_zero_works() { + assert!(Uint256::zero().is_zero()); + assert!(Uint256(U256::from(0u32)).is_zero()); + + assert!(!Uint256::from(1u32).is_zero()); + assert!(!Uint256::from(123u32).is_zero()); + } + + #[test] + fn uint256_wrapping_methods() { + // wrapping_add + assert_eq!(Uint256::from(2u32).wrapping_add(Uint256::from(2u32)), Uint256::from(4u32)); // non-wrapping + assert_eq!(Uint256::MAX.wrapping_add(Uint256::from(1u32)), Uint256::from(0u32)); // wrapping + + // wrapping_sub + assert_eq!(Uint256::from(7u32).wrapping_sub(Uint256::from(5u32)), Uint256::from(2u32)); // non-wrapping + assert_eq!(Uint256::from(0u32).wrapping_sub(Uint256::from(1u32)), Uint256::MAX); // wrapping + + // wrapping_mul + assert_eq!(Uint256::from(3u32).wrapping_mul(Uint256::from(2u32)), Uint256::from(6u32)); // non-wrapping + assert_eq!(Uint256::MAX.wrapping_mul(Uint256::from(2u32)), Uint256::MAX - Uint256::one()); // wrapping + + // wrapping_pow + assert_eq!(Uint256::from(2u32).wrapping_pow(3), Uint256::from(8u32)); // non-wrapping + assert_eq!(Uint256::MAX.wrapping_pow(2), Uint256::from(1u32)); // wrapping + } + + #[test] + fn uint256_json() { + let orig = Uint256::from(1234567890987654321u128); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Uint256 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn uint256_compare() { + let a = Uint256::from(12345u32); + let b = Uint256::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Uint256::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_math() { + let a = Uint256::from(12345u32); + let b = Uint256::from(23456u32); + + // test - with owned and reference right hand side + assert_eq!(b - a, Uint256::from(11111u32)); + assert_eq!(b - &a, Uint256::from(11111u32)); + + // test += with owned and reference right hand side + let mut c = Uint256::from(300000u32); + c += b; + assert_eq!(c, Uint256::from(323456u32)); + let mut d = Uint256::from(300000u32); + d += &b; + assert_eq!(d, Uint256::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Uint256::from(300000u32); + c -= b; + assert_eq!(c, Uint256::from(276544u32)); + let mut d = Uint256::from(300000u32); + d -= &b; + assert_eq!(d, Uint256::from(276544u32)); + + // error result on underflow (- would produce negative result) + let underflow_result = a.checked_sub(b); + let OverflowError { operation } = underflow_result.unwrap_err(); + assert_eq!(operation, OverflowOperation::Sub); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_add_works() { + assert_eq!(Uint256::from(2u32) + Uint256::from(1u32), Uint256::from(3u32)); + assert_eq!(Uint256::from(2u32) + Uint256::from(0u32), Uint256::from(2u32)); + + // works for refs + let a = Uint256::from(10u32); + let b = Uint256::from(3u32); + let expected = Uint256::from(13u32); + assert_eq!(a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint256_add_overflow_panics() { + let max = Uint256::new([255u8; 32]); + let _ = max + Uint256::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_sub_works() { + assert_eq!(Uint256::from(2u32) - Uint256::from(1u32), Uint256::from(1u32)); + assert_eq!(Uint256::from(2u32) - Uint256::from(0u32), Uint256::from(2u32)); + assert_eq!(Uint256::from(2u32) - Uint256::from(2u32), Uint256::from(0u32)); + + // works for refs + let a = Uint256::from(10u32); + let b = Uint256::from(3u32); + let expected = Uint256::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn uint256_sub_overflow_panics() { + let _ = Uint256::from(1u32) - Uint256::from(2u32); + } + + #[test] + fn uint256_sub_assign_works() { + let mut a = Uint256::from(14u32); + a -= Uint256::from(2u32); + assert_eq!(a, Uint256::from(12u32)); + + // works for refs + let mut a = Uint256::from(10u32); + let b = Uint256::from(3u32); + let expected = Uint256::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_mul_works() { + assert_eq!(Uint256::from(2u32) * Uint256::from(3u32), Uint256::from(6u32)); + assert_eq!(Uint256::from(2u32) * Uint256::zero(), Uint256::zero()); + + // works for refs + let a = Uint256::from(11u32); + let b = Uint256::from(3u32); + let expected = Uint256::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn uint256_mul_assign_works() { + let mut a = Uint256::from(14u32); + a *= Uint256::from(2u32); + assert_eq!(a, Uint256::from(28u32)); + + // works for refs + let mut a = Uint256::from(10u32); + let b = Uint256::from(3u32); + a *= &b; + assert_eq!(a, Uint256::from(30u32)); + } + + #[test] + fn uint256_pow_works() { + assert_eq!(Uint256::from(2u32).pow(2), Uint256::from(4u32)); + assert_eq!(Uint256::from(2u32).pow(10), Uint256::from(1024u32)); + } + + #[test] + #[should_panic] + fn uint256_pow_overflow_panics() { + _ = Uint256::MAX.pow(2u32); + } + + #[test] + fn uint256_multiply_ratio_works() { + let base = Uint256::from(500u32); + + // factor 1/1 + assert_eq!(base.multiply_ratio(1u128, 1u128), base); + assert_eq!(base.multiply_ratio(3u128, 3u128), base); + assert_eq!(base.multiply_ratio(654321u128, 654321u128), base); + assert_eq!(base.multiply_ratio(Uint256::MAX, Uint256::MAX), base); + + // factor 3/2 + assert_eq!(base.multiply_ratio(3u128, 2u128), Uint256::from(750u32)); + assert_eq!(base.multiply_ratio(333333u128, 222222u128), Uint256::from(750u32)); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(2u128, 3u128), Uint256::from(333u32)); + assert_eq!(base.multiply_ratio(222222u128, 333333u128), Uint256::from(333u32)); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(5u128, 6u128), Uint256::from(416u32)); + assert_eq!(base.multiply_ratio(100u128, 120u128), Uint256::from(416u32)); + } + + #[test] + fn uint256_multiply_ratio_does_not_overflow_when_result_fits() { + // Almost max value for Uint256. + let base = Uint256::MAX - Uint256::from(9u8); + + assert_eq!(base.multiply_ratio(2u128, 2u128), base); + } + + #[test] + #[should_panic] + fn uint256_multiply_ratio_panicks_on_overflow() { + // Almost max value for Uint256. + let base = Uint256::MAX - Uint256::from(9u8); + + assert_eq!(base.multiply_ratio(2u128, 1u128), base); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn uint256_multiply_ratio_panics_for_zero_denominator() { + _ = Uint256::from(500u32).multiply_ratio(1u128, 0u128); + } + + #[test] + fn uint256_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Uint256::from(500u32).checked_multiply_ratio(1u128, 0u128), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Uint256::from(500u32).checked_multiply_ratio(Uint256::MAX, 1u128), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn uint256_shr_works() { + let original = Uint256::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ]); + + let shifted = Uint256::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint256_shr_overflow_panics() { + let _ = Uint256::from(1u32) >> 256u32; + } + + #[test] + fn uint256_shl_works() { + let original = Uint256::new([ + 64u8, 128u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]); + + let shifted = Uint256::new([ + 2u8, 0u8, 4u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]); + + assert_eq!(original << 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint256_shl_overflow_panics() { + let _ = Uint256::from(1u32) << 256u32; + } + + #[test] + fn sum_works() { + let nums = vec![ + Uint256::from(17u32), + Uint256::from(123u32), + Uint256::from(540u32), + Uint256::from(82u32), + ]; + let expected = Uint256::from(762u32); + + let sum_as_ref: Uint256 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Uint256 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn uint256_methods() { + // checked_* + assert!(matches!(Uint256::MAX.checked_add(Uint256::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Uint256::from(1u32).checked_add(Uint256::from(1u32)), Ok(Uint256::from(2u32)),); + assert!(matches!( + Uint256::from(0u32).checked_sub(Uint256::from(1u32)), + Err(OverflowError { .. }) + )); + assert_eq!(Uint256::from(2u32).checked_sub(Uint256::from(1u32)), Ok(Uint256::from(1u32)),); + assert!(matches!(Uint256::MAX.checked_mul(Uint256::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Uint256::from(2u32).checked_mul(Uint256::from(2u32)), Ok(Uint256::from(4u32)),); + assert!(matches!(Uint256::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Uint256::from(2u32).checked_pow(3u32), Ok(Uint256::from(8u32)),); + assert!(matches!( + Uint256::MAX.checked_div(Uint256::from(0u32)), + Err(DivideByZeroError { .. }) + )); + assert_eq!(Uint256::from(6u32).checked_div(Uint256::from(2u32)), Ok(Uint256::from(3u32)),); + assert!(matches!( + Uint256::MAX.checked_div_euclid(Uint256::from(0u32)), + Err(DivideByZeroError { .. }) + )); + assert_eq!( + Uint256::from(6u32).checked_div_euclid(Uint256::from(2u32)), + Ok(Uint256::from(3u32)), + ); + assert_eq!( + Uint256::from(7u32).checked_div_euclid(Uint256::from(2u32)), + Ok(Uint256::from(3u32)), + ); + assert!(matches!( + Uint256::MAX.checked_rem(Uint256::from(0u32)), + Err(DivideByZeroError { .. }) + )); + + // saturating_* + assert_eq!(Uint256::MAX.saturating_add(Uint256::from(1u32)), Uint256::MAX); + assert_eq!(Uint256::from(0u32).saturating_sub(Uint256::from(1u32)), Uint256::from(0u32)); + assert_eq!(Uint256::MAX.saturating_mul(Uint256::from(2u32)), Uint256::MAX); + assert_eq!(Uint256::from(4u32).saturating_pow(2u32), Uint256::from(16u32)); + assert_eq!(Uint256::MAX.saturating_pow(2u32), Uint256::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_implements_rem() { + let a = Uint256::from(10u32); + assert_eq!(a % Uint256::from(10u32), Uint256::zero()); + assert_eq!(a % Uint256::from(2u32), Uint256::zero()); + assert_eq!(a % Uint256::from(1u32), Uint256::zero()); + assert_eq!(a % Uint256::from(3u32), Uint256::from(1u32)); + assert_eq!(a % Uint256::from(4u32), Uint256::from(2u32)); + + // works for refs + let a = Uint256::from(10u32); + let b = Uint256::from(3u32); + let expected = Uint256::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn uint256_rem_panics_for_zero() { + let _ = Uint256::from(10u32) % Uint256::zero(); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint256_rem_works() { + assert_eq!(Uint256::from(12u32) % Uint256::from(10u32), Uint256::from(2u32)); + assert_eq!(Uint256::from(50u32) % Uint256::from(5u32), Uint256::zero()); + + // works for refs + let a = Uint256::from(42u32); + let b = Uint256::from(5u32); + let expected = Uint256::from(2u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn uint256_rem_assign_works() { + let mut a = Uint256::from(30u32); + a %= Uint256::from(4u32); + assert_eq!(a, Uint256::from(2u32)); + + // works for refs + let mut a = Uint256::from(25u32); + let b = Uint256::from(6u32); + a %= &b; + assert_eq!(a, Uint256::from(1u32)); + } + + #[test] + fn uint256_strict_add_works() { + let a = Uint256::from(5u32); + let b = Uint256::from(3u32); + assert_eq!(a.strict_add(b), Uint256::from(8u32)); + assert_eq!(b.strict_add(a), Uint256::from(8u32)); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint256_strict_add_panics_on_overflow() { + let a = Uint256::MAX; + let b = Uint256::ONE; + let _ = a.strict_add(b); + } + + #[test] + fn uint256_strict_sub_works() { + let a = Uint256::from(5u32); + let b = Uint256::from(3u32); + assert_eq!(a.strict_sub(b), Uint256::from(2u32)); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn uint256_strict_sub_panics_on_overflow() { + let a = Uint256::ZERO; + let b = Uint256::ONE; + let _ = a.strict_sub(b); + } + + #[test] + fn uint256_abs_diff_works() { + let a = Uint256::from(42u32); + let b = Uint256::from(5u32); + let expected = Uint256::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + fn uint256_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (u64, u64, bool)| { + (Uint256::from(lhs), Uint256::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn mul_floor_works_with_zero() { + let fraction = (Uint256::zero(), Uint256::from(21u32)); + let res = Uint256::from(123456u32).mul_floor(fraction); + assert_eq!(Uint256::zero(), res) + } + + #[test] + fn mul_floor_does_nothing_with_one() { + let fraction = (Uint256::one(), Uint256::one()); + let res = Uint256::from(123456u32).mul_floor(fraction); + assert_eq!(Uint256::from(123456u32), res) + } + + #[test] + fn mul_floor_rounds_down_with_normal_case() { + let fraction = (Uint256::from(8u128), Uint256::from(21u128)); + let res = Uint256::from(123456u32).mul_floor(fraction); // 47030.8571 + assert_eq!(Uint256::from(47030u32), res) + } + + #[test] + fn mul_floor_does_not_round_on_even_divide() { + let fraction = (2u128, 5u128); + let res = Uint256::from(25u32).mul_floor(fraction); + assert_eq!(Uint256::from(10u32), res) + } + + #[test] + fn mul_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u128, 21u128); + let res = Uint256::MAX.mul_floor(fraction); // 44_111_272_090_406_169_685_169_899_050_928_726_801_245_708_444_053_548_205_507_651_050_633_573_196_165.71428571 + assert_eq!( + Uint256::from_str( + "44111272090406169685169899050928726801245708444053548205507651050633573196165" + ) + .unwrap(), + res + ) + } + + #[test] + fn mul_floor_works_with_decimal() { + let decimal = Decimal::from_ratio(8u128, 21u128); + let res = Uint256::from(123456u32).mul_floor(decimal); // 47030.8571 + assert_eq!(Uint256::from(47030u32), res) + } + + #[test] + fn mul_floor_works_with_decimal256() { + let decimal = Decimal256::from_ratio(8u128, 21u128); + let res = Uint256::from(123456u32).mul_floor(decimal); // 47030.8571 + assert_eq!(Uint256::from(47030u32), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_floor_panics_on_overflow() { + let fraction = (21u128, 8u128); + _ = Uint256::MAX.mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_overflow() { + let fraction = (21u128, 8u128); + assert_eq!( + Uint256::MAX.checked_mul_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint512", + target_type: "Uint256", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_floor_panics_on_zero_div() { + let fraction = (21u128, 0u128); + _ = Uint256::from(123456u32).mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_zero_div() { + let fraction = (21u128, 0u128); + assert_eq!( + Uint256::from(123456u32).checked_mul_floor(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + fn mul_ceil_works_with_zero() { + let fraction = (Uint256::zero(), Uint256::from(21u32)); + let res = Uint256::from(123456u32).mul_ceil(fraction); + assert_eq!(Uint256::zero(), res) + } + + #[test] + fn mul_ceil_does_nothing_with_one() { + let fraction = (Uint256::one(), Uint256::one()); + let res = Uint256::from(123456u32).mul_ceil(fraction); + assert_eq!(Uint256::from(123456u32), res) + } + + #[test] + fn mul_ceil_rounds_up_with_normal_case() { + let fraction = (8u128, 21u128); + let res = Uint256::from(123456u32).mul_ceil(fraction); // 47030.8571 + assert_eq!(Uint256::from(47031u32), res) + } + + #[test] + fn mul_ceil_does_not_round_on_even_divide() { + let fraction = (2u128, 5u128); + let res = Uint256::from(25u32).mul_ceil(fraction); + assert_eq!(Uint256::from(10u32), res) + } + + #[test] + fn mul_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u128, 21u128); + let res = Uint256::MAX.mul_ceil(fraction); // 44_111_272_090_406_169_685_169_899_050_928_726_801_245_708_444_053_548_205_507_651_050_633_573_196_165.71428571 + assert_eq!( + Uint256::from_str( + "44111272090406169685169899050928726801245708444053548205507651050633573196166" + ) + .unwrap(), + res + ) + } + + #[test] + fn mul_ceil_works_with_decimal() { + let decimal = Decimal::from_ratio(8u128, 21u128); + let res = Uint256::from(123456u32).mul_ceil(decimal); // 47030.8571 + assert_eq!(Uint256::from(47031u32), res) + } + + #[test] + fn mul_ceil_works_with_decimal256() { + let decimal = Decimal256::from_ratio(8u128, 21u128); + let res = Uint256::from(123456u32).mul_ceil(decimal); // 47030.8571 + assert_eq!(Uint256::from(47031u32), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_ceil_panics_on_overflow() { + let fraction = (21u128, 8u128); + _ = Uint256::MAX.mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_overflow() { + let fraction = (21u128, 8u128); + assert_eq!( + Uint256::MAX.checked_mul_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint512", + target_type: "Uint256", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_ceil_panics_on_zero_div() { + let fraction = (21u128, 0u128); + _ = Uint256::from(123456u32).mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_zero_div() { + let fraction = (21u128, 0u128); + assert_eq!( + Uint256::from(123456u32).checked_mul_ceil(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_floor_raises_with_zero() { + let fraction = (Uint256::zero(), Uint256::from(21u32)); + _ = Uint256::from(123456u128).div_floor(fraction); + } + + #[test] + fn div_floor_does_nothing_with_one() { + let fraction = (Uint256::one(), Uint256::one()); + let res = Uint256::from(123456u128).div_floor(fraction); + assert_eq!(Uint256::from(123456u128), res) + } + + #[test] + fn div_floor_rounds_down_with_normal_case() { + let fraction = (5u128, 21u128); + let res = Uint256::from(123456u128).div_floor(fraction); // 518515.2 + assert_eq!(Uint256::from(518515u128), res) + } + + #[test] + fn div_floor_does_not_round_on_even_divide() { + let fraction = (5u128, 2u128); + let res = Uint256::from(25u128).div_floor(fraction); + assert_eq!(Uint256::from(10u128), res) + } + + #[test] + fn div_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u128, 8u128); + let res = Uint256::MAX.div_floor(fraction); // 44_111_272_090_406_169_685_169_899_050_928_726_801_245_708_444_053_548_205_507_651_050_633_573_196_165.71428571 + assert_eq!( + Uint256::from_str( + "44111272090406169685169899050928726801245708444053548205507651050633573196165" + ) + .unwrap(), + res + ) + } + + #[test] + fn div_floor_works_with_decimal() { + let decimal = Decimal::from_ratio(21u128, 8u128); + let res = Uint256::from(123456u128).div_floor(decimal); // 47030.8571 + assert_eq!(Uint256::from(47030u128), res) + } + + #[test] + fn div_floor_works_with_decimal_evenly() { + let res = Uint256::from(60u128).div_floor(Decimal::from_atomics(6u128, 0).unwrap()); + assert_eq!(res, Uint256::from(10u128)); + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_floor_panics_on_overflow() { + let fraction = (8u128, 21u128); + _ = Uint256::MAX.div_floor(fraction); + } + + #[test] + fn div_floor_does_not_panic_on_overflow() { + let fraction = (8u128, 21u128); + assert_eq!( + Uint256::MAX.checked_div_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint512", + target_type: "Uint256", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_ceil_raises_with_zero() { + let fraction = (Uint256::zero(), Uint256::from(21u128)); + _ = Uint256::from(123456u128).div_ceil(fraction); + } + + #[test] + fn div_ceil_does_nothing_with_one() { + let fraction = (Uint256::one(), Uint256::one()); + let res = Uint256::from(123456u128).div_ceil(fraction); + assert_eq!(Uint256::from(123456u128), res) + } + + #[test] + fn div_ceil_rounds_up_with_normal_case() { + let fraction = (5u128, 21u128); + let res = Uint256::from(123456u128).div_ceil(fraction); // 518515.2 + assert_eq!(Uint256::from(518516u128), res) + } + + #[test] + fn div_ceil_does_not_round_on_even_divide() { + let fraction = (5u128, 2u128); + let res = Uint256::from(25u128).div_ceil(fraction); + assert_eq!(Uint256::from(10u128), res) + } + + #[test] + fn div_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u128, 8u128); + let res = Uint256::MAX.div_ceil(fraction); // 44_111_272_090_406_169_685_169_899_050_928_726_801_245_708_444_053_548_205_507_651_050_633_573_196_165.71428571 + assert_eq!( + Uint256::from_str( + "44111272090406169685169899050928726801245708444053548205507651050633573196166" + ) + .unwrap(), + res + ) + } + + #[test] + fn div_ceil_works_with_decimal() { + let decimal = Decimal::from_ratio(21u128, 8u128); + let res = Uint256::from(123456u128).div_ceil(decimal); // 47030.8571 + assert_eq!(Uint256::from(47031u128), res) + } + + #[test] + fn div_ceil_works_with_decimal_evenly() { + let res = Uint256::from(60u128).div_ceil(Decimal::from_atomics(6u128, 0).unwrap()); + assert_eq!(res, Uint256::from(10u128)); + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_ceil_panics_on_overflow() { + let fraction = (8u128, 21u128); + _ = Uint256::MAX.div_ceil(fraction); + } + + #[test] + fn div_ceil_does_not_panic_on_overflow() { + let fraction = (8u128, 21u128); + assert_eq!( + Uint256::MAX.checked_div_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint512", + target_type: "Uint256", + })), + ); + } +} diff --git a/cosmwasm/std/src/math/uint512.rs b/cosmwasm/std/src/math/uint512.rs new file mode 100644 index 00000000..783de3c5 --- /dev/null +++ b/cosmwasm/std/src/math/uint512.rs @@ -0,0 +1,1367 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, Shl, ShlAssign, Shr, + ShrAssign, Sub, SubAssign, + }, + str::FromStr, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + errors::{ + ConversionOverflowError, DivideByZeroError, OverflowError, OverflowOperation, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + Int128, Int256, Int512, Int64, Uint128, Uint256, Uint64, + __internal::forward_ref_partial_eq, +}; + +/// Used internally - we don't want to leak this type since we might change +/// the implementation in the future. +use bnum::types::U512; + +use super::{ + conversion::{forward_try_from, try_from_int_to_uint}, + num_consts::NumConsts, +}; + +/// An implementation of u512 that is using strings for JSON encoding/decoding, +/// such that the full u512 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances out of primitive uint types or `new` to provide big +/// endian bytes: +/// +/// ``` +/// # use cosmwasm_std::Uint512; +/// let a = Uint512::from(258u128); +/// let b = Uint512::new([ +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, +/// 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, +/// ]); +/// assert_eq!(a, b); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Uint512(#[schemars(with = "String")] pub(crate) U512); + +forward_ref_partial_eq!(Uint512, Uint512); + +impl Uint512 { + pub const MAX: Uint512 = Uint512(U512::MAX); + pub const MIN: Uint512 = Uint512(U512::ZERO); + + /// Creates a Uint512(value) from a big endian representation. It's just an alias for + /// `from_be_bytes`. + pub const fn new(value: [u8; 64]) -> Self { + Self::from_be_bytes(value) + } + + /// Creates a Uint512(0) + #[inline] + pub const fn zero() -> Self { + Uint512(U512::ZERO) + } + + /// Creates a Uint512(1) + #[inline] + pub const fn one() -> Self { + Self(U512::ONE) + } + + #[must_use] + pub const fn from_be_bytes(data: [u8; 64]) -> Self { + let words: [u64; 8] = [ + u64::from_le_bytes([ + data[63], data[62], data[61], data[60], data[59], data[58], data[57], data[56], + ]), + u64::from_le_bytes([ + data[55], data[54], data[53], data[52], data[51], data[50], data[49], data[48], + ]), + u64::from_le_bytes([ + data[47], data[46], data[45], data[44], data[43], data[42], data[41], data[40], + ]), + u64::from_le_bytes([ + data[39], data[38], data[37], data[36], data[35], data[34], data[33], data[32], + ]), + u64::from_le_bytes([ + data[31], data[30], data[29], data[28], data[27], data[26], data[25], data[24], + ]), + u64::from_le_bytes([ + data[23], data[22], data[21], data[20], data[19], data[18], data[17], data[16], + ]), + u64::from_le_bytes([ + data[15], data[14], data[13], data[12], data[11], data[10], data[9], data[8], + ]), + u64::from_le_bytes([ + data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], + ]), + ]; + Self(U512::from_digits(words)) + } + + #[must_use] + pub const fn from_le_bytes(data: [u8; 64]) -> Self { + let words: [u64; 8] = [ + u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]), + u64::from_le_bytes([ + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], + ]), + u64::from_le_bytes([ + data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23], + ]), + u64::from_le_bytes([ + data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31], + ]), + u64::from_le_bytes([ + data[32], data[33], data[34], data[35], data[36], data[37], data[38], data[39], + ]), + u64::from_le_bytes([ + data[40], data[41], data[42], data[43], data[44], data[45], data[46], data[47], + ]), + u64::from_le_bytes([ + data[48], data[49], data[50], data[51], data[52], data[53], data[54], data[55], + ]), + u64::from_le_bytes([ + data[56], data[57], data[58], data[59], data[60], data[61], data[62], data[63], + ]), + ]; + Self(U512::from_digits(words)) + } + + /// A conversion from `Uint256` that, unlike the one provided by the `From` trait, + /// can be used in a `const` context. + #[must_use] + pub const fn from_uint256(num: Uint256) -> Self { + let bytes = num.to_le_bytes(); + Self::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]) + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 64] { + let words = self.0.digits(); + let words = [ + words[7].to_be_bytes(), + words[6].to_be_bytes(), + words[5].to_be_bytes(), + words[4].to_be_bytes(), + words[3].to_be_bytes(), + words[2].to_be_bytes(), + words[1].to_be_bytes(), + words[0].to_be_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 8], [u8; 64]>(words) } + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 64] { + let words = self.0.digits(); + let words = [ + words[0].to_le_bytes(), + words[1].to_le_bytes(), + words[2].to_le_bytes(), + words[3].to_le_bytes(), + words[4].to_le_bytes(), + words[5].to_le_bytes(), + words[6].to_le_bytes(), + words[7].to_le_bytes(), + ]; + unsafe { core::mem::transmute::<[[u8; 8]; 8], [u8; 64]>(words) } + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0.is_zero() + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns the base 2 logarithm of the number, rounded down. + /// + /// # Panics + /// + /// This function will panic if `self` is zero. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ilog2(self) -> u32 { + self.0.checked_ilog2().unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + self.0.checked_div(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + self.checked_div(other) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + self.0 + .checked_shr(other) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Shr)) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 512 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + /// Strict integer addition. Computes `self + rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint512::add`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_add(self, rhs: Self) -> Self { + match self.0.checked_add(rhs.0) { + None => panic!("attempt to add with overflow"), + Some(sum) => Self(sum), + } + } + + /// Strict integer subtraction. Computes `self - rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint512::sub`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_sub(self, other: Self) -> Self { + match self.0.checked_sub(other.0) { + None => panic!("attempt to subtract with overflow"), + Some(diff) => Self(diff), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Self { + Self(self.0.abs_diff(other.0)) + } +} + +impl NumConsts for Uint512 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +impl From for Uint512 { + fn from(val: Uint256) -> Self { + let mut bytes = [0u8; 64]; + bytes[32..].copy_from_slice(&val.to_be_bytes()); + + Self::from_be_bytes(bytes) + } +} + +impl From for Uint512 { + fn from(val: Uint128) -> Self { + val.u128().into() + } +} + +impl From for Uint512 { + fn from(val: Uint64) -> Self { + val.u64().into() + } +} + +impl From for Uint512 { + fn from(val: u128) -> Self { + Uint512(val.into()) + } +} + +impl From for Uint512 { + fn from(val: u64) -> Self { + Uint512(val.into()) + } +} + +impl From for Uint512 { + fn from(val: u32) -> Self { + Uint512(val.into()) + } +} + +impl From for Uint512 { + fn from(val: u16) -> Self { + Uint512(val.into()) + } +} + +impl From for Uint512 { + fn from(val: u8) -> Self { + Uint512(val.into()) + } +} + +impl TryFrom for Uint256 { + type Error = ConversionOverflowError; + + fn try_from(value: Uint512) -> Result { + let bytes = value.to_be_bytes(); + let (first_bytes, last_bytes) = bytes.split_at(32); + + if first_bytes != [0u8; 32] { + return Err(ConversionOverflowError::new("Uint512", "Uint256")); + } + + Ok(Self::from_be_bytes(last_bytes.try_into().unwrap())) + } +} + +forward_try_from!(Uint512, Uint128); +forward_try_from!(Uint512, Uint64); + +// Int to Uint +try_from_int_to_uint!(Int64, Uint512); +try_from_int_to_uint!(Int128, Uint512); +try_from_int_to_uint!(Int256, Uint512); +try_from_int_to_uint!(Int512, Uint512); + +impl TryFrom<&str> for Uint512 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + Self::from_str(val) + } +} + +impl FromStr for Uint512 { + type Err = StdError; + + fn from_str(s: &str) -> Result { + match U512::from_str_radix(s, 10) { + Ok(u) => Ok(Self(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing u512: {e}"))), + } + } +} + +impl From for String { + fn from(original: Uint512) -> Self { + original.to_string() + } +} + +impl fmt::Display for Uint512 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Uint512 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + self.strict_add(rhs) + } +} +forward_ref_binop!(impl Add, add for Uint512, Uint512); + +impl Sub for Uint512 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + self.strict_sub(rhs) + } +} +forward_ref_binop!(impl Sub, sub for Uint512, Uint512); + +impl SubAssign for Uint512 { + fn sub_assign(&mut self, rhs: Uint512) { + self.0 = self.0.checked_sub(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Uint512, Uint512); + +impl Div for Uint512 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} + +impl<'a> Div<&'a Uint512> for Uint512 { + type Output = Self; + + fn div(self, rhs: &'a Uint512) -> Self::Output { + Self(self.0.checked_div(rhs.0).unwrap()) + } +} + +impl Rem for Uint512 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Uint512, Uint512); + +impl Not for Uint512 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl RemAssign for Uint512 { + fn rem_assign(&mut self, rhs: Uint512) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Uint512, Uint512); + +impl Mul for Uint512 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0.checked_mul(rhs.0).unwrap()) + } +} +forward_ref_binop!(impl Mul, mul for Uint512, Uint512); + +impl MulAssign for Uint512 { + fn mul_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_mul(rhs.0).unwrap(); + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Uint512, Uint512); + +impl Shr for Uint512 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + self.checked_shr(rhs).unwrap_or_else(|_| { + panic!("right shift error: {rhs} is larger or equal than the number of bits in Uint512",) + }) + } +} + +impl<'a> Shr<&'a u32> for Uint512 { + type Output = Self; + + fn shr(self, rhs: &'a u32) -> Self::Output { + Shr::::shr(self, *rhs) + } +} + +impl Shl for Uint512 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + self.checked_shl(rhs).expect("attempt to shift left with overflow") + } +} + +impl<'a> Shl<&'a u32> for Uint512 { + type Output = Self; + + fn shl(self, rhs: &'a u32) -> Self::Output { + self.shl(*rhs) + } +} + +impl AddAssign for Uint512 { + fn add_assign(&mut self, rhs: Uint512) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} + +impl<'a> AddAssign<&'a Uint512> for Uint512 { + fn add_assign(&mut self, rhs: &'a Uint512) { + self.0 = self.0.checked_add(rhs.0).unwrap(); + } +} + +impl DivAssign for Uint512 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} + +impl<'a> DivAssign<&'a Uint512> for Uint512 { + fn div_assign(&mut self, rhs: &'a Uint512) { + self.0 = self.0.checked_div(rhs.0).unwrap(); + } +} + +impl ShrAssign for Uint512 { + fn shr_assign(&mut self, rhs: u32) { + *self = Shr::::shr(*self, rhs); + } +} + +impl<'a> ShrAssign<&'a u32> for Uint512 { + fn shr_assign(&mut self, rhs: &'a u32) { + *self = Shr::::shr(*self, *rhs); + } +} + +impl ShlAssign for Uint512 { + fn shl_assign(&mut self, rhs: u32) { + *self = self.shl(rhs); + } +} + +impl<'a> ShlAssign<&'a u32> for Uint512 { + fn shl_assign(&mut self, rhs: &'a u32) { + *self = self.shl(*rhs); + } +} + +impl Serialize for Uint512 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Uint512 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Uint512Visitor) + } +} + +struct Uint512Visitor; + +impl<'de> de::Visitor<'de> for Uint512Visitor { + type Value = Uint512; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Uint512::try_from(v).map_err(|e| E::custom(format_args!("invalid Uint512 '{v}' - {e}"))) + } +} + +impl core::iter::Sum for Uint512 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::conversion::test_try_from_int_to_uint; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 64); + } + + #[test] + fn uint512_new_works() { + let num = Uint512::new([1; 64]); + let a: [u8; 64] = num.to_be_bytes(); + assert_eq!(a, [1; 64]); + + let be_bytes = [ + 0u8, 222u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let num = Uint512::new(be_bytes); + let resulting_bytes: [u8; 64] = num.to_be_bytes(); + assert_eq!(be_bytes, resulting_bytes); + } + + #[test] + fn uint512_not_works() { + let num = Uint512::new([1; 64]); + let a = (!num).to_be_bytes(); + assert_eq!(a, [254; 64]); + + assert_eq!(!Uint512::MAX, Uint512::MIN); + assert_eq!(!Uint512::MIN, Uint512::MAX); + } + + #[test] + fn uint512_zero_works() { + let zero = Uint512::zero(); + assert_eq!( + zero.to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } + + #[test] + fn uin512_one_works() { + let one = Uint512::one(); + assert_eq!( + one.to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); + } + + #[test] + fn uint512_endianness() { + let be_bytes = [ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, 3u8, + ]; + let le_bytes = [ + 3u8, 2u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]; + + // These should all be the same. + let num1 = Uint512::new(be_bytes); + let num2 = Uint512::from_be_bytes(be_bytes); + let num3 = Uint512::from_le_bytes(le_bytes); + assert_eq!(num1, Uint512::from(65536u32 + 512 + 3)); + assert_eq!(num1, num2); + assert_eq!(num1, num3); + } + + #[test] + fn uint512_convert_from() { + let a = Uint512::from(5u128); + assert_eq!(a.0, U512::from(5u32)); + + let a = Uint512::from(5u64); + assert_eq!(a.0, U512::from(5u32)); + + let a = Uint512::from(5u32); + assert_eq!(a.0, U512::from(5u32)); + + let a = Uint512::from(5u16); + assert_eq!(a.0, U512::from(5u32)); + + let a = Uint512::from(5u8); + assert_eq!(a.0, U512::from(5u32)); + + let result = Uint512::try_from("34567"); + assert_eq!(result.unwrap().0, U512::from_str_radix("34567", 10).unwrap()); + + let result = Uint512::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn uint512_try_from_signed_works() { + test_try_from_int_to_uint::("Int64", "Uint512"); + test_try_from_int_to_uint::("Int128", "Uint512"); + test_try_from_int_to_uint::("Int256", "Uint512"); + test_try_from_int_to_uint::("Int512", "Uint512"); + } + + #[test] + fn uint512_try_into() { + assert!(Uint64::try_from(Uint512::MAX).is_err()); + assert!(Uint128::try_from(Uint512::MAX).is_err()); + assert!(Uint256::try_from(Uint512::MAX).is_err()); + + assert_eq!(Uint64::try_from(Uint512::zero()), Ok(Uint64::zero())); + assert_eq!(Uint128::try_from(Uint512::zero()), Ok(Uint128::zero())); + assert_eq!(Uint256::try_from(Uint512::zero()), Ok(Uint256::zero())); + + assert_eq!(Uint64::try_from(Uint512::from(42u64)), Ok(Uint64::from(42u64))); + assert_eq!(Uint128::try_from(Uint512::from(42u128)), Ok(Uint128::from(42u128))); + assert_eq!(Uint256::try_from(Uint512::from(42u128)), Ok(Uint256::from(42u128))); + } + + #[test] + fn uint512_convert_to_uint128() { + let source = Uint512::from(42u128); + let target = Uint128::try_from(source); + assert_eq!(target, Ok(Uint128::new(42u128))); + + let source = Uint512::MAX; + let target = Uint128::try_from(source); + assert_eq!(target, Err(ConversionOverflowError::new("Uint512", "Uint128"))); + } + + #[test] + fn uint512_from_uint256() { + assert_eq!( + Uint512::from_uint256(Uint256::from_str("123").unwrap()), + Uint512::from_str("123").unwrap() + ); + + assert_eq!( + Uint512::from_uint256(Uint256::from_str("9785746283745").unwrap()), + Uint512::from_str("9785746283745").unwrap() + ); + + assert_eq!( + Uint512::from_uint256( + Uint256::from_str( + "97857462837575757832978493758398593853985452378423874623874628736482736487236" + ) + .unwrap() + ), + Uint512::from_str( + "97857462837575757832978493758398593853985452378423874623874628736482736487236" + ) + .unwrap() + ); + } + + #[test] + fn uint512_implements_display() { + let a = Uint512::from(12345u32); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Uint512::zero(); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn uint512_display_padding_works() { + // width > natural representation + let a = Uint512::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + + // width < natural representation + let a = Uint512::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + } + + #[test] + fn uint512_to_be_bytes_works() { + assert_eq!( + Uint512::zero().to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + assert_eq!( + Uint512::MAX.to_be_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ] + ); + assert_eq!( + Uint512::from(1u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(64, "big")]` + assert_eq!( + Uint512::from(240282366920938463463374607431768124608u128).to_be_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 196, 179, 87, 165, + 121, 59, 133, 246, 117, 221, 191, 255, 254, 172, 192 + ] + ); + assert_eq!( + Uint512::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ]) + .to_be_bytes(), + [ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ] + ); + } + + #[test] + fn uint512_to_le_bytes_works() { + assert_eq!( + Uint512::zero().to_le_bytes(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + Uint512::MAX.to_le_bytes(), + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + ] + ); + assert_eq!( + Uint512::from(1u128).to_le_bytes(), + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(64, "little")]` + assert_eq!( + Uint512::from(240282366920938463463374607431768124608u128).to_le_bytes(), + [ + 192, 172, 254, 255, 191, 221, 117, 246, 133, 59, 121, 165, 87, 179, 196, 180, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + Uint512::from_be_bytes([ + 17, 4, 23, 32, 87, 67, 123, 200, 58, 91, 0, 38, 33, 21, 67, 78, 87, 76, 65, 54, + 211, 201, 192, 7, 42, 233, 2, 240, 200, 115, 150, 240, 218, 88, 106, 45, 208, 134, + 238, 119, 85, 22, 14, 88, 166, 195, 154, 73, 64, 10, 44, 59, 13, 22, 47, 12, 99, 8, + 252, 96, 230, 187, 38, 29 + ]) + .to_le_bytes(), + [ + 29, 38, 187, 230, 96, 252, 8, 99, 12, 47, 22, 13, 59, 44, 10, 64, 73, 154, 195, + 166, 88, 14, 22, 85, 119, 238, 134, 208, 45, 106, 88, 218, 240, 150, 115, 200, 240, + 2, 233, 42, 7, 192, 201, 211, 54, 65, 76, 87, 78, 67, 21, 33, 38, 0, 91, 58, 200, + 123, 67, 87, 32, 23, 4, 17 + ] + ); + } + + #[test] + fn uint512_is_zero_works() { + assert!(Uint512::zero().is_zero()); + assert!(Uint512(U512::from(0u32)).is_zero()); + + assert!(!Uint512::from(1u32).is_zero()); + assert!(!Uint512::from(123u32).is_zero()); + } + + #[test] + fn uint512_wrapping_methods() { + // wrapping_add + assert_eq!(Uint512::from(2u32).wrapping_add(Uint512::from(2u32)), Uint512::from(4u32)); // non-wrapping + assert_eq!(Uint512::MAX.wrapping_add(Uint512::from(1u32)), Uint512::from(0u32)); // wrapping + + // wrapping_sub + assert_eq!(Uint512::from(7u32).wrapping_sub(Uint512::from(5u32)), Uint512::from(2u32)); // non-wrapping + assert_eq!(Uint512::from(0u32).wrapping_sub(Uint512::from(1u32)), Uint512::MAX); // wrapping + + // wrapping_mul + assert_eq!(Uint512::from(3u32).wrapping_mul(Uint512::from(2u32)), Uint512::from(6u32)); // non-wrapping + assert_eq!(Uint512::MAX.wrapping_mul(Uint512::from(2u32)), Uint512::MAX - Uint512::one()); // wrapping + + // wrapping_pow + assert_eq!(Uint512::from(2u32).wrapping_pow(3), Uint512::from(8u32)); // non-wrapping + assert_eq!(Uint512::MAX.wrapping_pow(2), Uint512::from(1u32)); // wrapping + } + + #[test] + fn uint512_json() { + let orig = Uint512::from(1234567890987654321u128); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Uint512 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn uint512_compare() { + let a = Uint512::from(12345u32); + let b = Uint512::from(23456u32); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Uint512::from(12345u32)); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_math() { + let a = Uint512::from(12345u32); + let b = Uint512::from(23456u32); + + // test += with owned and reference right hand side + let mut c = Uint512::from(300000u32); + c += b; + assert_eq!(c, Uint512::from(323456u32)); + let mut d = Uint512::from(300000u32); + d += &b; + assert_eq!(d, Uint512::from(323456u32)); + + // test -= with owned and reference right hand side + let mut c = Uint512::from(300000u32); + c -= b; + assert_eq!(c, Uint512::from(276544u32)); + let mut d = Uint512::from(300000u32); + d -= &b; + assert_eq!(d, Uint512::from(276544u32)); + + // error result on underflow (- would produce negative result) + let underflow_result = a.checked_sub(b); + let OverflowError { operation } = underflow_result.unwrap_err(); + assert_eq!(operation, OverflowOperation::Sub); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_add_works() { + assert_eq!(Uint512::from(2u32) + Uint512::from(1u32), Uint512::from(3u32)); + assert_eq!(Uint512::from(2u32) + Uint512::from(0u32), Uint512::from(2u32)); + + // works for refs + let a = Uint512::from(10u32); + let b = Uint512::from(3u32); + let expected = Uint512::from(13u32); + assert_eq!(a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint512_add_overflow_panics() { + let max = Uint512::MAX; + let _ = max + Uint512::from(12u32); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_sub_works() { + assert_eq!(Uint512::from(2u32) - Uint512::from(1u32), Uint512::from(1u32)); + assert_eq!(Uint512::from(2u32) - Uint512::from(0u32), Uint512::from(2u32)); + assert_eq!(Uint512::from(2u32) - Uint512::from(2u32), Uint512::from(0u32)); + + // works for refs + let a = Uint512::from(10u32); + let b = Uint512::from(3u32); + let expected = Uint512::from(7u32); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn uint512_sub_overflow_panics() { + let _ = Uint512::from(1u32) - Uint512::from(2u32); + } + + #[test] + fn uint512_sub_assign_works() { + let mut a = Uint512::from(14u32); + a -= Uint512::from(2u32); + assert_eq!(a, Uint512::from(12u32)); + + // works for refs + let mut a = Uint512::from(10u32); + let b = Uint512::from(3u32); + let expected = Uint512::from(7u32); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_mul_works() { + assert_eq!(Uint512::from(2u32) * Uint512::from(3u32), Uint512::from(6u32)); + assert_eq!(Uint512::from(2u32) * Uint512::zero(), Uint512::zero()); + + // works for refs + let a = Uint512::from(11u32); + let b = Uint512::from(3u32); + let expected = Uint512::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn uint512_mul_assign_works() { + let mut a = Uint512::from(14u32); + a *= Uint512::from(2u32); + assert_eq!(a, Uint512::from(28u32)); + + // works for refs + let mut a = Uint512::from(10u32); + let b = Uint512::from(3u32); + a *= &b; + assert_eq!(a, Uint512::from(30u32)); + } + + #[test] + fn uint512_pow_works() { + assert_eq!(Uint512::from(2u32).pow(2), Uint512::from(4u32)); + assert_eq!(Uint512::from(2u32).pow(10), Uint512::from(1024u32)); + } + + #[test] + #[should_panic] + fn uint512_pow_overflow_panics() { + _ = Uint512::MAX.pow(2u32); + } + + #[test] + fn uint512_shr_works() { + let original = Uint512::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8, + ]); + + let shifted = Uint512::new([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8, + ]); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint512_shr_overflow_panics() { + let _ = Uint512::from(1u32) >> 512u32; + } + + #[test] + fn uint512_shl_works() { + let original = Uint512::new([ + 64u8, 128u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]); + + let shifted = Uint512::new([ + 2u8, 0u8, 4u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + ]); + + assert_eq!(original << 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint512_shl_overflow_panics() { + let _ = Uint512::from(1u32) << 512u32; + } + + #[test] + fn sum_works() { + let nums = vec![ + Uint512::from(17u32), + Uint512::from(123u32), + Uint512::from(540u32), + Uint512::from(82u32), + ]; + let expected = Uint512::from(762u32); + + let sum_as_ref: Uint512 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Uint512 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn uint512_methods() { + // checked_* + assert!(matches!(Uint512::MAX.checked_add(Uint512::from(1u32)), Err(OverflowError { .. }))); + assert_eq!(Uint512::from(1u32).checked_add(Uint512::from(1u32)), Ok(Uint512::from(2u32)),); + assert!(matches!( + Uint512::from(0u32).checked_sub(Uint512::from(1u32)), + Err(OverflowError { .. }) + )); + assert_eq!(Uint512::from(2u32).checked_sub(Uint512::from(1u32)), Ok(Uint512::from(1u32)),); + assert!(matches!(Uint512::MAX.checked_mul(Uint512::from(2u32)), Err(OverflowError { .. }))); + assert_eq!(Uint512::from(2u32).checked_mul(Uint512::from(2u32)), Ok(Uint512::from(4u32)),); + assert!(matches!(Uint512::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert_eq!(Uint512::from(2u32).checked_pow(3u32), Ok(Uint512::from(8u32)),); + assert!(matches!( + Uint512::MAX.checked_div(Uint512::from(0u32)), + Err(DivideByZeroError { .. }) + )); + assert_eq!(Uint512::from(6u32).checked_div(Uint512::from(2u32)), Ok(Uint512::from(3u32)),); + assert!(matches!( + Uint512::MAX.checked_div_euclid(Uint512::from(0u32)), + Err(DivideByZeroError { .. }) + )); + assert_eq!( + Uint512::from(6u32).checked_div_euclid(Uint512::from(2u32)), + Ok(Uint512::from(3u32)), + ); + assert_eq!( + Uint512::from(7u32).checked_div_euclid(Uint512::from(2u32)), + Ok(Uint512::from(3u32)), + ); + assert!(matches!( + Uint512::MAX.checked_rem(Uint512::from(0u32)), + Err(DivideByZeroError { .. }) + )); + + // saturating_* + assert_eq!(Uint512::MAX.saturating_add(Uint512::from(1u32)), Uint512::MAX); + assert_eq!(Uint512::from(0u32).saturating_sub(Uint512::from(1u32)), Uint512::from(0u32)); + assert_eq!(Uint512::MAX.saturating_mul(Uint512::from(2u32)), Uint512::MAX); + assert_eq!(Uint512::from(4u32).saturating_pow(2u32), Uint512::from(16u32)); + assert_eq!(Uint512::MAX.saturating_pow(2u32), Uint512::MAX); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_implements_rem() { + let a = Uint512::from(10u32); + assert_eq!(a % Uint512::from(10u32), Uint512::zero()); + assert_eq!(a % Uint512::from(2u32), Uint512::zero()); + assert_eq!(a % Uint512::from(1u32), Uint512::zero()); + assert_eq!(a % Uint512::from(3u32), Uint512::from(1u32)); + assert_eq!(a % Uint512::from(4u32), Uint512::from(2u32)); + + // works for refs + let a = Uint512::from(10u32); + let b = Uint512::from(3u32); + let expected = Uint512::from(1u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn uint512_rem_panics_for_zero() { + let _ = Uint512::from(10u32) % Uint512::zero(); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint512_rem_works() { + assert_eq!(Uint512::from(12u32) % Uint512::from(10u32), Uint512::from(2u32)); + assert_eq!(Uint512::from(50u32) % Uint512::from(5u32), Uint512::zero()); + + // works for refs + let a = Uint512::from(42u32); + let b = Uint512::from(5u32); + let expected = Uint512::from(2u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn uint512_rem_assign_works() { + let mut a = Uint512::from(30u32); + a %= Uint512::from(4u32); + assert_eq!(a, Uint512::from(2u32)); + + // works for refs + let mut a = Uint512::from(25u32); + let b = Uint512::from(6u32); + a %= &b; + assert_eq!(a, Uint512::from(1u32)); + } + + #[test] + fn uint512_strict_add_works() { + let a = Uint512::from(5u32); + let b = Uint512::from(3u32); + assert_eq!(a.strict_add(b), Uint512::from(8u32)); + assert_eq!(b.strict_add(a), Uint512::from(8u32)); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint512_strict_add_panics_on_overflow() { + let a = Uint512::MAX; + let b = Uint512::ONE; + let _ = a.strict_add(b); + } + + #[test] + fn uint512_strict_sub_works() { + let a = Uint512::from(5u32); + let b = Uint512::from(3u32); + assert_eq!(a.strict_sub(b), Uint512::from(2u32)); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn uint512_strict_sub_panics_on_overflow() { + let a = Uint512::ZERO; + let b = Uint512::ONE; + let _ = a.strict_sub(b); + } + + #[test] + fn uint512_abs_diff_works() { + let a = Uint512::from(42u32); + let b = Uint512::from(5u32); + let expected = Uint512::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + fn uint512_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected): (u64, u64, bool)| { + (Uint512::from(lhs), Uint512::from(rhs), expected) + }); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } +} diff --git a/cosmwasm/std/src/math/uint64.rs b/cosmwasm/std/src/math/uint64.rs new file mode 100644 index 00000000..6491f62e --- /dev/null +++ b/cosmwasm/std/src/math/uint64.rs @@ -0,0 +1,1362 @@ +use alloc::string::{String, ToString}; +use core::{ + fmt, + ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, Shl, ShlAssign, Shr, + ShrAssign, Sub, SubAssign, + }, +}; +use serde::{de, ser, Deserialize, Deserializer, Serialize}; + +use crate::{ + __internal::forward_ref_partial_eq, + errors::{ + CheckedMultiplyFractionError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, + OverflowOperation, StdError, + }, + forward_ref::{forward_ref_binop, forward_ref_op_assign}, + impl_mul_fraction, Fraction, Int128, Int256, Int512, Int64, Uint128, +}; + +use super::{conversion::forward_try_from, num_consts::NumConsts}; + +/// A thin wrapper around u64 that is using strings for JSON encoding/decoding, +/// such that the full u64 range can be used for clients that convert JSON numbers to floats, +/// like JavaScript and jq. +/// +/// # Examples +/// +/// Use `from` to create instances of this and `u64` to get the value out: +/// +/// ``` +/// # use cosmwasm_std::Uint64; +/// let a = Uint64::from(42u64); +/// assert_eq!(a.u64(), 42); +/// +/// let b = Uint64::from(70u32); +/// assert_eq!(b.u64(), 70); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] +pub struct Uint64(#[schemars(with = "String")] pub(crate) u64); + +forward_ref_partial_eq!(Uint64, Uint64); + +impl Uint64 { + pub const MAX: Self = Self(u64::MAX); + pub const MIN: Self = Self(u64::MIN); + + /// Creates a Uint64(value). + /// + /// This method is less flexible than `from` but can be called in a const context. + pub const fn new(value: u64) -> Self { + Uint64(value) + } + + /// Creates a Uint64(0) + #[inline] + pub const fn zero() -> Self { + Uint64(0) + } + + /// Creates a Uint64(1) + #[inline] + pub const fn one() -> Self { + Self(1) + } + + /// Returns a copy of the internal data + pub const fn u64(&self) -> u64 { + self.0 + } + + /// Returns a copy of the number as big endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + + /// Returns a copy of the number as little endian bytes. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + #[must_use] + pub const fn is_zero(&self) -> bool { + self.0 == 0 + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn pow(self, exp: u32) -> Self { + match self.0.checked_pow(exp) { + Some(val) => Self(val), + None => panic!("attempt to exponentiate with overflow"), + } + } + + /// Returns the base 2 logarithm of the number, rounded down. + /// + /// # Panics + /// + /// This function will panic if `self` is zero. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn ilog2(self) -> u32 { + self.0.checked_ilog2().unwrap() + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Uint64 { + match self.checked_multiply_ratio(numerator, denominator) { + Ok(value) => value, + Err(CheckedMultiplyRatioError::DivideByZero) => { + panic!("Denominator must not be zero") + }, + Err(CheckedMultiplyRatioError::Overflow) => panic!("Multiplication overflow"), + } + } + + /// Returns `self * numerator / denominator`. + /// + /// Due to the nature of the integer division involved, the result is always floored. + /// E.g. 5 * 99/100 = 4. + pub fn checked_multiply_ratio, B: Into>( + &self, + numerator: A, + denominator: B, + ) -> Result { + let numerator = numerator.into(); + let denominator = denominator.into(); + if denominator == 0 { + return Err(CheckedMultiplyRatioError::DivideByZero); + } + match (self.full_mul(numerator) / Uint128::from(denominator)).try_into() { + Ok(ratio) => Ok(ratio), + Err(_) => Err(CheckedMultiplyRatioError::Overflow), + } + } + + /// Multiplies two `Uint64`/`u64` values without overflow, producing an + /// [`Uint128`]. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Uint64; + /// + /// let a = Uint64::MAX; + /// let result = a.full_mul(2u32); + /// assert_eq!(result.to_string(), "36893488147419103230"); + /// ``` + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn full_mul(self, rhs: impl Into) -> Uint128 { + Uint128::from(self).checked_mul(Uint128::from(rhs.into())).unwrap() + } + + pub fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Add)) + } + + pub fn checked_sub(self, other: Self) -> Result { + self.0 + .checked_sub(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Sub)) + } + + pub fn checked_mul(self, other: Self) -> Result { + self.0 + .checked_mul(other.0) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Mul)) + } + + pub fn checked_pow(self, exp: u32) -> Result { + self.0 + .checked_pow(exp) + .map(Self) + .ok_or_else(|| OverflowError::new(OverflowOperation::Pow)) + } + + pub fn checked_div(self, other: Self) -> Result { + self.0.checked_div(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_div_euclid(self, other: Self) -> Result { + self.0.checked_div_euclid(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_rem(self, other: Self) -> Result { + self.0.checked_rem(other.0).map(Self).ok_or(DivideByZeroError) + } + + pub fn checked_shr(self, other: u32) -> Result { + if other >= 64 { + return Err(OverflowError::new(OverflowOperation::Shr)); + } + + Ok(Self(self.0.shr(other))) + } + + pub fn checked_shl(self, other: u32) -> Result { + if other >= 64 { + return Err(OverflowError::new(OverflowOperation::Shl)); + } + + Ok(Self(self.0.shl(other))) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_add(self, other: Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_sub(self, other: Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_mul(self, other: Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub fn wrapping_pow(self, other: u32) -> Self { + Self(self.0.wrapping_pow(other)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn saturating_pow(self, exp: u32) -> Self { + Self(self.0.saturating_pow(exp)) + } + + /// Strict integer addition. Computes `self + rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint64::add`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_add(self, rhs: Self) -> Self { + match self.0.checked_add(rhs.u64()) { + None => panic!("attempt to add with overflow"), + Some(sum) => Self(sum), + } + } + + /// Strict integer subtraction. Computes `self - rhs`, panicking if overflow occurred. + /// + /// This is the same as [`Uint64::sub`] but const. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn strict_sub(self, other: Self) -> Self { + match self.0.checked_sub(other.u64()) { + None => panic!("attempt to subtract with overflow"), + Some(diff) => Self(diff), + } + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub const fn abs_diff(self, other: Self) -> Self { + Self(if self.0 < other.0 { other.0 - self.0 } else { self.0 - other.0 }) + } +} + +impl NumConsts for Uint64 { + const ZERO: Self = Self::zero(); + const ONE: Self = Self::one(); + const MAX: Self = Self::MAX; + const MIN: Self = Self::MIN; +} + +impl_mul_fraction!(Uint64); + +// `From` is implemented manually instead of +// using `impl> From for Uint64` because +// of the conflict with `TryFrom<&str>` as described here +// https://stackoverflow.com/questions/63136970/how-do-i-work-around-the-upstream-crates-may-add-a-new-impl-of-trait-error + +// uint to Uint +impl From for Uint64 { + fn from(val: u64) -> Self { + Uint64(val) + } +} + +impl From for Uint64 { + fn from(val: u32) -> Self { + Uint64(val.into()) + } +} + +impl From for Uint64 { + fn from(val: u16) -> Self { + Uint64(val.into()) + } +} + +impl From for Uint64 { + fn from(val: u8) -> Self { + Uint64(val.into()) + } +} + +// Int to Uint +forward_try_from!(Int64, Uint64); +forward_try_from!(Int128, Uint64); +forward_try_from!(Int256, Uint64); +forward_try_from!(Int512, Uint64); + +impl TryFrom<&str> for Uint64 { + type Error = StdError; + + fn try_from(val: &str) -> Result { + match val.parse::() { + Ok(u) => Ok(Uint64(u)), + Err(e) => Err(StdError::generic_err(format!("Parsing u64: {e}"))), + } + } +} + +impl From for String { + fn from(original: Uint64) -> Self { + original.to_string() + } +} + +impl From for u64 { + fn from(original: Uint64) -> Self { + original.0 + } +} + +impl fmt::Display for Uint64 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Add for Uint64 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + self.strict_add(rhs) + } +} +forward_ref_binop!(impl Add, add for Uint64, Uint64); + +impl Sub for Uint64 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + self.strict_sub(rhs) + } +} +forward_ref_binop!(impl Sub, sub for Uint64, Uint64); + +impl SubAssign for Uint64 { + fn sub_assign(&mut self, rhs: Uint64) { + *self = *self - rhs; + } +} +forward_ref_op_assign!(impl SubAssign, sub_assign for Uint64, Uint64); + +impl Mul for Uint64 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.u64().checked_mul(rhs.u64()).expect("attempt to multiply with overflow")) + } +} +forward_ref_binop!(impl Mul, mul for Uint64, Uint64); + +impl MulAssign for Uint64 { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} +forward_ref_op_assign!(impl MulAssign, mul_assign for Uint64, Uint64); + +impl Div for Uint64 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.u64().checked_div(rhs.u64()).unwrap()) + } +} + +impl<'a> Div<&'a Uint64> for Uint64 { + type Output = Self; + + fn div(self, rhs: &'a Uint64) -> Self::Output { + Self(self.u64().checked_div(rhs.u64()).unwrap()) + } +} + +impl Rem for Uint64 { + type Output = Self; + + /// # Panics + /// + /// This operation will panic if `rhs` is zero. + #[inline] + fn rem(self, rhs: Self) -> Self { + Self(self.0.rem(rhs.0)) + } +} +forward_ref_binop!(impl Rem, rem for Uint64, Uint64); + +impl Not for Uint64 { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl RemAssign for Uint64 { + fn rem_assign(&mut self, rhs: Uint64) { + *self = *self % rhs; + } +} +forward_ref_op_assign!(impl RemAssign, rem_assign for Uint64, Uint64); + +impl Shr for Uint64 { + type Output = Self; + + fn shr(self, rhs: u32) -> Self::Output { + Self(self.u64().checked_shr(rhs).unwrap()) + } +} + +impl<'a> Shr<&'a u32> for Uint64 { + type Output = Self; + + fn shr(self, rhs: &'a u32) -> Self::Output { + Self(self.u64().checked_shr(*rhs).unwrap()) + } +} + +impl Shl for Uint64 { + type Output = Self; + + fn shl(self, rhs: u32) -> Self::Output { + Self(self.u64().checked_shl(rhs).expect("attempt to shift left with overflow")) + } +} + +impl<'a> Shl<&'a u32> for Uint64 { + type Output = Self; + + fn shl(self, rhs: &'a u32) -> Self::Output { + self.shl(*rhs) + } +} + +impl AddAssign for Uint64 { + fn add_assign(&mut self, rhs: Uint64) { + self.0 = self.0.checked_add(rhs.u64()).unwrap(); + } +} + +impl<'a> AddAssign<&'a Uint64> for Uint64 { + fn add_assign(&mut self, rhs: &'a Uint64) { + self.0 = self.0.checked_add(rhs.u64()).unwrap(); + } +} + +impl DivAssign for Uint64 { + fn div_assign(&mut self, rhs: Self) { + self.0 = self.0.checked_div(rhs.u64()).unwrap(); + } +} + +impl<'a> DivAssign<&'a Uint64> for Uint64 { + fn div_assign(&mut self, rhs: &'a Uint64) { + self.0 = self.0.checked_div(rhs.u64()).unwrap(); + } +} + +impl ShrAssign for Uint64 { + fn shr_assign(&mut self, rhs: u32) { + self.0 = self.0.checked_shr(rhs).unwrap(); + } +} + +impl<'a> ShrAssign<&'a u32> for Uint64 { + fn shr_assign(&mut self, rhs: &'a u32) { + self.0 = self.0.checked_shr(*rhs).unwrap(); + } +} + +impl ShlAssign for Uint64 { + fn shl_assign(&mut self, rhs: u32) { + *self = self.shl(rhs); + } +} + +impl<'a> ShlAssign<&'a u32> for Uint64 { + fn shl_assign(&mut self, rhs: &'a u32) { + *self = self.shl(*rhs); + } +} + +impl Serialize for Uint64 { + /// Serializes as an integer string using base 10 + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Uint64 { + /// Deserialized from an integer string using base 10 + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(Uint64Visitor) + } +} + +struct Uint64Visitor; + +impl<'de> de::Visitor<'de> for Uint64Visitor { + type Value = Uint64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string-encoded integer") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v.parse::() { + Ok(u) => Ok(Uint64(u)), + Err(e) => Err(E::custom(format_args!("invalid Uint64 '{v}' - {e}"))), + } + } +} + +impl core::iter::Sum for Uint64 +where + Self: Add, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), Add::add) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::CheckedMultiplyFractionError::{ConversionOverflow, DivideByZero}, + math::conversion::test_try_from_int_to_uint, + ConversionOverflowError, + }; + + use alloc::string::ToString; + + #[test] + fn size_of_works() { + assert_eq!(core::mem::size_of::(), 8); + } + + #[test] + fn uint64_not_works() { + assert_eq!(!Uint64::new(1234806), Uint64::new(!1234806)); + + assert_eq!(!Uint64::MAX, Uint64::new(!u64::MAX)); + assert_eq!(!Uint64::MIN, Uint64::new(!u64::MIN)); + } + + #[test] + fn uint64_zero_works() { + let zero = Uint64::zero(); + assert_eq!(zero.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0]); + } + + #[test] + fn uint64_one_works() { + let one = Uint64::one(); + assert_eq!(one.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]); + } + + #[test] + fn uint64_convert_into() { + let original = Uint64(12345); + let a = u64::from(original); + assert_eq!(a, 12345); + + let original = Uint64(12345); + let a = String::from(original); + assert_eq!(a, "12345"); + } + + #[test] + fn uint64_convert_from() { + let a = Uint64::from(5u64); + assert_eq!(a.0, 5); + + let a = Uint64::from(5u32); + assert_eq!(a.0, 5); + + let a = Uint64::from(5u16); + assert_eq!(a.0, 5); + + let a = Uint64::from(5u8); + assert_eq!(a.0, 5); + + let result = Uint64::try_from("34567"); + assert_eq!(result.unwrap().0, 34567); + + let result = Uint64::try_from("1.23"); + assert!(result.is_err()); + } + + #[test] + fn uint64_try_from_signed_works() { + test_try_from_int_to_uint::("Int64", "Uint64"); + test_try_from_int_to_uint::("Int128", "Uint64"); + test_try_from_int_to_uint::("Int256", "Uint64"); + test_try_from_int_to_uint::("Int512", "Uint64"); + } + + #[test] + fn uint64_implements_display() { + let a = Uint64(12345); + assert_eq!(format!("Embedded: {a}"), "Embedded: 12345"); + assert_eq!(a.to_string(), "12345"); + + let a = Uint64(0); + assert_eq!(format!("Embedded: {a}"), "Embedded: 0"); + assert_eq!(a.to_string(), "0"); + } + + #[test] + fn uint64_display_padding_works() { + // width > natural representation + let a = Uint64::from(123u64); + assert_eq!(format!("Embedded: {a:05}"), "Embedded: 00123"); + + // width < natural representation + let a = Uint64::from(123u64); + assert_eq!(format!("Embedded: {a:02}"), "Embedded: 123"); + } + + #[test] + fn uint64_to_be_bytes_works() { + assert_eq!(Uint64::zero().to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(Uint64::MAX.to_be_bytes(), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + assert_eq!(Uint64::new(1).to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]); + // Python: `[b for b in (63374607431768124608).to_bytes(8, "big")]` + assert_eq!( + Uint64::new(874607431768124608).to_be_bytes(), + [12, 35, 58, 211, 72, 116, 172, 192] + ); + } + + #[test] + fn uint64_to_le_bytes_works() { + assert_eq!(Uint64::zero().to_le_bytes(), [0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(Uint64::MAX.to_le_bytes(), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + assert_eq!(Uint64::new(1).to_le_bytes(), [1, 0, 0, 0, 0, 0, 0, 0]); + // Python: `[b for b in (240282366920938463463374607431768124608).to_bytes(16, "little")]` + assert_eq!( + Uint64::new(874607431768124608).to_le_bytes(), + [192, 172, 116, 72, 211, 58, 35, 12] + ); + } + + #[test] + fn uint64_is_zero_works() { + assert!(Uint64::zero().is_zero()); + assert!(Uint64(0).is_zero()); + + assert!(!Uint64(1).is_zero()); + assert!(!Uint64(123).is_zero()); + } + + #[test] + fn uint64_json() { + let orig = Uint64(1234567890987654321); + let serialized = serde_json::to_vec(&orig).unwrap(); + assert_eq!(serialized.as_slice(), b"\"1234567890987654321\""); + let parsed: Uint64 = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(parsed, orig); + } + + #[test] + fn uint64_compare() { + let a = Uint64(12345); + let b = Uint64(23456); + + assert!(a < b); + assert!(b > a); + assert_eq!(a, Uint64(12345)); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_math() { + let a = Uint64(12345); + let b = Uint64(23456); + + // test - with owned and reference right hand side + assert_eq!((b.checked_sub(a)).unwrap(), Uint64(11111)); + + // test += with owned and reference right hand side + let mut c = Uint64(300000); + c += b; + assert_eq!(c, Uint64(323456)); + let mut d = Uint64(300000); + d += &b; + assert_eq!(d, Uint64(323456)); + + // error result on underflow (- would produce negative result) + let underflow_result = a.checked_sub(b); + let OverflowError { operation } = underflow_result.unwrap_err(); + assert_eq!(operation, OverflowOperation::Sub); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_add_works() { + assert_eq!(Uint64::from(2u32) + Uint64::from(1u32), Uint64::from(3u32)); + assert_eq!(Uint64::from(2u32) + Uint64::from(0u32), Uint64::from(2u32)); + + // works for refs + let a = Uint64::from(10u32); + let b = Uint64::from(3u32); + let expected = Uint64::from(13u32); + assert_eq!(a + b, expected); + assert_eq!(a + &b, expected); + assert_eq!(&a + b, expected); + assert_eq!(&a + &b, expected); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint64_add_overflow_panics() { + let max = Uint64::MAX; + let _ = max + Uint64(12); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_sub_works() { + assert_eq!(Uint64(2) - Uint64(1), Uint64(1)); + assert_eq!(Uint64(2) - Uint64(0), Uint64(2)); + assert_eq!(Uint64(2) - Uint64(2), Uint64(0)); + + // works for refs + let a = Uint64::new(10); + let b = Uint64::new(3); + let expected = Uint64::new(7); + assert_eq!(a - b, expected); + assert_eq!(a - &b, expected); + assert_eq!(&a - b, expected); + assert_eq!(&a - &b, expected); + } + + #[test] + #[should_panic] + fn uint64_sub_overflow_panics() { + let _ = Uint64(1) - Uint64(2); + } + + #[test] + fn uint64_sub_assign_works() { + let mut a = Uint64(14); + a -= Uint64(2); + assert_eq!(a, Uint64(12)); + + // works for refs + let mut a = Uint64::new(10); + let b = Uint64::new(3); + let expected = Uint64::new(7); + a -= &b; + assert_eq!(a, expected); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_mul_works() { + assert_eq!(Uint64::from(2u32) * Uint64::from(3u32), Uint64::from(6u32)); + assert_eq!(Uint64::from(2u32) * Uint64::zero(), Uint64::zero()); + + // works for refs + let a = Uint64::from(11u32); + let b = Uint64::from(3u32); + let expected = Uint64::from(33u32); + assert_eq!(a * b, expected); + assert_eq!(a * &b, expected); + assert_eq!(&a * b, expected); + assert_eq!(&a * &b, expected); + } + + #[test] + fn uint64_mul_assign_works() { + let mut a = Uint64::from(14u32); + a *= Uint64::from(2u32); + assert_eq!(a, Uint64::from(28u32)); + + // works for refs + let mut a = Uint64::from(10u32); + let b = Uint64::from(3u32); + a *= &b; + assert_eq!(a, Uint64::from(30u32)); + } + + #[test] + fn uint64_pow_works() { + assert_eq!(Uint64::from(2u32).pow(2), Uint64::from(4u32)); + assert_eq!(Uint64::from(2u32).pow(10), Uint64::from(1024u32)); + } + + #[test] + #[should_panic] + fn uint64_pow_overflow_panics() { + _ = Uint64::MAX.pow(2u32); + } + + #[test] + #[should_panic] + fn uint64_math_overflow_panics() { + // almost_max is 2^64 - 10 + let almost_max = Uint64(18446744073709551606); + let _ = almost_max + Uint64(12); + } + + #[test] + fn uint64_multiply_ratio_works() { + let base = Uint64(500); + + // factor 1/1 + assert_eq!(base.multiply_ratio(1u64, 1u64), base); + assert_eq!(base.multiply_ratio(3u64, 3u64), base); + assert_eq!(base.multiply_ratio(654321u64, 654321u64), base); + assert_eq!(base.multiply_ratio(u64::MAX, u64::MAX), base); + + // factor 3/2 + assert_eq!(base.multiply_ratio(3u64, 2u64), Uint64(750)); + assert_eq!(base.multiply_ratio(333333u64, 222222u64), Uint64(750)); + + // factor 2/3 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(2u64, 3u64), Uint64(333)); + assert_eq!(base.multiply_ratio(222222u64, 333333u64), Uint64(333)); + + // factor 5/6 (integer devision always floors the result) + assert_eq!(base.multiply_ratio(5u64, 6u64), Uint64(416)); + assert_eq!(base.multiply_ratio(100u64, 120u64), Uint64(416)); + } + + #[test] + fn uint64_multiply_ratio_does_not_overflow_when_result_fits() { + // Almost max value for Uint64. + let base = Uint64(u64::MAX - 9); + + assert_eq!(base.multiply_ratio(2u64, 2u64), base); + } + + #[test] + #[should_panic] + fn uint64_multiply_ratio_panicks_on_overflow() { + // Almost max value for Uint64. + let base = Uint64(u64::MAX - 9); + + assert_eq!(base.multiply_ratio(2u64, 1u64), base); + } + + #[test] + #[should_panic(expected = "Denominator must not be zero")] + fn uint64_multiply_ratio_panics_for_zero_denominator() { + _ = Uint64(500).multiply_ratio(1u64, 0u64); + } + + #[test] + fn uint64_checked_multiply_ratio_does_not_panic() { + assert_eq!( + Uint64(500u64).checked_multiply_ratio(1u64, 0u64), + Err(CheckedMultiplyRatioError::DivideByZero), + ); + assert_eq!( + Uint64(500u64).checked_multiply_ratio(u64::MAX, 1u64), + Err(CheckedMultiplyRatioError::Overflow), + ); + } + + #[test] + fn uint64_shr_works() { + let original = Uint64::new(u64::from_be_bytes([0u8, 0u8, 0u8, 0u8, 2u8, 0u8, 4u8, 2u8])); + + let shifted = Uint64::new(u64::from_be_bytes([0u8, 0u8, 0u8, 0u8, 0u8, 128u8, 1u8, 0u8])); + + assert_eq!(original >> 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint64_shr_overflow_panics() { + let _ = Uint64::from(1u32) >> 64u32; + } + + #[test] + fn uint64_shl_works() { + let original = Uint64::new(u64::from_be_bytes([64u8, 128u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8])); + + let shifted = Uint64::new(u64::from_be_bytes([2u8, 0u8, 4u8, 0u8, 0u8, 0u8, 0u8, 0u8])); + + assert_eq!(original << 2u32, shifted); + } + + #[test] + #[should_panic] + fn uint64_shl_overflow_panics() { + let _ = Uint64::from(1u32) << 64u32; + } + + #[test] + fn sum_works() { + let nums = vec![Uint64(17), Uint64(123), Uint64(540), Uint64(82)]; + let expected = Uint64(762); + + let sum_as_ref: Uint64 = nums.iter().sum(); + assert_eq!(expected, sum_as_ref); + + let sum_as_owned: Uint64 = nums.into_iter().sum(); + assert_eq!(expected, sum_as_owned); + } + + #[test] + fn uint64_methods() { + // checked_* + assert!(matches!(Uint64::MAX.checked_add(Uint64(1)), Err(OverflowError { .. }))); + assert!(matches!(Uint64(1).checked_add(Uint64(1)), Ok(Uint64(2)))); + assert!(matches!(Uint64(0).checked_sub(Uint64(1)), Err(OverflowError { .. }))); + assert!(matches!(Uint64(2).checked_sub(Uint64(1)), Ok(Uint64(1)))); + assert!(matches!(Uint64::MAX.checked_mul(Uint64(2)), Err(OverflowError { .. }))); + assert!(matches!(Uint64(2).checked_mul(Uint64(2)), Ok(Uint64(4)))); + assert!(matches!(Uint64::MAX.checked_pow(2u32), Err(OverflowError { .. }))); + assert!(matches!(Uint64(2).checked_pow(3), Ok(Uint64(8)))); + assert!(matches!(Uint64::MAX.checked_div(Uint64(0)), Err(DivideByZeroError { .. }))); + assert!(matches!(Uint64(6).checked_div(Uint64(2)), Ok(Uint64(3)))); + assert!(matches!(Uint64::MAX.checked_div_euclid(Uint64(0)), Err(DivideByZeroError { .. }))); + assert!(matches!(Uint64(6).checked_div_euclid(Uint64(2)), Ok(Uint64(3)),)); + assert!(matches!(Uint64::MAX.checked_rem(Uint64(0)), Err(DivideByZeroError { .. }))); + assert!(matches!(Uint64(7).checked_rem(Uint64(2)), Ok(Uint64(1)))); + + // saturating_* + assert_eq!(Uint64::MAX.saturating_add(Uint64(1)), Uint64::MAX); + assert_eq!(Uint64(0).saturating_sub(Uint64(1)), Uint64(0)); + assert_eq!(Uint64::MAX.saturating_mul(Uint64(2)), Uint64::MAX); + assert_eq!(Uint64::MAX.saturating_pow(2), Uint64::MAX); + } + + #[test] + fn uint64_wrapping_methods() { + // wrapping_add + assert_eq!(Uint64(2).wrapping_add(Uint64(2)), Uint64(4)); // non-wrapping + assert_eq!(Uint64::MAX.wrapping_add(Uint64(1)), Uint64(0)); // wrapping + + // wrapping_sub + assert_eq!(Uint64(7).wrapping_sub(Uint64(5)), Uint64(2)); // non-wrapping + assert_eq!(Uint64(0).wrapping_sub(Uint64(1)), Uint64::MAX); // wrapping + + // wrapping_mul + assert_eq!(Uint64(3).wrapping_mul(Uint64(2)), Uint64(6)); // non-wrapping + assert_eq!(Uint64::MAX.wrapping_mul(Uint64(2)), Uint64::MAX - Uint64::one()); // wrapping + + // wrapping_pow + assert_eq!(Uint64(2).wrapping_pow(3), Uint64(8)); // non-wrapping + assert_eq!(Uint64::MAX.wrapping_pow(2), Uint64(1)); // wrapping + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_implements_rem() { + let a = Uint64::new(10); + assert_eq!(a % Uint64::new(10), Uint64::zero()); + assert_eq!(a % Uint64::new(2), Uint64::zero()); + assert_eq!(a % Uint64::new(1), Uint64::zero()); + assert_eq!(a % Uint64::new(3), Uint64::new(1)); + assert_eq!(a % Uint64::new(4), Uint64::new(2)); + + // works for refs + let a = Uint64::new(10); + let b = Uint64::new(3); + let expected = Uint64::new(1); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + #[should_panic(expected = "divisor of zero")] + fn uint64_rem_panics_for_zero() { + let _ = Uint64::new(10) % Uint64::zero(); + } + + #[test] + #[allow(clippy::op_ref)] + fn uint64_rem_works() { + assert_eq!(Uint64::from(12u32) % Uint64::from(10u32), Uint64::from(2u32)); + assert_eq!(Uint64::from(50u32) % Uint64::from(5u32), Uint64::zero()); + + // works for refs + let a = Uint64::from(42u32); + let b = Uint64::from(5u32); + let expected = Uint64::from(2u32); + assert_eq!(a % b, expected); + assert_eq!(a % &b, expected); + assert_eq!(&a % b, expected); + assert_eq!(&a % &b, expected); + } + + #[test] + fn uint64_rem_assign_works() { + let mut a = Uint64::from(30u32); + a %= Uint64::from(4u32); + assert_eq!(a, Uint64::from(2u32)); + + // works for refs + let mut a = Uint64::from(25u32); + let b = Uint64::from(6u32); + a %= &b; + assert_eq!(a, Uint64::from(1u32)); + } + + #[test] + fn uint64_strict_add_works() { + let a = Uint64::new(5); + let b = Uint64::new(3); + assert_eq!(a.strict_add(b), Uint64::new(8)); + assert_eq!(b.strict_add(a), Uint64::new(8)); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn uint64_strict_add_panics_on_overflow() { + let a = Uint64::MAX; + let b = Uint64::ONE; + let _ = a.strict_add(b); + } + + #[test] + fn uint64_strict_sub_works() { + let a = Uint64::new(5); + let b = Uint64::new(3); + assert_eq!(a.strict_sub(b), Uint64::new(2)); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn uint64_strict_sub_panics_on_overflow() { + let a = Uint64::ZERO; + let b = Uint64::ONE; + let _ = a.strict_sub(b); + } + + #[test] + fn uint64_abs_diff_works() { + let a = Uint64::from(42u32); + let b = Uint64::from(5u32); + let expected = Uint64::from(37u32); + assert_eq!(a.abs_diff(b), expected); + assert_eq!(b.abs_diff(a), expected); + } + + #[test] + fn uint64_partial_eq() { + let test_cases = [(1, 1, true), (42, 42, true), (42, 24, false), (0, 0, true)] + .into_iter() + .map(|(lhs, rhs, expected)| (Uint64::new(lhs), Uint64::new(rhs), expected)); + + #[allow(clippy::op_ref)] + for (lhs, rhs, expected) in test_cases { + assert_eq!(lhs == rhs, expected); + assert_eq!(&lhs == rhs, expected); + assert_eq!(lhs == &rhs, expected); + assert_eq!(&lhs == &rhs, expected); + } + } + + #[test] + fn mul_floor_works_with_zero() { + let fraction = (0u32, 21u32); + let res = Uint64::new(123456).mul_floor(fraction); + assert_eq!(Uint64::zero(), res) + } + + #[test] + fn mul_floor_does_nothing_with_one() { + let fraction = (Uint64::one(), Uint64::one()); + let res = Uint64::new(123456).mul_floor(fraction); + assert_eq!(Uint64::new(123456), res) + } + + #[test] + fn mul_floor_rounds_down_with_normal_case() { + let fraction = (8u64, 21u64); + let res = Uint64::new(123456).mul_floor(fraction); // 47030.8571 + assert_eq!(Uint64::new(47030), res) + } + + #[test] + fn mul_floor_does_not_round_on_even_divide() { + let fraction = (2u64, 5u64); + let res = Uint64::new(25).mul_floor(fraction); + assert_eq!(Uint64::new(10), res) + } + + #[test] + fn mul_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u64, 21u64); + let res = Uint64::MAX.mul_floor(fraction); // 7_027_331_075_698_876_805.71428571 + assert_eq!(Uint64::new(7_027_331_075_698_876_805), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_floor_panics_on_overflow() { + let fraction = (21u64, 8u64); + _ = Uint64::MAX.mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_overflow() { + let fraction = (21u64, 8u64); + assert_eq!( + Uint64::MAX.checked_mul_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint128", + target_type: "Uint64", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_floor_panics_on_zero_div() { + let fraction = (21u64, 0u64); + _ = Uint64::new(123456).mul_floor(fraction); + } + + #[test] + fn checked_mul_floor_does_not_panic_on_zero_div() { + let fraction = (21u64, 0u64); + assert_eq!( + Uint64::new(123456).checked_mul_floor(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + fn mul_ceil_works_with_zero() { + let fraction = (Uint64::zero(), Uint64::new(21)); + let res = Uint64::new(123456).mul_ceil(fraction); + assert_eq!(Uint64::zero(), res) + } + + #[test] + fn mul_ceil_does_nothing_with_one() { + let fraction = (Uint64::one(), Uint64::one()); + let res = Uint64::new(123456).mul_ceil(fraction); + assert_eq!(Uint64::new(123456), res) + } + + #[test] + fn mul_ceil_rounds_up_with_normal_case() { + let fraction = (8u64, 21u64); + let res = Uint64::new(123456).mul_ceil(fraction); // 47030.8571 + assert_eq!(Uint64::new(47031), res) + } + + #[test] + fn mul_ceil_does_not_round_on_even_divide() { + let fraction = (2u64, 5u64); + let res = Uint64::new(25).mul_ceil(fraction); + assert_eq!(Uint64::new(10), res) + } + + #[test] + fn mul_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (8u64, 21u64); + let res = Uint64::MAX.mul_ceil(fraction); // 7_027_331_075_698_876_805.71428571 + assert_eq!(Uint64::new(7_027_331_075_698_876_806), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn mul_ceil_panics_on_overflow() { + let fraction = (21u64, 8u64); + _ = Uint64::MAX.mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_overflow() { + let fraction = (21u64, 8u64); + assert_eq!( + Uint64::MAX.checked_mul_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint128", + target_type: "Uint64", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn mul_ceil_panics_on_zero_div() { + let fraction = (21u64, 0u64); + _ = Uint64::new(123456).mul_ceil(fraction); + } + + #[test] + fn checked_mul_ceil_does_not_panic_on_zero_div() { + let fraction = (21u64, 0u64); + assert_eq!( + Uint64::new(123456).checked_mul_ceil(fraction), + Err(DivideByZero(DivideByZeroError)), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_floor_raises_with_zero() { + let fraction = (Uint64::zero(), Uint64::new(21)); + _ = Uint64::new(123456).div_floor(fraction); + } + + #[test] + fn div_floor_does_nothing_with_one() { + let fraction = (Uint64::one(), Uint64::one()); + let res = Uint64::new(123456).div_floor(fraction); + assert_eq!(Uint64::new(123456), res) + } + + #[test] + fn div_floor_rounds_down_with_normal_case() { + let fraction = (5u64, 21u64); + let res = Uint64::new(123456).div_floor(fraction); // 518515.2 + assert_eq!(Uint64::new(518515), res) + } + + #[test] + fn div_floor_does_not_round_on_even_divide() { + let fraction = (5u64, 2u64); + let res = Uint64::new(25).div_floor(fraction); + assert_eq!(Uint64::new(10), res) + } + + #[test] + fn div_floor_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u64, 8u64); + let res = Uint64::MAX.div_floor(fraction); // 7_027_331_075_698_876_805.71428 + assert_eq!(Uint64::new(7_027_331_075_698_876_805), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_floor_panics_on_overflow() { + let fraction = (8u64, 21u64); + _ = Uint64::MAX.div_floor(fraction); + } + + #[test] + fn div_floor_does_not_panic_on_overflow() { + let fraction = (8u64, 21u64); + assert_eq!( + Uint64::MAX.checked_div_floor(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint128", + target_type: "Uint64", + })), + ); + } + + #[test] + #[should_panic(expected = "DivideByZeroError")] + fn div_ceil_raises_with_zero() { + let fraction = (Uint64::zero(), Uint64::new(21)); + _ = Uint64::new(123456).div_ceil(fraction); + } + + #[test] + fn div_ceil_does_nothing_with_one() { + let fraction = (Uint64::one(), Uint64::one()); + let res = Uint64::new(123456).div_ceil(fraction); + assert_eq!(Uint64::new(123456), res) + } + + #[test] + fn div_ceil_rounds_up_with_normal_case() { + let fraction = (5u64, 21u64); + let res = Uint64::new(123456).div_ceil(fraction); // 518515.2 + assert_eq!(Uint64::new(518516), res) + } + + #[test] + fn div_ceil_does_not_round_on_even_divide() { + let fraction = (5u64, 2u64); + let res = Uint64::new(25).div_ceil(fraction); + assert_eq!(Uint64::new(10), res) + } + + #[test] + fn div_ceil_works_when_operation_temporarily_takes_above_max() { + let fraction = (21u64, 8u64); + let res = Uint64::MAX.div_ceil(fraction); // 7_027_331_075_698_876_805.71428 + assert_eq!(Uint64::new(7_027_331_075_698_876_806), res) + } + + #[test] + #[should_panic(expected = "ConversionOverflowError")] + fn div_ceil_panics_on_overflow() { + let fraction = (8u64, 21u64); + _ = Uint64::MAX.div_ceil(fraction); + } + + #[test] + fn div_ceil_does_not_panic_on_overflow() { + let fraction = (8u64, 21u64); + assert_eq!( + Uint64::MAX.checked_div_ceil(fraction), + Err(ConversionOverflow(ConversionOverflowError { + source_type: "Uint128", + target_type: "Uint64", + })), + ); + } +} diff --git a/cosmwasm/std/src/memory.rs b/cosmwasm/std/src/memory.rs new file mode 100644 index 00000000..ab4655df --- /dev/null +++ b/cosmwasm/std/src/memory.rs @@ -0,0 +1,168 @@ +use crate::prelude::*; +use core::{any::TypeId, marker::PhantomData, mem, ops::Deref, slice}; + +/// This trait is used to indicate whether a region is borrowed or owned +pub trait Ownership: 'static {} + +impl Ownership for Borrowed {} + +impl Ownership for Owned {} + +/// This type is used to indicate that the region is borrowed and must not be deallocated +pub struct Borrowed; + +/// This type is used to indicate that the region is owned by the region and must be deallocated +pub struct Owned; + +/// Describes some data allocated in Wasm's linear memory. +/// A pointer to an instance of this can be returned over FFI boundaries. +/// +/// This struct is crate internal since the cosmwasm-vm defines the same type independently. +#[repr(C)] +pub struct Region { + /// The beginning of the region expressed as bytes from the beginning of the linear memory + pub offset: u32, + /// The number of bytes available in this region + pub capacity: u32, + /// The number of bytes used in this region + pub length: u32, + + _marker: PhantomData, +} + +const _: () = { + assert!(mem::size_of::>() == 12); + assert!(mem::size_of::>() == 12); +}; + +impl Region { + pub fn from_slice(slice: &[u8]) -> Self { + unsafe { Self::from_parts(slice.as_ptr(), slice.len(), slice.len()) } + } +} + +impl Region { + /// Construct a region from an existing vector + pub fn from_vec(vec: Vec) -> Self { + let region = unsafe { Self::from_parts(vec.as_ptr(), vec.capacity(), vec.len()) }; + mem::forget(vec); + region + } + + /// Reconstruct a region from a raw pointer pointing to a `Box`. + /// You'll want to use this when you received a region from the VM and want to dereference its + /// contents. + /// + /// # Safety + /// + /// - The pointer must not be null + /// - The pointer must be heap allocated + /// - This region must point to a valid memory region + /// - The memory region this region points to must be heap allocated as well + pub unsafe fn from_heap_ptr(ptr: *mut Self) -> Box { + assert!(!ptr.is_null(), "Region pointer is null"); + Box::from_raw(ptr) + } + + /// Construct a new empty region with *at least* a capacity of what you passed in and a length + /// of 0 + pub fn with_capacity(cap: usize) -> Self { + let data = Vec::with_capacity(cap); + let region = Self::from_vec(data); + region + } + + /// Transform the region into a vector + pub fn into_vec(self) -> Vec { + let vector = unsafe { + Vec::from_raw_parts( + self.offset as *mut u8, + self.length as usize, + self.capacity as usize, + ) + }; + mem::forget(self); + vector + } +} + +impl Region +where + O: Ownership, +{ + unsafe fn from_parts(ptr: *const u8, capacity: usize, length: usize) -> Self { + // Well, this technically violates pointer provenance rules. + // But there isn't a stable API for it, so that's the best we can do, I guess. + Region { + offset: u32::try_from(ptr as usize).expect("pointer doesn't fit in u32"), + capacity: u32::try_from(capacity).expect("capacity doesn't fit in u32"), + length: u32::try_from(length).expect("length doesn't fit in u32"), + + _marker: PhantomData, + } + } + + /// Access the memory region this region points to in form of a byte slice + pub fn as_bytes(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.offset as *const u8, self.length as usize) } + } + + /// Obtain the pointer to the region + /// + /// This is nothing but `&self as *const Region` but makes sure the correct generic parameter + /// is used + pub fn as_ptr(&self) -> *const Self { + self + } + + /// Transform the region into an unmanaged mutable pointer + /// + /// This means we move this regions onto the heap (note, only the *structure* of the region, not + /// the *contents of the pointer* we manage internally). To then deallocate this structure, + /// you'll have to reconstruct the region via [`Region::from_heap_ptr`] and drop it. + pub fn to_heap_ptr(self) -> *mut Self { + let boxed = Box::new(self); + Box::into_raw(boxed) + } +} + +impl Deref for Region +where + O: Ownership, +{ + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_bytes() + } +} + +impl Drop for Region +where + O: Ownership, +{ + fn drop(&mut self) { + // Since we can't specialize the drop impl we need to perform a runtime check + if TypeId::of::() == TypeId::of::() { + let region_start = self.offset as *mut u8; + + // This case is explicitely disallowed by Vec + // "The pointer will never be null, so this type is null-pointer-optimized." + assert!(!region_start.is_null(), "Region starts at null pointer"); + + unsafe { + let data = + Vec::from_raw_parts(region_start, self.length as usize, self.capacity as usize); + + drop(data); + } + } + } +} + +/// Returns the address of the optional Region as an offset in linear memory, +/// or zero if not present +#[cfg(feature = "iterator")] +pub fn get_optional_region_address(region: &Option<&Region>) -> u32 { + region.map(|r| r.as_ptr() as u32).unwrap_or(0) +} diff --git a/cosmwasm/std/src/metadata.rs b/cosmwasm/std/src/metadata.rs new file mode 100644 index 00000000..3024b139 --- /dev/null +++ b/cosmwasm/std/src/metadata.rs @@ -0,0 +1,25 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +/// Replicates the cosmos-sdk bank module Metadata type +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] +pub struct DenomMetadata { + pub description: String, + pub denom_units: Vec, + pub base: String, + pub display: String, + pub name: String, + pub symbol: String, + pub uri: String, + pub uri_hash: String, +} + +/// Replicates the cosmos-sdk bank module DenomUnit type +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] +pub struct DenomUnit { + pub denom: String, + pub exponent: u32, + pub aliases: Vec, +} diff --git a/cosmwasm/std/src/never.rs b/cosmwasm/std/src/never.rs new file mode 100644 index 00000000..a5db9fea --- /dev/null +++ b/cosmwasm/std/src/never.rs @@ -0,0 +1,45 @@ +/// Never can never be instantiated. This can be used in places +/// where we want to ensure that no error is returned, such as +/// the `ibc_packet_receive` entry point. +/// +/// In contrast to `Empty`, this does not have a JSON schema +/// and cannot be used for message and query types. +/// +/// Once the ! type is stable, this is not needed anymore. +/// See . +/// +/// ## Examples +/// +/// When using `Never` in a `Result`, we can unwrap in a type-safe way: +/// +/// ``` +/// use cosmwasm_std::Never; +/// +/// pub fn safe_unwrap(res: Result) -> T { +/// match res { +/// Ok(value) => value, +/// Err(err) => match err {}, +/// } +/// } +/// +/// let res: Result = Ok(5); +/// assert_eq!(safe_unwrap(res), 5); +/// ``` +pub enum Never {} + +// The Debug implementation is needed to allow the use of `Result::unwrap`. +impl core::fmt::Debug for Never { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Unreachable because no instance of Never can exist + match *self {} + } +} + +// The Display implementation is needed to fulfill the ToString requirement of +// entry point errors: `Result, E>` with `E: ToString`. +impl core::fmt::Display for Never { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Unreachable because no instance of Never can exist + match *self {} + } +} diff --git a/cosmwasm/std/src/pagination.rs b/cosmwasm/std/src/pagination.rs new file mode 100644 index 00000000..3d84fe07 --- /dev/null +++ b/cosmwasm/std/src/pagination.rs @@ -0,0 +1,12 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::Binary; + +/// Simplified version of the PageRequest type for pagination from the cosmos-sdk +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] +pub struct PageRequest { + pub key: Option, + pub limit: u32, + pub reverse: bool, +} diff --git a/cosmwasm/std/src/panic.rs b/cosmwasm/std/src/panic.rs new file mode 100644 index 00000000..a0bb3f1e --- /dev/null +++ b/cosmwasm/std/src/panic.rs @@ -0,0 +1,14 @@ +/// Installs a panic handler that aborts the contract execution +/// and sends the panic message and location to the host. +/// +/// This overrides any previous panic handler. See +/// for details. +#[cfg(all(feature = "abort", target_arch = "wasm32"))] +pub fn install_panic_handler() { + use super::imports::handle_panic; + std::panic::set_hook(Box::new(|info| { + // E.g. "panicked at 'oh no (a = 3)', src/contract.rs:51:5" + let full_message = info.to_string(); + handle_panic(&full_message); + })); +} diff --git a/cosmwasm/std/src/prelude.rs b/cosmwasm/std/src/prelude.rs new file mode 100644 index 00000000..46f5b754 --- /dev/null +++ b/cosmwasm/std/src/prelude.rs @@ -0,0 +1,5 @@ +pub use alloc::{ + boxed::Box, + string::{String, ToString}, + vec::Vec, +}; diff --git a/cosmwasm/std/src/query/bank.rs b/cosmwasm/std/src/query/bank.rs new file mode 100644 index 00000000..28f08e29 --- /dev/null +++ b/cosmwasm/std/src/query/bank.rs @@ -0,0 +1,114 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::Coin; + +#[cfg(feature = "cosmwasm_1_3")] +use crate::PageRequest; +use crate::{prelude::*, Binary, DenomMetadata}; + +use super::query_response::QueryResponseType; + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum BankQuery { + /// This calls into the native bank module for querying the total supply of one denomination. + /// It does the same as the SupplyOf call in Cosmos SDK's RPC API. + /// Return value is of type SupplyResponse. + #[cfg(feature = "cosmwasm_1_1")] + Supply { denom: String }, + /// This calls into the native bank module for one denomination + /// Return value is BalanceResponse + Balance { address: String, denom: String }, + /// This calls into the native bank module for all denominations. + /// Note that this may be much more expensive than Balance and should be avoided if possible. + /// Return value is AllBalanceResponse. + AllBalances { address: String }, + /// This calls into the native bank module for querying metadata for a specific bank token. + /// Return value is DenomMetadataResponse + #[cfg(feature = "cosmwasm_1_3")] + DenomMetadata { denom: String }, + /// This calls into the native bank module for querying metadata for all bank tokens that have + /// a metadata entry. Return value is AllDenomMetadataResponse + #[cfg(feature = "cosmwasm_1_3")] + AllDenomMetadata { pagination: Option }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct SupplyResponse { + /// Always returns a Coin with the requested denom. + /// This will be of zero amount if the denom does not exist. + pub amount: Coin, +} + +impl_response_constructor!(SupplyResponse, amount: Coin); + +impl QueryResponseType for SupplyResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct BalanceResponse { + /// Always returns a Coin with the requested denom. + /// This may be of 0 amount if no such funds. + pub amount: Coin, +} + +impl_response_constructor!(BalanceResponse, amount: Coin); + +impl QueryResponseType for BalanceResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct AllBalanceResponse { + /// Returns all non-zero coins held by this account. + pub amount: Vec, +} + +impl_response_constructor!(AllBalanceResponse, amount: Vec); + +impl QueryResponseType for AllBalanceResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct DenomMetadataResponse { + /// The metadata for the queried denom. + pub metadata: DenomMetadata, +} + +impl_response_constructor!(DenomMetadataResponse, metadata: DenomMetadata); + +impl QueryResponseType for DenomMetadataResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct AllDenomMetadataResponse { + /// Always returns metadata for all token denoms on the base chain. + pub metadata: Vec, + pub next_key: Option, +} + +impl_response_constructor!( + AllDenomMetadataResponse, + metadata: Vec, + next_key: Option +); + +impl QueryResponseType for AllDenomMetadataResponse {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn private_constructor_works() { + let response = AllBalanceResponse::new(vec![Coin::new(1234u128, "uatom")]); + assert_eq!(response, AllBalanceResponse { amount: vec![Coin::new(1234u128, "uatom")] }); + } +} diff --git a/cosmwasm/std/src/query/distribution.rs b/cosmwasm/std/src/query/distribution.rs new file mode 100644 index 00000000..ed053f62 --- /dev/null +++ b/cosmwasm/std/src/query/distribution.rs @@ -0,0 +1,109 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Addr, Decimal256}; + +use super::query_response::QueryResponseType; + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DistributionQuery { + /// See + DelegatorWithdrawAddress { delegator_address: String }, + /// See + #[cfg(feature = "cosmwasm_1_4")] + DelegationRewards { delegator_address: String, validator_address: String }, + /// See + #[cfg(feature = "cosmwasm_1_4")] + DelegationTotalRewards { delegator_address: String }, + /// See + #[cfg(feature = "cosmwasm_1_4")] + DelegatorValidators { delegator_address: String }, +} + +/// See +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct DelegatorWithdrawAddressResponse { + pub withdraw_address: Addr, +} + +impl_response_constructor!(DelegatorWithdrawAddressResponse, withdraw_address: Addr); +impl QueryResponseType for DelegatorWithdrawAddressResponse {} + +/// See +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct DelegationRewardsResponse { + pub rewards: Vec, +} + +impl_response_constructor!(DelegationRewardsResponse, rewards: Vec); +impl QueryResponseType for DelegationRewardsResponse {} + +/// A coin type with decimal amount. +/// Modeled after the Cosmos SDK's [DecCoin] type. +/// However, in contrast to the Cosmos SDK the `amount` string MUST always have a dot at JSON level, +/// see . +/// Also if Cosmos SDK choses to migrate away from fixed point decimals +/// (as shown [here](https://github.com/cosmos/cosmos-sdk/blob/v0.47.4/x/group/internal/math/dec.go#L13-L21 and discussed [here](https://github.com/cosmos/cosmos-sdk/issues/11783)), +/// wasmd needs to truncate the decimal places to 18. +/// +/// [DecCoin]: (https://github.com/cosmos/cosmos-sdk/blob/v0.47.4/proto/cosmos/base/v1beta1/coin.proto#L28-L38) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DecCoin { + pub denom: String, + /// An amount in the base denom of the distributed token. + /// + /// Some chains have choosen atto (10^-18) for their token's base denomination. If we used + /// `Decimal` here, we could only store 340282366920938463463.374607431768211455atoken which + /// is 340.28 TOKEN. + pub amount: Decimal256, +} + +impl DecCoin { + pub fn new(amount: impl Into, denom: impl Into) -> Self { + Self { denom: denom.into(), amount: amount.into() } + } +} + +/// See +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct DelegationTotalRewardsResponse { + pub rewards: Vec, + pub total: Vec, +} + +impl_response_constructor!( + DelegationTotalRewardsResponse, + rewards: Vec, + total: Vec +); +impl QueryResponseType for DelegationTotalRewardsResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct DelegatorReward { + pub validator_address: String, + pub reward: Vec, +} +impl_response_constructor!( + DelegatorReward, + validator_address: String, + reward: Vec +); + +/// See +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct DelegatorValidatorsResponse { + pub validators: Vec, +} + +impl_response_constructor!(DelegatorValidatorsResponse, validators: Vec); +impl QueryResponseType for DelegatorValidatorsResponse {} diff --git a/cosmwasm/std/src/query/ibc.rs b/cosmwasm/std/src/query/ibc.rs new file mode 100644 index 00000000..c4f34795 --- /dev/null +++ b/cosmwasm/std/src/query/ibc.rs @@ -0,0 +1,52 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ibc::IbcChannel, prelude::*}; + +/// These are queries to the various IBC modules to see the state of the contract's +/// IBC connection. These will return errors if the contract is not "ibc enabled" +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IbcQuery { + /// Gets the Port ID the current contract is bound to. + /// + /// Returns a `PortIdResponse`. + PortId {}, + /// Lists all channels that are bound to a given port. + /// If `port_id` is omitted, this list all channels bound to the contract's port. + /// + /// Returns a `ListChannelsResponse`. + ListChannels { port_id: Option }, + /// Lists all information for a (portID, channelID) pair. + /// If port_id is omitted, it will default to the contract's own channel. + /// (To save a PortId{} call) + /// + /// Returns a `ChannelResponse`. + Channel { channel_id: String, port_id: Option }, + // TODO: Add more +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct PortIdResponse { + pub port_id: String, +} + +impl_response_constructor!(PortIdResponse, port_id: String); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct ListChannelsResponse { + pub channels: Vec, +} + +impl_response_constructor!(ListChannelsResponse, channels: Vec); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct ChannelResponse { + pub channel: Option, +} + +impl_response_constructor!(ChannelResponse, channel: Option); diff --git a/cosmwasm/std/src/query/mod.rs b/cosmwasm/std/src/query/mod.rs new file mode 100644 index 00000000..76627a4f --- /dev/null +++ b/cosmwasm/std/src/query/mod.rs @@ -0,0 +1,166 @@ +// needed because the derive macros on QueryRequest use the deprecated `Stargate` variant +#![allow(deprecated)] + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Binary, Empty}; + +/// Implements a hidden constructor for query responses. +macro_rules! impl_response_constructor { + ( $response:ty, $( $field: ident : $t: ty),* ) => { + impl $response { + /// Constructor for testing frameworks such as cw-multi-test. + /// This is required because query response types should be #[non_exhaustive]. + /// As a contract developer you should not need this constructor since + /// query responses are constructed for you via deserialization. + /// + /// Warning: This can change in breaking ways in minor versions. + #[doc(hidden)] + #[allow(dead_code)] + pub fn new($( $field: $t),*) -> Self { + Self { $( $field ),* } + } + } + }; +} + +mod bank; +mod distribution; +mod ibc; +mod query_response; +mod staking; +mod wasm; + +pub use bank::*; +pub use distribution::*; +pub use ibc::*; +pub use staking::*; +pub use wasm::*; + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryRequest { + Bank(BankQuery), + Custom(C), + #[cfg(feature = "staking")] + Staking(StakingQuery), + #[cfg(feature = "cosmwasm_1_3")] + Distribution(DistributionQuery), + /// A Stargate query is encoded the same way as abci_query, with path and protobuf encoded + /// request data. The format is defined in [ADR-21](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-021-protobuf-query-encoding.md). + /// The response is protobuf encoded data directly without a JSON response wrapper. + /// The caller is responsible for compiling the proper protobuf definitions for both requests + /// and responses. + #[cfg(feature = "stargate")] + #[deprecated = "Please use the GrpcQuery instead"] + Stargate { + /// this is the fully qualified service path used for routing, + /// eg. "/cosmos_sdk.x.bank.v1.Query/QueryBalance" + path: String, + /// this is the expected protobuf message type (not any), binary encoded + data: Binary, + }, + #[cfg(feature = "stargate")] + Ibc(IbcQuery), + Wasm(WasmQuery), + #[cfg(feature = "cosmwasm_2_0")] + Grpc(GrpcQuery), +} + +/// Queries the chain using a grpc query. +/// This allows to query information that is not exposed in our API. +/// The chain needs to allowlist the supported queries. +/// The drawback of this query is that you have to handle the protobuf encoding and decoding +/// yourself. +/// +/// The returned data is protobuf encoded. The protobuf type depends on the query. +/// Because of this, using it with the [`query`](crate::QuerierWrapper::query) function will result +/// in a deserialization error. +/// Use [`raw_query`](crate::Querier::raw_query) or +/// [`query_grpc`](crate::QuerierWrapper::query_grpc) instead. +/// +/// To find the path, as well as the request and response types, +/// you can query the chain's gRPC endpoint using a tool like +/// [grpcurl](https://github.com/fullstorydev/grpcurl). +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct GrpcQuery { + /// The fully qualified endpoint path used for routing. + /// It follows the format `/service_path/method_name`, + /// eg. "/cosmos.authz.v1beta1.Query/Grants" + pub path: String, + /// The expected protobuf message type (not [Any](https://protobuf.dev/programming-guides/proto3/#any)), binary encoded + pub data: Binary, +} + +/// A trait that is required to avoid conflicts with other query types like BankQuery and WasmQuery +/// in generic implementations. +/// You need to implement it in your custom query type. +/// +/// # Examples +/// +/// ``` +/// # use cosmwasm_std::CustomQuery; +/// # use schemars::JsonSchema; +/// # use serde::{Deserialize, Serialize}; +/// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +/// #[serde(rename_all = "snake_case")] +/// pub enum MyCustomQuery { +/// Ping {}, +/// Capitalized { text: String }, +/// } +/// +/// impl CustomQuery for MyCustomQuery {} +/// ``` +pub trait CustomQuery: Serialize + Clone {} +// We require `Clone` because `Clone` in `QueryRequest` is only derived for +// `C: Clone` and we want consistent behaviour for all `QueryRequest` + +impl CustomQuery for Empty {} + +impl From for QueryRequest { + fn from(msg: BankQuery) -> Self { + QueryRequest::Bank(msg) + } +} + +impl From for QueryRequest { + fn from(msg: C) -> Self { + QueryRequest::Custom(msg) + } +} + +#[cfg(feature = "staking")] +impl From for QueryRequest { + fn from(msg: StakingQuery) -> Self { + QueryRequest::Staking(msg) + } +} + +impl From for QueryRequest { + fn from(msg: WasmQuery) -> Self { + QueryRequest::Wasm(msg) + } +} + +#[cfg(feature = "cosmwasm_2_0")] +impl From for QueryRequest { + fn from(msg: GrpcQuery) -> Self { + QueryRequest::Grpc(msg) + } +} + +#[cfg(feature = "stargate")] +impl From for QueryRequest { + fn from(msg: IbcQuery) -> Self { + QueryRequest::Ibc(msg) + } +} + +#[cfg(feature = "cosmwasm_1_3")] +impl From for QueryRequest { + fn from(msg: DistributionQuery) -> Self { + QueryRequest::Distribution(msg) + } +} diff --git a/cosmwasm/std/src/query/query_response.rs b/cosmwasm/std/src/query/query_response.rs new file mode 100644 index 00000000..c8385f2f --- /dev/null +++ b/cosmwasm/std/src/query/query_response.rs @@ -0,0 +1,19 @@ +use core::fmt::Debug; + +use serde::de::DeserializeOwned; + +/// A marker trait for query response types. +/// +/// Those types have in common that they should be `#[non_exhaustive]` in order +/// to allow adding fields in a backwards compatible way. In contracts they are +/// only constructed through deserialization. We want to make it hard for +/// contract developers to construct those types themselves as this is most likely +/// not what they should do. +/// +/// In hosts they are constructed as follows: +/// - wasmvm: Go types with the same JSON layout +/// - multi-test/cw-sdk: create a default instance and mutate the fields +/// +/// This trait is crate-internal and can change any time. +#[allow(dead_code)] // This is used to statically ensure all the types have a shared set of traits +pub(crate) trait QueryResponseType: DeserializeOwned + Debug + PartialEq + Clone {} diff --git a/cosmwasm/std/src/query/staking.rs b/cosmwasm/std/src/query/staking.rs new file mode 100644 index 00000000..c2b0f90a --- /dev/null +++ b/cosmwasm/std/src/query/staking.rs @@ -0,0 +1,196 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Addr, Coin, Decimal}; + +use super::query_response::QueryResponseType; + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StakingQuery { + /// Returns the denomination that can be bonded (if there are multiple native tokens on the + /// chain) + BondedDenom {}, + /// AllDelegations will return all delegations by the delegator + AllDelegations { delegator: String }, + /// Delegation will return more detailed info on a particular + /// delegation, defined by delegator/validator pair + Delegation { delegator: String, validator: String }, + /// Returns all validators in the currently active validator set. + /// + /// The query response type is `AllValidatorsResponse`. + AllValidators {}, + /// Returns the validator at the given address. Returns None if the validator is + /// not part of the currently active validator set. + /// + /// The query response type is `ValidatorResponse`. + Validator { + /// The validator's address (e.g. (e.g. cosmosvaloper1...)) + address: String, + }, +} + +/// BondedDenomResponse is data format returned from StakingRequest::BondedDenom query +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct BondedDenomResponse { + pub denom: String, +} + +impl QueryResponseType for BondedDenomResponse {} + +impl_response_constructor!(BondedDenomResponse, denom: String); + +/// DelegationsResponse is data format returned from StakingRequest::AllDelegations query +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct AllDelegationsResponse { + pub delegations: Vec, +} + +impl QueryResponseType for AllDelegationsResponse {} + +impl_response_constructor!(AllDelegationsResponse, delegations: Vec); + +/// Delegation is basic (cheap to query) data about a delegation. +/// +/// Instances are created in the querier. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct Delegation { + pub delegator: Addr, + /// A validator address (e.g. cosmosvaloper1...) + pub validator: String, + /// How much we have locked in the delegation + pub amount: Coin, +} + +impl_response_constructor!(Delegation, delegator: Addr, validator: String, amount: Coin); + +impl From for Delegation { + fn from(full: FullDelegation) -> Self { + Delegation { delegator: full.delegator, validator: full.validator, amount: full.amount } + } +} + +/// DelegationResponse is data format returned from StakingRequest::Delegation query +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub struct DelegationResponse { + pub delegation: Option, +} + +impl QueryResponseType for DelegationResponse {} + +impl_response_constructor!(DelegationResponse, delegation: Option); + +/// FullDelegation is all the info on the delegation, some (like accumulated_reward and +/// can_redelegate) is expensive to query. +/// +/// Instances are created in the querier. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct FullDelegation { + pub delegator: Addr, + /// A validator address (e.g. cosmosvaloper1...) + pub validator: String, + /// How much we have locked in the delegation + pub amount: Coin, + /// can_redelegate captures how much can be immediately redelegated. + /// 0 is no redelegation and can_redelegate == amount is redelegate all + /// but there are many places between the two + pub can_redelegate: Coin, + /// How much we can currently withdraw + pub accumulated_rewards: Vec, +} + +impl_response_constructor!( + FullDelegation, + delegator: Addr, + validator: String, + amount: Coin, + can_redelegate: Coin, + accumulated_rewards: Vec +); + +impl FullDelegation { + /// Creates a new delegation. + /// + /// If fields get added to the [`FullDelegation`] struct in the future, this constructor will + /// provide default values for them, but these default values may not be sensible. + pub fn create( + delegator: Addr, + validator: String, + amount: Coin, + can_redelegate: Coin, + accumulated_rewards: Vec, + ) -> Self { + Self { delegator, validator, amount, can_redelegate, accumulated_rewards } + } +} + +/// The data format returned from StakingRequest::AllValidators query +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct AllValidatorsResponse { + pub validators: Vec, +} + +impl QueryResponseType for AllValidatorsResponse {} + +impl_response_constructor!(AllValidatorsResponse, validators: Vec); + +/// The data format returned from StakingRequest::Validator query +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct ValidatorResponse { + pub validator: Option, +} + +impl QueryResponseType for ValidatorResponse {} + +impl_response_constructor!(ValidatorResponse, validator: Option); + +/// Instances are created in the querier. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct Validator { + /// The operator address of the validator (e.g. cosmosvaloper1...). + /// See https://github.com/cosmos/cosmos-sdk/blob/v0.47.4/proto/cosmos/staking/v1beta1/staking.proto#L95-L96 + /// for more information. + /// + /// This uses `String` instead of `Addr` since the bech32 address prefix is different from + /// the ones that regular user accounts use. + pub address: String, + pub commission: Decimal, + pub max_commission: Decimal, + /// The maximum daily increase of the commission + pub max_change_rate: Decimal, +} + +impl_response_constructor!( + Validator, + address: String, + commission: Decimal, + max_commission: Decimal, + max_change_rate: Decimal +); + +impl Validator { + /// Creates a new validator. + /// + /// If fields get added to the [`Validator`] struct in the future, this constructor will + /// provide default values for them, but these default values may not be sensible. + pub fn create( + address: String, + commission: Decimal, + max_commission: Decimal, + max_change_rate: Decimal, + ) -> Self { + Self { address, commission, max_commission, max_change_rate } + } +} diff --git a/cosmwasm/std/src/query/wasm.rs b/cosmwasm/std/src/query/wasm.rs new file mode 100644 index 00000000..28cf8729 --- /dev/null +++ b/cosmwasm/std/src/query/wasm.rs @@ -0,0 +1,144 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Addr, Binary, Checksum}; + +use super::query_response::QueryResponseType; + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WasmQuery { + /// this queries the public API of another contract at a known address (with known ABI) + /// Return value is whatever the contract returns (caller should know), wrapped in a + /// ContractResult that is JSON encoded. + Smart { + contract_addr: String, + /// msg is the json-encoded QueryMsg struct + msg: Binary, + }, + /// this queries the raw kv-store of the contract. + /// returns the raw, unparsed data stored at that key, which may be an empty vector if not + /// present + Raw { + contract_addr: String, + /// Key is the raw key used in the contracts Storage + key: Binary, + }, + /// Returns a [`ContractInfoResponse`] with metadata on the contract from the runtime + ContractInfo { contract_addr: String }, + /// Returns a [`CodeInfoResponse`] with metadata of the code + #[cfg(feature = "cosmwasm_1_2")] + CodeInfo { code_id: u64 }, +} + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct ContractInfoResponse { + pub code_id: u64, + /// address that instantiated this contract + pub creator: Addr, + /// admin who can run migrations (if any) + pub admin: Option, + /// if set, the contract is pinned to the cache, and thus uses less gas when called + pub pinned: bool, + /// set if this contract has bound an IBC port + pub ibc_port: Option, +} + +impl QueryResponseType for ContractInfoResponse {} + +impl_response_constructor!( + ContractInfoResponse, + code_id: u64, + creator: Addr, + admin: Option, + pinned: bool, + ibc_port: Option +); + +/// The essential data from wasmd's [CodeInfo]/[CodeInfoResponse]. +/// +/// `code_hash`/`data_hash` was renamed to `checksum` to follow the CosmWasm +/// convention and naming in `instantiate2_address`. +/// +/// [CodeInfo]: https://github.com/CosmWasm/wasmd/blob/v0.30.0/proto/cosmwasm/wasm/v1/types.proto#L62-L72 +/// [CodeInfoResponse]: https://github.com/CosmWasm/wasmd/blob/v0.30.0/proto/cosmwasm/wasm/v1/query.proto#L184-L199 +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CodeInfoResponse { + pub code_id: u64, + /// The address that initially stored the code + pub creator: Addr, + /// The hash of the Wasm blob + pub checksum: Checksum, +} + +impl_response_constructor!( + CodeInfoResponse, + code_id: u64, + creator: Addr, + checksum: Checksum +); + +impl QueryResponseType for CodeInfoResponse {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::to_json_binary; + + #[test] + fn wasm_query_contract_info_serialization() { + let query = WasmQuery::ContractInfo { contract_addr: "aabbccdd456".into() }; + let json = to_json_binary(&query).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"contract_info":{"contract_addr":"aabbccdd456"}}"#, + ); + } + + #[test] + #[cfg(feature = "cosmwasm_1_2")] + fn wasm_query_code_info_serialization() { + let query = WasmQuery::CodeInfo { code_id: 70 }; + let json = to_json_binary(&query).unwrap(); + assert_eq!(String::from_utf8_lossy(&json), r#"{"code_info":{"code_id":70}}"#,); + } + + #[test] + fn contract_info_response_serialization() { + let response = ContractInfoResponse { + code_id: 67, + creator: Addr::unchecked("jane"), + admin: Some(Addr::unchecked("king")), + pinned: true, + ibc_port: Some("wasm.123".to_string()), + }; + let json = to_json_binary(&response).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"code_id":67,"creator":"jane","admin":"king","pinned":true,"ibc_port":"wasm.123"}"#, + ); + } + + #[test] + #[cfg(feature = "cosmwasm_1_2")] + fn code_info_response_serialization() { + use crate::Checksum; + + let response = CodeInfoResponse { + code_id: 67, + creator: Addr::unchecked("jane"), + checksum: Checksum::from_hex( + "f7bb7b18fb01bbf425cf4ed2cd4b7fb26a019a7fc75a4dc87e8a0b768c501f00", + ) + .unwrap(), + }; + let json = to_json_binary(&response).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"code_id":67,"creator":"jane","checksum":"f7bb7b18fb01bbf425cf4ed2cd4b7fb26a019a7fc75a4dc87e8a0b768c501f00"}"#, + ); + } +} diff --git a/cosmwasm/std/src/results/contract_result.rs b/cosmwasm/std/src/results/contract_result.rs new file mode 100644 index 00000000..a6ff1e92 --- /dev/null +++ b/cosmwasm/std/src/results/contract_result.rs @@ -0,0 +1,173 @@ +use core::fmt; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +/// This is the final result type that is created and serialized in a contract for +/// every init/execute/migrate call. The VM then deserializes this type to distinguish +/// between successful and failed executions. +/// +/// We use a custom type here instead of Rust's Result because we want to be able to +/// define the serialization, which is a public interface. Every language that compiles +/// to Wasm and runs in the ComsWasm VM needs to create the same JSON representation. +/// +/// # Examples +/// +/// Success: +/// +/// ``` +/// # use cosmwasm_std::{to_vec, ContractResult, Response}; +/// let response: Response = Response::default(); +/// let result: ContractResult = ContractResult::Ok(response); +/// assert_eq!(to_vec(&result).unwrap(), br#"{"ok":{"messages":[],"attributes":[],"events":[],"data":null}}"#); +/// ``` +/// +/// Failure: +/// +/// ``` +/// # use cosmwasm_std::{to_vec, ContractResult, Response}; +/// let error_msg = String::from("Something went wrong"); +/// let result: ContractResult = ContractResult::Err(error_msg); +/// assert_eq!(to_vec(&result).unwrap(), br#"{"error":"Something went wrong"}"#); +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ContractResult { + Ok(S), + /// An error type that every custom error created by contract developers can be converted to. + /// This could potientially have more structure, but String is the easiest. + #[serde(rename = "error")] + Err(String), +} + +// Implementations here mimic the Result API and should be implemented via a conversion to Result +// to ensure API consistency +impl ContractResult { + /// Converts a `ContractResult` to a `Result` as a convenient way + /// to access the full Result API. + pub fn into_result(self) -> Result { + Result::::from(self) + } + + pub fn unwrap(self) -> S { + self.into_result().unwrap() + } + + pub fn is_ok(&self) -> bool { + matches!(self, ContractResult::Ok(_)) + } + + pub fn is_err(&self) -> bool { + matches!(self, ContractResult::Err(_)) + } +} + +impl ContractResult { + pub fn unwrap_err(self) -> String { + self.into_result().unwrap_err() + } +} + +impl From> for ContractResult { + fn from(original: Result) -> ContractResult { + match original { + Ok(value) => ContractResult::Ok(value), + Err(err) => ContractResult::Err(err.to_string()), + } + } +} + +impl From> for Result { + fn from(original: ContractResult) -> Result { + match original { + ContractResult::Ok(value) => Ok(value), + ContractResult::Err(err) => Err(err), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{from_json, to_json_vec, Response, StdError, StdResult}; + + #[test] + fn contract_result_serialization_works() { + let result = ContractResult::Ok(12); + assert_eq!(&to_json_vec(&result).unwrap(), b"{\"ok\":12}"); + + let result = ContractResult::Ok("foo"); + assert_eq!(&to_json_vec(&result).unwrap(), b"{\"ok\":\"foo\"}"); + + let result: ContractResult = ContractResult::Ok(Response::default()); + assert_eq!( + to_json_vec(&result).unwrap(), + br#"{"ok":{"messages":[],"attributes":[],"events":[],"data":null}}"# + ); + + let result: ContractResult = ContractResult::Err("broken".to_string()); + assert_eq!(&to_json_vec(&result).unwrap(), b"{\"error\":\"broken\"}"); + } + + #[test] + fn contract_result_deserialization_works() { + let result: ContractResult = from_json(br#"{"ok":12}"#).unwrap(); + assert_eq!(result, ContractResult::Ok(12)); + + let result: ContractResult = from_json(br#"{"ok":"foo"}"#).unwrap(); + assert_eq!(result, ContractResult::Ok("foo".to_string())); + + let result: ContractResult = + from_json(br#"{"ok":{"messages":[],"attributes":[],"events":[],"data":null}}"#) + .unwrap(); + assert_eq!(result, ContractResult::Ok(Response::default())); + + let result: ContractResult = from_json(br#"{"error":"broken"}"#).unwrap(); + assert_eq!(result, ContractResult::Err("broken".to_string())); + + // ignores whitespace + let result: ContractResult = from_json(b" {\n\t \"ok\": 5898\n} ").unwrap(); + assert_eq!(result, ContractResult::Ok(5898)); + + // fails for additional attributes + let parse: StdResult> = from_json(br#"{"unrelated":321,"ok":4554}"#); + match parse.unwrap_err() { + StdError::ParseErr { .. } => {}, + err => panic!("Unexpected error: {err:?}"), + } + let parse: StdResult> = from_json(br#"{"ok":4554,"unrelated":321}"#); + match parse.unwrap_err() { + StdError::ParseErr { .. } => {}, + err => panic!("Unexpected error: {err:?}"), + } + let parse: StdResult> = + from_json(br#"{"ok":4554,"error":"What's up now?"}"#); + match parse.unwrap_err() { + StdError::ParseErr { .. } => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + #[test] + fn can_convert_from_core_result() { + let original: Result = Ok(Response::default()); + let converted: ContractResult = original.into(); + assert_eq!(converted, ContractResult::Ok(Response::default())); + + let original: Result = Err(StdError::generic_err("broken")); + let converted: ContractResult = original.into(); + assert_eq!(converted, ContractResult::Err("Generic error: broken".to_string())); + } + + #[test] + fn can_convert_to_core_result() { + let original = ContractResult::Ok(Response::default()); + let converted: Result = original.into(); + assert_eq!(converted, Ok(Response::default())); + + let original = ContractResult::Err("went wrong".to_string()); + let converted: Result = original.into(); + assert_eq!(converted, Err("went wrong".to_string())); + } +} diff --git a/cosmwasm/std/src/results/cosmos_msg.rs b/cosmwasm/std/src/results/cosmos_msg.rs new file mode 100644 index 00000000..0231c533 --- /dev/null +++ b/cosmwasm/std/src/results/cosmos_msg.rs @@ -0,0 +1,682 @@ +#![allow(deprecated)] + +use core::fmt; +use derive_more::Debug; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "stargate")] +use crate::ibc::IbcMsg; +#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] +use crate::Decimal; +use crate::{coin::Coin, prelude::*, to_json_binary, Binary, StdResult}; + +use super::Empty; + +/// Like CustomQuery for better type clarity. +/// Also makes it shorter to use as a trait bound. +pub trait CustomMsg: Serialize + Clone + fmt::Debug + PartialEq + JsonSchema {} + +impl CustomMsg for Empty {} + +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +// See https://github.com/serde-rs/serde/issues/1296 why we cannot add De-Serialize trait bounds to T +pub enum CosmosMsg { + Bank(BankMsg), + // by default we use RawMsg, but a contract can override that + // to call into more app-specific code (whatever they define) + Custom(T), + #[cfg(feature = "staking")] + Staking(StakingMsg), + #[cfg(feature = "staking")] + Distribution(DistributionMsg), + /// This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md) + #[cfg(feature = "stargate")] + #[deprecated = "Use `CosmosMsg::Any` instead (if you only target CosmWasm 2+ chains)"] + Stargate { + type_url: String, + value: Binary, + }, + /// `CosmosMsg::Any` is the replaces the "stargate message" – a message wrapped + /// in a [protobuf Any](https://protobuf.dev/programming-guides/proto3/#any) + /// that is suppored by the chain. It behaves the same as + /// `CosmosMsg::Stargate` but has a better name and slightly improved syntax. + /// + /// This is feature-gated at compile time with `cosmwasm_2_0` because + /// a chain running CosmWasm < 2.0 cannot process this. + #[cfg(feature = "cosmwasm_2_0")] + Any(AnyMsg), + #[cfg(feature = "stargate")] + Ibc(IbcMsg), + Wasm(WasmMsg), + #[cfg(feature = "stargate")] + Gov(GovMsg), +} + +impl CosmosMsg { + /// Convert this this [`CosmosMsg`] to a [`CosmosMsg`] with a different custom message + /// type. This allows easier interactions between code written for a specific chain and + /// code written for multiple chains. + /// If this is the [`CosmosMsg::Custom`] variant, the function returns `None`. + pub fn change_custom(self) -> Option> { + Some(match self { + CosmosMsg::Bank(msg) => CosmosMsg::Bank(msg), + CosmosMsg::Custom(_) => return None, + #[cfg(feature = "staking")] + CosmosMsg::Staking(msg) => CosmosMsg::Staking(msg), + #[cfg(feature = "staking")] + CosmosMsg::Distribution(msg) => CosmosMsg::Distribution(msg), + #[cfg(feature = "stargate")] + CosmosMsg::Stargate { type_url, value } => CosmosMsg::Stargate { type_url, value }, + #[cfg(feature = "cosmwasm_2_0")] + CosmosMsg::Any(msg) => CosmosMsg::Any(msg), + #[cfg(feature = "stargate")] + CosmosMsg::Ibc(msg) => CosmosMsg::Ibc(msg), + CosmosMsg::Wasm(msg) => CosmosMsg::Wasm(msg), + #[cfg(feature = "stargate")] + CosmosMsg::Gov(msg) => CosmosMsg::Gov(msg), + }) + } +} + +/// The message types of the bank module. +/// +/// See https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum BankMsg { + /// Sends native tokens from the contract to the given address. + /// + /// This is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). + /// `from_address` is automatically filled with the current contract's address. + Send { to_address: String, amount: Vec }, + /// This will burn the given coins from the contract's account. + /// There is no Cosmos SDK message that performs this, but it can be done by calling the bank + /// keeper. Important if a contract controls significant token supply that must be retired. + Burn { amount: Vec }, +} + +/// The message types of the staking module. +/// +/// See https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto +#[cfg(feature = "staking")] +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StakingMsg { + /// This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). + /// `delegator_address` is automatically filled with the current contract's address. + Delegate { validator: String, amount: Coin }, + /// This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). + /// `delegator_address` is automatically filled with the current contract's address. + Undelegate { validator: String, amount: Coin }, + /// This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). + /// `delegator_address` is automatically filled with the current contract's address. + Redelegate { src_validator: String, dst_validator: String, amount: Coin }, +} + +/// The message types of the distribution module. +/// +/// See https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto +#[cfg(feature = "staking")] +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DistributionMsg { + /// This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). + /// `delegator_address` is automatically filled with the current contract's address. + SetWithdrawAddress { + /// The `withdraw_address` + address: String, + }, + /// This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). + /// `delegator_address` is automatically filled with the current contract's address. + WithdrawDelegatorReward { + /// The `validator_address` + validator: String, + }, + /// This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). + /// `depositor` is automatically filled with the current contract's address. + #[cfg(feature = "cosmwasm_1_3")] + FundCommunityPool { + /// The amount to spend + amount: Vec, + }, +} + +/// A message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). +/// This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct AnyMsg { + pub type_url: String, + pub value: Binary, +} + +#[allow(dead_code)] +struct BinaryToStringEncoder<'a>(&'a Binary); + +impl fmt::Display for BinaryToStringEncoder<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match core::str::from_utf8(self.0.as_slice()) { + Ok(s) => f.write_str(s), + Err(_) => fmt::Debug::fmt(self.0, f), + } + } +} + +/// The message types of the wasm module. +/// +/// See https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WasmMsg { + /// Dispatches a call to another contract at a known address (with known ABI). + /// + /// This is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). + /// `sender` is automatically filled with the current contract's address. + Execute { + contract_addr: String, + /// msg is the json-encoded ExecuteMsg struct (as raw Binary) + #[debug("{}", BinaryToStringEncoder(msg))] + msg: Binary, + funds: Vec, + }, + /// Instantiates a new contracts from previously uploaded Wasm code. + /// + /// The contract address is non-predictable. But it is guaranteed that + /// when emitting the same Instantiate message multiple times, + /// multiple instances on different addresses will be generated. See also + /// Instantiate2. + /// + /// This is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). + /// `sender` is automatically filled with the current contract's address. + Instantiate { + admin: Option, + code_id: u64, + /// msg is the JSON-encoded InstantiateMsg struct (as raw Binary) + #[debug("{}", BinaryToStringEncoder(msg))] + msg: Binary, + funds: Vec, + /// A human-readable label for the contract. + /// + /// Valid values should: + /// - not be empty + /// - not be bigger than 128 bytes (or some chain-specific limit) + /// - not start / end with whitespace + label: String, + }, + /// Instantiates a new contracts from previously uploaded Wasm code + /// using a predictable address derivation algorithm implemented in + /// [`cosmwasm_std::instantiate2_address`]. + /// + /// This is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). + /// `sender` is automatically filled with the current contract's address. + /// `fix_msg` is automatically set to false. + #[cfg(feature = "cosmwasm_1_2")] + Instantiate2 { + admin: Option, + code_id: u64, + /// A human-readable label for the contract. + /// + /// Valid values should: + /// - not be empty + /// - not be bigger than 128 bytes (or some chain-specific limit) + /// - not start / end with whitespace + label: String, + /// msg is the JSON-encoded InstantiateMsg struct (as raw Binary) + #[debug("{}", BinaryToStringEncoder(msg))] + msg: Binary, + funds: Vec, + salt: Binary, + }, + /// Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to + /// customize behavior. + /// + /// Only the contract admin (as defined in wasmd), if any, is able to make this call. + /// + /// This is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). + /// `sender` is automatically filled with the current contract's address. + Migrate { + contract_addr: String, + /// the code_id of the new logic to place in the given contract + new_code_id: u64, + /// msg is the json-encoded MigrateMsg struct that will be passed to the new code + #[debug("{}", BinaryToStringEncoder(msg))] + msg: Binary, + }, + /// Sets a new admin (for migrate) on the given contract. + /// Fails if this contract is not currently admin of the target contract. + UpdateAdmin { contract_addr: String, admin: String }, + /// Clears the admin on the given contract, so no more migration possible. + /// Fails if this contract is not currently admin of the target contract. + ClearAdmin { contract_addr: String }, +} + +/// This message type allows the contract interact with the [x/gov] module in order +/// to cast votes. +/// +/// [x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov +/// +/// ## Examples +/// +/// Cast a simple vote: +/// +/// ``` +/// # use cosmwasm_std::{ +/// # HexBinary, +/// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, +/// # Response, QueryResponse, +/// # }; +/// # type ExecuteMsg = (); +/// use cosmwasm_std::{GovMsg, VoteOption}; +/// +/// #[entry_point] +/// pub fn execute( +/// deps: DepsMut, +/// env: Env, +/// info: MessageInfo, +/// msg: ExecuteMsg, +/// ) -> Result { +/// // ... +/// Ok(Response::new().add_message(GovMsg::Vote { +/// proposal_id: 4, +/// option: VoteOption::Yes, +/// })) +/// } +/// ``` +/// +/// Cast a weighted vote: +/// +/// ``` +/// # use cosmwasm_std::{ +/// # HexBinary, +/// # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, +/// # Response, QueryResponse, +/// # }; +/// # type ExecuteMsg = (); +/// # #[cfg(feature = "cosmwasm_1_2")] +/// use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption}; +/// +/// # #[cfg(feature = "cosmwasm_1_2")] +/// #[entry_point] +/// pub fn execute( +/// deps: DepsMut, +/// env: Env, +/// info: MessageInfo, +/// msg: ExecuteMsg, +/// ) -> Result { +/// // ... +/// Ok(Response::new().add_message(GovMsg::VoteWeighted { +/// proposal_id: 4, +/// options: vec![ +/// WeightedVoteOption { +/// option: VoteOption::Yes, +/// weight: Decimal::percent(65), +/// }, +/// WeightedVoteOption { +/// option: VoteOption::Abstain, +/// weight: Decimal::percent(35), +/// }, +/// ], +/// })) +/// } +/// ``` +#[cfg(feature = "stargate")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GovMsg { + /// This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address. + Vote { + proposal_id: u64, + /// The vote option. + /// + /// This used to be called "vote", but was changed for consistency with Cosmos SDK. + option: VoteOption, + }, + /// This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address. + #[cfg(feature = "cosmwasm_1_2")] + VoteWeighted { proposal_id: u64, options: Vec }, +} + +#[cfg(feature = "stargate")] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VoteOption { + Yes, + No, + Abstain, + NoWithVeto, +} + +#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct WeightedVoteOption { + pub option: VoteOption, + pub weight: Decimal, +} + +/// Shortcut helper as the construction of WasmMsg::Instantiate can be quite verbose in contract +/// code. +/// +/// When using this, `admin` is always unset. If you need more flexibility, create the message +/// directly. +pub fn wasm_instantiate( + code_id: u64, + msg: &impl Serialize, + funds: Vec, + label: String, +) -> StdResult { + let payload = to_json_binary(msg)?; + Ok(WasmMsg::Instantiate { admin: None, code_id, msg: payload, funds, label }) +} + +/// Shortcut helper as the construction of WasmMsg::Execute can be quite verbose in contract code +pub fn wasm_execute( + contract_addr: impl Into, + msg: &impl Serialize, + funds: Vec, +) -> StdResult { + let payload = to_json_binary(msg)?; + Ok(WasmMsg::Execute { contract_addr: contract_addr.into(), msg: payload, funds }) +} + +impl From for CosmosMsg { + fn from(msg: BankMsg) -> Self { + CosmosMsg::Bank(msg) + } +} + +#[cfg(feature = "staking")] +impl From for CosmosMsg { + fn from(msg: StakingMsg) -> Self { + CosmosMsg::Staking(msg) + } +} + +#[cfg(feature = "staking")] +impl From for CosmosMsg { + fn from(msg: DistributionMsg) -> Self { + CosmosMsg::Distribution(msg) + } +} + +// By implementing `From for cosmwasm_std::AnyMsg`, +// you automatically get a MyType -> CosmosMsg conversion. +#[cfg(feature = "cosmwasm_2_0")] +impl, T> From for CosmosMsg { + fn from(source: S) -> Self { + CosmosMsg::::Any(source.into()) + } +} + +impl From for CosmosMsg { + fn from(msg: WasmMsg) -> Self { + CosmosMsg::Wasm(msg) + } +} + +#[cfg(feature = "stargate")] +impl From for CosmosMsg { + fn from(msg: IbcMsg) -> Self { + CosmosMsg::Ibc(msg) + } +} + +#[cfg(feature = "stargate")] +impl From for CosmosMsg { + fn from(msg: GovMsg) -> Self { + CosmosMsg::Gov(msg) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{coin, coins}; + use fmt::Debug; + + #[test] + fn from_bank_msg_works() { + let to_address = String::from("you"); + let amount = coins(1015, "earth"); + let bank = BankMsg::Send { to_address, amount }; + let msg: CosmosMsg = bank.clone().into(); + match msg { + CosmosMsg::Bank(msg) => assert_eq!(bank, msg), + _ => panic!("must encode in Bank variant"), + } + } + + #[test] + #[cfg(feature = "cosmwasm_2_0")] + fn from_any_msg_works() { + // should work with AnyMsg + let any = AnyMsg { + type_url: "/cosmos.foo.v1beta.MsgBar".to_string(), + value: Binary::from_base64("5yu/rQ+HrMcxH1zdga7P5hpGMLE=").unwrap(), + }; + let msg: CosmosMsg = any.clone().into(); + assert!(matches!(msg, CosmosMsg::Any(a) if a == any)); + + // should work with Into + struct IntoAny; + + impl From for AnyMsg { + fn from(_: IntoAny) -> Self { + AnyMsg { + type_url: "/cosmos.foo.v1beta.MsgBar".to_string(), + value: Binary::from_base64("5yu/rQ+HrMcxH1zdga7P5hpGMLE=").unwrap(), + } + } + } + + let msg: CosmosMsg = IntoAny.into(); + assert!(matches!( + msg, + CosmosMsg::Any(a) if a == any + )); + } + + #[test] + fn wasm_msg_serializes_to_correct_json() { + // Instantiate with admin + let msg = WasmMsg::Instantiate { + admin: Some("king".to_string()), + code_id: 7897, + msg: br#"{"claim":{}}"#.into(), + funds: vec![], + label: "my instance".to_string(), + }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"instantiate":{"admin":"king","code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[],"label":"my instance"}}"#, + ); + + // Instantiate without admin + let msg = WasmMsg::Instantiate { + admin: None, + code_id: 7897, + msg: br#"{"claim":{}}"#.into(), + funds: vec![], + label: "my instance".to_string(), + }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"instantiate":{"admin":null,"code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[],"label":"my instance"}}"#, + ); + + // Instantiate with funds + let msg = WasmMsg::Instantiate { + admin: None, + code_id: 7897, + msg: br#"{"claim":{}}"#.into(), + funds: vec![coin(321, "stones")], + label: "my instance".to_string(), + }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"instantiate":{"admin":null,"code_id":7897,"msg":"eyJjbGFpbSI6e319","funds":[{"denom":"stones","amount":"321"}],"label":"my instance"}}"#, + ); + + // Instantiate2 + #[cfg(feature = "cosmwasm_1_2")] + { + let msg = WasmMsg::Instantiate2 { + admin: None, + code_id: 7897, + label: "my instance".to_string(), + msg: br#"{"claim":{}}"#.into(), + funds: vec![coin(321, "stones")], + salt: Binary::from_base64("UkOVazhiwoo=").unwrap(), + }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"instantiate2":{"admin":null,"code_id":7897,"label":"my instance","msg":"eyJjbGFpbSI6e319","funds":[{"denom":"stones","amount":"321"}],"salt":"UkOVazhiwoo="}}"#, + ); + } + } + + #[test] + #[cfg(feature = "cosmwasm_2_0")] + fn any_msg_serializes_to_correct_json() { + // Same serialization as CosmosMsg::Stargate (see above), except the top level key + let msg: CosmosMsg = CosmosMsg::Any(AnyMsg { + type_url: "/cosmos.foo.v1beta.MsgBar".to_string(), + value: Binary::from_base64("5yu/rQ+HrMcxH1zdga7P5hpGMLE=").unwrap(), + }); + let json = crate::to_json_string(&msg).unwrap(); + assert_eq!( + json, + r#"{"any":{"type_url":"/cosmos.foo.v1beta.MsgBar","value":"5yu/rQ+HrMcxH1zdga7P5hpGMLE="}}"#, + ); + } + + // #[test] + // #[cfg(feature = "cosmwasm_1_3")] + // fn msg_distribution_serializes_to_correct_json() { + // // FundCommunityPool + // let fund_coins = vec![coin(200, "feathers"), coin(200, "stones")]; + // let fund_msg = DistributionMsg::FundCommunityPool { amount: fund_coins }; + // let fund_json = to_json_binary(&fund_msg).unwrap(); + // assert_eq!( + // String::from_utf8_lossy(&fund_json), + // r#"{"fund_community_pool":{"amount":[{"denom":"feathers","amount":"200"},{"denom":"stones", + // "amount":"200"}]}}"#, ); + + // // SetWithdrawAddress + // let set_msg = DistributionMsg::SetWithdrawAddress { address: String::from("withdrawer") }; + // let set_json = to_json_binary(&set_msg).unwrap(); + // assert_eq!( + // String::from_utf8_lossy(&set_json), + // r#"{"set_withdraw_address":{"address":"withdrawer"}}"#, + // ); + + // // WithdrawDelegatorRewards + // let withdraw_msg = + // DistributionMsg::WithdrawDelegatorReward { validator: String::from("fancyoperator") }; + // let withdraw_json = to_json_binary(&withdraw_msg).unwrap(); + // assert_eq!( + // String::from_utf8_lossy(&withdraw_json), + // r#"{"withdraw_delegator_reward":{"validator":"fancyoperator"}}"# + // ); + // } + + // #[test] + // fn wasm_msg_debug_decodes_binary_string_when_possible() { + // #[cosmwasm_schema::cw_serde] + // enum ExecuteMsg { + // Mint { coin: Coin }, + // } + + // let msg = WasmMsg::Execute { + // contract_addr: "joe".to_string(), + // msg: to_json_binary(&ExecuteMsg::Mint { coin: coin(10, "BTC") }).unwrap(), + // funds: vec![], + // }; + + // assert_eq!( + // format!("{msg:?}"), + // "Execute { contract_addr: \"joe\", msg: + // {\"mint\":{\"coin\":{\"denom\":\"BTC\",\"amount\":\"10\"}}}, funds: [] }" ); + // } + + #[test] + fn wasm_msg_debug_dumps_binary_when_not_utf8() { + let msg = WasmMsg::Execute { + contract_addr: "joe".to_string(), + msg: Binary::from([0, 159, 146, 150]), + funds: vec![], + }; + + assert_eq!( + format!("{msg:?}"), + "Execute { contract_addr: \"joe\", msg: Binary(009f9296), funds: [] }" + ); + } + + #[test] + #[cfg(feature = "stargate")] + fn gov_msg_serializes_to_correct_json() { + // Vote + let msg = GovMsg::Vote { proposal_id: 4, option: VoteOption::NoWithVeto }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"vote":{"proposal_id":4,"option":"no_with_veto"}}"#, + ); + + // VoteWeighted + #[cfg(feature = "cosmwasm_1_2")] + { + let msg = GovMsg::VoteWeighted { + proposal_id: 25, + options: vec![ + WeightedVoteOption { weight: Decimal::percent(25), option: VoteOption::Yes }, + WeightedVoteOption { weight: Decimal::percent(25), option: VoteOption::No }, + WeightedVoteOption { + weight: Decimal::percent(50), + option: VoteOption::Abstain, + }, + ], + }; + + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"vote_weighted":{"proposal_id":25,"options":[{"option":"yes","weight":"0.25"},{"option":"no","weight":"0.25"},{"option":"abstain","weight":"0.5"}]}}"#, + ); + } + } + + #[test] + fn change_custom_works() { + #[derive(Debug, PartialEq, Eq, Clone)] + struct Custom { + _a: i32, + } + let send = BankMsg::Send { to_address: "you".to_string(), amount: coins(1015, "earth") }; + // Custom to Empty + let msg: CosmosMsg = send.clone().into(); + let msg2: CosmosMsg = msg.change_custom().unwrap(); + assert_eq!(msg2, CosmosMsg::Bank(send.clone())); + let custom = CosmosMsg::Custom(Custom { _a: 5 }); + let converted = custom.change_custom::(); + assert_eq!(converted, None); + + // Empty to Custom + let msg: CosmosMsg = send.clone().into(); + let msg2: CosmosMsg = msg.change_custom().unwrap(); + assert_eq!(msg2, CosmosMsg::Bank(send)); + let empty = CosmosMsg::Custom(Empty {}); + let converted = empty.change_custom::(); + assert_eq!(converted, None); + } +} diff --git a/cosmwasm/std/src/results/empty.rs b/cosmwasm/std/src/results/empty.rs new file mode 100644 index 00000000..f60bdf56 --- /dev/null +++ b/cosmwasm/std/src/results/empty.rs @@ -0,0 +1,37 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// An empty struct that serves as a placeholder in different places, +/// such as contracts that don't set a custom message. +/// +/// It is designed to be expressable in correct JSON and JSON Schema but +/// contains no meaningful data. Previously we used enums without cases, +/// but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default)] +pub struct Empty {} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{from_json, to_json_vec}; + + #[test] + fn empty_can_be_instantiated() { + let instance = Empty::default(); + assert_eq!(instance, Empty {}); + } + + #[test] + fn empty_can_be_instantiated_serialized_and_deserialized() { + let instance = Empty {}; + let serialized = to_json_vec(&instance).unwrap(); + assert_eq!(serialized, b"{}"); + + let deserialized: Empty = from_json(b"{}").unwrap(); + assert_eq!(deserialized, instance); + + let deserialized: Empty = from_json(b"{\"stray\":\"data\"}").unwrap(); + assert_eq!(deserialized, instance); + } +} diff --git a/cosmwasm/std/src/results/events.rs b/cosmwasm/std/src/results/events.rs new file mode 100644 index 00000000..adbe5143 --- /dev/null +++ b/cosmwasm/std/src/results/events.rs @@ -0,0 +1,155 @@ +use crate::__internal::forward_ref_partial_eq; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +/// A full [*Cosmos SDK* event]. +/// +/// This version uses string attributes (similar to [*Cosmos SDK* StringEvent]), +/// which then get magically converted to bytes for Tendermint somewhere between +/// the Rust-Go interface, JSON deserialization and the `NewEvent` call in Cosmos SDK. +/// +/// [*Cosmos SDK* event]: https://docs.cosmos.network/main/learn/advanced/events +/// [*Cosmos SDK* StringEvent]: https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/base/abci/v1beta1/abci.proto#L56-L70 +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct Event { + /// The event type. This is renamed to "ty" because "type" is reserved in Rust. This sucks, we + /// know. + #[serde(rename = "type")] + pub ty: String, + /// The attributes to be included in the event. + /// + /// You can learn more about these from [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub attributes: Vec, +} + +forward_ref_partial_eq!(Event, Event); + +impl Event { + /// Create a new event with the given type and an empty list of attributes. + pub fn new(ty: impl Into) -> Self { + Event { ty: ty.into(), attributes: Vec::with_capacity(10) } + } + + /// Add an attribute to the event. + pub fn add_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.attributes.push(Attribute { key: key.into(), value: value.into() }); + self + } + + /// Bulk add attributes to the event. + /// + /// Anything that can be turned into an iterator and yields something + /// that can be converted into an `Attribute` is accepted. + pub fn add_attributes>( + mut self, + attrs: impl IntoIterator, + ) -> Self { + self.attributes.extend(attrs.into_iter().map(A::into)); + self + } +} + +/// An key value pair that is used in the context of event attributes in logs +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] +pub struct Attribute { + pub key: String, + pub value: String, +} + +forward_ref_partial_eq!(Attribute, Attribute); + +impl Attribute { + /// Creates a new Attribute. `attr` is just an alias for this. + pub fn new(key: impl Into, value: impl Into) -> Self { + let key = key.into(); + + #[cfg(debug_assertions)] + if key.starts_with('_') { + panic!( + "attribute key `{key}` is invalid - keys starting with an underscore are reserved" + ); + } + + Self { key, value: value.into() } + } +} + +impl, V: Into> From<(K, V)> for Attribute { + fn from((k, v): (K, V)) -> Self { + Attribute::new(k, v) + } +} + +impl, V: AsRef> PartialEq<(K, V)> for Attribute { + fn eq(&self, (k, v): &(K, V)) -> bool { + (self.key.as_str(), self.value.as_str()) == (k.as_ref(), v.as_ref()) + } +} + +impl, V: AsRef> PartialEq for (K, V) { + fn eq(&self, attr: &Attribute) -> bool { + attr == self + } +} + +impl, V: AsRef> PartialEq<(K, V)> for &Attribute { + fn eq(&self, (k, v): &(K, V)) -> bool { + (self.key.as_str(), self.value.as_str()) == (k.as_ref(), v.as_ref()) + } +} + +impl, V: AsRef> PartialEq<&Attribute> for (K, V) { + fn eq(&self, attr: &&Attribute) -> bool { + attr == self + } +} + +/// Creates a new Attribute. `Attribute::new` is an alias for this. +#[inline] +pub fn attr(key: impl Into, value: impl Into) -> Attribute { + Attribute::new(key, value) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Uint128; + + #[test] + fn event_construction() { + let event_direct = Event { + ty: "test".to_string(), + attributes: vec![attr("foo", "bar"), attr("bar", "baz")], + }; + let event_builder = Event::new("test").add_attributes(vec![("foo", "bar"), ("bar", "baz")]); + + assert_eq!(event_direct, event_builder); + } + + #[test] + #[should_panic] + fn attribute_new_reserved_key_panicks() { + Attribute::new("_invalid", "value"); + } + + #[test] + #[should_panic] + fn attribute_new_reserved_key_panicks2() { + Attribute::new("_", "value"); + } + + #[test] + fn attr_works_for_different_types() { + let expected = ("foo", "42"); + + assert_eq!(attr("foo", "42"), expected); + assert_eq!(attr("foo", "42"), expected); + assert_eq!(attr("foo", "42"), expected); + assert_eq!(attr("foo", Uint128::new(42)), expected); + } +} diff --git a/cosmwasm/std/src/results/mod.rs b/cosmwasm/std/src/results/mod.rs new file mode 100644 index 00000000..39539356 --- /dev/null +++ b/cosmwasm/std/src/results/mod.rs @@ -0,0 +1,28 @@ +//! This module contains the messages that are sent from the contract to the VM as an execution +//! result + +mod contract_result; +mod cosmos_msg; +mod empty; +mod events; +mod query; +mod response; +mod submessages; +mod system_result; + +pub use contract_result::ContractResult; +#[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] +pub use cosmos_msg::WeightedVoteOption; +pub use cosmos_msg::{ + wasm_execute, wasm_instantiate, AnyMsg, BankMsg, CosmosMsg, CustomMsg, WasmMsg, +}; +#[cfg(feature = "staking")] +pub use cosmos_msg::{DistributionMsg, StakingMsg}; +#[cfg(feature = "stargate")] +pub use cosmos_msg::{GovMsg, VoteOption}; +pub use empty::Empty; +pub use events::{attr, Attribute, Event}; +pub use query::QueryResponse; +pub use response::Response; +pub use submessages::{MsgResponse, Reply, ReplyOn, SubMsg, SubMsgResponse, SubMsgResult}; +pub use system_result::SystemResult; diff --git a/cosmwasm/std/src/results/query.rs b/cosmwasm/std/src/results/query.rs new file mode 100644 index 00000000..8619ab7e --- /dev/null +++ b/cosmwasm/std/src/results/query.rs @@ -0,0 +1 @@ +pub type QueryResponse = crate::Binary; diff --git a/cosmwasm/std/src/results/response.rs b/cosmwasm/std/src/results/response.rs new file mode 100644 index 00000000..97d92530 --- /dev/null +++ b/cosmwasm/std/src/results/response.rs @@ -0,0 +1,409 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Binary}; + +use super::{Attribute, CosmosMsg, Empty, Event, SubMsg}; + +/// A response of a contract entry point, such as `instantiate`, `execute` or `migrate`. +/// +/// This type can be constructed directly at the end of the call. Alternatively a +/// mutable response instance can be created early in the contract's logic and +/// incrementally be updated. +/// +/// ## Examples +/// +/// Direct: +/// +/// ``` +/// # use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo}; +/// # type InstantiateMsg = (); +/// # +/// use cosmwasm_std::{attr, Response, StdResult}; +/// +/// pub fn instantiate( +/// deps: DepsMut, +/// _env: Env, +/// _info: MessageInfo, +/// msg: InstantiateMsg, +/// ) -> StdResult { +/// // ... +/// +/// Ok(Response::new().add_attribute("action", "instantiate")) +/// } +/// ``` +/// +/// Mutating: +/// +/// ``` +/// # use cosmwasm_std::{coins, BankMsg, Binary, DepsMut, Env, MessageInfo, SubMsg}; +/// # type InstantiateMsg = (); +/// # type MyError = (); +/// # +/// use cosmwasm_std::Response; +/// +/// pub fn instantiate( +/// deps: DepsMut, +/// _env: Env, +/// info: MessageInfo, +/// msg: InstantiateMsg, +/// ) -> Result { +/// let mut response = Response::new() +/// .add_attribute("Let the", "hacking begin") +/// .add_message(BankMsg::Send { +/// to_address: String::from("recipient"), +/// amount: coins(128, "uint"), +/// }) +/// .add_attribute("foo", "bar") +/// .set_data(b"the result data"); +/// Ok(response) +/// } +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[non_exhaustive] +pub struct Response { + /// Optional list of messages to pass. These will be executed in order. + /// If the ReplyOn variant matches the result (Always, Success on Ok, Error on Err), + /// the runtime will invoke this contract's `reply` entry point + /// after execution. Otherwise, they act like "fire and forget". + /// Use `SubMsg::new` to create messages with the older "fire and forget" semantics. + pub messages: Vec>, + /// The attributes that will be emitted as part of a "wasm" event. + /// + /// More info about events (and their attributes) can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub attributes: Vec, + /// Extra, custom events separate from the main `wasm` one. These will have + /// `wasm-` prepended to the type. + /// + /// More info about events can be found in [*Cosmos SDK* docs]. + /// + /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events + pub events: Vec, + /// The binary payload to include in the response. + pub data: Option, +} + +impl Default for Response { + fn default() -> Self { + Response { messages: vec![], attributes: vec![], events: vec![], data: None } + } +} + +impl Response { + pub fn new() -> Self { + Self::default() + } + + /// Add an attribute included in the main `wasm` event. + /// + /// For working with optional values or optional attributes, see + /// [`add_attributes`][Self::add_attributes]. + pub fn add_attribute(mut self, key: impl Into, value: impl Into) -> Self { + self.attributes.push(Attribute::new(key, value)); + self + } + + /// This creates a "fire and forget" message, by using `SubMsg::new()` to wrap it, + /// and adds it to the list of messages to process. + pub fn add_message(mut self, msg: impl Into>) -> Self { + self.messages.push(SubMsg::new(msg)); + self + } + + /// This takes an explicit SubMsg (creates via eg. `reply_on_error`) + /// and adds it to the list of messages to process. + pub fn add_submessage(mut self, msg: SubMsg) -> Self { + self.messages.push(msg); + self + } + + /// Adds an extra event to the response, separate from the main `wasm` event + /// that is always created. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided type + /// of event. + pub fn add_event(mut self, event: impl Into) -> Self { + self.events.push(event.into()); + self + } + + /// Bulk add attributes included in the main `wasm` event. + /// + /// Anything that can be turned into an iterator and yields something + /// that can be converted into an `Attribute` is accepted. + /// + /// ## Examples + /// + /// Adding a list of attributes using the pair notation for key and value: + /// + /// ``` + /// use cosmwasm_std::Response; + /// + /// let attrs = vec![ + /// ("action", "reaction"), + /// ("answer", "42"), + /// ("another", "attribute"), + /// ]; + /// let res: Response = Response::new().add_attributes(attrs.clone()); + /// assert_eq!(res.attributes, attrs); + /// ``` + /// + /// Adding an optional value as an optional attribute by turning it into a list of 0 or 1 + /// elements: + /// + /// ``` + /// use cosmwasm_std::{Attribute, Response}; + /// + /// // Some value + /// let value: Option = Some("sarah".to_string()); + /// let attribute: Option = value.map(|v| Attribute::new("winner", v)); + /// let res: Response = Response::new().add_attributes(attribute); + /// assert_eq!(res.attributes, [Attribute { + /// key: "winner".to_string(), + /// value: "sarah".to_string(), + /// }]); + /// + /// // No value + /// let value: Option = None; + /// let attribute: Option = value.map(|v| Attribute::new("winner", v)); + /// let res: Response = Response::new().add_attributes(attribute); + /// assert_eq!(res.attributes.len(), 0); + /// ``` + pub fn add_attributes>( + mut self, + attrs: impl IntoIterator, + ) -> Self { + self.attributes.extend(attrs.into_iter().map(A::into)); + self + } + + /// Bulk add "fire and forget" messages to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{CosmosMsg, Response}; + /// + /// fn make_response_with_msgs(msgs: Vec) -> Response { + /// Response::new().add_messages(msgs) + /// } + /// ``` + pub fn add_messages>>(self, msgs: impl IntoIterator) -> Self { + self.add_submessages(msgs.into_iter().map(SubMsg::new)) + } + + /// Bulk add explicit SubMsg structs to the list of messages to process. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{SubMsg, Response}; + /// + /// fn make_response_with_submsgs(msgs: Vec) -> Response { + /// Response::new().add_submessages(msgs) + /// } + /// ``` + pub fn add_submessages(mut self, msgs: impl IntoIterator>) -> Self { + self.messages.extend(msgs); + self + } + + /// Bulk add custom events to the response. These are separate from the main + /// `wasm` event. + /// + /// The `wasm-` prefix will be appended by the runtime to the provided types + /// of events. + pub fn add_events(mut self, events: impl IntoIterator) -> Self + where + E: Into, + { + self.events.extend(events.into_iter().map(|e| e.into())); + self + } + + /// Set the binary data included in the response. + pub fn set_data(mut self, data: impl Into) -> Self { + self.data = Some(data.into()); + self + } + + /// Convert this [`Response`] to a [`Response`] with a different custom message type. + /// This allows easier interactions between code written for a specific chain and + /// code written for multiple chains. + /// If this contains a [`CosmosMsg::Custom`] submessage, the function returns `None`. + pub fn change_custom(self) -> Option> { + Some(Response { + messages: self + .messages + .into_iter() + .map(|msg| msg.change_custom()) + .collect::>>()?, + attributes: self.attributes, + events: self.events, + data: self.data, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{super::BankMsg, *}; + use crate::{ + coins, from_json, + results::submessages::{ReplyOn, UNUSED_MSG_ID}, + to_json_vec, ContractResult, + }; + + #[test] + fn response_add_attributes_works() { + let res = Response::::new().add_attributes(core::iter::empty::()); + assert_eq!(res.attributes.len(), 0); + + let res = Response::::new().add_attributes([Attribute::new("test", "ing")]); + assert_eq!(res.attributes.len(), 1); + assert_eq!( + res.attributes[0], + Attribute { key: "test".to_string(), value: "ing".to_string() } + ); + + let attrs = vec![("action", "reaction"), ("answer", "42"), ("another", "attribute")]; + let res: Response = Response::new().add_attributes(attrs.clone()); + assert_eq!(res.attributes, attrs); + + let optional = Option::::None; + let res: Response = Response::new().add_attributes(optional); + assert_eq!(res.attributes.len(), 0); + + let optional = Option::::Some(Attribute::new("test", "ing")); + let res: Response = Response::new().add_attributes(optional); + assert_eq!(res.attributes.len(), 1); + assert_eq!( + res.attributes[0], + Attribute { key: "test".to_string(), value: "ing".to_string() } + ); + } + + #[test] + fn can_serialize_and_deserialize_init_response() { + let original = Response { + messages: vec![ + SubMsg { + id: 12, + payload: Binary::new(vec![9, 8, 7, 6, 5]), + msg: BankMsg::Send { + to_address: String::from("checker"), + amount: coins(888, "moon"), + } + .into(), + gas_limit: Some(12345u64), + reply_on: ReplyOn::Always, + }, + SubMsg { + id: UNUSED_MSG_ID, + payload: Binary::default(), + msg: BankMsg::Send { + to_address: String::from("you"), + amount: coins(1015, "earth"), + } + .into(), + gas_limit: None, + reply_on: ReplyOn::Never, + }, + ], + attributes: vec![Attribute { key: "action".to_string(), value: "release".to_string() }], + events: vec![], + data: Some(Binary::from([0xAA, 0xBB])), + }; + let serialized = to_json_vec(&original).expect("encode contract result"); + let deserialized: Response = from_json(serialized).expect("decode contract result"); + assert_eq!(deserialized, original); + } + + #[test] + fn contract_result_is_ok_works() { + let success = ContractResult::<()>::Ok(()); + let failure = ContractResult::<()>::Err("broken".to_string()); + assert!(success.is_ok()); + assert!(!failure.is_ok()); + } + + #[test] + fn contract_result_is_err_works() { + let success = ContractResult::<()>::Ok(()); + let failure = ContractResult::<()>::Err("broken".to_string()); + assert!(failure.is_err()); + assert!(!success.is_err()); + } + + // struct implements `Into` + #[derive(Clone)] + struct OurEvent { + msg: String, + } + + // allow define `into` rather than `from` to define `into` clearly + #[allow(clippy::from_over_into)] + impl Into for OurEvent { + fn into(self) -> Event { + Event::new("our_event").add_attribute("msg", self.msg) + } + } + + #[test] + fn add_event_takes_into_event() { + let msg = "message".to_string(); + let our_event = OurEvent { msg }; + let event: Event = our_event.clone().into(); + let actual = Response::::new().add_event(our_event); + let expected = Response::::new().add_event(event); + assert_eq!(expected, actual); + } + + #[test] + fn add_events_takes_into_event() { + let msg1 = "foo".to_string(); + let msg2 = "bare".to_string(); + let our_event1 = OurEvent { msg: msg1 }; + let our_event2 = OurEvent { msg: msg2 }; + let events: Vec = vec![our_event1.clone().into(), our_event2.clone().into()]; + let actual = Response::::new().add_events([our_event1, our_event2]); + let expected = Response::::new().add_events(events); + assert_eq!(expected, actual); + } + + #[test] + fn change_custom_works() { + let response: Response = Response { + messages: vec![SubMsg::new(BankMsg::Send { + to_address: "address".to_string(), + amount: coins(123, "earth"), + })], + attributes: vec![Attribute::new("foo", "bar")], + events: vec![Event::new("our_event").add_attribute("msg", "hello")], + data: None, + }; + let converted_resp: Response = response.clone().change_custom().unwrap(); + assert_eq!( + converted_resp.messages, + vec![SubMsg::new(BankMsg::Send { + to_address: "address".to_string(), + amount: coins(123, "earth"), + })] + ); + assert_eq!(converted_resp.attributes, response.attributes); + assert_eq!(converted_resp.events, response.events); + assert_eq!(converted_resp.data, response.data); + + // response with custom message + let response = Response { + messages: vec![SubMsg::new(CosmosMsg::Custom(Empty {}))], + attributes: vec![Attribute::new("foo", "bar")], + events: vec![Event::new("our_event").add_attribute("msg", "hello")], + data: None, + }; + + assert_eq!(response.change_custom::(), None); + } +} diff --git a/cosmwasm/std/src/results/submessages.rs b/cosmwasm/std/src/results/submessages.rs new file mode 100644 index 00000000..0219e4f2 --- /dev/null +++ b/cosmwasm/std/src/results/submessages.rs @@ -0,0 +1,615 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, Binary}; + +use super::{CosmosMsg, Empty, Event}; + +/// Use this to define when the contract gets a response callback. +/// If you only need it for errors or success you can select just those in order +/// to save gas. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ReplyOn { + /// Always perform a callback after SubMsg is processed + Always, + /// Only callback if SubMsg returned an error, no callback on success case + Error, + /// Only callback if SubMsg was successful, no callback on error case + Success, + /// Never make a callback - this is like the original CosmosMsg semantics + Never, +} + +/// A submessage that will guarantee a `reply` call on success or error, depending on +/// the `reply_on` setting. If you do not need to process the result, use regular messages instead. +/// +/// Note: On error the submessage execution will revert any partial state changes due to this +/// message, but not revert any state changes in the calling contract. If this is required, it must +/// be done manually in the `reply` entry point. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct SubMsg { + /// An arbitrary ID chosen by the contract. + /// This is typically used to match `Reply`s in the `reply` entry point to the submessage. + pub id: u64, + /// Some arbirary data that the contract can set in an application specific way. + /// This is just passed into the `reply` entry point and is not stored to state. + /// Any encoding can be used. If `id` is used to identify a particular action, + /// the encoding can also be different for each of those actions since you can match `id` + /// first and then start processing the `payload`. + /// + /// The environment restricts the length of this field in order to avoid abuse. The limit + /// is environment specific and can change over time. The initial default is 128 KiB. + /// + /// Unset/nil/null cannot be differentiated from empty data. + /// + /// On chains running CosmWasm 1.x this field will be ignored. + #[serde(default)] + pub payload: Binary, + pub msg: CosmosMsg, + /// Gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). + /// + /// Setting this to `None` means unlimited. Then the submessage execution can consume all gas + /// of the current execution context. + pub gas_limit: Option, + pub reply_on: ReplyOn, +} + +/// This is used for cases when we use ReplyOn::Never and the id doesn't matter +pub const UNUSED_MSG_ID: u64 = 0; + +impl SubMsg { + /// Creates a "fire and forget" message with the pre-0.14 semantics. + /// Since this is just an alias for [`SubMsg::reply_never`] it is somewhat recommended + /// to use the latter in order to make the behaviour more explicit in the caller code. + /// But that's up to you for now. + /// + /// By default, the submessage's gas limit will be unlimited. Use [`SubMsg::with_gas_limit`] to + /// change it. Setting `payload` is not advised as this will never be used. + pub fn new(msg: impl Into>) -> Self { + Self::reply_never(msg) + } + + /// Creates a `SubMsg` that will provide a `reply` with the given `id` if the message returns + /// `Ok`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. + /// Use [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. + pub fn reply_on_success(msg: impl Into>, id: u64) -> Self { + Self::reply_on(msg.into(), id, ReplyOn::Success) + } + + /// Creates a `SubMsg` that will provide a `reply` with the given `id` if the message returns + /// `Err`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. + /// Use [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. + pub fn reply_on_error(msg: impl Into>, id: u64) -> Self { + Self::reply_on(msg.into(), id, ReplyOn::Error) + } + + /// Create a `SubMsg` that will always provide a `reply` with the given `id`. + /// + /// By default, the submessage's `payload` will be empty and the gas limit will be unlimited. + /// Use [`SubMsg::with_payload`] and [`SubMsg::with_gas_limit`] to change those. + pub fn reply_always(msg: impl Into>, id: u64) -> Self { + Self::reply_on(msg.into(), id, ReplyOn::Always) + } + + /// Create a `SubMsg` that will never `reply`. This is equivalent to standard message semantics. + /// + /// By default, the submessage's gas limit will be unlimited. Use [`SubMsg::with_gas_limit`] to + /// change it. Setting `payload` is not advised as this will never be used. + pub fn reply_never(msg: impl Into>) -> Self { + Self::reply_on(msg.into(), UNUSED_MSG_ID, ReplyOn::Never) + } + + /// Add a gas limit to the submessage. + /// This gas limit measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{coins, BankMsg, ReplyOn, SubMsg}; + /// # let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + /// let sub_msg: SubMsg = SubMsg::reply_always(msg, 1234).with_gas_limit(60_000); + /// assert_eq!(sub_msg.id, 1234); + /// assert_eq!(sub_msg.gas_limit, Some(60_000)); + /// assert_eq!(sub_msg.reply_on, ReplyOn::Always); + /// ``` + pub fn with_gas_limit(mut self, limit: u64) -> Self { + self.gas_limit = Some(limit); + self + } + + /// Add a payload to the submessage. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{coins, BankMsg, Binary, ReplyOn, SubMsg}; + /// # let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + /// let sub_msg: SubMsg = SubMsg::reply_always(msg, 1234) + /// .with_payload(vec![1, 2, 3, 4]); + /// assert_eq!(sub_msg.id, 1234); + /// assert_eq!(sub_msg.payload, Binary::new(vec![1, 2, 3, 4])); + /// assert_eq!(sub_msg.reply_on, ReplyOn::Always); + /// ``` + pub fn with_payload(mut self, payload: impl Into) -> Self { + self.payload = payload.into(); + self + } + + fn reply_on(msg: CosmosMsg, id: u64, reply_on: ReplyOn) -> Self { + SubMsg { id, payload: Default::default(), msg, reply_on, gas_limit: None } + } + + /// Convert this [`SubMsg`] to a [`SubMsg`] with a different generic type. + /// This allows easier interactions between code written for a specific chain and + /// code written for multiple chains. + /// If this is a [`CosmosMsg::Custom`] submessage, the function returns `None`. + pub fn change_custom(self) -> Option> { + Some(SubMsg { + id: self.id, + payload: self.payload, + msg: self.msg.change_custom::()?, + gas_limit: self.gas_limit, + reply_on: self.reply_on, + }) + } +} + +/// The result object returned to `reply`. We always get the ID from the submessage +/// back and then must handle success and error cases ourselves. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Reply { + /// The ID that the contract set when emitting the `SubMsg`. + /// Use this to identify which submessage triggered the `reply`. + pub id: u64, + /// Some arbirary data that the contract set when emitting the `SubMsg`. + /// This is just passed into the `reply` entry point and is not stored to state. + /// + /// Unset/nil/null cannot be differentiated from empty data. + /// + /// On chains running CosmWasm 1.x this field is never filled. + #[serde(default)] + pub payload: Binary, + /// The amount of gas used by the submessage, + /// measured in [Cosmos SDK gas](https://github.com/CosmWasm/cosmwasm/blob/main/docs/GAS.md). + /// + /// This only contains a useful value on chains running CosmWasm 2.0 or higher. + /// On older chains, this field is always 0. + #[serde(default)] + pub gas_used: u64, + pub result: SubMsgResult, +} + +/// This is the result type that is returned from a sub message execution. +/// +/// We use a custom type here instead of Rust's Result because we want to be able to +/// define the serialization, which is a public interface. Every language that compiles +/// to Wasm and runs in the ComsWasm VM needs to create the same JSON representation. +/// +/// Until version 1.0.0-beta5, `ContractResult` was used instead +/// of this type. Once serialized, the two types are the same. However, in the Rust type +/// system we want different types for clarity and documenation reasons. +/// +/// # Examples +/// +/// Success: +/// +/// ``` +/// # use cosmwasm_std::{to_json_string, Binary, Event, SubMsgResponse, SubMsgResult}; +/// #[allow(deprecated)] +/// let response = SubMsgResponse { +/// data: Some(Binary::from_base64("MTIzCg==").unwrap()), +/// events: vec![Event::new("wasm").add_attribute("fo", "ba")], +/// msg_responses: vec![], +/// }; +/// let result: SubMsgResult = SubMsgResult::Ok(response); +/// assert_eq!( +/// to_json_string(&result).unwrap(), +/// r#"{"ok":{"events":[{"type":"wasm","attributes":[{"key":"fo","value":"ba"}]}],"data":"MTIzCg==","msg_responses":[]}}"#, +/// ); +/// ``` +/// +/// Failure: +/// +/// ``` +/// # use cosmwasm_std::{to_json_string, SubMsgResult, Response}; +/// let error_msg = String::from("Something went wrong"); +/// let result = SubMsgResult::Err(error_msg); +/// assert_eq!(to_json_string(&result).unwrap(), r#"{"error":"Something went wrong"}"#); +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SubMsgResult { + Ok(SubMsgResponse), + /// An error type that every custom error created by contract developers can be converted to. + /// This could potientially have more structure, but String is the easiest. + #[serde(rename = "error")] + Err(String), +} + +// Implementations here mimic the Result API and should be implemented via a conversion to Result +// to ensure API consistency +impl SubMsgResult { + /// Converts a `SubMsgResult` to a `Result` as a convenient way + /// to access the full Result API. + pub fn into_result(self) -> Result { + Result::::from(self) + } + + pub fn unwrap(self) -> SubMsgResponse { + self.into_result().unwrap() + } + + pub fn unwrap_err(self) -> String { + self.into_result().unwrap_err() + } + + pub fn is_ok(&self) -> bool { + matches!(self, SubMsgResult::Ok(_)) + } + + pub fn is_err(&self) -> bool { + matches!(self, SubMsgResult::Err(_)) + } +} + +impl From> for SubMsgResult { + fn from(original: Result) -> SubMsgResult { + match original { + Ok(value) => SubMsgResult::Ok(value), + Err(err) => SubMsgResult::Err(err.to_string()), + } + } +} + +impl From for Result { + fn from(original: SubMsgResult) -> Result { + match original { + SubMsgResult::Ok(value) => Ok(value), + SubMsgResult::Err(err) => Err(err), + } + } +} + +/// The information we get back from a successful sub message execution, +/// with full Cosmos SDK events. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct SubMsgResponse { + pub events: Vec, + #[deprecated = "Deprecated in the Cosmos SDK in favor of msg_responses. If your chain is running on CosmWasm 2.0 or higher, msg_responses will be filled. For older versions, the data field is still needed since msg_responses is empty in those cases."] + pub data: Option, + /// The responses from the messages emitted by the submessage. + /// In most cases, this is equivalent to the Cosmos SDK's [MsgResponses], which usually + /// contains a [single message]. However, wasmd allows chains to translate a single contract + /// message into multiple SDK messages. In that case all the MsgResponses from each are + /// concatenated into this flattened `Vec`. + /// + /// [MsgResponses]: https://github.com/cosmos/cosmos-sdk/blob/316750cc8cd8b3296fa233f4da2e39cbcdc34517/proto/cosmos/base/abci/v1beta1/abci.proto#L106-L109 + /// [single message]: https://github.com/cosmos/cosmos-sdk/blob/v0.50.4/baseapp/baseapp.go#L1020-L1023 + #[serde(default)] + pub msg_responses: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct MsgResponse { + pub type_url: String, + pub value: Binary, +} + +#[cfg(test)] +#[allow(deprecated)] +mod tests { + use super::*; + use crate::{coins, from_json, to_json_vec, Attribute, BankMsg, StdError, StdResult}; + + #[test] + fn sub_msg_new_works() { + let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + let sub_msg: SubMsg = SubMsg::new(msg.clone()); + // id and payload don't matter since there is no reply + assert_eq!(sub_msg.reply_on, ReplyOn::Never); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_reply_never_works() { + let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + let sub_msg: SubMsg = SubMsg::reply_never(msg.clone()); + // id and payload don't matter since there is no reply + assert_eq!(sub_msg.reply_on, ReplyOn::Never); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_reply_always_works() { + let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + let sub_msg: SubMsg = SubMsg::reply_always(msg.clone(), 54); + assert_eq!(sub_msg.id, 54); + assert_eq!(sub_msg.payload, Binary::default()); + assert_eq!(sub_msg.reply_on, ReplyOn::Always); + assert_eq!(sub_msg.gas_limit, None); + assert_eq!(sub_msg.msg, CosmosMsg::from(msg)); + } + + #[test] + fn sub_msg_with_gas_limit_works() { + let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + let sub_msg: SubMsg = SubMsg::reply_never(msg); + assert_eq!(sub_msg.gas_limit, None); + let sub_msg = sub_msg.with_gas_limit(20); + assert_eq!(sub_msg.gas_limit, Some(20)); + } + + #[test] + fn sub_msg_with_payload_works() { + let msg = BankMsg::Send { to_address: String::from("you"), amount: coins(1015, "earth") }; + let sub_msg: SubMsg = SubMsg::reply_never(msg); + assert_eq!(sub_msg.payload, Binary::default()); + let sub_msg = sub_msg.with_payload(vec![0xAA, 3, 5, 1, 2]); + assert_eq!(sub_msg.payload, Binary::new(vec![0xAA, 3, 5, 1, 2])); + } + + #[test] + fn sub_msg_result_serialization_works() { + let result = + SubMsgResult::Ok(SubMsgResponse { data: None, msg_responses: vec![], events: vec![] }); + assert_eq!( + &to_json_vec(&result).unwrap(), + br#"{"ok":{"events":[],"data":null,"msg_responses":[]}}"# + ); + + let result = SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + msg_responses: vec![MsgResponse { + type_url: "URL".to_string(), + value: Binary::from_base64("MTIzCg==").unwrap(), + }], + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + }); + println!("{}", &crate::to_json_string(&result).unwrap()); + assert_eq!( + &to_json_vec(&result).unwrap(), + br#"{"ok":{"events":[{"type":"wasm","attributes":[{"key":"fo","value":"ba"}]}],"data":"MTIzCg==","msg_responses":[{"type_url":"URL","value":"MTIzCg=="}]}}"# + ); + + let result: SubMsgResult = SubMsgResult::Err("broken".to_string()); + assert_eq!(&to_json_vec(&result).unwrap(), b"{\"error\":\"broken\"}"); + } + + #[test] + fn sub_msg_result_deserialization_works() { + // should work without `msg_responses` + let result: SubMsgResult = from_json(br#"{"ok":{"events":[]}}"#).unwrap(); + assert_eq!( + result, + SubMsgResult::Ok(SubMsgResponse { events: vec![], data: None, msg_responses: vec![] }) + ); + + // should work with `data` and no `msg_responses` + // this is the case for pre-2.0 CosmWasm chains + let result: SubMsgResult = from_json(br#"{"ok":{"events":[],"data":"aGk="}}"#).unwrap(); + assert_eq!( + result, + SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("aGk=").unwrap()), + msg_responses: vec![] + }) + ); + + let result: SubMsgResult = from_json( + br#"{"ok":{"events":[{"type":"wasm","attributes":[{"key":"fo","value":"ba"}]}],"data":"MTIzCg==", + "msg_responses":[{"type_url":"URL","value":"MTIzCg=="}]}}"#).unwrap(); + assert_eq!( + result, + SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + msg_responses: vec![MsgResponse { + type_url: "URL".to_string(), + value: Binary::from_base64("MTIzCg==").unwrap(), + }], + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + }) + ); + + let result: SubMsgResult = from_json(br#"{"error":"broken"}"#).unwrap(); + assert_eq!(result, SubMsgResult::Err("broken".to_string())); + + // fails for additional attributes + let parse: StdResult = from_json(br#"{"unrelated":321,"error":"broken"}"#); + match parse.unwrap_err() { + StdError::ParseErr { .. } => {}, + err => panic!("Unexpected error: {err:?}"), + } + let parse: StdResult = from_json(br#"{"error":"broken","unrelated":321}"#); + match parse.unwrap_err() { + StdError::ParseErr { .. } => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + #[test] + fn sub_msg_result_unwrap_works() { + let response = SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + msg_responses: vec![MsgResponse { + type_url: "URL".to_string(), + value: Binary::from_base64("MTIzCg==").unwrap(), + }], + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + }; + let success = SubMsgResult::Ok(response.clone()); + assert_eq!(success.unwrap(), response); + } + + #[test] + #[should_panic] + fn sub_msg_result_unwrap_panicks_for_err() { + let failure = SubMsgResult::Err("broken".to_string()); + let _ = failure.unwrap(); + } + + #[test] + fn sub_msg_result_unwrap_err_works() { + let failure = SubMsgResult::Err("broken".to_string()); + assert_eq!(failure.unwrap_err(), "broken"); + } + + #[test] + #[should_panic] + fn sub_msg_result_unwrap_err_panics_for_ok() { + let response = SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + msg_responses: vec![], + }; + let success = SubMsgResult::Ok(response); + let _ = success.unwrap_err(); + } + + #[test] + fn sub_msg_result_is_ok_works() { + let success = SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + msg_responses: vec![], + }); + let failure = SubMsgResult::Err("broken".to_string()); + assert!(success.is_ok()); + assert!(!failure.is_ok()); + } + + #[test] + fn sub_msg_result_is_err_works() { + let success = SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![Event::new("wasm").add_attribute("fo", "ba")], + msg_responses: vec![], + }); + let failure = SubMsgResult::Err("broken".to_string()); + assert!(failure.is_err()); + assert!(!success.is_err()); + } + + #[test] + fn sub_msg_result_can_convert_from_core_result() { + let original: Result = Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![], + msg_responses: vec![], + }); + let converted: SubMsgResult = original.into(); + assert_eq!( + converted, + SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![], + msg_responses: vec![], + }) + ); + + let original: Result = Err(StdError::generic_err("broken")); + let converted: SubMsgResult = original.into(); + assert_eq!(converted, SubMsgResult::Err("Generic error: broken".to_string())); + } + + #[test] + fn sub_msg_result_can_convert_to_core_result() { + let original = SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![], + msg_responses: vec![], + }); + let converted: Result = original.into(); + assert_eq!( + converted, + Ok(SubMsgResponse { + data: Some(Binary::from_base64("MTIzCg==").unwrap()), + events: vec![], + msg_responses: vec![], + }) + ); + + let original = SubMsgResult::Err("went wrong".to_string()); + let converted: Result = original.into(); + assert_eq!(converted, Err("went wrong".to_string())); + } + + #[test] + fn reply_deserialization_works() { + // 1.x reply without payload (from https://github.com/CosmWasm/cosmwasm/issues/1909) + let reply: Reply = from_json(r#"{"gas_used":4312324,"id":75,"result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs="}}}"#).unwrap(); + assert_eq!( + reply, + Reply { + id: 75, + payload: Binary::default(), + gas_used: 4312324, + result: SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("PwCqXKs=").unwrap()), + events: vec![Event { + ty: "hi".to_string(), + attributes: vec![Attribute { + key: "si".to_string(), + value: "claro".to_string(), + }] + }], + msg_responses: vec![], + }) + } + ); + + // with payload (manually added to the above test) + let reply: Reply = from_json(r#"{"gas_used":4312324,"id":75,"payload":"3NxjC5U=","result":{"ok":{"events":[{"type":"hi","attributes":[{"key":"si","value":"claro"}]}],"data":"PwCqXKs="}}}"#).unwrap(); + assert_eq!( + reply, + Reply { + id: 75, + payload: Binary::from_base64("3NxjC5U=").unwrap(), + gas_used: 4312324, + result: SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("PwCqXKs=").unwrap()), + events: vec![Event { + ty: "hi".to_string(), + attributes: vec![Attribute { + key: "si".to_string(), + value: "claro".to_string(), + }] + }], + msg_responses: vec![], + }) + } + ); + } + + #[test] + fn reply_serialization_cosmwasm_1() { + // json coming from wasmvm 1.5.0 + let json = r#"{"id":1234,"result":{"ok":{"events":[{"type":"message","attributes":[{"key":"signer","value":"caller-addr"}]}],"data":"Zm9vYmFy"}}}"#; + + let reply: Reply = from_json(json).unwrap(); + assert_eq!(reply.id, 1234); + assert_eq!(reply.payload, Binary::default()); + assert_eq!( + reply.result, + SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from_base64("Zm9vYmFy").unwrap()), + events: vec![Event { + ty: "message".to_string(), + attributes: vec![Attribute { + key: "signer".to_string(), + value: "caller-addr".to_string() + }] + }], + msg_responses: vec![] + }) + ); + assert_eq!(reply.gas_used, 0); + } +} diff --git a/cosmwasm/std/src/results/system_result.rs b/cosmwasm/std/src/results/system_result.rs new file mode 100644 index 00000000..22ce5c68 --- /dev/null +++ b/cosmwasm/std/src/results/system_result.rs @@ -0,0 +1,76 @@ +use core::fmt; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::SystemError; + +/// This is the outer result type returned by a querier to the contract. +/// +/// We use a custom type here instead of Rust's Result because we want to be able to +/// define the serialization, which is a public interface. Every language that compiles +/// to Wasm and runs in the ComsWasm VM needs to create the same JSON representation. +/// +/// # Examples +/// +/// Success: +/// +/// ``` +/// # use cosmwasm_std::{to_vec, Binary, ContractResult, SystemResult}; +/// let data = Binary::from(b"hello, world"); +/// let result = SystemResult::Ok(ContractResult::Ok(data)); +/// assert_eq!(to_vec(&result).unwrap(), br#"{"ok":{"ok":"aGVsbG8sIHdvcmxk"}}"#); +/// ``` +/// +/// Failure: +/// +/// ``` +/// # use cosmwasm_std::{to_vec, Binary, ContractResult, SystemResult, SystemError}; +/// let error = SystemError::Unknown {}; +/// let result: SystemResult = SystemResult::Err(error); +/// assert_eq!(to_vec(&result).unwrap(), br#"{"error":{"unknown":{}}}"#); +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SystemResult { + Ok(S), + #[serde(rename = "error")] + Err(SystemError), +} + +// Implementations here mimic the Result API and should be implemented via a conversion to Result +// to ensure API consistency +impl SystemResult { + /// Converts a `ContractResult` to a `Result` as a convenient way + /// to access the full Result API. + pub fn into_result(self) -> Result { + Result::::from(self) + } + + pub fn unwrap(self) -> S { + self.into_result().unwrap() + } +} + +impl SystemResult { + pub fn unwrap_err(self) -> SystemError { + self.into_result().unwrap_err() + } +} + +impl From> for SystemResult { + fn from(original: Result) -> SystemResult { + match original { + Ok(value) => SystemResult::Ok(value), + Err(err) => SystemResult::Err(err), + } + } +} + +impl From> for Result { + fn from(original: SystemResult) -> Result { + match original { + SystemResult::Ok(value) => Ok(value), + SystemResult::Err(err) => Err(err), + } + } +} diff --git a/cosmwasm/std/src/sections.rs b/cosmwasm/std/src/sections.rs new file mode 100644 index 00000000..6b89cc20 --- /dev/null +++ b/cosmwasm/std/src/sections.rs @@ -0,0 +1,144 @@ +use crate::{conversion::force_to_u32, prelude::*}; + +/// A sections decoder for the special case of two elements +#[allow(dead_code)] // used in Wasm and tests only +pub fn decode_sections2(data: Vec) -> (Vec, Vec) { + let (rest, second) = split_tail(data); + let (_, first) = split_tail(rest); + (first, second) +} + +/// Encodes multiple sections of data into one vector. +/// +/// Each section is suffixed by a section length encoded as big endian uint32. +/// Using suffixes instead of prefixes allows reading sections in reverse order, +/// such that the first element does not need to be re-allocated if the contract's +/// data structure supports truncation (such as a Rust vector). +/// +/// The resulting data looks like this: +/// +/// ```ignore +/// section1 || section1_len || section2 || section2_len || section3 || section3_len || … +/// ``` +#[allow(dead_code)] // used in Wasm and tests only +pub fn encode_sections(sections: &[&[u8]]) -> Vec { + let mut out_len: usize = sections.iter().map(|section| section.len()).sum(); + out_len += 4 * sections.len(); + let mut out_data = Vec::with_capacity(out_len); + for §ion in sections { + let section_len = force_to_u32(section.len()).to_be_bytes(); + out_data.extend(section); + out_data.extend_from_slice(§ion_len); + } + debug_assert_eq!(out_data.len(), out_len); + debug_assert_eq!(out_data.capacity(), out_len); + out_data +} + +/// Splits data into the last section ("tail") and the rest. +/// The tail's length information is cut off, such that it is ready to use. +/// The rest is basically unparsed and contails the lengths of the remaining sections. +/// +/// While the tail is copied into a new vector, the rest is only truncated such that +/// no re-allocation is necessary. +/// +/// If `data` contains one section only, `data` is moved into the tail entirely +fn split_tail(data: Vec) -> (Vec, Vec) { + let tail_len: usize = if data.len() >= 4 { + u32::from_be_bytes([ + data[data.len() - 4], + data[data.len() - 3], + data[data.len() - 2], + data[data.len() - 1], + ]) as usize + } else { + panic!("Cannot read section length"); + }; + let rest_len_end = data.len() - 4 - tail_len; + + let (rest, mut tail) = if rest_len_end == 0 { + // i.e. all data is the tail + (Vec::new(), data) + } else { + let mut rest = data; + let tail = rest.split_off(rest_len_end); + (rest, tail) + }; + tail.truncate(tail_len); // cut off length + (rest, tail) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_sections2_works() { + let data = b"\xAA\0\0\0\x01\xBB\xCC\0\0\0\x02".to_vec(); + assert_eq!(decode_sections2(data), (vec![0xAA], vec![0xBB, 0xCC])); + + let data = b"\xDE\xEF\x62\0\0\0\x03\0\0\0\0".to_vec(); + assert_eq!(decode_sections2(data), (vec![0xDE, 0xEF, 0x62], vec![])); + + let data = b"\0\0\0\0\xDE\xEF\x62\0\0\0\x03".to_vec(); + assert_eq!(decode_sections2(data), (vec![], vec![0xDE, 0xEF, 0x62])); + + let data = b"\0\0\0\0\0\0\0\0".to_vec(); + assert_eq!(decode_sections2(data), (vec![], vec![])); + + let data = b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\0\0\0\x13\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\0\0\x01\x15".to_vec(); + assert_eq!(decode_sections2(data), (vec![0xFF; 19], vec![0x9D; 277])); + } + + #[test] + fn decode_sections2_preserved_first_vector() { + let original = b"\xAA\0\0\0\x01\xBB\xCC\0\0\0\x02".to_vec(); + let original_capacity = original.capacity(); + let original_ptr = original.as_ptr(); + let (first, second) = decode_sections2(original); + + // This is not copied + assert_eq!(first.capacity(), original_capacity); + assert_eq!(first.as_ptr(), original_ptr); + + // This is a copy + assert_ne!(second.capacity(), original_capacity); + assert_ne!(second.as_ptr(), original_ptr); + } + + #[test] + fn encode_sections_works_for_empty_sections() { + let enc = encode_sections(&[]); + assert_eq!(enc, b"" as &[u8]); + let enc = encode_sections(&[&[]]); + assert_eq!(enc, b"\0\0\0\0" as &[u8]); + let enc = encode_sections(&[&[], &[]]); + assert_eq!(enc, b"\0\0\0\0\0\0\0\0" as &[u8]); + let enc = encode_sections(&[&[], &[], &[]]); + assert_eq!(enc, b"\0\0\0\0\0\0\0\0\0\0\0\0" as &[u8]); + } + + #[test] + fn encode_sections_works_for_one_element() { + let enc = encode_sections(&[]); + assert_eq!(enc, b"" as &[u8]); + let enc = encode_sections(&[&[0xAA]]); + assert_eq!(enc, b"\xAA\0\0\0\x01" as &[u8]); + let enc = encode_sections(&[&[0xAA, 0xBB]]); + assert_eq!(enc, b"\xAA\xBB\0\0\0\x02" as &[u8]); + let enc = encode_sections(&[&[0x9D; 277]]); + assert_eq!(enc, b"\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\x9D\0\0\x01\x15" as &[u8]); + } + + #[test] + fn encode_sections_works_for_multiple_elements() { + let enc = encode_sections(&[&[0xAA]]); + assert_eq!(enc, b"\xAA\0\0\0\x01" as &[u8]); + let enc = encode_sections(&[&[0xAA], &[0xDE, 0xDE]]); + assert_eq!(enc, b"\xAA\0\0\0\x01\xDE\xDE\0\0\0\x02" as &[u8]); + let enc = encode_sections(&[&[0xAA], &[0xDE, 0xDE], &[]]); + assert_eq!(enc, b"\xAA\0\0\0\x01\xDE\xDE\0\0\0\x02\0\0\0\0" as &[u8]); + let enc = encode_sections(&[&[0xAA], &[0xDE, 0xDE], &[], &[0xFF; 19]]); + assert_eq!(enc, b"\xAA\0\0\0\x01\xDE\xDE\0\0\0\x02\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\0\0\0\x13" as &[u8]); + } +} diff --git a/cosmwasm/std/src/serde.rs b/cosmwasm/std/src/serde.rs new file mode 100644 index 00000000..d844da4d --- /dev/null +++ b/cosmwasm/std/src/serde.rs @@ -0,0 +1,172 @@ +// This file simply re-exports some methods from serde_json +// The reason is two fold: +// 1. To easily ensure that all calling libraries use the same version (minimize code size) +// 2. To allow us to switch out to eg. serde-json-core more easily + +use core::any::type_name; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{Binary, StdError, StdResult}; + +use crate::prelude::*; + +#[deprecated = "use from_json instead"] +pub fn from_slice(value: &[u8]) -> StdResult { + from_json(value) +} + +#[deprecated = "use from_json instead"] +pub fn from_binary(value: &Binary) -> StdResult { + from_json(value) +} + +/// Deserializes the given JSON bytes to a data structure. +/// +/// Errors if the input is not valid JSON or cannot be deserialized to the given type. +pub fn from_json(value: impl AsRef<[u8]>) -> StdResult { + serde_json_wasm::from_slice(value.as_ref()) + .map_err(|e| StdError::parse_err(type_name::(), e)) +} + +#[deprecated = "use to_json_vec instead"] +pub fn to_vec(data: &T) -> StdResult> +where + T: Serialize + ?Sized, +{ + to_json_vec(data) +} + +#[deprecated = "use to_json_binary instead"] +pub fn to_binary(data: &T) -> StdResult +where + T: Serialize + ?Sized, +{ + to_json_binary(data) +} + +/// Serializes the given data structure as a JSON byte vector. +pub fn to_json_vec(data: &T) -> StdResult> +where + T: Serialize + ?Sized, +{ + serde_json_wasm::to_vec(data).map_err(|e| StdError::serialize_err(type_name::(), e)) +} + +/// Serializes the given data structure as a JSON string. +pub fn to_json_string(data: &T) -> StdResult +where + T: Serialize + ?Sized, +{ + serde_json_wasm::to_string(data).map_err(|e| StdError::serialize_err(type_name::(), e)) +} + +/// Serializes the given data structure as JSON bytes. +pub fn to_json_binary(data: &T) -> StdResult +where + T: Serialize + ?Sized, +{ + to_json_vec(data).map(Binary::new) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Serialize, Deserialize, Debug, PartialEq)] + #[serde(rename_all = "snake_case")] + enum SomeMsg { + Refund {}, + ReleaseAll { image: String, amount: u32, time: u64, karma: i32 }, + Cowsay { text: String }, + } + + #[test] + fn to_json_vec_works() { + let msg = SomeMsg::Refund {}; + let serialized = to_json_vec(&msg).unwrap(); + assert_eq!(serialized, br#"{"refund":{}}"#); + + let msg = SomeMsg::ReleaseAll { + image: "foo".to_string(), + amount: 42, + time: 9007199254740999, // Number.MAX_SAFE_INTEGER + 7 + karma: -17, + }; + let serialized = String::from_utf8(to_json_vec(&msg).unwrap()).unwrap(); + assert_eq!( + serialized, + r#"{"release_all":{"image":"foo","amount":42,"time":9007199254740999,"karma":-17}}"# + ); + } + + #[test] + fn from_json_works() { + let deserialized: SomeMsg = from_json(br#"{"refund":{}}"#).unwrap(); + assert_eq!(deserialized, SomeMsg::Refund {}); + + let expected = SomeMsg::ReleaseAll { + image: "foo".to_string(), + amount: 42, + time: 18446744073709551615, + karma: -17, + }; + // &[u8] + let deserialized: SomeMsg = from_json( + br#"{"release_all":{"image":"foo","amount":42,"time":18446744073709551615,"karma":-17}}"#, + ) + .unwrap(); + assert_eq!(deserialized, expected); + + // &str + let deserialized: SomeMsg = from_json( + r#"{"release_all":{"image":"foo","amount":42,"time":18446744073709551615,"karma":-17}}"#, + ) + .unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn from_json_or_binary() { + let msg = SomeMsg::Refund {}; + let serialized: Binary = to_json_binary(&msg).unwrap(); + + let parse_binary: SomeMsg = from_json(&serialized).unwrap(); + assert_eq!(parse_binary, msg); + + let parse_slice: SomeMsg = from_json(serialized.as_slice()).unwrap(); + assert_eq!(parse_slice, msg); + } + + #[test] + fn to_json_vec_works_for_special_chars() { + let msg = SomeMsg::Cowsay { text: "foo\"bar\\\"bla".to_string() }; + let serialized = String::from_utf8(to_json_vec(&msg).unwrap()).unwrap(); + assert_eq!(serialized, r#"{"cowsay":{"text":"foo\"bar\\\"bla"}}"#); + } + + #[test] + fn from_json_works_for_special_chars() { + let deserialized: SomeMsg = from_json(br#"{"cowsay":{"text":"foo\"bar\\\"bla"}}"#).unwrap(); + assert_eq!(deserialized, SomeMsg::Cowsay { text: "foo\"bar\\\"bla".to_string() }); + } + + #[test] + fn to_json_string_works() { + let msg = SomeMsg::Refund {}; + let serialized = to_json_string(&msg).unwrap(); + assert_eq!(serialized, r#"{"refund":{}}"#); + + let msg = SomeMsg::ReleaseAll { + image: "foo".to_string(), + amount: 42, + time: 9007199254740999, // Number.MAX_SAFE_INTEGER + 7 + karma: -17, + }; + let serialized = to_json_string(&msg).unwrap(); + assert_eq!( + serialized, + r#"{"release_all":{"image":"foo","amount":42,"time":9007199254740999,"karma":-17}}"# + ); + } +} diff --git a/cosmwasm/std/src/stdack.rs b/cosmwasm/std/src/stdack.rs new file mode 100644 index 00000000..4cec4ca9 --- /dev/null +++ b/cosmwasm/std/src/stdack.rs @@ -0,0 +1,163 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{prelude::*, to_json_binary, Binary}; + +/// This is a standard IBC acknowledgement type. IBC application are free +/// to use any acknowledgement format they want. However, for compatibility +/// purposes it is recommended to use this. +/// +/// The original proto definition can be found at +/// and . +/// +/// In contrast to the original idea, [ICS-20](https://github.com/cosmos/ibc/tree/ed849c7bacf16204e9509f0f0df325391f3ce25c/spec/app/ics-020-fungible-token-transfer#technical-specification) and CosmWasm IBC protocols +/// use JSON instead of a protobuf serialization. +/// +/// For compatibility, we use the field name "result" for the success case in JSON. +/// However, all Rust APIs use the term "success" for clarity and discriminability from [Result]. +/// +/// If ibc_receive_packet returns Err(), then x/wasm runtime will rollback the state and +/// return an error message in this format. +/// +/// ## Examples +/// +/// For your convenience, there are success and error constructors. +/// +/// ``` +/// use cosmwasm_std::StdAck; +/// +/// let ack1 = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. +/// assert!(ack1.is_success()); +/// +/// let ack2 = StdAck::error("kaputt"); // Some free text error message +/// assert!(ack2.is_error()); +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StdAck { + #[serde(rename = "result")] + Success(Binary), + Error(String), +} + +impl StdAck { + /// Creates a success ack with the given data + pub fn success(data: impl Into) -> Self { + StdAck::Success(data.into()) + } + + /// Creates an error ack + pub fn error(err: impl Into) -> Self { + StdAck::Error(err.into()) + } + + #[must_use = "if you intended to assert that this is a success, consider `.unwrap()` instead"] + #[inline] + pub const fn is_success(&self) -> bool { + matches!(*self, StdAck::Success(_)) + } + + #[must_use = "if you intended to assert that this is an error, consider `.unwrap_err()` instead"] + #[inline] + pub const fn is_error(&self) -> bool { + !self.is_success() + } + + /// Serialized the ack to binary using JSON. This used for setting the acknowledgement + /// field in IbcReceiveResponse. + /// + /// ## Examples + /// + /// Show how the acknowledgement looks on the write: + /// + /// ``` + /// # use cosmwasm_std::StdAck; + /// let ack1 = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// assert_eq!(ack1.to_binary(), br#"{"result":"AQ=="}"#); + /// + /// let ack2 = StdAck::error("kaputt"); // Some free text error message + /// assert_eq!(ack2.to_binary(), br#"{"error":"kaputt"}"#); + /// ``` + /// + /// Set acknowledgement field in `IbcReceiveResponse`: + /// + /// ``` + /// use cosmwasm_std::{StdAck, IbcReceiveResponse}; + /// + /// let ack = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// + /// let res: IbcReceiveResponse = IbcReceiveResponse::new(ack.to_binary()); + /// let res: IbcReceiveResponse = IbcReceiveResponse::new(ack); // Does the same but consumes the instance + /// ``` + pub fn to_binary(&self) -> Binary { + // We need a non-failing StdAck -> Binary conversion to allow using StdAck in + // `impl Into` arguments. + // Pretty sure this cannot fail. If that changes we can create a non-failing implementation + // here. + to_json_binary(&self).unwrap() + } + + pub fn unwrap(self) -> Binary { + match self { + StdAck::Success(data) => data, + StdAck::Error(err) => panic!("{}", err), + } + } + + pub fn unwrap_err(self) -> String { + match self { + StdAck::Success(_) => panic!("not an error"), + StdAck::Error(err) => err, + } + } +} + +impl From for Binary { + fn from(original: StdAck) -> Binary { + original.to_binary() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stdack_success_works() { + let success = StdAck::success(b"foo"); + match success { + StdAck::Success(data) => assert_eq!(data, b"foo"), + StdAck::Error(_err) => panic!("must not be an error"), + } + } + + #[test] + fn stdack_error_works() { + let err = StdAck::error("bar"); + match err { + StdAck::Success(_data) => panic!("must not be a success"), + StdAck::Error(err) => assert_eq!(err, "bar"), + } + } + + #[test] + fn stdack_is_success_is_error_work() { + let success = StdAck::success(b"foo"); + let err = StdAck::error("bar"); + // is_success + assert!(success.is_success()); + assert!(!err.is_success()); + // is_eror + assert!(!success.is_error()); + assert!(err.is_error()); + } + + #[test] + fn stdack_to_binary_works() { + let ack1 = StdAck::success(b"\x01"); + assert_eq!(ack1.to_binary(), br#"{"result":"AQ=="}"#); + + let ack2 = StdAck::error("kaputt"); + assert_eq!(ack2.to_binary(), br#"{"error":"kaputt"}"#); + } +} diff --git a/cosmwasm/std/src/storage.rs b/cosmwasm/std/src/storage.rs new file mode 100644 index 00000000..c10dbd79 --- /dev/null +++ b/cosmwasm/std/src/storage.rs @@ -0,0 +1,321 @@ +use alloc::collections::BTreeMap; +use core::fmt; +#[cfg(feature = "iterator")] +use core::iter; +#[cfg(feature = "iterator")] +use core::ops::{Bound, RangeBounds}; + +#[cfg(feature = "iterator")] +use crate::iterator::{Order, Record}; +use crate::{prelude::*, traits::Storage}; + +#[derive(Default)] +pub struct MemoryStorage { + data: BTreeMap, Vec>, +} + +impl MemoryStorage { + pub fn new() -> Self { + MemoryStorage::default() + } +} + +impl Storage for MemoryStorage { + fn get(&self, key: &[u8]) -> Option> { + self.data.get(key).cloned() + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + if value.is_empty() { + panic!("TL;DR: Value must not be empty in Storage::set but in most cases you can use Storage::remove instead. Long story: Getting empty values from storage is not well supported at the moment. Some of our internal interfaces cannot differentiate between a non-existent key and an empty value. Right now, you cannot rely on the behaviour of empty values. To protect you from trouble later on, we stop here. Sorry for the inconvenience! We highly welcome you to contribute to CosmWasm, making this more solid one way or the other."); + } + + self.data.insert(key.to_vec(), value.to_vec()); + } + + fn remove(&mut self, key: &[u8]) { + self.data.remove(key); + } + + #[cfg(feature = "iterator")] + /// range allows iteration over a set of keys, either forwards or backwards + /// uses standard rust range notation, and eg db.range(b"foo"..b"bar") also works reverse + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + let bounds = range_bounds(start, end); + + // BTreeMap.range panics if range is start > end. + // However, this cases represent just empty range and we treat it as such. + match (bounds.start_bound(), bounds.end_bound()) { + (Bound::Included(start), Bound::Excluded(end)) if start > end => { + return Box::new(iter::empty()); + }, + _ => {}, + } + + let iter = self.data.range(bounds); + match order { + Order::Ascending => Box::new(iter.map(clone_item)), + Order::Descending => Box::new(iter.rev().map(clone_item)), + } + } +} + +/// This debug implementation is made for inspecting storages in unit testing. +/// It is made for human readability only and the output can change at any time. +impl fmt::Debug for MemoryStorage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MemoryStorage ({} entries)", self.data.len())?; + f.write_str(" {\n")?; + for (key, value) in &self.data { + f.write_str(" 0x")?; + for byte in key { + write!(f, "{byte:02x}")?; + } + f.write_str(": 0x")?; + for byte in value { + write!(f, "{byte:02x}")?; + } + f.write_str("\n")?; + } + f.write_str("}")?; + Ok(()) + } +} + +#[cfg(feature = "iterator")] +fn range_bounds(start: Option<&[u8]>, end: Option<&[u8]>) -> impl RangeBounds> { + ( + start.map_or(Bound::Unbounded, |x| Bound::Included(x.to_vec())), + end.map_or(Bound::Unbounded, |x| Bound::Excluded(x.to_vec())), + ) +} + +#[cfg(feature = "iterator")] +/// The BTreeMap specific key-value pair reference type, as returned by BTreeMap, +/// Vec>::range. This is internal as it can change any time if the map implementation is swapped +/// out. +type BTreeMapRecordRef<'a> = (&'a Vec, &'a Vec); + +#[cfg(feature = "iterator")] +fn clone_item(item_ref: BTreeMapRecordRef) -> Record { + let (key, value) = item_ref; + (key.clone(), value.clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_and_set() { + let mut store = MemoryStorage::new(); + assert_eq!(store.get(b"foo"), None); + store.set(b"foo", b"bar"); + assert_eq!(store.get(b"foo"), Some(b"bar".to_vec())); + assert_eq!(store.get(b"food"), None); + } + + #[test] + #[should_panic( + expected = "Getting empty values from storage is not well supported at the moment." + )] + fn set_panics_for_empty() { + let mut store = MemoryStorage::new(); + store.set(b"foo", b""); + } + + #[test] + fn delete() { + let mut store = MemoryStorage::new(); + store.set(b"foo", b"bar"); + store.set(b"food", b"bank"); + store.remove(b"foo"); + + assert_eq!(store.get(b"foo"), None); + assert_eq!(store.get(b"food"), Some(b"bank".to_vec())); + } + + #[test] + #[cfg(feature = "iterator")] + fn iterator() { + let mut store = MemoryStorage::new(); + store.set(b"foo", b"bar"); + + // ensure we had previously set "foo" = "bar" + assert_eq!(store.get(b"foo"), Some(b"bar".to_vec())); + assert_eq!(store.range(None, None, Order::Ascending).count(), 1); + + // setup - add some data, and delete part of it as well + store.set(b"ant", b"hill"); + store.set(b"ze", b"bra"); + + // noise that should be ignored + store.set(b"bye", b"bye"); + store.remove(b"bye"); + + // unbounded + { + let iter = store.range(None, None, Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"ant".to_vec(), b"hill".to_vec()), + (b"foo".to_vec(), b"bar".to_vec()), + (b"ze".to_vec(), b"bra".to_vec()), + ] + ); + } + + // unbounded (descending) + { + let iter = store.range(None, None, Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"ze".to_vec(), b"bra".to_vec()), + (b"foo".to_vec(), b"bar".to_vec()), + (b"ant".to_vec(), b"hill".to_vec()), + ] + ); + } + + // bounded + { + let iter = store.range(Some(b"f"), Some(b"n"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![(b"foo".to_vec(), b"bar".to_vec())]); + } + + // bounded (descending) + { + let iter = store.range(Some(b"air"), Some(b"loop"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![(b"foo".to_vec(), b"bar".to_vec()), (b"ant".to_vec(), b"hill".to_vec()),] + ); + } + + // bounded empty [a, a) + { + let iter = store.range(Some(b"foo"), Some(b"foo"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, a) (descending) + { + let iter = store.range(Some(b"foo"), Some(b"foo"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, b) with b < a + { + let iter = store.range(Some(b"z"), Some(b"a"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, b) with b < a (descending) + { + let iter = store.range(Some(b"z"), Some(b"a"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // right unbounded + { + let iter = store.range(Some(b"f"), None, Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![(b"foo".to_vec(), b"bar".to_vec()), (b"ze".to_vec(), b"bra".to_vec()),] + ); + } + + // right unbounded (descending) + { + let iter = store.range(Some(b"f"), None, Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![(b"ze".to_vec(), b"bra".to_vec()), (b"foo".to_vec(), b"bar".to_vec()),] + ); + } + + // left unbounded + { + let iter = store.range(None, Some(b"f"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![(b"ant".to_vec(), b"hill".to_vec()),]); + } + + // left unbounded (descending) + { + let iter = store.range(None, Some(b"no"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![(b"foo".to_vec(), b"bar".to_vec()), (b"ant".to_vec(), b"hill".to_vec()),] + ); + } + } + + #[test] + fn memory_storage_implements_debug() { + let store = MemoryStorage::new(); + assert_eq!( + format!("{store:?}"), + "MemoryStorage (0 entries) {\n\ + }" + ); + + // With one element + let mut store = MemoryStorage::new(); + store.set(&[0x00, 0xAB, 0xDD], &[0xFF, 0xD5]); + assert_eq!( + format!("{store:?}"), + "MemoryStorage (1 entries) {\n\ + \x20\x200x00abdd: 0xffd5\n\ + }" + ); + + // Sorted by key + let mut store = MemoryStorage::new(); + store.set(&[0x00, 0xAB, 0xDD], &[0xFF, 0xD5]); + store.set(&[0x00, 0xAB, 0xEE], &[0xFF, 0xD5]); + store.set(&[0x00, 0xAB, 0xCC], &[0xFF, 0xD5]); + assert_eq!( + format!("{store:?}"), + "MemoryStorage (3 entries) {\n\ + \x20\x200x00abcc: 0xffd5\n\ + \x20\x200x00abdd: 0xffd5\n\ + \x20\x200x00abee: 0xffd5\n\ + }" + ); + + // Different lengths + let mut store = MemoryStorage::new(); + store.set(&[0xAA], &[0x11]); + store.set(&[0xAA, 0xBB], &[0x11, 0x22]); + store.set(&[0xAA, 0xBB, 0xCC], &[0x11, 0x22, 0x33]); + store.set(&[0xAA, 0xBB, 0xCC, 0xDD], &[0x11, 0x22, 0x33, 0x44]); + assert_eq!( + format!("{store:?}"), + "MemoryStorage (4 entries) {\n\ + \x20\x200xaa: 0x11\n\ + \x20\x200xaabb: 0x1122\n\ + \x20\x200xaabbcc: 0x112233\n\ + \x20\x200xaabbccdd: 0x11223344\n\ + }" + ); + } +} diff --git a/cosmwasm/std/src/storage_keys/length_prefixed.rs b/cosmwasm/std/src/storage_keys/length_prefixed.rs new file mode 100644 index 00000000..91497fe5 --- /dev/null +++ b/cosmwasm/std/src/storage_keys/length_prefixed.rs @@ -0,0 +1,225 @@ +//! This module is an implemention of a namespacing scheme described +//! in https://github.com/webmaster128/key-namespacing#length-prefixed-keys +//! +//! Everything in this file is only responsible for building such keys +//! and is in no way specific to any kind of storage. + +use crate::prelude::*; + +/// Calculates the raw key prefix for a given namespace as documented +/// in https://github.com/webmaster128/key-namespacing#length-prefixed-keys +pub fn to_length_prefixed(namespace_component: &[u8]) -> Vec { + let mut out = Vec::with_capacity(namespace_component.len() + 2); + out.extend_from_slice(&encode_length(namespace_component)); + out.extend_from_slice(namespace_component); + out +} + +/// Calculates the raw key prefix for a given nested namespace +/// as documented in https://github.com/webmaster128/key-namespacing#nesting +pub fn to_length_prefixed_nested(namespace: &[&[u8]]) -> Vec { + let mut size = 0; + for component in namespace { + size += component.len() + 2; + } + + let mut out = Vec::with_capacity(size); + for component in namespace { + out.extend_from_slice(&encode_length(component)); + out.extend_from_slice(component); + } + out +} + +/// Encodes the length of a given namespace component +/// as a 2 byte big endian encoded integer +fn encode_length(namespace_component: &[u8]) -> [u8; 2] { + if namespace_component.len() > 0xFFFF { + panic!("only supports namespace components up to length 0xFFFF") + } + let length_bytes = (namespace_component.len() as u32).to_be_bytes(); + [length_bytes[2], length_bytes[3]] +} + +/// Encodes a namespace + key to a raw storage key. +/// +/// This is equivalent concat(to_length_prefixed_nested(namespace), key) +/// but more efficient when the namespace serialization is not persisted because +/// here we only need one vector allocation. +pub fn namespace_with_key(namespace: &[&[u8]], key: &[u8]) -> Vec { + // As documented in docs/STORAGE_KEYS.md, we know the final size of the key, + // which allows us to avoid reallocations of vectors. + let mut size = key.len(); + for component in namespace { + size += 2 /* encoded component length */ + component.len() /* the actual component data */; + } + + let mut out = Vec::with_capacity(size); + for component in namespace { + out.extend_from_slice(&encode_length(component)); + out.extend_from_slice(component); + } + out.extend_from_slice(key); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_length_prefixed_works() { + assert_eq!(to_length_prefixed(b""), b"\x00\x00"); + assert_eq!(to_length_prefixed(b"a"), b"\x00\x01a"); + assert_eq!(to_length_prefixed(b"ab"), b"\x00\x02ab"); + assert_eq!(to_length_prefixed(b"abc"), b"\x00\x03abc"); + } + + #[test] + fn to_length_prefixed_works_for_long_prefix() { + let long_namespace1 = vec![0; 256]; + let prefix1 = to_length_prefixed(&long_namespace1); + assert_eq!(prefix1.len(), 256 + 2); + assert_eq!(&prefix1[0..2], b"\x01\x00"); + + let long_namespace2 = vec![0; 30000]; + let prefix2 = to_length_prefixed(&long_namespace2); + assert_eq!(prefix2.len(), 30000 + 2); + assert_eq!(&prefix2[0..2], b"\x75\x30"); + + let long_namespace3 = vec![0; 0xFFFF]; + let prefix3 = to_length_prefixed(&long_namespace3); + assert_eq!(prefix3.len(), 0xFFFF + 2); + assert_eq!(&prefix3[0..2], b"\xFF\xFF"); + } + + #[test] + #[should_panic(expected = "only supports namespace components up to length 0xFFFF")] + fn to_length_prefixed_panics_for_too_long_prefix() { + let limit = 0xFFFF; + let long_namespace = vec![0; limit + 1]; + to_length_prefixed(&long_namespace); + } + + #[test] + fn to_length_prefixed_calculates_capacity_correctly() { + // Those tests cannot guarantee the required capacity was calculated correctly before + // the vector allocation but increase the likelyhood of a proper implementation. + + let key = to_length_prefixed(b""); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed(b"h"); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed(b"hij"); + assert_eq!(key.capacity(), key.len()); + } + + #[test] + fn to_length_prefixed_nested_works() { + assert_eq!(to_length_prefixed_nested(&[]), b""); + assert_eq!(to_length_prefixed_nested(&[b""]), b"\x00\x00"); + assert_eq!(to_length_prefixed_nested(&[b"", b""]), b"\x00\x00\x00\x00"); + + assert_eq!(to_length_prefixed_nested(&[b"a"]), b"\x00\x01a"); + assert_eq!(to_length_prefixed_nested(&[b"a", b"ab"]), b"\x00\x01a\x00\x02ab"); + assert_eq!( + to_length_prefixed_nested(&[b"a", b"ab", b"abc"]), + b"\x00\x01a\x00\x02ab\x00\x03abc" + ); + } + + #[test] + fn to_length_prefixed_nested_returns_the_same_as_to_length_prefixed_for_one_element() { + let tests = [b"" as &[u8], b"x" as &[u8], b"abababab" as &[u8]]; + + for test in tests { + assert_eq!(to_length_prefixed_nested(&[test]), to_length_prefixed(test)); + } + } + + #[test] + fn to_length_prefixed_nested_allows_many_long_namespaces() { + // The 0xFFFF limit is for each namespace, not for the combination of them + + let long_namespace1 = vec![0xaa; 0xFFFD]; + let long_namespace2 = vec![0xbb; 0xFFFE]; + let long_namespace3 = vec![0xcc; 0xFFFF]; + + let prefix = + to_length_prefixed_nested(&[&long_namespace1, &long_namespace2, &long_namespace3]); + assert_eq!(&prefix[0..2], b"\xFF\xFD"); + assert_eq!(&prefix[2..(2 + 0xFFFD)], long_namespace1.as_slice()); + assert_eq!(&prefix[(2 + 0xFFFD)..(2 + 0xFFFD + 2)], b"\xFF\xFe"); + assert_eq!( + &prefix[(2 + 0xFFFD + 2)..(2 + 0xFFFD + 2 + 0xFFFE)], + long_namespace2.as_slice() + ); + assert_eq!(&prefix[(2 + 0xFFFD + 2 + 0xFFFE)..(2 + 0xFFFD + 2 + 0xFFFE + 2)], b"\xFF\xFf"); + assert_eq!( + &prefix[(2 + 0xFFFD + 2 + 0xFFFE + 2)..(2 + 0xFFFD + 2 + 0xFFFE + 2 + 0xFFFF)], + long_namespace3.as_slice() + ); + } + + #[test] + fn to_length_prefixed_nested_calculates_capacity_correctly() { + // Those tests cannot guarantee the required capacity was calculated correctly before + // the vector allocation but increase the likelyhood of a proper implementation. + + let key = to_length_prefixed_nested(&[]); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed_nested(&[b""]); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed_nested(&[b"a"]); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed_nested(&[b"a", b"bc"]); + assert_eq!(key.capacity(), key.len()); + + let key = to_length_prefixed_nested(&[b"a", b"bc", b"def"]); + assert_eq!(key.capacity(), key.len()); + } + + #[test] + fn encode_length_works() { + assert_eq!(encode_length(b""), *b"\x00\x00"); + assert_eq!(encode_length(b"a"), *b"\x00\x01"); + assert_eq!(encode_length(b"aa"), *b"\x00\x02"); + assert_eq!(encode_length(b"aaa"), *b"\x00\x03"); + assert_eq!(encode_length(&vec![1; 255]), *b"\x00\xff"); + assert_eq!(encode_length(&vec![1; 256]), *b"\x01\x00"); + assert_eq!(encode_length(&vec![1; 12345]), *b"\x30\x39"); + assert_eq!(encode_length(&vec![1; 65535]), *b"\xff\xff"); + } + + #[test] + #[should_panic(expected = "only supports namespace components up to length 0xFFFF")] + fn encode_length_panics_for_large_values() { + encode_length(&vec![1; 65536]); + } + + #[test] + fn namespace_with_key_works() { + // Empty namespace + let enc = namespace_with_key(&[], b"foo"); + assert_eq!(enc, b"foo"); + let enc = namespace_with_key(&[], b""); + assert_eq!(enc, b""); + + // One component namespace + let enc = namespace_with_key(&[b"bar"], b"foo"); + assert_eq!(enc, b"\x00\x03barfoo"); + let enc = namespace_with_key(&[b"bar"], b""); + assert_eq!(enc, b"\x00\x03bar"); + + // Multi component namespace + let enc = namespace_with_key(&[b"bar", b"cool"], b"foo"); + assert_eq!(enc, b"\x00\x03bar\x00\x04coolfoo"); + let enc = namespace_with_key(&[b"bar", b"cool"], b""); + assert_eq!(enc, b"\x00\x03bar\x00\x04cool"); + } +} diff --git a/cosmwasm/std/src/storage_keys/mod.rs b/cosmwasm/std/src/storage_keys/mod.rs new file mode 100644 index 00000000..630feaa1 --- /dev/null +++ b/cosmwasm/std/src/storage_keys/mod.rs @@ -0,0 +1,5 @@ +mod length_prefixed; + +// Please note that the entire storage_keys module is public. So be careful +// when adding elements here. +pub use length_prefixed::{namespace_with_key, to_length_prefixed, to_length_prefixed_nested}; diff --git a/cosmwasm/std/src/testing/assertions.rs b/cosmwasm/std/src/testing/assertions.rs new file mode 100644 index 00000000..f6f32a74 --- /dev/null +++ b/cosmwasm/std/src/testing/assertions.rs @@ -0,0 +1,183 @@ +use crate::{Decimal, Uint128}; + +use alloc::string::String; +#[cfg(test)] +use core::hash::{Hash, Hasher}; +use core::str::FromStr as _; + +/// Asserts that two expressions are approximately equal to each other. +/// +/// The `max_rel_diff` argument defines the maximum relative difference +/// of the `left` and `right` values. +/// +/// On panic, this macro will print the values of the arguments and +/// the actual relative difference. +/// +/// Like [`assert_eq!`], this macro has a second form, where a custom +/// panic message can be provided. +#[macro_export] +macro_rules! assert_approx_eq { + ($left:expr, $right:expr, $max_rel_diff:expr $(,)?) => {{ + $crate::testing::assert_approx_eq_impl($left, $right, $max_rel_diff, None); + }}; + ($left:expr, $right:expr, $max_rel_diff:expr, $($args:tt)+) => {{ + $crate::testing::assert_approx_eq_impl($left, $right, $max_rel_diff, Some(format!($($args)*))); + }}; +} + +/// Tests that type `T` implements `Eq` and `Hash` traits correctly. +/// +/// `left` and `right` must be unequal objects. +/// +/// Some object pairs may produce the same hash causing test failure. +/// In those cases try different objects. The test uses stable hasher +/// so once working pair is identified, the test’s going to continue +/// passing. +#[macro_export] +#[cfg(test)] +macro_rules! assert_hash_works { + ($left:expr, $right:expr $(,)?) => {{ + $crate::testing::assert_hash_works_impl($left, $right, None); + }}; + ($left:expr, $right:expr, $($args:tt)+) => {{ + $crate::testing::assert_hash_works_impl($left, $right, Some(format!($($args)*))); + }}; +} + +/// Implementation for the [`cosmwasm_std::assert_approx_eq`] macro. This does not provide any +/// stability guarantees and may change any time. +#[track_caller] +#[doc(hidden)] +pub fn assert_approx_eq_impl>( + left: U, + right: U, + max_rel_diff: &str, + panic_msg: Option, +) { + let left = left.into(); + let right = right.into(); + + if left == right { + // If both values are equal, we don't need to check the relative difference. + // We check this first to avoid division by zero below. + return; + } + + let max_rel_diff = Decimal::from_str(max_rel_diff).unwrap(); + + let largest = core::cmp::max(left, right); + let rel_diff = Decimal::from_ratio(left.abs_diff(right), largest); + + if rel_diff > max_rel_diff { + do_panic(format_args!("assertion failed: `(left ≈ right)`\nleft: {left}\nright: {right}\nrelative difference: {rel_diff}\nmax allowed relative difference: {max_rel_diff}"), panic_msg); + } +} + +/// Tests that type `T` implements `Eq` and `Hash` traits correctly. +/// +/// `left` and `right` must be unequal objects. +/// +/// Some object pairs may produce the same hash causing test failure. In those +/// cases try different objects. The test uses stable hasher so once working +/// pair is identified, the test’s going to continue passing. +#[track_caller] +#[doc(hidden)] +#[cfg(test)] +pub fn assert_hash_works_impl(left: T, right: T, panic_msg: Option) { + fn hash(value: &impl Hash) -> u64 { + let mut hasher = crc32fast::Hasher::default(); + value.hash(&mut hasher); + hasher.finish() + } + + // Check clone + #[allow(clippy::redundant_clone)] + let clone = left.clone(); + if left != clone { + do_panic("assertion failed: `left == left.clone()`", panic_msg); + } + if hash(&left) != hash(&clone) { + do_panic("assertion failed: `hash(left) == hash(left.clone())`", panic_msg); + } + + // Check different object + if left == right { + do_panic("assertion failed: `left != right`", panic_msg); + } + if hash(&left) == hash(&right) { + do_panic("assertion failed: `hash(left) != hash(right)`", panic_msg); + } +} + +/// Panics concatenating both arguments. +/// +/// If second argument is `None` panics with just the first argument as message. +/// Otherwise, formats the panic message as `{reason}:\n{panic_msg}`. +#[track_caller] +fn do_panic(reason: impl core::fmt::Display, panic_msg: Option) -> ! { + match panic_msg { + Some(panic_msg) => panic!("{reason}:\n{panic_msg}"), + None => panic!("{reason}"), + } +} + +#[cfg(test)] +mod tests { + #[test] + fn assert_approx() { + assert_approx_eq!(9_u32, 10_u32, "0.12"); + assert_approx_eq!(9_u64, 10_u64, "0.12"); + assert_approx_eq!( + 9_000_000_000_000_000_000_000_000_000_000_000_000_u128, + 10_000_000_000_000_000_000_000_000_000_000_000_000_u128, + "0.10" + ); + assert_approx_eq!(0_u32, 0_u32, "0.12"); + assert_approx_eq!(1_u64, 0_u64, "1"); + assert_approx_eq!(0_u64, 1_u64, "1"); + assert_approx_eq!(5_u64, 0_u64, "1"); + assert_approx_eq!(0_u64, 5_u64, "1"); + } + + #[test] + fn assert_approx_with_vars() { + let a = 66_u32; + let b = 67_u32; + assert_approx_eq!(a, b, "0.02"); + + let a = 66_u64; + let b = 67_u64; + assert_approx_eq!(a, b, "0.02"); + + let a = 66_u128; + let b = 67_u128; + assert_approx_eq!(a, b, "0.02"); + } + + #[test] + #[should_panic( + expected = "assertion failed: `(left ≈ right)`\nleft: 8\nright: 10\nrelative difference: 0.2\nmax allowed relative difference: 0.12" + )] + fn assert_approx_fail() { + assert_approx_eq!(8_u32, 10_u32, "0.12"); + } + + #[test] + #[should_panic( + expected = "assertion failed: `(left ≈ right)`\nleft: 17\nright: 20\nrelative difference: 0.15\nmax allowed relative difference: 0.12:\nsome extra info about the error: Foo(8)" + )] + fn assert_approx_with_custom_panic_msg() { + let adjective = "extra"; + #[allow(dead_code)] + #[derive(Debug)] + struct Foo(u32); + assert_approx_eq!( + 17_u32, + 20_u32, + "0.12", + "some {adjective} {} about the error: {:?}", + "info", + Foo(8), + ); + } +} diff --git a/cosmwasm/std/src/testing/message_info.rs b/cosmwasm/std/src/testing/message_info.rs new file mode 100644 index 00000000..975183f8 --- /dev/null +++ b/cosmwasm/std/src/testing/message_info.rs @@ -0,0 +1,69 @@ +use crate::{Addr, Coin, MessageInfo}; + +/// A constructor function for [`MessageInfo`]. +/// +/// This is designed for writing contract tests. +/// It lives in `cosmwasm_std::testing` because constructing MessageInfo +/// objects is not something that you usually need in contract code. +/// +/// ## Examples +/// +/// ``` +/// # use cosmwasm_std::{DepsMut, Env, Response, MessageInfo, StdResult}; +/// # struct InstantiateMsg { +/// # pub verifier: String, +/// # pub beneficiary: String, +/// # } +/// # pub fn instantiate( +/// # _deps: DepsMut, +/// # _env: Env, +/// # _info: MessageInfo, +/// # _msg: InstantiateMsg, +/// # ) -> StdResult { +/// # Ok(Response::new().add_attribute("action", "instantiate")) +/// # } +/// use cosmwasm_std::coins; +/// use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; +/// +/// let mut deps = mock_dependencies(); +/// +/// // Create some Addr instances for testing +/// let creator = deps.api.addr_make("creator"); +/// let verifier = deps.api.addr_make("verifies"); +/// let beneficiary = deps.api.addr_make("benefits"); +/// +/// let msg = InstantiateMsg { +/// verifier: verifier.to_string(), +/// beneficiary: beneficiary.to_string(), +/// }; +/// let info = message_info(&creator, &coins(1000, "earth")); +/// let response = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); +/// assert_eq!(response.messages.len(), 0); +/// ``` +pub fn message_info(sender: &Addr, funds: &[Coin]) -> MessageInfo { + MessageInfo { sender: sender.clone(), funds: funds.to_vec() } +} + +#[cfg(test)] +mod tests { + use crate::{coins, Uint128}; + + use super::*; + + #[test] + fn message_info_works() { + let addr = Addr::unchecked("cosmwasm1..."); + + let info = message_info(&addr, &[]); + assert_eq!(info, MessageInfo { sender: addr.clone(), funds: vec![] }); + + let info = message_info(&addr, &coins(123, "foo")); + assert_eq!( + info, + MessageInfo { + sender: addr.clone(), + funds: vec![Coin { amount: Uint128::new(123), denom: "foo".to_string() }], + } + ); + } +} diff --git a/cosmwasm/std/src/testing/mock.rs b/cosmwasm/std/src/testing/mock.rs new file mode 100644 index 00000000..650a86c1 --- /dev/null +++ b/cosmwasm/std/src/testing/mock.rs @@ -0,0 +1,2442 @@ +use crate::{prelude::*, Addr, CanonicalAddr, HashFunction, Timestamp}; +use alloc::collections::BTreeMap; +#[cfg(feature = "cosmwasm_1_3")] +use alloc::collections::BTreeSet; +use bech32::{encode, primitives::decode::CheckedHrpstring, Bech32, Hrp}; +use core::marker::PhantomData; +#[cfg(feature = "cosmwasm_1_3")] +use core::ops::Bound; +use rand_core::OsRng; +use serde::de::DeserializeOwned; +#[cfg(feature = "stargate")] +use serde::Serialize; +use sha2::{Digest, Sha256}; + +#[cfg(feature = "stargate")] +use crate::ibc::{ + IbcAcknowledgement, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcEndpoint, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcTimeoutBlock, +}; +#[cfg(feature = "cosmwasm_1_1")] +use crate::query::SupplyResponse; +#[cfg(feature = "staking")] +use crate::query::{ + AllDelegationsResponse, AllValidatorsResponse, BondedDenomResponse, DelegationResponse, + FullDelegation, StakingQuery, Validator, ValidatorResponse, +}; +#[cfg(feature = "cosmwasm_1_3")] +use crate::query::{DelegatorWithdrawAddressResponse, DistributionQuery}; +use crate::{ + coin::Coin, + deps::OwnedDeps, + from_json, + query::{AllBalanceResponse, BalanceResponse, BankQuery, CustomQuery, QueryRequest, WasmQuery}, + results::{ContractResult, Empty, SystemResult}, + storage::MemoryStorage, + to_json_binary, + traits::{Api, Querier, QuerierResult}, + types::{BlockInfo, ContractInfo, Env, MessageInfo, TransactionInfo}, + Attribute, Binary, DenomMetadata, RecoverPubkeyError, StdError, StdResult, SystemError, + Uint128, VerificationError, +}; +#[cfg(feature = "cosmwasm_1_3")] +use crate::{ + query::{AllDenomMetadataResponse, DecCoin, DenomMetadataResponse}, + PageRequest, +}; +#[cfg(feature = "stargate")] +use crate::{ChannelResponse, IbcQuery, ListChannelsResponse, PortIdResponse}; +#[cfg(feature = "cosmwasm_1_4")] +use crate::{Decimal256, DelegationRewardsResponse, DelegatorValidatorsResponse}; + +pub const MOCK_CONTRACT_ADDR: &str = "cosmos2contract"; + +/// Creates all external requirements that can be injected for unit tests. +/// +/// See also [`mock_dependencies_with_balance`] and [`mock_dependencies_with_balances`] +/// if you want to start with some initial balances. +pub fn mock_dependencies() -> OwnedDeps { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::default(), + custom_query_type: PhantomData, + } +} + +/// Creates all external requirements that can be injected for unit tests. +/// +/// It sets the given balance for the contract itself, nothing else. +pub fn mock_dependencies_with_balance( + contract_balance: &[Coin], +) -> OwnedDeps { + mock_dependencies_with_balances(&[(MOCK_CONTRACT_ADDR, contract_balance)]) +} + +/// Initializes the querier along with the mock_dependencies. +/// Sets all balances provided (you must explicitly set contract balance if desired). +pub fn mock_dependencies_with_balances( + balances: &[(&str, &[Coin])], +) -> OwnedDeps { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::new(balances), + custom_query_type: PhantomData, + } +} + +// Use MemoryStorage implementation (which is valid in non-testcode) +// We can later make simplifications here if needed +pub type MockStorage = MemoryStorage; + +/// Default prefix used when creating Bech32 encoded address. +const BECH32_PREFIX: &str = "cosmwasm"; + +// MockApi zero pads all human addresses to make them fit the canonical_length +// it trims off zeros for the reverse operation. +// not really smart, but allows us to see a difference (and consistent length for canonical +// addresses) +#[derive(Copy, Clone)] +pub struct MockApi { + /// Prefix used for creating addresses in Bech32 encoding. + bech32_prefix: &'static str, +} + +impl Default for MockApi { + fn default() -> Self { + MockApi { bech32_prefix: BECH32_PREFIX } + } +} + +impl Api for MockApi { + fn addr_validate(&self, input: &str) -> StdResult { + let canonical = self.addr_canonicalize(input)?; + let normalized = self.addr_humanize(&canonical)?; + if input != normalized.as_str() { + return Err(StdError::generic_err("Invalid input: address not normalized")); + } + Ok(Addr::unchecked(input)) + } + + fn addr_canonicalize(&self, input: &str) -> StdResult { + let hrp_str = CheckedHrpstring::new::(input) + .map_err(|_| StdError::generic_err("Error decoding bech32"))?; + + if !hrp_str.hrp().as_bytes().eq_ignore_ascii_case(self.bech32_prefix.as_bytes()) { + return Err(StdError::generic_err("Wrong bech32 prefix")); + } + + let bytes: Vec = hrp_str.byte_iter().collect(); + validate_length(&bytes)?; + Ok(bytes.into()) + } + + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { + validate_length(canonical.as_ref())?; + + let prefix = Hrp::parse(self.bech32_prefix) + .map_err(|_| StdError::generic_err("Invalid bech32 prefix"))?; + encode::(prefix, canonical.as_slice()) + .map(Addr::unchecked) + .map_err(|_| StdError::generic_err("Bech32 encoding error")) + } + + fn bls12_381_aggregate_g1(&self, g1s: &[u8]) -> Result<[u8; 48], VerificationError> { + cosmwasm_crypto::bls12_381_aggregate_g1(g1s).map_err(Into::into) + } + + fn bls12_381_aggregate_g2(&self, g2s: &[u8]) -> Result<[u8; 96], VerificationError> { + cosmwasm_crypto::bls12_381_aggregate_g2(g2s).map_err(Into::into) + } + + fn bls12_381_pairing_equality( + &self, + ps: &[u8], + qs: &[u8], + r: &[u8], + s: &[u8], + ) -> Result { + cosmwasm_crypto::bls12_381_pairing_equality(ps, qs, r, s).map_err(Into::into) + } + + fn bls12_381_hash_to_g1( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 48], VerificationError> { + Ok(cosmwasm_crypto::bls12_381_hash_to_g1(hash_function.into(), msg, dst)) + } + + fn bls12_381_hash_to_g2( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 96], VerificationError> { + Ok(cosmwasm_crypto::bls12_381_hash_to_g2(hash_function.into(), msg, dst)) + } + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::secp256k1_verify(message_hash, signature, public_key)?) + } + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + let pubkey = + cosmwasm_crypto::secp256k1_recover_pubkey(message_hash, signature, recovery_param)?; + Ok(pubkey.to_vec()) + } + + fn secp256r1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::secp256r1_verify(message_hash, signature, public_key)?) + } + + fn secp256r1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + let pubkey = + cosmwasm_crypto::secp256r1_recover_pubkey(message_hash, signature, recovery_param)?; + Ok(pubkey.to_vec()) + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::ed25519_verify(message, signature, public_key)?) + } + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + Ok(cosmwasm_crypto::ed25519_batch_verify(&mut OsRng, messages, signatures, public_keys)?) + } + + fn debug(&self, #[allow(unused)] message: &str) { + println!("{message}"); + } +} + +impl MockApi { + /// Returns [MockApi] with Bech32 prefix set to provided value. + /// + /// Bech32 prefix must not be empty. + /// + /// # Example + /// + /// ``` + /// # use cosmwasm_std::Addr; + /// # use cosmwasm_std::testing::MockApi; + /// # + /// let mock_api = MockApi::default().with_prefix("juno"); + /// let addr = mock_api.addr_make("creator"); + /// + /// assert_eq!(addr.to_string(), "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"); + /// ``` + pub fn with_prefix(mut self, prefix: &'static str) -> Self { + self.bech32_prefix = prefix; + self + } + + /// Returns an address built from provided input string. + /// + /// # Example + /// + /// ``` + /// # use cosmwasm_std::Addr; + /// # use cosmwasm_std::testing::MockApi; + /// # + /// let mock_api = MockApi::default(); + /// let addr = mock_api.addr_make("creator"); + /// + /// assert_eq!(addr.to_string(), "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp"); + /// ``` + /// + /// # Panics + /// + /// This function panics when generating a valid address is not possible, + /// especially when Bech32 prefix set in function [with_prefix](Self::with_prefix) is empty. + pub fn addr_make(&self, input: &str) -> Addr { + let digest = Sha256::digest(input); + + let prefix = match Hrp::parse(self.bech32_prefix) { + Ok(prefix) => prefix, + Err(reason) => panic!("Generating address failed with reason: {reason}"), + }; + + match encode::(prefix, &digest) { + Ok(address) => Addr::unchecked(address), + Err(reason) => panic!("Generating address failed with reason: {reason}"), + } + } +} + +/// Does basic validation of the number of bytes in a canonical address +fn validate_length(bytes: &[u8]) -> StdResult<()> { + match bytes.len() { + 1..=255 => Ok(()), + _ => Err(StdError::generic_err("Invalid canonical address length")), + } +} + +/// Returns a default environment with height, time, chain_id, and contract address +/// You can submit as is to most contracts, or modify height/time if you want to +/// test for expiration. +/// +/// This is intended for use in test code only. +pub fn mock_env() -> Env { + Env { + block: BlockInfo { + height: 12_345, + time: Timestamp::from_nanos(1_571_797_419_879_305_533), + chain_id: "cosmos-testnet-14002".to_string(), + }, + transaction: Some(TransactionInfo { index: 3 }), + contract: ContractInfo { address: Addr::unchecked(MOCK_CONTRACT_ADDR) }, + } +} + +/// Just set sender and funds for the message. +/// This is intended for use in test code only. +#[deprecated(note = "This is inconvenient and unsafe. Use message_info instead.")] +pub fn mock_info(sender: &str, funds: &[Coin]) -> MessageInfo { + MessageInfo { sender: Addr::unchecked(sender), funds: funds.to_vec() } +} + +/// Creates an IbcChannel for testing. You set a few key parameters for handshaking, +/// If you want to set more, use this as a default and mutate other fields +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel(my_channel_id: &str, order: IbcOrder, version: &str) -> IbcChannel { + IbcChannel { + endpoint: IbcEndpoint { + port_id: "my_port".to_string(), + channel_id: my_channel_id.to_string(), + }, + counterparty_endpoint: IbcEndpoint { + port_id: "their_port".to_string(), + channel_id: "channel-7".to_string(), + }, + order, + version: version.to_string(), + connection_id: "connection-2".to_string(), + } +} + +/// Creates a IbcChannelOpenMsg::OpenInit for testing ibc_channel_open. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_open_init( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelOpenMsg { + IbcChannelOpenMsg::new_init(mock_ibc_channel(my_channel_id, order, version)) +} + +/// Creates a IbcChannelOpenMsg::OpenTry for testing ibc_channel_open. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_open_try( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelOpenMsg { + IbcChannelOpenMsg::new_try(mock_ibc_channel(my_channel_id, order, version), version) +} + +/// Creates a IbcChannelConnectMsg::ConnectAck for testing ibc_channel_connect. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_connect_ack( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelConnectMsg { + IbcChannelConnectMsg::new_ack(mock_ibc_channel(my_channel_id, order, version), version) +} + +/// Creates a IbcChannelConnectMsg::ConnectConfirm for testing ibc_channel_connect. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_connect_confirm( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelConnectMsg { + IbcChannelConnectMsg::new_confirm(mock_ibc_channel(my_channel_id, order, version)) +} + +/// Creates a IbcChannelCloseMsg::CloseInit for testing ibc_channel_close. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_close_init( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelCloseMsg { + IbcChannelCloseMsg::new_init(mock_ibc_channel(my_channel_id, order, version)) +} + +/// Creates a IbcChannelCloseMsg::CloseConfirm for testing ibc_channel_close. +#[cfg(feature = "stargate")] +pub fn mock_ibc_channel_close_confirm( + my_channel_id: &str, + order: IbcOrder, + version: &str, +) -> IbcChannelCloseMsg { + IbcChannelCloseMsg::new_confirm(mock_ibc_channel(my_channel_id, order, version)) +} + +/// Creates a IbcPacketReceiveMsg for testing ibc_packet_receive. You set a few key parameters that +/// are often parsed. If you want to set more, use this as a default and mutate other fields +#[cfg(feature = "stargate")] +pub fn mock_ibc_packet_recv( + my_channel_id: &str, + data: &impl Serialize, +) -> StdResult { + Ok(IbcPacketReceiveMsg::new( + IbcPacket { + data: to_json_binary(data)?, + src: IbcEndpoint { + port_id: "their-port".to_string(), + channel_id: "channel-1234".to_string(), + }, + dest: IbcEndpoint { port_id: "our-port".to_string(), channel_id: my_channel_id.into() }, + sequence: 27, + timeout: IbcTimeoutBlock { revision: 1, height: 12345678 }.into(), + }, + Addr::unchecked("relayer"), + )) +} + +/// Creates a IbcPacket for testing ibc_packet_{ack,timeout}. You set a few key parameters that are +/// often parsed. If you want to set more, use this as a default and mutate other fields. +/// The difference from mock_ibc_packet_recv is if `my_channel_id` is src or dest. +#[cfg(feature = "stargate")] +fn mock_ibc_packet(my_channel_id: &str, data: &impl Serialize) -> StdResult { + Ok(IbcPacket { + data: to_json_binary(data)?, + src: IbcEndpoint { port_id: "their-port".to_string(), channel_id: my_channel_id.into() }, + dest: IbcEndpoint { + port_id: "our-port".to_string(), + channel_id: "channel-1234".to_string(), + }, + sequence: 29, + timeout: IbcTimeoutBlock { revision: 1, height: 432332552 }.into(), + }) +} + +/// Creates a IbcPacketAckMsg for testing ibc_packet_ack. You set a few key parameters that are +/// often parsed. If you want to set more, use this as a default and mutate other fields. +/// The difference from mock_ibc_packet_recv is if `my_channel_id` is src or dest. +#[cfg(feature = "stargate")] +pub fn mock_ibc_packet_ack( + my_channel_id: &str, + data: &impl Serialize, + ack: IbcAcknowledgement, +) -> StdResult { + let packet = mock_ibc_packet(my_channel_id, data)?; + + Ok(IbcPacketAckMsg::new(ack, packet, Addr::unchecked("relayer"))) +} + +/// Creates a IbcPacketTimeoutMsg for testing ibc_packet_timeout. You set a few key parameters that +/// are often parsed. If you want to set more, use this as a default and mutate other fields. +/// The difference from mock_ibc_packet_recv is if `my_channel_id` is src or dest. +#[cfg(feature = "stargate")] +pub fn mock_ibc_packet_timeout( + my_channel_id: &str, + data: &impl Serialize, +) -> StdResult { + let packet = mock_ibc_packet(my_channel_id, data)?; + Ok(IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer"))) +} + +/// The same type as cosmwasm-std's QuerierResult, but easier to reuse in +/// cosmwasm-vm. It might diverge from QuerierResult at some point. +pub type MockQuerierCustomHandlerResult = SystemResult>; + +/// MockQuerier holds an immutable table of bank balances +/// and configurable handlers for Wasm queries and custom queries. +pub struct MockQuerier { + pub bank: BankQuerier, + #[cfg(feature = "staking")] + pub staking: StakingQuerier, + #[cfg(feature = "cosmwasm_1_3")] + pub distribution: DistributionQuerier, + wasm: WasmQuerier, + #[cfg(feature = "stargate")] + pub ibc: IbcQuerier, + /// A handler to handle custom queries. This is set to a dummy handler that + /// always errors by default. Update it via `with_custom_handler`. + /// + /// Use box to avoid the need of another generic type + custom_handler: Box Fn(&'a C) -> MockQuerierCustomHandlerResult>, +} + +impl MockQuerier { + pub fn new(balances: &[(&str, &[Coin])]) -> Self { + MockQuerier { + bank: BankQuerier::new(balances), + #[cfg(feature = "cosmwasm_1_3")] + distribution: DistributionQuerier::default(), + #[cfg(feature = "staking")] + staking: StakingQuerier::default(), + wasm: WasmQuerier::default(), + #[cfg(feature = "stargate")] + ibc: IbcQuerier::default(), + // strange argument notation suggested as a workaround here: https://github.com/rust-lang/rust/issues/41078#issuecomment-294296365 + custom_handler: Box::from(|_: &_| -> MockQuerierCustomHandlerResult { + SystemResult::Err(SystemError::UnsupportedRequest { kind: "custom".to_string() }) + }), + } + } + + pub fn update_wasm(&mut self, handler: WH) + where + WH: Fn(&WasmQuery) -> QuerierResult + 'static, + { + self.wasm.update_handler(handler) + } + + pub fn with_custom_handler(mut self, handler: CH) -> Self + where + CH: Fn(&C) -> MockQuerierCustomHandlerResult + 'static, + { + self.custom_handler = Box::from(handler); + self + } +} + +impl Default for MockQuerier { + fn default() -> Self { + MockQuerier::new(&[]) + } +} + +impl Querier for MockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }), + }; + self.handle_query(&request) + } +} + +impl MockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Bank(bank_query) => self.bank.query(bank_query), + QueryRequest::Custom(custom_query) => (*self.custom_handler)(custom_query), + #[cfg(feature = "staking")] + QueryRequest::Staking(staking_query) => self.staking.query(staking_query), + #[cfg(feature = "cosmwasm_1_3")] + QueryRequest::Distribution(distribution_query) => self.distribution.query(distribution_query), + QueryRequest::Wasm(msg) => self.wasm.query(msg), + #[cfg(feature = "stargate")] + #[allow(deprecated)] + QueryRequest::Stargate { .. } => + SystemResult::Err(SystemError::UnsupportedRequest { kind: "Stargate".to_string() }), + #[cfg(feature = "cosmwasm_2_0")] + QueryRequest::Grpc(_) => + SystemResult::Err(SystemError::UnsupportedRequest { kind: "GRPC".to_string() }), + #[cfg(feature = "stargate")] + QueryRequest::Ibc(msg) => self.ibc.query(msg), + } + } +} + +struct WasmQuerier { + /// A handler to handle Wasm queries. This is set to a dummy handler that + /// always errors by default. Update it via `with_custom_handler`. + /// + /// Use box to avoid the need of generic type. + handler: Box Fn(&'a WasmQuery) -> QuerierResult>, +} + +impl WasmQuerier { + fn new(handler: Box Fn(&'a WasmQuery) -> QuerierResult>) -> Self { + Self { handler } + } + + fn update_handler(&mut self, handler: WH) + where + WH: Fn(&WasmQuery) -> QuerierResult + 'static, + { + self.handler = Box::from(handler) + } + + fn query(&self, request: &WasmQuery) -> QuerierResult { + (*self.handler)(request) + } +} + +impl Default for WasmQuerier { + fn default() -> Self { + let handler = Box::from(|request: &WasmQuery| -> QuerierResult { + let err = match request { + WasmQuery::Smart { contract_addr, .. } => + SystemError::NoSuchContract { addr: contract_addr.clone() }, + WasmQuery::Raw { contract_addr, .. } => + SystemError::NoSuchContract { addr: contract_addr.clone() }, + WasmQuery::ContractInfo { contract_addr, .. } => + SystemError::NoSuchContract { addr: contract_addr.clone() }, + #[cfg(feature = "cosmwasm_1_2")] + WasmQuery::CodeInfo { code_id, .. } => SystemError::NoSuchCode { code_id: *code_id }, + }; + SystemResult::Err(err) + }); + Self::new(handler) + } +} + +#[derive(Clone, Default)] +pub struct BankQuerier { + #[allow(dead_code)] + /// BTreeMap + supplies: BTreeMap, + /// BTreeMap + balances: BTreeMap>, + /// Vec + denom_metadata: BTreeMap, DenomMetadata>, +} + +impl BankQuerier { + pub fn new(balances: &[(&str, &[Coin])]) -> Self { + let balances: BTreeMap<_, _> = + balances.iter().map(|(s, c)| (s.to_string(), c.to_vec())).collect(); + + BankQuerier { + supplies: Self::calculate_supplies(&balances), + balances, + denom_metadata: BTreeMap::new(), + } + } + + /// set a new balance for the given address and return the old balance + pub fn update_balance( + &mut self, + addr: impl Into, + balance: Vec, + ) -> Option> { + let result = self.balances.insert(addr.into(), balance); + self.supplies = Self::calculate_supplies(&self.balances); + + result + } + + pub fn set_denom_metadata(&mut self, denom_metadata: &[DenomMetadata]) { + self.denom_metadata = + denom_metadata.iter().map(|d| (d.base.as_bytes().to_vec(), d.clone())).collect(); + } + + fn calculate_supplies(balances: &BTreeMap>) -> BTreeMap { + let mut supplies = BTreeMap::new(); + + let all_coins = balances.iter().flat_map(|(_, coins)| coins); + + for coin in all_coins { + *supplies.entry(coin.denom.clone()).or_insert_with(Uint128::zero) += coin.amount; + } + + supplies + } + + pub fn query(&self, request: &BankQuery) -> QuerierResult { + let contract_result: ContractResult = match request { + #[cfg(feature = "cosmwasm_1_1")] + BankQuery::Supply { denom } => { + let amount = self.supplies.get(denom).cloned().unwrap_or_else(Uint128::zero); + let bank_res = SupplyResponse { amount: Coin { amount, denom: denom.to_string() } }; + to_json_binary(&bank_res).into() + }, + BankQuery::Balance { address, denom } => { + // proper error on not found, serialize result on found + let amount = self + .balances + .get(address) + .and_then(|v| v.iter().find(|c| &c.denom == denom).map(|c| c.amount)) + .unwrap_or_default(); + let bank_res = + BalanceResponse { amount: Coin { amount, denom: denom.to_string() } }; + to_json_binary(&bank_res).into() + }, + BankQuery::AllBalances { address } => { + // proper error on not found, serialize result on found + let bank_res = AllBalanceResponse { + amount: self.balances.get(address).cloned().unwrap_or_default(), + }; + to_json_binary(&bank_res).into() + }, + #[cfg(feature = "cosmwasm_1_3")] + BankQuery::DenomMetadata { denom } => { + let denom_metadata = self.denom_metadata.get(denom.as_bytes()); + match denom_metadata { + Some(m) => { + let metadata_res = DenomMetadataResponse { metadata: m.clone() }; + to_json_binary(&metadata_res).into() + }, + None => return SystemResult::Err(SystemError::Unknown {}), + } + }, + #[cfg(feature = "cosmwasm_1_3")] + BankQuery::AllDenomMetadata { pagination } => { + let default_pagination = PageRequest { key: None, limit: 100, reverse: false }; + let pagination = pagination.as_ref().unwrap_or(&default_pagination); + + // range of all denoms after the given key (or until the key for reverse) + let range = match (pagination.reverse, &pagination.key) { + (_, None) => (Bound::Unbounded, Bound::Unbounded), + (true, Some(key)) => (Bound::Unbounded, Bound::Included(key.as_slice())), + (false, Some(key)) => (Bound::Included(key.as_slice()), Bound::Unbounded), + }; + let iter = self.denom_metadata.range::<[u8], _>(range); + // using dynamic dispatch here to reduce code duplication and since this is only + // testing code + let iter: Box> = + if pagination.reverse { Box::new(iter.rev()) } else { Box::new(iter) }; + + let mut metadata: Vec<_> = iter + // take the requested amount + 1 to get the next key + .take((pagination.limit.saturating_add(1)) as usize) + .map(|(_, m)| m.clone()) + .collect(); + + // if we took more than requested, remove the last element (the next key), + // otherwise this is the last batch + let next_key = if metadata.len() > pagination.limit as usize { + metadata.pop().map(|m| Binary::from(m.base.as_bytes())) + } else { + None + }; + + let metadata_res = AllDenomMetadataResponse { metadata, next_key }; + to_json_binary(&metadata_res).into() + }, + }; + // system result is always ok in the mock implementation + SystemResult::Ok(contract_result) + } +} + +#[cfg(feature = "stargate")] +#[derive(Clone, Default)] +pub struct IbcQuerier { + port_id: String, + channels: Vec, +} + +#[cfg(feature = "stargate")] +impl IbcQuerier { + /// Create a mock querier where: + /// - port_id is the port the "contract" is bound to + /// - channels are a list of ibc channels + pub fn new(port_id: &str, channels: &[IbcChannel]) -> Self { + IbcQuerier { port_id: port_id.to_string(), channels: channels.to_vec() } + } + + /// Update the querier's configuration + pub fn update(&mut self, port_id: impl Into, channels: &[IbcChannel]) { + self.port_id = port_id.into(); + self.channels = channels.to_vec(); + } + + pub fn query(&self, request: &IbcQuery) -> QuerierResult { + let contract_result: ContractResult = match request { + IbcQuery::Channel { channel_id, port_id } => { + let channel = self + .channels + .iter() + .find(|c| match port_id { + Some(p) => c.endpoint.channel_id.eq(channel_id) && c.endpoint.port_id.eq(p), + None => + c.endpoint.channel_id.eq(channel_id) && + c.endpoint.port_id == self.port_id, + }) + .cloned(); + let res = ChannelResponse { channel }; + to_json_binary(&res).into() + }, + IbcQuery::ListChannels { port_id } => { + let channels = self + .channels + .iter() + .filter(|c| match port_id { + Some(p) => c.endpoint.port_id.eq(p), + None => c.endpoint.port_id == self.port_id, + }) + .cloned() + .collect(); + let res = ListChannelsResponse { channels }; + to_json_binary(&res).into() + }, + IbcQuery::PortId {} => { + let res = PortIdResponse { port_id: self.port_id.clone() }; + to_json_binary(&res).into() + }, + }; + // system result is always ok in the mock implementation + SystemResult::Ok(contract_result) + } +} + +#[cfg(feature = "staking")] +#[derive(Clone, Default)] +pub struct StakingQuerier { + denom: String, + validators: Vec, + delegations: Vec, +} + +#[cfg(feature = "staking")] +impl StakingQuerier { + pub fn new(denom: &str, validators: &[Validator], delegations: &[FullDelegation]) -> Self { + StakingQuerier { + denom: denom.to_string(), + validators: validators.to_vec(), + delegations: delegations.to_vec(), + } + } + + /// Update the querier's configuration + pub fn update( + &mut self, + denom: impl Into, + validators: &[Validator], + delegations: &[FullDelegation], + ) { + self.denom = denom.into(); + self.validators = validators.to_vec(); + self.delegations = delegations.to_vec(); + } + + pub fn query(&self, request: &StakingQuery) -> QuerierResult { + let contract_result: ContractResult = match request { + StakingQuery::BondedDenom {} => { + let res = BondedDenomResponse { denom: self.denom.clone() }; + to_json_binary(&res).into() + }, + StakingQuery::AllValidators {} => { + let res = AllValidatorsResponse { validators: self.validators.clone() }; + to_json_binary(&res).into() + }, + StakingQuery::Validator { address } => { + let validator: Option = + self.validators.iter().find(|validator| validator.address == *address).cloned(); + let res = ValidatorResponse { validator }; + to_json_binary(&res).into() + }, + StakingQuery::AllDelegations { delegator } => { + let delegations: Vec<_> = self + .delegations + .iter() + .filter(|d| d.delegator.as_str() == delegator) + .cloned() + .map(|d| d.into()) + .collect(); + let res = AllDelegationsResponse { delegations }; + to_json_binary(&res).into() + }, + StakingQuery::Delegation { delegator, validator } => { + let delegation = self + .delegations + .iter() + .find(|d| d.delegator.as_str() == delegator && d.validator == *validator); + let res = DelegationResponse { delegation: delegation.cloned() }; + to_json_binary(&res).into() + }, + }; + // system result is always ok in the mock implementation + SystemResult::Ok(contract_result) + } +} + +#[cfg(feature = "cosmwasm_1_3")] +#[derive(Clone, Default)] +pub struct DistributionQuerier { + withdraw_addresses: BTreeMap, + /// Mock of accumulated rewards, indexed first by delegator and then validator address. + rewards: BTreeMap>>, + /// Mock of validators that a delegator has bonded to. + validators: BTreeMap>, +} + +#[cfg(feature = "cosmwasm_1_3")] +impl DistributionQuerier { + pub fn new(withdraw_addresses: T) -> Self + where + T: IntoIterator, + { + DistributionQuerier { + withdraw_addresses: withdraw_addresses.into_iter().collect(), + ..Default::default() + } + } + + pub fn set_withdraw_address( + &mut self, + delegator_address: impl Into, + withdraw_address: impl Into, + ) { + self.withdraw_addresses + .insert(delegator_address.into(), withdraw_address.into()); + } + + /// Sets multiple withdraw addresses. + /// + /// This allows passing multiple tuples of `(delegator_address, withdraw_address)`. + /// It does not overwrite existing entries. + pub fn set_withdraw_addresses( + &mut self, + withdraw_addresses: impl IntoIterator, impl Into)>, + ) { + for (d, w) in withdraw_addresses { + self.set_withdraw_address(d, w); + } + } + + pub fn clear_withdraw_addresses(&mut self) { + self.withdraw_addresses.clear(); + } + + /// Sets accumulated rewards for a given validator and delegator pair. + pub fn set_rewards( + &mut self, + validator: impl Into, + delegator: impl Into, + rewards: Vec, + ) { + self.rewards + .entry(delegator.into()) + .or_default() + .insert(validator.into(), rewards); + } + + /// Sets the validators a given delegator has bonded to. + pub fn set_validators( + &mut self, + delegator: impl Into, + validators: impl IntoIterator>, + ) { + self.validators + .insert(delegator.into(), validators.into_iter().map(Into::into).collect()); + } + + pub fn query(&self, request: &DistributionQuery) -> QuerierResult { + let contract_result: ContractResult = match request { + DistributionQuery::DelegatorWithdrawAddress { delegator_address } => { + let res = DelegatorWithdrawAddressResponse { + withdraw_address: Addr::unchecked( + self.withdraw_addresses.get(delegator_address).unwrap_or(delegator_address), + ), + }; + to_json_binary(&res).into() + }, + #[cfg(feature = "cosmwasm_1_4")] + DistributionQuery::DelegationRewards { delegator_address, validator_address } => { + let res = DelegationRewardsResponse { + rewards: self + .rewards + .get(delegator_address) + .and_then(|v| v.get(validator_address)) + .cloned() + .unwrap_or_default(), + }; + to_json_binary(&res).into() + }, + #[cfg(feature = "cosmwasm_1_4")] + DistributionQuery::DelegationTotalRewards { delegator_address } => { + let validator_rewards = + self.validator_rewards(delegator_address).unwrap_or_default(); + let res = crate::DelegationTotalRewardsResponse { + total: validator_rewards + .iter() + .fold(BTreeMap::<&str, DecCoin>::new(), |mut acc, rewards| { + for coin in &rewards.reward { + acc.entry(&coin.denom) + .or_insert_with(|| DecCoin { + denom: coin.denom.clone(), + amount: Decimal256::zero(), + }) + .amount += coin.amount; + } + + acc + }) + .into_values() + .collect(), + rewards: validator_rewards, + }; + to_json_binary(&res).into() + }, + #[cfg(feature = "cosmwasm_1_4")] + DistributionQuery::DelegatorValidators { delegator_address } => { + let res = DelegatorValidatorsResponse { + validators: self + .validators + .get(delegator_address) + .map(|set| set.iter().cloned().collect()) + .unwrap_or_default(), + }; + to_json_binary(&res).into() + }, + }; + // system result is always ok in the mock implementation + SystemResult::Ok(contract_result) + } + + /// Helper method to get all rewards for a given delegator. + #[cfg(feature = "cosmwasm_1_4")] + fn validator_rewards(&self, delegator_address: &str) -> Option> { + let validator_rewards = self.rewards.get(delegator_address)?; + + Some( + validator_rewards + .iter() + .map(|(validator, rewards)| crate::DelegatorReward { + validator_address: validator.clone(), + reward: rewards.clone(), + }) + .collect(), + ) + } +} + +/// Only for test code. This bypasses assertions in new, allowing us to create _* +/// Attributes to simulate responses from the blockchain +pub fn mock_wasmd_attr(key: impl Into, value: impl Into) -> Attribute { + Attribute { key: key.into(), value: value.into() } +} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "cosmwasm_1_3")] + use crate::DenomUnit; + use crate::{coin, coins, instantiate2_address, ContractInfoResponse, HexBinary, Response}; + #[cfg(feature = "staking")] + use crate::{Decimal, Delegation}; + use base64::{engine::general_purpose, Engine}; + use cosmwasm_core::BLS12_381_G1_GENERATOR; + use hex_literal::hex; + use serde::Deserialize; + + const SECP256K1_MSG_HASH_HEX: &str = + "5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"; + const SECP256K1_SIG_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; + const SECP256K1_PUBKEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + + const SECP256R1_MSG_HASH_HEX: &str = + "5eb28029ebf3c7025ff2fc2f6de6f62aecf6a72139e1cba5f20d11bbef036a7f"; + const SECP256R1_SIG_HEX: &str = "e67a9717ccf96841489d6541f4f6adb12d17b59a6bef847b6183b8fcf16a32eb9ae6ba6d637706849a6a9fc388cf0232d85c26ea0d1fe7437adb48de58364333"; + const SECP256R1_PUBKEY_HEX: &str = "0468229b48c2fe19d3db034e4c15077eb7471a66031f28a980821873915298ba76303e8ee3742a893f78b810991da697083dd8f11128c47651c27a56740a80c24c"; + + const ED25519_MSG_HEX: &str = "72"; + const ED25519_SIG_HEX: &str = "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"; + const ED25519_PUBKEY_HEX: &str = + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"; + + // See https://github.com/drand/kyber-bls12381/issues/22 and + // https://github.com/drand/drand/pull/1249 + const DOMAIN_HASH_TO_G2: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + + /// Public key League of Entropy Mainnet (curl -sS https://drand.cloudflare.com/info) + const PK_LEO_MAINNET: [u8; 48] = hex!("868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31"); + + // const ETH_BLOCK_HEADER: &[u8] = + // include_bytes!("../../../crypto/testdata/eth-headers/1699693797.394876721s.json"); + + #[test] + fn mock_info_works() { + #[allow(deprecated)] + let info = mock_info("my name", &coins(100, "atom")); + assert_eq!( + info, + MessageInfo { + sender: Addr::unchecked("my name"), + funds: vec![Coin { amount: 100u128.into(), denom: "atom".into() }] + } + ); + } + + #[test] + fn addr_validate_works() { + // default prefix is 'cosmwasm' + let api = MockApi::default(); + + // valid + let humanized = "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp"; + let addr = api.addr_validate(humanized).unwrap(); + assert_eq!(addr.as_str(), humanized); + + // invalid: too short + api.addr_validate("").unwrap_err(); + // invalid: not normalized + api.addr_validate("Foobar123").unwrap_err(); + api.addr_validate("FOOBAR123").unwrap_err(); + } + + #[test] + fn addr_canonicalize_works() { + let api = MockApi::default(); + + api.addr_canonicalize( + "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp", + ) + .unwrap(); + + // is case insensitive + let data1 = api + .addr_canonicalize( + "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp", + ) + .unwrap(); + let data2 = api + .addr_canonicalize( + "COSMWASM1H34LMPYWH4UPNJDG90CJF4J70AEE6Z8QQFSPUGAMJP42E4Q28KQS8S7VCP", + ) + .unwrap(); + assert_eq!(data1, data2); + } + + #[test] + fn canonicalize_and_humanize_restores_original() { + // create api with 'cosmwasm' prefix + let api = MockApi::default(); + + // normalizes input + let original = + String::from("COSMWASM1H34LMPYWH4UPNJDG90CJF4J70AEE6Z8QQFSPUGAMJP42E4Q28KQS8S7VCP"); + let canonical = api.addr_canonicalize(&original).unwrap(); + let recovered = api.addr_humanize(&canonical).unwrap(); + assert_eq!( + recovered.as_str(), + "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp" + ); + + // create api with 'juno' prefix + let api = MockApi::default().with_prefix("juno"); + + // long input (Juno contract address) + let original = + String::from("juno1v82su97skv6ucfqvuvswe0t5fph7pfsrtraxf0x33d8ylj5qnrysdvkc95"); + let canonical = api.addr_canonicalize(&original).unwrap(); + let recovered = api.addr_humanize(&canonical).unwrap(); + assert_eq!(recovered.as_str(), original); + } + + #[test] + fn addr_canonicalize_short_input() { + let api = MockApi::default(); + + // empty address should fail + let empty = "cosmwasm1pj90vm"; + assert!(api + .addr_canonicalize(empty) + .unwrap_err() + .to_string() + .contains("Invalid canonical address length")); + + // one byte address should work + let human = "cosmwasm1qqvk2mde"; + assert_eq!(api.addr_canonicalize(human).unwrap().as_ref(), [0u8]); + } + + #[test] + fn addr_canonicalize_long_input() { + let api = MockApi::default(); + let human = + "cosmwasm1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqehqqkz"; + let err = api.addr_canonicalize(human).unwrap_err(); + assert!(err.to_string().contains("Invalid canonical address length")); + } + + #[test] + fn addr_humanize_input_length() { + let api = MockApi::default(); + let input = CanonicalAddr::from(vec![]); + assert_eq!( + api.addr_humanize(&input).unwrap_err(), + StdError::generic_err("Invalid canonical address length") + ); + } + + // #[test] + // fn bls12_381_aggregate_g1_works() { + // #[derive(serde::Deserialize)] + // struct EthHeader { + // public_keys: Vec, + // aggregate_pubkey: String, + // } + + // let api = MockApi::default(); + // let header: EthHeader = serde_json::from_slice(ETH_BLOCK_HEADER).unwrap(); + // let expected = general_purpose::STANDARD.decode(header.aggregate_pubkey).unwrap(); + + // let pubkeys: Vec = header + // .public_keys + // .into_iter() + // .flat_map(|key| general_purpose::STANDARD.decode(key).unwrap()) + // .collect(); + // let sum = api.bls12_381_aggregate_g1(&pubkeys).unwrap(); + + // assert_eq!(expected, sum); + // } + + #[test] + fn bls12_381_aggregate_g2_works() { + let api = MockApi::default(); + + let points: Vec = [ + hex!("b6ed936746e01f8ecf281f020953fbf1f01debd5657c4a383940b020b26507f6076334f91e2366c96e9ab279fb5158090352ea1c5b0c9274504f4f0e7053af24802e51e4568d164fe986834f41e55c8e850ce1f98458c0cfc9ab380b55285a55"), + hex!("b23c46be3a001c63ca711f87a005c200cc550b9429d5f4eb38d74322144f1b63926da3388979e5321012fb1a0526bcd100b5ef5fe72628ce4cd5e904aeaa3279527843fae5ca9ca675f4f51ed8f83bbf7155da9ecc9663100a885d5dc6df96d9"), + hex!("948a7cb99f76d616c2c564ce9bf4a519f1bea6b0a624a02276443c245854219fabb8d4ce061d255af5330b078d5380681751aa7053da2c98bae898edc218c75f07e24d8802a17cd1f6833b71e58f5eb5b94208b4d0bb3848cecb075ea21be115"), + ] + .into_iter() + .flatten() + .collect(); + + let expected = hex!("9683b3e6701f9a4b706709577963110043af78a5b41991b998475a3d3fd62abf35ce03b33908418efc95a058494a8ae504354b9f626231f6b3f3c849dfdeaf5017c4780e2aee1850ceaf4b4d9ce70971a3d2cfcd97b7e5ecf6759f8da5f76d31"); + let sum = api.bls12_381_aggregate_g2(&points).unwrap(); + + assert_eq!(sum, expected); + } + + #[test] + fn bls12_381_pairing_equality_works() { + let api = MockApi::default(); + + let dst = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; + let ps = hex!("a491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79ab301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81b53d21a4cfd562c469cc81514d4ce5a6b577d8403d32a394dc265dd190b47fa9f829fdd7963afdf972e5e77854051f6f"); + let qs: Vec = [ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("5656565656565656565656565656565656565656565656565656565656565656"), + hex!("abababababababababababababababababababababababababababababababab"), + ] + .into_iter() + .flat_map(|msg| api.bls12_381_hash_to_g2(HashFunction::Sha256, &msg, dst).unwrap()) + .collect(); + let s = hex!("9104e74b9dfd3ad502f25d6a5ef57db0ed7d9a0e00f3500586d8ce44231212542fcfaf87840539b398bf07626705cf1105d246ca1062c6c2e1a53029a0f790ed5e3cb1f52f8234dc5144c45fc847c0cd37a92d68e7c5ba7c648a8a339f171244"); + + let is_valid = + api.bls12_381_pairing_equality(&ps, &qs, &BLS12_381_G1_GENERATOR, &s).unwrap(); + assert!(is_valid); + } + + #[test] + fn bls12_381_hash_to_g1_works() { + // See: ; Section J.9.1 + + let api = MockApi::default(); + let msg = b"abc"; + let dst = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; + + let hashed_point = api.bls12_381_hash_to_g1(HashFunction::Sha256, msg, dst).unwrap(); + let mut serialized_expected_compressed = hex!("03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903"); + // Set the compression tag + serialized_expected_compressed[0] |= 0b1000_0000; + + assert_eq!(hashed_point, serialized_expected_compressed); + } + + #[test] + fn bls12_381_hash_to_g2_works() { + let api = MockApi::default(); + let msg = b"abc"; + let dst = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"; + + let hashed_point = api.bls12_381_hash_to_g2(HashFunction::Sha256, msg, dst).unwrap(); + let mut serialized_expected_compressed = hex!("139cddbccdc5e91b9623efd38c49f81a6f83f175e80b06fc374de9eb4b41dfe4ca3a230ed250fbe3a2acf73a41177fd802c2d18e033b960562aae3cab37a27ce00d80ccd5ba4b7fe0e7a210245129dbec7780ccc7954725f4168aff2787776e6"); + // Set the compression tag + serialized_expected_compressed[0] |= 0b1000_0000; + + assert_eq!(hashed_point, serialized_expected_compressed); + } + + #[test] + fn bls12_318_pairing_equality_works() { + fn build_bls_message(round: u64, previous_signature: &[u8]) -> Vec { + Sha256::new() + .chain_update(previous_signature) + .chain_update(round.to_be_bytes()) + .finalize() + .to_vec() + } + + let api = MockApi::default(); + + let previous_signature = hex::decode("a609e19a03c2fcc559e8dae14900aaefe517cb55c840f6e69bc8e4f66c8d18e8a609685d9917efbfb0c37f058c2de88f13d297c7e19e0ab24813079efe57a182554ff054c7638153f9b26a60e7111f71a0ff63d9571704905d3ca6df0b031747").unwrap(); + let signature = hex::decode("82f5d3d2de4db19d40a6980e8aa37842a0e55d1df06bd68bddc8d60002e8e959eb9cfa368b3c1b77d18f02a54fe047b80f0989315f83b12a74fd8679c4f12aae86eaf6ab5690b34f1fddd50ee3cc6f6cdf59e95526d5a5d82aaa84fa6f181e42").unwrap(); + let round: u64 = 72785; + + let msg = build_bls_message(round, &previous_signature); + let msg_point = + api.bls12_381_hash_to_g2(HashFunction::Sha256, &msg, DOMAIN_HASH_TO_G2).unwrap(); + + let is_valid = api + .bls12_381_pairing_equality( + &BLS12_381_G1_GENERATOR, + &signature, + &PK_LEO_MAINNET, + &msg_point, + ) + .unwrap(); + + assert!(is_valid); + } + + // Basic "works" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256k1_verify_works() { + let api = MockApi::default(); + + let hash = hex::decode(SECP256K1_MSG_HASH_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIG_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBKEY_HEX).unwrap(); + + assert!(api.secp256k1_verify(&hash, &signature, &public_key).unwrap()); + } + + // Basic "fails" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256k1_verify_fails() { + let api = MockApi::default(); + + let mut hash = hex::decode(SECP256K1_MSG_HASH_HEX).unwrap(); + // alter hash + hash[0] ^= 0x01; + let signature = hex::decode(SECP256K1_SIG_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBKEY_HEX).unwrap(); + + assert!(!api.secp256k1_verify(&hash, &signature, &public_key).unwrap()); + } + + // Basic "errors" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256k1_verify_errs() { + let api = MockApi::default(); + + let hash = hex::decode(SECP256K1_MSG_HASH_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIG_HEX).unwrap(); + let public_key = vec![]; + + let res = api.secp256k1_verify(&hash, &signature, &public_key); + assert_eq!(res.unwrap_err(), VerificationError::InvalidPubkeyFormat); + } + + #[test] + fn secp256k1_recover_pubkey_works() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + let pubkey = api.secp256k1_recover_pubkey(&hash, &signature, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + } + + #[test] + fn secp256k1_recover_pubkey_fails_for_wrong_recovery_param() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let _recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + // Wrong recovery param leads to different pubkey + let pubkey = api.secp256k1_recover_pubkey(&hash, &signature, 0).unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Invalid recovery param leads to error + let result = api.secp256k1_recover_pubkey(&hash, &signature, 42); + match result.unwrap_err() { + RecoverPubkeyError::InvalidRecoveryParam => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + #[test] + fn secp256k1_recover_pubkey_fails_for_wrong_hash() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + // Wrong hash + let mut corrupted_hash = hash; + corrupted_hash[0] ^= 0x01; + let pubkey = api + .secp256k1_recover_pubkey(&corrupted_hash, &signature, recovery_param) + .unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Malformed hash + let mut malformed_hash = hash.to_vec(); + malformed_hash.push(0x8a); + let result = api.secp256k1_recover_pubkey(&malformed_hash, &signature, recovery_param); + match result.unwrap_err() { + RecoverPubkeyError::InvalidHashFormat => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + // Basic "works" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256r1_verify_works() { + let api = MockApi::default(); + + let hash = hex::decode(SECP256R1_MSG_HASH_HEX).unwrap(); + let signature = hex::decode(SECP256R1_SIG_HEX).unwrap(); + let public_key = hex::decode(SECP256R1_PUBKEY_HEX).unwrap(); + + assert!(api.secp256r1_verify(&hash, &signature, &public_key).unwrap()); + } + + // Basic "fails" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256r1_verify_fails() { + let api = MockApi::default(); + + let mut hash = hex::decode(SECP256R1_MSG_HASH_HEX).unwrap(); + // alter hash + hash[0] ^= 0x01; + let signature = hex::decode(SECP256R1_SIG_HEX).unwrap(); + let public_key = hex::decode(SECP256R1_PUBKEY_HEX).unwrap(); + + assert!(!api.secp256r1_verify(&hash, &signature, &public_key).unwrap()); + } + + // Basic "errors" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn secp256r1_verify_errs() { + let api = MockApi::default(); + + let hash = hex::decode(SECP256R1_MSG_HASH_HEX).unwrap(); + let signature = hex::decode(SECP256R1_SIG_HEX).unwrap(); + let public_key = vec![]; + + let res = api.secp256r1_verify(&hash, &signature, &public_key); + assert_eq!(res.unwrap_err(), VerificationError::InvalidPubkeyFormat); + } + + #[test] + fn secp256r1_recover_pubkey_works() { + let api = MockApi::default(); + + let hash = hex!("17b03f9f00f6692ccdde485fc63c4530751ef35da6f71336610944b0894fcfb8"); + let signature = hex!("9886ae46c1415c3bc959e82b760ad760aab66885a84e620aa339fdf102465c422bf3a80bc04faa35ebecc0f4864ac02d349f6f126e0f988501b8d3075409a26c"); + let recovery_param = 0; + let expected = hex!("0451f99d2d52d4a6e734484a018b7ca2f895c2929b6754a3a03224d07ae61166ce4737da963c6ef7247fb88d19f9b0c667cac7fe12837fdab88c66f10d3c14cad1"); + + let pubkey = api.secp256r1_recover_pubkey(&hash, &signature, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + } + + #[test] + fn secp256r1_recover_pubkey_fails_for_wrong_recovery_param() { + let api = MockApi::default(); + + let hash = hex!("17b03f9f00f6692ccdde485fc63c4530751ef35da6f71336610944b0894fcfb8"); + let signature = hex!("9886ae46c1415c3bc959e82b760ad760aab66885a84e620aa339fdf102465c422bf3a80bc04faa35ebecc0f4864ac02d349f6f126e0f988501b8d3075409a26c"); + let expected = hex!("0451f99d2d52d4a6e734484a018b7ca2f895c2929b6754a3a03224d07ae61166ce4737da963c6ef7247fb88d19f9b0c667cac7fe12837fdab88c66f10d3c14cad1"); + + // Wrong recovery param leads to different pubkey + let pubkey = api.secp256r1_recover_pubkey(&hash, &signature, 1).unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Invalid recovery param leads to error + let result = api.secp256r1_recover_pubkey(&hash, &signature, 42); + match result.unwrap_err() { + RecoverPubkeyError::InvalidRecoveryParam => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + #[test] + fn secp256r1_recover_pubkey_fails_for_wrong_hash() { + let api = MockApi::default(); + + let hash = hex!("17b03f9f00f6692ccdde485fc63c4530751ef35da6f71336610944b0894fcfb8"); + let signature = hex!("9886ae46c1415c3bc959e82b760ad760aab66885a84e620aa339fdf102465c422bf3a80bc04faa35ebecc0f4864ac02d349f6f126e0f988501b8d3075409a26c"); + let recovery_param = 0; + let expected = hex!("0451f99d2d52d4a6e734484a018b7ca2f895c2929b6754a3a03224d07ae61166ce4737da963c6ef7247fb88d19f9b0c667cac7fe12837fdab88c66f10d3c14cad1"); + + // Wrong hash + let mut corrupted_hash = hash; + corrupted_hash[0] ^= 0x01; + let pubkey = api + .secp256r1_recover_pubkey(&corrupted_hash, &signature, recovery_param) + .unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Malformed hash + let mut malformed_hash = hash.to_vec(); + malformed_hash.push(0x8a); + let result = api.secp256r1_recover_pubkey(&malformed_hash, &signature, recovery_param); + match result.unwrap_err() { + RecoverPubkeyError::InvalidHashFormat => {}, + err => panic!("Unexpected error: {err:?}"), + } + } + + // Basic "works" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_works() { + let api = MockApi::default(); + + let msg = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + assert!(api.ed25519_verify(&msg, &signature, &public_key).unwrap()); + } + + // Basic "fails" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_fails() { + let api = MockApi::default(); + + let mut msg = hex::decode(ED25519_MSG_HEX).unwrap(); + // alter msg + msg[0] ^= 0x01; + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + assert!(!api.ed25519_verify(&msg, &signature, &public_key).unwrap()); + } + + // Basic "errors" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_errs() { + let api = MockApi::default(); + + let msg = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = vec![]; + + let res = api.ed25519_verify(&msg, &signature, &public_key); + assert_eq!(res.unwrap_err(), VerificationError::InvalidPubkeyFormat); + } + + // Basic "works" test. + #[test] + fn ed25519_batch_verify_works() { + let api = MockApi::default(); + + let msg = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + let msgs: Vec<&[u8]> = vec![&msg]; + let signatures: Vec<&[u8]> = vec![&signature]; + let public_keys: Vec<&[u8]> = vec![&public_key]; + + assert!(api.ed25519_batch_verify(&msgs, &signatures, &public_keys).unwrap()); + } + + // Basic "fails" test. + #[test] + fn ed25519_batch_verify_fails() { + let api = MockApi::default(); + + let mut msg = hex::decode(ED25519_MSG_HEX).unwrap(); + // alter msg + msg[0] ^= 0x01; + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + let msgs: Vec<&[u8]> = vec![&msg]; + let signatures: Vec<&[u8]> = vec![&signature]; + let public_keys: Vec<&[u8]> = vec![&public_key]; + + assert!(!api.ed25519_batch_verify(&msgs, &signatures, &public_keys).unwrap()); + } + + // Basic "errors" test. + #[test] + fn ed25519_batch_verify_errs() { + let api = MockApi::default(); + + let msg = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key: Vec = vec![0u8; 0]; + + let msgs: Vec<&[u8]> = vec![msg.as_slice()]; + let signatures: Vec<&[u8]> = vec![signature.as_slice()]; + let public_keys: Vec<&[u8]> = vec![&public_key]; + + let res = api.ed25519_batch_verify(&msgs, &signatures, &public_keys); + assert_eq!(res.unwrap_err(), VerificationError::InvalidPubkeyFormat); + } + + #[cfg(feature = "cosmwasm_1_1")] + #[test] + fn bank_querier_supply() { + let addr1 = String::from("foo"); + let balance1 = vec![coin(123, "ELF"), coin(777, "FLY")]; + + let addr2 = String::from("bar"); + let balance2 = coins(321, "ELF"); + + let bank = BankQuerier::new(&[(&addr1, &balance1), (&addr2, &balance2)]); + + let elf = bank.query(&BankQuery::Supply { denom: "ELF".to_string() }).unwrap().unwrap(); + let res: SupplyResponse = from_json(elf).unwrap(); + assert_eq!(res.amount, coin(444, "ELF")); + + let fly = bank.query(&BankQuery::Supply { denom: "FLY".to_string() }).unwrap().unwrap(); + let res: SupplyResponse = from_json(fly).unwrap(); + assert_eq!(res.amount, coin(777, "FLY")); + + // if a denom does not exist, should return zero amount, instead of throwing an error + let atom = bank.query(&BankQuery::Supply { denom: "ATOM".to_string() }).unwrap().unwrap(); + let res: SupplyResponse = from_json(atom).unwrap(); + assert_eq!(res.amount, coin(0, "ATOM")); + } + + #[test] + fn bank_querier_all_balances() { + let addr = String::from("foobar"); + let balance = vec![coin(123, "ELF"), coin(777, "FLY")]; + let bank = BankQuerier::new(&[(&addr, &balance)]); + + let all = bank.query(&BankQuery::AllBalances { address: addr }).unwrap().unwrap(); + let res: AllBalanceResponse = from_json(all).unwrap(); + assert_eq!(&res.amount, &balance); + } + + #[test] + fn bank_querier_one_balance() { + let addr = String::from("foobar"); + let balance = vec![coin(123, "ELF"), coin(777, "FLY")]; + let bank = BankQuerier::new(&[(&addr, &balance)]); + + // one match + let fly = bank + .query(&BankQuery::Balance { address: addr.clone(), denom: "FLY".to_string() }) + .unwrap() + .unwrap(); + let res: BalanceResponse = from_json(fly).unwrap(); + assert_eq!(res.amount, coin(777, "FLY")); + + // missing denom + let miss = bank + .query(&BankQuery::Balance { address: addr, denom: "MISS".to_string() }) + .unwrap() + .unwrap(); + let res: BalanceResponse = from_json(miss).unwrap(); + assert_eq!(res.amount, coin(0, "MISS")); + } + + #[test] + fn bank_querier_missing_account() { + let addr = String::from("foobar"); + let balance = vec![coin(123, "ELF"), coin(777, "FLY")]; + let bank = BankQuerier::new(&[(&addr, &balance)]); + + // all balances on empty account is empty vec + let all = bank + .query(&BankQuery::AllBalances { address: String::from("elsewhere") }) + .unwrap() + .unwrap(); + let res: AllBalanceResponse = from_json(all).unwrap(); + assert_eq!(res.amount, vec![]); + + // any denom on balances on empty account is empty coin + let miss = bank + .query(&BankQuery::Balance { + address: String::from("elsewhere"), + denom: "ELF".to_string(), + }) + .unwrap() + .unwrap(); + let res: BalanceResponse = from_json(miss).unwrap(); + assert_eq!(res.amount, coin(0, "ELF")); + } + + #[cfg(feature = "cosmwasm_1_3")] + #[test] + fn bank_querier_metadata_works() { + let mut bank = BankQuerier::new(&[]); + bank.set_denom_metadata( + &(0..100) + .map(|i| DenomMetadata { + symbol: format!("FOO{i}"), + name: "Foo".to_string(), + description: "Foo coin".to_string(), + denom_units: vec![DenomUnit { + denom: "ufoo".to_string(), + exponent: 8, + aliases: vec!["microfoo".to_string(), "foobar".to_string()], + }], + display: "FOO".to_string(), + base: format!("ufoo{i}"), + uri: "https://foo.bar".to_string(), + uri_hash: "foo".to_string(), + }) + .collect::>(), + ); + + // querying first 10 should work + let res = bank + .query(&BankQuery::AllDenomMetadata { + pagination: Some(PageRequest { key: None, limit: 10, reverse: false }), + }) + .unwrap() + .unwrap(); + let res: AllDenomMetadataResponse = from_json(res).unwrap(); + assert_eq!(res.metadata.len(), 10); + assert!(res.next_key.is_some()); + + // querying next 10 should also work + let res2 = bank + .query(&BankQuery::AllDenomMetadata { + pagination: Some(PageRequest { key: res.next_key, limit: 10, reverse: false }), + }) + .unwrap() + .unwrap(); + let res2: AllDenomMetadataResponse = from_json(res2).unwrap(); + assert_eq!(res2.metadata.len(), 10); + assert_ne!(res.metadata.last(), res2.metadata.first()); + // should have no overlap + for m in res.metadata { + assert!(!res2.metadata.contains(&m)); + } + + // querying all 100 should work + let res = bank + .query(&BankQuery::AllDenomMetadata { + pagination: Some(PageRequest { key: None, limit: 100, reverse: true }), + }) + .unwrap() + .unwrap(); + let res: AllDenomMetadataResponse = from_json(res).unwrap(); + assert_eq!(res.metadata.len(), 100); + assert!(res.next_key.is_none(), "no more data should be available"); + assert_eq!(res.metadata[0].symbol, "FOO99", "should have been reversed"); + + let more_res = bank + .query(&BankQuery::AllDenomMetadata { + pagination: Some(PageRequest { key: res.next_key, limit: u32::MAX, reverse: true }), + }) + .unwrap() + .unwrap(); + let more_res: AllDenomMetadataResponse = from_json(more_res).unwrap(); + assert_eq!(more_res.metadata, res.metadata, "should be same as previous query"); + } + + #[cfg(feature = "cosmwasm_1_3")] + #[test] + fn distribution_querier_delegator_withdraw_address() { + let mut distribution = DistributionQuerier::default(); + distribution.set_withdraw_address("addr0", "withdraw0"); + + let query = + DistributionQuery::DelegatorWithdrawAddress { delegator_address: "addr0".to_string() }; + + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegatorWithdrawAddressResponse = from_json(res).unwrap(); + assert_eq!(res.withdraw_address.as_str(), "withdraw0"); + + let query = + DistributionQuery::DelegatorWithdrawAddress { delegator_address: "addr1".to_string() }; + + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegatorWithdrawAddressResponse = from_json(res).unwrap(); + assert_eq!(res.withdraw_address.as_str(), "addr1"); + } + + #[cfg(feature = "cosmwasm_1_4")] + #[test] + fn distribution_querier_delegator_validators() { + let mut distribution = DistributionQuerier::default(); + distribution.set_validators("addr0", ["valoper1", "valoper2"]); + + let query = + DistributionQuery::DelegatorValidators { delegator_address: "addr0".to_string() }; + + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegatorValidatorsResponse = from_json(res).unwrap(); + assert_eq!(res.validators, ["valoper1", "valoper2"]); + + let query = + DistributionQuery::DelegatorValidators { delegator_address: "addr1".to_string() }; + + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegatorValidatorsResponse = from_json(res).unwrap(); + assert_eq!(res.validators, ([] as [String; 0])); + } + + #[cfg(feature = "cosmwasm_1_4")] + #[test] + fn distribution_querier_delegation_rewards() { + use crate::{Decimal256, DelegationTotalRewardsResponse, DelegatorReward}; + + let mut distribution = DistributionQuerier::default(); + let valoper0_rewards = vec![ + DecCoin::new(Decimal256::from_atomics(1234u128, 0).unwrap(), "uatom"), + DecCoin::new(Decimal256::from_atomics(56781234u128, 4).unwrap(), "utest"), + ]; + distribution.set_rewards("valoper0", "addr0", valoper0_rewards.clone()); + + // both exist / are set + let query = DistributionQuery::DelegationRewards { + delegator_address: "addr0".to_string(), + validator_address: "valoper0".to_string(), + }; + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegationRewardsResponse = from_json(res).unwrap(); + assert_eq!(res.rewards, valoper0_rewards); + + // delegator does not exist + let query = DistributionQuery::DelegationRewards { + delegator_address: "nonexistent".to_string(), + validator_address: "valoper0".to_string(), + }; + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegationRewardsResponse = from_json(res).unwrap(); + assert_eq!(res.rewards.len(), 0); + + // validator does not exist + let query = DistributionQuery::DelegationRewards { + delegator_address: "addr0".to_string(), + validator_address: "valopernonexistent".to_string(), + }; + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegationRewardsResponse = from_json(res).unwrap(); + assert_eq!(res.rewards.len(), 0); + + // add one more validator + let valoper1_rewards = vec![DecCoin::new(Decimal256::one(), "uatom")]; + distribution.set_rewards("valoper1", "addr0", valoper1_rewards.clone()); + + // total rewards + let query = + DistributionQuery::DelegationTotalRewards { delegator_address: "addr0".to_string() }; + let res = distribution.query(&query).unwrap().unwrap(); + let res: DelegationTotalRewardsResponse = from_json(res).unwrap(); + assert_eq!( + res.rewards, + vec![ + DelegatorReward { validator_address: "valoper0".into(), reward: valoper0_rewards }, + DelegatorReward { validator_address: "valoper1".into(), reward: valoper1_rewards }, + ] + ); + assert_eq!( + res.total, + [ + DecCoin::new( + Decimal256::from_atomics(1234u128, 0).unwrap() + Decimal256::one(), + "uatom" + ), + // total for utest should still be the same + DecCoin::new(Decimal256::from_atomics(56781234u128, 4).unwrap(), "utest") + ] + ); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_channel_existing() { + let chan1 = mock_ibc_channel("channel-0", IbcOrder::Ordered, "ibc"); + let chan2 = mock_ibc_channel("channel-1", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1.clone(), chan2]); + + // query existing + let query = &IbcQuery::Channel { + channel_id: "channel-0".to_string(), + port_id: Some("my_port".to_string()), + }; + let raw = ibc.query(query).unwrap().unwrap(); + let chan: ChannelResponse = from_json(raw).unwrap(); + assert_eq!(chan.channel, Some(chan1)); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_channel_existing_no_port() { + let chan1 = IbcChannel { + endpoint: IbcEndpoint { + port_id: "myport".to_string(), + channel_id: "channel-0".to_string(), + }, + counterparty_endpoint: IbcEndpoint { + port_id: "their_port".to_string(), + channel_id: "channel-7".to_string(), + }, + order: IbcOrder::Ordered, + version: "ibc".to_string(), + connection_id: "connection-2".to_string(), + }; + let chan2 = mock_ibc_channel("channel-1", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1.clone(), chan2]); + + // query existing + let query = &IbcQuery::Channel { + channel_id: "channel-0".to_string(), + port_id: Some("myport".to_string()), + }; + let raw = ibc.query(query).unwrap().unwrap(); + let chan: ChannelResponse = from_json(raw).unwrap(); + assert_eq!(chan.channel, Some(chan1)); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_channel_none() { + let chan1 = mock_ibc_channel("channel-0", IbcOrder::Ordered, "ibc"); + let chan2 = mock_ibc_channel("channel-1", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1, chan2]); + + // query non-existing + let query = &IbcQuery::Channel { channel_id: "channel-0".to_string(), port_id: None }; + let raw = ibc.query(query).unwrap().unwrap(); + let chan: ChannelResponse = from_json(raw).unwrap(); + assert_eq!(chan.channel, None); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_channels_matching() { + let chan1 = mock_ibc_channel("channel-0", IbcOrder::Ordered, "ibc"); + let chan2 = mock_ibc_channel("channel-1", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1.clone(), chan2.clone()]); + + // query channels matching "my_port" (should match both above) + let query = &IbcQuery::ListChannels { port_id: Some("my_port".to_string()) }; + let raw = ibc.query(query).unwrap().unwrap(); + let res: ListChannelsResponse = from_json(raw).unwrap(); + assert_eq!(res.channels, vec![chan1, chan2]); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_channels_no_matching() { + let chan1 = mock_ibc_channel("channel-0", IbcOrder::Ordered, "ibc"); + let chan2 = mock_ibc_channel("channel-1", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1, chan2]); + + // query channels matching "myport" (should be none) + let query = &IbcQuery::ListChannels { port_id: None }; + let raw = ibc.query(query).unwrap().unwrap(); + let res: ListChannelsResponse = from_json(raw).unwrap(); + assert_eq!(res.channels, vec![]); + } + + #[cfg(feature = "stargate")] + #[test] + fn ibc_querier_port() { + let chan1 = mock_ibc_channel("channel-0", IbcOrder::Ordered, "ibc"); + + let ibc = IbcQuerier::new("myport", &[chan1]); + + // query channels matching "myport" (should be none) + let query = &IbcQuery::PortId {}; + let raw = ibc.query(query).unwrap().unwrap(); + let res: PortIdResponse = from_json(raw).unwrap(); + assert_eq!(res.port_id, "myport"); + } + + #[cfg(feature = "staking")] + #[test] + fn staking_querier_all_validators() { + let val1 = Validator { + address: String::from("validator-one"), + commission: Decimal::percent(1), + max_commission: Decimal::percent(3), + max_change_rate: Decimal::percent(1), + }; + let val2 = Validator { + address: String::from("validator-two"), + commission: Decimal::permille(15), + max_commission: Decimal::permille(40), + max_change_rate: Decimal::permille(5), + }; + + let staking = StakingQuerier::new("ustake", &[val1.clone(), val2.clone()], &[]); + + // one match + let raw = staking.query(&StakingQuery::AllValidators {}).unwrap().unwrap(); + let vals: AllValidatorsResponse = from_json(raw).unwrap(); + assert_eq!(vals.validators, vec![val1, val2]); + } + + #[cfg(feature = "staking")] + #[test] + fn staking_querier_validator() { + let address1 = String::from("validator-one"); + let address2 = String::from("validator-two"); + let address_non_existent = String::from("wannabe-validator"); + + let val1 = Validator { + address: address1.clone(), + commission: Decimal::percent(1), + max_commission: Decimal::percent(3), + max_change_rate: Decimal::percent(1), + }; + let val2 = Validator { + address: address2.clone(), + commission: Decimal::permille(15), + max_commission: Decimal::permille(40), + max_change_rate: Decimal::permille(5), + }; + + let staking = StakingQuerier::new("ustake", &[val1.clone(), val2.clone()], &[]); + + // query 1 + let raw = staking.query(&StakingQuery::Validator { address: address1 }).unwrap().unwrap(); + let res: ValidatorResponse = from_json(raw).unwrap(); + assert_eq!(res.validator, Some(val1)); + + // query 2 + let raw = staking.query(&StakingQuery::Validator { address: address2 }).unwrap().unwrap(); + let res: ValidatorResponse = from_json(raw).unwrap(); + assert_eq!(res.validator, Some(val2)); + + // query non-existent + let raw = staking + .query(&StakingQuery::Validator { address: address_non_existent }) + .unwrap() + .unwrap(); + let res: ValidatorResponse = from_json(raw).unwrap(); + assert_eq!(res.validator, None); + } + + #[cfg(feature = "staking")] + // gets delegators from query or panic + fn get_all_delegators( + staking: &StakingQuerier, + delegator: impl Into, + ) -> Vec { + let raw = staking + .query(&StakingQuery::AllDelegations { delegator: delegator.into() }) + .unwrap() + .unwrap(); + let dels: AllDelegationsResponse = from_json(raw).unwrap(); + dels.delegations + } + + #[cfg(feature = "staking")] + // gets full delegators from query or panic + fn get_delegator( + staking: &StakingQuerier, + delegator: impl Into, + validator: impl Into, + ) -> Option { + let raw = staking + .query(&StakingQuery::Delegation { + delegator: delegator.into(), + validator: validator.into(), + }) + .unwrap() + .unwrap(); + let dels: DelegationResponse = from_json(raw).unwrap(); + dels.delegation + } + + #[cfg(feature = "staking")] + #[test] + fn staking_querier_delegations() { + let val1 = String::from("validator-one"); + let val2 = String::from("validator-two"); + + let user_a = Addr::unchecked("investor"); + let user_b = Addr::unchecked("speculator"); + let user_c = Addr::unchecked("hodler"); + + // we need multiple validators per delegator, so the queries provide different results + let del1a = FullDelegation { + delegator: user_a.clone(), + validator: val1.clone(), + amount: coin(100, "ustake"), + can_redelegate: coin(100, "ustake"), + accumulated_rewards: coins(5, "ustake"), + }; + let del2a = FullDelegation { + delegator: user_a.clone(), + validator: val2.clone(), + amount: coin(500, "ustake"), + can_redelegate: coin(500, "ustake"), + accumulated_rewards: coins(20, "ustake"), + }; + + // note we cannot have multiple delegations on one validator, they are collapsed into one + let del1b = FullDelegation { + delegator: user_b.clone(), + validator: val1.clone(), + amount: coin(500, "ustake"), + can_redelegate: coin(0, "ustake"), + accumulated_rewards: coins(0, "ustake"), + }; + + // and another one on val2 + let del2c = FullDelegation { + delegator: user_c.clone(), + validator: val2.clone(), + amount: coin(8888, "ustake"), + can_redelegate: coin(4567, "ustake"), + accumulated_rewards: coins(900, "ustake"), + }; + + let staking = StakingQuerier::new( + "ustake", + &[], + &[del1a.clone(), del1b.clone(), del2a.clone(), del2c.clone()], + ); + + // get all for user a + let dels = get_all_delegators(&staking, user_a.clone()); + assert_eq!(dels, vec![del1a.clone().into(), del2a.clone().into()]); + + // get all for user b + let dels = get_all_delegators(&staking, user_b.clone()); + assert_eq!(dels, vec![del1b.clone().into()]); + + // get all for user c + let dels = get_all_delegators(&staking, user_c.clone()); + assert_eq!(dels, vec![del2c.clone().into()]); + + // for user with no delegations... + let dels = get_all_delegators(&staking, String::from("no one")); + assert_eq!(dels, vec![]); + + // filter a by validator (1 and 1) + let dels = get_delegator(&staking, user_a.clone(), val1.clone()); + assert_eq!(dels, Some(del1a)); + let dels = get_delegator(&staking, user_a, val2.clone()); + assert_eq!(dels, Some(del2a)); + + // filter b by validator (2 and 0) + let dels = get_delegator(&staking, user_b.clone(), val1.clone()); + assert_eq!(dels, Some(del1b)); + let dels = get_delegator(&staking, user_b, val2.clone()); + assert_eq!(dels, None); + + // filter c by validator (0 and 1) + let dels = get_delegator(&staking, user_c.clone(), val1); + assert_eq!(dels, None); + let dels = get_delegator(&staking, user_c, val2); + assert_eq!(dels, Some(del2c)); + } + + #[test] + fn wasm_querier_works() { + let mut querier = WasmQuerier::default(); + + let any_addr = "foo".to_string(); + + // By default, querier errors for WasmQuery::Raw + let system_err = querier + .query(&WasmQuery::Raw { contract_addr: any_addr.clone(), key: b"the key".into() }) + .unwrap_err(); + match system_err { + SystemError::NoSuchContract { addr } => assert_eq!(addr, any_addr), + err => panic!("Unexpected error: {err:?}"), + } + + // By default, querier errors for WasmQuery::Smart + let system_err = querier + .query(&WasmQuery::Smart { contract_addr: any_addr.clone(), msg: b"{}".into() }) + .unwrap_err(); + match system_err { + SystemError::NoSuchContract { addr } => assert_eq!(addr, any_addr), + err => panic!("Unexpected error: {err:?}"), + } + + // By default, querier errors for WasmQuery::ContractInfo + let system_err = querier + .query(&WasmQuery::ContractInfo { contract_addr: any_addr.clone() }) + .unwrap_err(); + match system_err { + SystemError::NoSuchContract { addr } => assert_eq!(addr, any_addr), + err => panic!("Unexpected error: {err:?}"), + } + + #[cfg(feature = "cosmwasm_1_2")] + { + // By default, querier errors for WasmQuery::CodeInfo + let system_err = querier.query(&WasmQuery::CodeInfo { code_id: 4 }).unwrap_err(); + match system_err { + SystemError::NoSuchCode { code_id } => assert_eq!(code_id, 4), + err => panic!("Unexpected error: {err:?}"), + } + } + + querier.update_handler(|request| { + let api = MockApi::default(); + let contract1 = api.addr_make("contract1"); + let mut storage1 = BTreeMap::::default(); + storage1.insert(b"the key".into(), b"the value".into()); + + match request { + WasmQuery::Raw { contract_addr, key } => { + let Ok(addr) = api.addr_validate(contract_addr) else { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }); + }; + if addr == contract1 { + if let Some(value) = storage1.get(key) { + SystemResult::Ok(ContractResult::Ok(value.clone())) + } else { + SystemResult::Ok(ContractResult::Ok(Binary::default())) + } + } else { + SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + }, + WasmQuery::Smart { contract_addr, msg } => { + let Ok(addr) = api.addr_validate(contract_addr) else { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }); + }; + if addr == contract1 { + #[derive(Deserialize)] + struct MyMsg {} + let _msg: MyMsg = match from_json(msg) { + Ok(msg) => msg, + Err(err) => + return SystemResult::Ok(ContractResult::Err(err.to_string())), + }; + let response: Response = Response::new().set_data(b"good"); + SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) + } else { + SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + }, + WasmQuery::ContractInfo { contract_addr } => { + let Ok(addr) = api.addr_validate(contract_addr) else { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }); + }; + if addr == contract1 { + let response = ContractInfoResponse { + code_id: 4, + creator: Addr::unchecked("lalala"), + admin: None, + pinned: false, + ibc_port: None, + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) + } else { + SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + }, + #[cfg(feature = "cosmwasm_1_2")] + WasmQuery::CodeInfo { code_id } => { + use crate::{Checksum, CodeInfoResponse}; + let code_id = *code_id; + if code_id == 4 { + let response = CodeInfoResponse { + code_id, + creator: Addr::unchecked("lalala"), + checksum: Checksum::from_hex( + "84cf20810fd429caf58898c3210fcb71759a27becddae08dbde8668ea2f4725d", + ) + .unwrap(), + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) + } else { + SystemResult::Err(SystemError::NoSuchCode { code_id }) + } + }, + } + }); + + let contract_addr = MockApi::default().addr_make("contract1"); + + // WasmQuery::Raw + let result = querier.query(&WasmQuery::Raw { + contract_addr: contract_addr.clone().into(), + key: b"the key".into(), + }); + + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!(value, b"the value" as &[u8]), + res => panic!("Unexpected result: {res:?}"), + } + let result = querier.query(&WasmQuery::Raw { + contract_addr: contract_addr.clone().into(), + key: b"other key".into(), + }); + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!(value, b"" as &[u8]), + res => panic!("Unexpected result: {res:?}"), + } + + // WasmQuery::Smart + let result = querier.query(&WasmQuery::Smart { + contract_addr: contract_addr.clone().into(), + msg: b"{}".into(), + }); + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!( + value, + br#"{"messages":[],"attributes":[],"events":[],"data":"Z29vZA=="}"# as &[u8] + ), + res => panic!("Unexpected result: {res:?}"), + } + let result = querier.query(&WasmQuery::Smart { + contract_addr: contract_addr.clone().into(), + msg: b"a broken request".into(), + }); + match result { + SystemResult::Ok(ContractResult::Err(err)) => { + assert_eq!(err, "Error parsing into type cosmwasm_std::testing::mock::tests::wasm_querier_works::{{closure}}::MyMsg: Invalid type") + }, + res => panic!("Unexpected result: {res:?}"), + } + + // WasmQuery::ContractInfo + let result = + querier.query(&WasmQuery::ContractInfo { contract_addr: contract_addr.into() }); + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!( + value, + br#"{"code_id":4,"creator":"lalala","admin":null,"pinned":false,"ibc_port":null}"# + as &[u8] + ), + res => panic!("Unexpected result: {res:?}"), + } + + // WasmQuery::CodeInfo + #[cfg(feature = "cosmwasm_1_2")] + { + let result = querier.query(&WasmQuery::CodeInfo { code_id: 4 }); + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!( + value, + br#"{"code_id":4,"creator":"lalala","checksum":"84cf20810fd429caf58898c3210fcb71759a27becddae08dbde8668ea2f4725d"}"# + ), + res => panic!("Unexpected result: {res:?}"), + } + } + } + + #[test] + fn making_an_address_works() { + let mock_api = MockApi::default(); + + assert_eq!( + mock_api.addr_make("creator").to_string(), + "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp", + ); + + assert_eq!( + mock_api.addr_make("").to_string(), + "cosmwasm1uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sly4xh9", + ); + + let mock_api = MockApi::default().with_prefix("juno"); + assert_eq!( + mock_api.addr_make("creator").to_string(), + "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp", + ); + } + + #[test] + fn colon_in_prefix_is_valid() { + let mock_api = MockApi::default().with_prefix("did:com:"); + let addr = mock_api + .addr_validate("did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu") + .unwrap(); + + assert_eq!(addr.as_str(), "did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu"); + } + + #[test] + #[should_panic( + expected = "Generating address failed with reason: hrp is empty, must have at least 1 character" + )] + fn making_an_address_with_empty_prefix_should_panic() { + MockApi::default().with_prefix("").addr_make("creator"); + } + + #[test] + #[cfg(feature = "cosmwasm_1_3")] + fn distribution_querier_new_works() { + let addresses = [ + ("addr0000".to_string(), "addr0001".to_string()), + ("addr0002".to_string(), "addr0001".to_string()), + ]; + let btree_map = BTreeMap::from(addresses.clone()); + + // should still work with HashMap + let hashmap = std::collections::HashMap::from(addresses.clone()); + let querier = DistributionQuerier::new(hashmap); + assert_eq!(querier.withdraw_addresses, btree_map); + + // should work with BTreeMap + let querier = DistributionQuerier::new(btree_map.clone()); + assert_eq!(querier.withdraw_addresses, btree_map); + + // should work with array + let querier = DistributionQuerier::new(addresses); + assert_eq!(querier.withdraw_addresses, btree_map); + } + + #[test] + fn instantiate2_address_can_be_humanized() { + let mock_api = MockApi::default(); + + let contract_addr = + mock_api.addr_canonicalize(mock_api.addr_make("contract").as_str()).unwrap(); + let checksum = + HexBinary::from_hex("9af782a3a1bcbcd22dbb6a45c751551d9af782a3a1bcbcd22dbb6a45c751551d") + .unwrap(); + let salt = b"instance 1231"; + let canonical_addr = instantiate2_address(&checksum, &contract_addr, salt).unwrap(); + // we are not interested in the exact humanization, just that it works + mock_api.addr_humanize(&canonical_addr).unwrap(); + } +} diff --git a/cosmwasm/std/src/testing/mod.rs b/cosmwasm/std/src/testing/mod.rs new file mode 100644 index 00000000..29544d0a --- /dev/null +++ b/cosmwasm/std/src/testing/mod.rs @@ -0,0 +1,32 @@ +#![cfg(not(target_arch = "wasm32"))] + +// Exposed for testing only +// Both unit tests and integration tests are compiled to native code, so everything in here does not +// need to compile to Wasm. + +mod assertions; +mod message_info; +mod mock; + +pub use assertions::assert_approx_eq_impl; +#[cfg(test)] +pub use assertions::assert_hash_works_impl; + +pub use message_info::message_info; +#[allow(deprecated)] +pub use mock::mock_info; +#[cfg(feature = "cosmwasm_1_3")] +pub use mock::DistributionQuerier; +#[cfg(feature = "staking")] +pub use mock::StakingQuerier; +pub use mock::{ + mock_dependencies, mock_dependencies_with_balance, mock_dependencies_with_balances, mock_env, + mock_wasmd_attr, BankQuerier, MockApi, MockQuerier, MockQuerierCustomHandlerResult, + MockStorage, MOCK_CONTRACT_ADDR, +}; +#[cfg(feature = "stargate")] +pub use mock::{ + mock_ibc_channel, mock_ibc_channel_close_confirm, mock_ibc_channel_close_init, + mock_ibc_channel_connect_ack, mock_ibc_channel_connect_confirm, mock_ibc_channel_open_init, + mock_ibc_channel_open_try, mock_ibc_packet_ack, mock_ibc_packet_recv, mock_ibc_packet_timeout, +}; diff --git a/cosmwasm/std/src/timestamp.rs b/cosmwasm/std/src/timestamp.rs new file mode 100644 index 00000000..2974e3b6 --- /dev/null +++ b/cosmwasm/std/src/timestamp.rs @@ -0,0 +1,361 @@ +use core::fmt; +use serde::{Deserialize, Serialize}; + +use crate::Uint64; + +/// A point in time in nanosecond precision. +/// +/// This type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z. +/// +/// ## Examples +/// +/// ``` +/// # use cosmwasm_std::Timestamp; +/// let ts = Timestamp::from_nanos(1_000_000_202); +/// assert_eq!(ts.nanos(), 1_000_000_202); +/// assert_eq!(ts.seconds(), 1); +/// assert_eq!(ts.subsec_nanos(), 202); +/// +/// let ts = ts.plus_seconds(2); +/// assert_eq!(ts.nanos(), 3_000_000_202); +/// assert_eq!(ts.seconds(), 3); +/// assert_eq!(ts.subsec_nanos(), 202); +/// ``` +#[derive( + Serialize, + Deserialize, + Copy, + Clone, + Default, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + schemars::JsonSchema, +)] +pub struct Timestamp(Uint64); + +impl Timestamp { + /// Creates a timestamp from nanoseconds since epoch + pub const fn from_nanos(nanos_since_epoch: u64) -> Self { + Timestamp(Uint64::new(nanos_since_epoch)) + } + + /// Creates a timestamp from seconds since epoch + pub const fn from_seconds(seconds_since_epoch: u64) -> Self { + Timestamp(Uint64::new(seconds_since_epoch * 1_000_000_000)) + } + + /// Adds the given amount of days to the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result exceeds the value range of [`Timestamp`]. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn plus_days(&self, addition: u64) -> Timestamp { + self.plus_hours(addition * 24) + } + + /// Adds the given amount of hours to the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result exceeds the value range of [`Timestamp`]. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn plus_hours(&self, addition: u64) -> Timestamp { + self.plus_minutes(addition * 60) + } + + /// Adds the given amount of minutes to the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result exceeds the value range of [`Timestamp`]. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn plus_minutes(&self, addition: u64) -> Timestamp { + self.plus_seconds(addition * 60) + } + + /// Adds the given amount of seconds to the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result exceeds the value range of [`Timestamp`]. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn plus_seconds(&self, addition: u64) -> Timestamp { + self.plus_nanos(addition * 1_000_000_000) + } + + /// Adds the given amount of nanoseconds to the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result exceeds the value range of [`Timestamp`]. + #[must_use = "this returns the result of the operation, without modifying the original"] + // no #[inline] here as this could be shared with all the callers + pub const fn plus_nanos(&self, addition: u64) -> Timestamp { + let nanos = self.0.strict_add(Uint64::new(addition)); + Timestamp(nanos) + } + + /// Subtracts the given amount of days from the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result is not >= 0. I.e. times before epoch cannot be represented. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn minus_days(&self, subtrahend: u64) -> Timestamp { + self.minus_hours(subtrahend * 24) + } + + /// Subtracts the given amount of hours from the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result is not >= 0. I.e. times before epoch cannot be represented. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn minus_hours(&self, subtrahend: u64) -> Timestamp { + self.minus_minutes(subtrahend * 60) + } + + /// Subtracts the given amount of minutes from the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result is not >= 0. I.e. times before epoch cannot be represented. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn minus_minutes(&self, subtrahend: u64) -> Timestamp { + self.minus_seconds(subtrahend * 60) + } + + /// Subtracts the given amount of seconds from the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result is not >= 0. I.e. times before epoch cannot be represented. + #[must_use = "this returns the result of the operation, without modifying the original"] + #[inline] + pub const fn minus_seconds(&self, subtrahend: u64) -> Timestamp { + self.minus_nanos(subtrahend * 1_000_000_000) + } + + /// Subtracts the given amount of nanoseconds from the timestamp and + /// returns the result. The original value remains unchanged. + /// + /// Panics if the result is not >= 0. I.e. times before epoch cannot be represented. + #[must_use = "this returns the result of the operation, without modifying the original"] + // no #[inline] here as this could be shared with all the callers + pub const fn minus_nanos(&self, subtrahend: u64) -> Timestamp { + Timestamp(self.0.strict_sub(Uint64::new(subtrahend))) + } + + /// Returns nanoseconds since epoch + #[inline] + pub fn nanos(&self) -> u64 { + self.0.u64() + } + + /// Returns seconds since epoch (truncate nanoseconds) + #[inline] + pub fn seconds(&self) -> u64 { + self.0.u64() / 1_000_000_000 + } + + /// Returns nanoseconds since the last whole second (the remainder truncated + /// by `seconds()`) + #[inline] + pub fn subsec_nanos(&self) -> u64 { + self.0.u64() % 1_000_000_000 + } +} + +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let whole = self.seconds(); + let fractional = self.subsec_nanos(); + write!(f, "{whole}.{fractional:09}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn timestamp_from_nanos() { + let t = Timestamp::from_nanos(123); + assert_eq!(t.0.u64(), 123); + let t = Timestamp::from_nanos(0); + assert_eq!(t.0.u64(), 0); + } + + #[test] + fn timestamp_from_seconds() { + let t = Timestamp::from_seconds(123); + assert_eq!(t.0.u64(), 123_000_000_000); + let t = Timestamp::from_seconds(0); + assert_eq!(t.0.u64(), 0); + } + + #[test] + fn timestamp_plus_seconds() { + let sum = Timestamp::from_nanos(123).plus_seconds(42); + assert_eq!(sum.0.u64(), 42_000_000_123); + let sum = Timestamp::from_nanos(123).plus_seconds(0); + assert_eq!(sum.0.u64(), 123); + } + + #[test] + fn timestamp_plus_nanos() { + let sum = Timestamp::from_nanos(123).plus_nanos(3); + assert_eq!(sum.0.u64(), 126); + let sum = Timestamp::from_nanos(123).plus_nanos(0); + assert_eq!(sum.0.u64(), 123); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn timestamp_plus_nanos_panics_on_overflow() { + let max = Timestamp::from_nanos(u64::MAX); + let _earlier = max.plus_nanos(20); + } + + #[test] + fn timestamp_minus_seconds() { + let earlier = Timestamp::from_seconds(123).minus_seconds(0); + assert_eq!(earlier.0.u64(), 123_000_000_000); + let earlier = Timestamp::from_seconds(123).minus_seconds(3); + assert_eq!(earlier.0.u64(), 120_000_000_000); + let earlier = Timestamp::from_seconds(123).minus_seconds(123); + assert_eq!(earlier.0.u64(), 0); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn timestamp_minus_seconds_panics_on_overflow() { + let _earlier = Timestamp::from_seconds(100).minus_seconds(101); + } + + #[test] + fn timestamp_minus_nanos() { + let earlier = Timestamp::from_seconds(123).minus_nanos(0); + assert_eq!(earlier.0.u64(), 123_000_000_000); + let earlier = Timestamp::from_seconds(123).minus_nanos(3); + assert_eq!(earlier.0.u64(), 122_999_999_997); + let earlier = Timestamp::from_seconds(123).minus_nanos(123_000_000_000); + assert_eq!(earlier.0.u64(), 0); + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn timestamp_minus_nanos_panics_on_overflow() { + let _earlier = Timestamp::from_nanos(100).minus_nanos(101); + } + + #[test] + fn timestamp_plus_days() { + let ts = Timestamp::from_seconds(123).plus_days(0); + assert_eq!(ts.0.u64(), 123_000_000_000); + let ts = Timestamp::from_seconds(123).plus_days(10); + assert_eq!(ts.0.u64(), 864_123_000_000_000); + } + + #[test] + fn timestamp_minus_days() { + let ts = Timestamp::from_seconds(123).minus_days(0); + assert_eq!(ts.0.u64(), 123_000_000_000); + let ts = Timestamp::from_seconds(2 * 86400 + 123).minus_days(1); + assert_eq!(ts.0.u64(), 86_523_000_000_000); + let ts = Timestamp::from_seconds(86400).minus_days(1); + assert_eq!(ts.0.u64(), 0); + } + + #[test] + fn timestamp_plus_hours() { + let ts = Timestamp::from_seconds(123).plus_hours(0); + assert_eq!(ts.0.u64(), 123_000_000_000); + let ts = Timestamp::from_seconds(123).plus_hours(2); + assert_eq!(ts.0.u64(), 123_000_000_000 + 60 * 60 * 2 * 1_000_000_000); + } + + #[test] + fn timestamp_minus_hours() { + let ts = Timestamp::from_seconds(2 * 60 * 60).minus_hours(0); + assert_eq!(ts.0.u64(), 2 * 60 * 60 * 1_000_000_000); + let ts = Timestamp::from_seconds(2 * 60 * 60 + 123).minus_hours(1); + assert_eq!(ts.0.u64(), 60 * 60 * 1_000_000_000 + 123_000_000_000); + } + + #[test] + fn timestamp_plus_minutes() { + let ts = Timestamp::from_seconds(123).plus_minutes(0); + assert_eq!(ts.0.u64(), 123_000_000_000); + let ts = Timestamp::from_seconds(123).plus_minutes(2); + assert_eq!(ts.0.u64(), 123_000_000_000 + 60 * 2 * 1_000_000_000); + } + + #[test] + fn timestamp_minus_minutes() { + let ts = Timestamp::from_seconds(5 * 60).minus_minutes(0); + assert_eq!(ts.0.u64(), 5 * 60 * 1_000_000_000); + let ts = Timestamp::from_seconds(5 * 60 + 123).minus_minutes(1); + assert_eq!(ts.0.u64(), 4 * 60 * 1_000_000_000 + 123_000_000_000); + } + + #[test] + fn timestamp_nanos() { + let sum = Timestamp::from_nanos(123); + assert_eq!(sum.nanos(), 123); + let sum = Timestamp::from_nanos(0); + assert_eq!(sum.nanos(), 0); + let sum = Timestamp::from_nanos(987654321000); + assert_eq!(sum.nanos(), 987654321000); + } + + #[test] + fn timestamp_seconds() { + let sum = Timestamp::from_nanos(987654321000); + assert_eq!(sum.seconds(), 987); + let sum = Timestamp::from_seconds(1234567).plus_nanos(8765436); + assert_eq!(sum.seconds(), 1234567); + } + + #[test] + fn timestamp_subsec_nanos() { + let sum = Timestamp::from_nanos(987654321000); + assert_eq!(sum.subsec_nanos(), 654321000); + let sum = Timestamp::from_seconds(1234567).plus_nanos(8765436); + assert_eq!(sum.subsec_nanos(), 8765436); + } + + #[test] + fn timestamp_implements_display() { + let embedded = format!("Time: {}", Timestamp::from_nanos(0)); + assert_eq!(embedded, "Time: 0.000000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(1)); + assert_eq!(embedded, "Time: 0.000000001"); + let embedded = format!("Time: {}", Timestamp::from_nanos(10)); + assert_eq!(embedded, "Time: 0.000000010"); + let embedded = format!("Time: {}", Timestamp::from_nanos(100)); + assert_eq!(embedded, "Time: 0.000000100"); + let embedded = format!("Time: {}", Timestamp::from_nanos(1000)); + assert_eq!(embedded, "Time: 0.000001000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(10000)); + assert_eq!(embedded, "Time: 0.000010000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(100000)); + assert_eq!(embedded, "Time: 0.000100000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(1000000)); + assert_eq!(embedded, "Time: 0.001000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(1000000)); + assert_eq!(embedded, "Time: 0.001000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(10000000)); + assert_eq!(embedded, "Time: 0.010000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(100000000)); + assert_eq!(embedded, "Time: 0.100000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(1000000000)); + assert_eq!(embedded, "Time: 1.000000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(10000000000)); + assert_eq!(embedded, "Time: 10.000000000"); + let embedded = format!("Time: {}", Timestamp::from_nanos(100000000000)); + assert_eq!(embedded, "Time: 100.000000000"); + } +} diff --git a/cosmwasm/std/src/traits.rs b/cosmwasm/std/src/traits.rs new file mode 100644 index 00000000..7eafc7e7 --- /dev/null +++ b/cosmwasm/std/src/traits.rs @@ -0,0 +1,737 @@ +use core::{marker::PhantomData, ops::Deref}; +use serde::{de::DeserializeOwned, Serialize}; + +#[cfg(feature = "iterator")] +use crate::iterator::{Order, Record}; +#[cfg(feature = "cosmwasm_1_2")] +use crate::query::CodeInfoResponse; +#[cfg(feature = "cosmwasm_1_1")] +use crate::query::SupplyResponse; +#[cfg(feature = "staking")] +use crate::query::{ + AllDelegationsResponse, AllValidatorsResponse, BondedDenomResponse, Delegation, + DelegationResponse, FullDelegation, StakingQuery, Validator, ValidatorResponse, +}; +#[cfg(feature = "cosmwasm_1_3")] +use crate::query::{ + AllDenomMetadataResponse, DelegatorWithdrawAddressResponse, DenomMetadataResponse, + DistributionQuery, +}; +use crate::{ + coin::Coin, + from_json, + prelude::*, + query::{AllBalanceResponse, BalanceResponse, BankQuery, CustomQuery, QueryRequest, WasmQuery}, + results::{ContractResult, Empty, SystemResult}, + to_json_binary, to_json_vec, Addr, Binary, CanonicalAddr, ContractInfoResponse, + RecoverPubkeyError, StdError, StdResult, VerificationError, +}; +#[cfg(feature = "cosmwasm_1_3")] +use crate::{DenomMetadata, PageRequest}; + +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum HashFunction { + Sha256 = 0, +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for cosmwasm_crypto::HashFunction { + fn from(value: HashFunction) -> Self { + match value { + HashFunction::Sha256 => cosmwasm_crypto::HashFunction::Sha256, + } + } +} + +/// Storage provides read and write access to a persistent storage. +/// If you only want to provide read access, provide `&Storage` +pub trait Storage { + /// Returns None when key does not exist. + /// Returns Some(Vec) when key exists. + /// + /// Note: Support for differentiating between a non-existent key and a key with empty value + /// is not great yet and might not be possible in all backends. But we're trying to get there. + fn get(&self, key: &[u8]) -> Option>; + + /// Allows iteration over a set of key/value pairs, either forwards or backwards. + /// + /// The bound `start` is inclusive and `end` is exclusive. + /// If `start` is lexicographically greater than or equal to `end`, an empty range is described, + /// mo matter of the order. + #[cfg(feature = "iterator")] + #[allow(unused_variables)] + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + // This default implementation is just to avoid breaking code when enabling the `iterator` + // feature. Any actual `Storage` impl should override this method. + unimplemented!("This storage does not support ranging. Make sure to override the `range` method in your `Storage` implementation.") + } + + /// Allows iteration over a set of keys, either forwards or backwards. + /// + /// The bound `start` is inclusive and `end` is exclusive. + /// If `start` is lexicographically greater than or equal to `end`, an empty range is described, + /// mo matter of the order. + /// + /// The default implementation uses [`Storage::range`] and discards the values. More efficient + /// implementations might be possible depending on the storage. + #[cfg(feature = "iterator")] + fn range_keys<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + Box::new(self.range(start, end, order).map(|(k, _v)| k)) + } + + /// Allows iteration over a set of values, either forwards or backwards. + /// + /// The bound `start` is inclusive and `end` is exclusive. + /// If `start` is lexicographically greater than or equal to `end`, an empty range is described, + /// mo matter of the order. + /// + /// The default implementation uses [`Storage::range`] and discards the keys. More efficient + /// implementations might be possible depending on the storage. + #[cfg(feature = "iterator")] + fn range_values<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + Box::new(self.range(start, end, order).map(|(_k, v)| v)) + } + + fn set(&mut self, key: &[u8], value: &[u8]); + + /// Removes a database entry at `key`. + /// + /// The current interface does not allow to differentiate between a key that existed + /// before and one that didn't exist. See https://github.com/CosmWasm/cosmwasm/issues/290 + fn remove(&mut self, key: &[u8]); +} + +/// Api are callbacks to system functions implemented outside of the wasm modules. +/// Currently it just supports address conversion but we could add eg. crypto functions here. +/// +/// This is a trait to allow mocks in the test code. Its members have a read-only +/// reference to the Api instance to allow accessing configuration. +/// Implementations must not have mutable state, such that an instance can freely +/// be copied and shared between threads without affecting the behaviour. +/// Given an Api instance, all members should return the same value when called with the same +/// arguments. In particular this means the result must not depend in the state of the chain. +/// If you need to access chaim state, you probably want to use the Querier. +/// Side effects (such as logging) are allowed. +/// +/// We can use feature flags to opt-in to non-essential methods +/// for backwards compatibility in systems that don't have them all. +pub trait Api { + /// Takes a human readable address and validates if it is valid. + /// If it the validation succeeds, a `Addr` containing the same data as the input is returned. + /// + /// This validation checks two things: + /// 1. The address is valid in the sense that it can be converted to a canonical representation + /// by the backend. + /// 2. The address is normalized, i.e. `humanize(canonicalize(input)) == input`. + /// + /// Check #2 is typically needed for upper/lower case representations of the same + /// address that are both valid according to #1. This way we ensure uniqueness + /// of the human readable address. Clients should perform the normalization before sending + /// the addresses to the CosmWasm stack. But please note that the definition of normalized + /// depends on the backend. + /// + /// ## Examples + /// + /// ``` + /// # use cosmwasm_std::{Api, Addr}; + /// # use cosmwasm_std::testing::MockApi; + /// let api = MockApi::default().with_prefix("juno"); + /// let input = "juno1v82su97skv6ucfqvuvswe0t5fph7pfsrtraxf0x33d8ylj5qnrysdvkc95"; + /// let validated: Addr = api.addr_validate(input).unwrap(); + /// assert_eq!(validated.as_str(), input); + /// ``` + fn addr_validate(&self, human: &str) -> StdResult; + + /// Takes a human readable address and returns a canonical binary representation of it. + /// This can be used when a compact representation is needed. + /// + /// Please note that the length of the resulting address is defined by the chain and + /// can vary from address to address. On Cosmos chains 20 and 32 bytes are typically used. + /// But that might change. So your contract should not make assumptions on the size. + fn addr_canonicalize(&self, human: &str) -> StdResult; + + /// Takes a canonical address and returns a human readble address. + /// This is the inverse of [`addr_canonicalize`]. + /// + /// [`addr_canonicalize`]: Api::addr_canonicalize + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult; + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result; + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError>; + + #[allow(unused_variables)] + fn bls12_381_aggregate_g1(&self, g1s: &[u8]) -> Result<[u8; 48], VerificationError> { + // Support for BLS12-381 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + #[allow(unused_variables)] + fn bls12_381_aggregate_g2(&self, g2s: &[u8]) -> Result<[u8; 96], VerificationError> { + // Support for BLS12-381 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + /// Checks the following pairing equality: + /// + /// e(p_1, q_1) × e(p_2, q_2) × … × e(p_n, q_n) = e(s, q) + /// + /// The argument `ps` contain the points p_1, ..., p_n ∈ G1 as a concatenation of 48 byte + /// elements. The argument `qs` contain the points q_1, ..., q_n ∈ G2 as a concatenation of 96 + /// byte elements. + /// + /// ## Examples + /// + /// A simple signature check with one pairing on the left hand side (e(p, q) = e(s, q)): + /// + /// ``` + /// # use cosmwasm_std::{Api, HashFunction, StdResult}; + /// pub fn verify( + /// api: &dyn Api, + /// g1_generator: &[u8], + /// signature: &[u8], + /// pubkey: &[u8], + /// msg: &[u8], + /// dst: &[u8], + /// ) -> StdResult { + /// let msg_hashed = api.bls12_381_hash_to_g2(HashFunction::Sha256, msg, dst)?; + /// api.bls12_381_pairing_equality(g1_generator, signature, pubkey, &msg_hashed) + /// .map_err(Into::into) + /// } + /// ``` + #[allow(unused_variables)] + fn bls12_381_pairing_equality( + &self, + ps: &[u8], + qs: &[u8], + r: &[u8], + s: &[u8], + ) -> Result { + // Support for BLS12-381 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + #[allow(unused_variables)] + fn bls12_381_hash_to_g1( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 48], VerificationError> { + // Support for BLS12-381 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + #[allow(unused_variables)] + fn bls12_381_hash_to_g2( + &self, + hash_function: HashFunction, + msg: &[u8], + dst: &[u8], + ) -> Result<[u8; 96], VerificationError> { + // Support for BLS12-381 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + #[allow(unused_variables)] + fn secp256r1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + // Support for secp256r1 is added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + #[allow(unused_variables)] + fn secp256r1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + // Support for secp256r1 was added in 2.1, i.e. we can't add a compile time requirement for + // new function. Any implementation of the Api trait which does not implement this + // function but tries to call it will panic at runtime. We don't assume such cases exist. + // See also https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-default-item + unimplemented!() + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result; + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result; + + /// Emits a debugging message that is handled depending on the environment (typically printed to + /// console or ignored). Those messages are not persisted to chain. + fn debug(&self, message: &str); +} + +/// A short-hand alias for the two-level query result (1. accessing the contract, 2. executing query +/// in the contract) +pub type QuerierResult = SystemResult>; + +pub trait Querier { + /// raw_query is all that must be implemented for the Querier. + /// This allows us to pass through binary queries from one level to another without + /// knowing the custom format, or we can decode it, with the knowledge of the allowed + /// types. People using the querier probably want one of the simpler auto-generated + /// helper methods + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; +} + +#[derive(Clone)] +pub struct QuerierWrapper<'a, C: CustomQuery = Empty> { + querier: &'a dyn Querier, + custom_query_type: PhantomData, +} + +// Use custom implementation on order to implement Copy in case `C` is not `Copy`. +// See "There is a small difference between the two: the derive strategy will also +// place a Copy bound on type parameters, which isn’t always desired." +// https://doc.rust-lang.org/std/marker/trait.Copy.html +impl<'a, C: CustomQuery> Copy for QuerierWrapper<'a, C> {} + +/// This allows us to use self.raw_query to access the querier. +/// It also allows external callers to access the querier easily. +impl<'a, C: CustomQuery> Deref for QuerierWrapper<'a, C> { + type Target = dyn Querier + 'a; + + fn deref(&self) -> &Self::Target { + self.querier + } +} + +impl<'a, C: CustomQuery> QuerierWrapper<'a, C> { + pub fn new(querier: &'a dyn Querier) -> Self { + QuerierWrapper { querier, custom_query_type: PhantomData } + } + + /// This allows to convert any `QuerierWrapper` into a `QuerierWrapper` generic + /// over `Empty` custom query type. + pub fn into_empty(self) -> QuerierWrapper<'a, Empty> { + QuerierWrapper { querier: self.querier, custom_query_type: PhantomData } + } + + /// Makes the query and parses the response. + /// + /// Any error (System Error, Error or called contract, or Parse Error) are flattened into + /// one level. Only use this if you don't need to check the SystemError + /// eg. If you don't differentiate between contract missing and contract returned error + pub fn query(&self, request: &QueryRequest) -> StdResult { + self.query_raw(request).and_then(|raw| from_json(raw)) + } + + /// Internal helper to avoid code duplication. + /// Performs a query and returns the binary result without deserializing it, + /// wrapping any errors that may occur into `StdError`. + fn query_raw(&self, request: &QueryRequest) -> StdResult { + let raw = to_json_vec(request).map_err(|serialize_err| { + StdError::generic_err(format!("Serializing QueryRequest: {serialize_err}")) + })?; + match self.raw_query(&raw) { + SystemResult::Err(system_err) => + Err(StdError::generic_err(format!("Querier system error: {system_err}"))), + SystemResult::Ok(ContractResult::Err(contract_err)) => + Err(StdError::generic_err(format!("Querier contract error: {contract_err}"))), + SystemResult::Ok(ContractResult::Ok(value)) => Ok(value), + } + } + + #[cfg(feature = "cosmwasm_1_1")] + pub fn query_supply(&self, denom: impl Into) -> StdResult { + let request = BankQuery::Supply { denom: denom.into() }.into(); + let res: SupplyResponse = self.query(&request)?; + Ok(res.amount) + } + + pub fn query_balance( + &self, + address: impl Into, + denom: impl Into, + ) -> StdResult { + let request = BankQuery::Balance { address: address.into(), denom: denom.into() }.into(); + let res: BalanceResponse = self.query(&request)?; + Ok(res.amount) + } + + pub fn query_all_balances(&self, address: impl Into) -> StdResult> { + let request = BankQuery::AllBalances { address: address.into() }.into(); + let res: AllBalanceResponse = self.query(&request)?; + Ok(res.amount) + } + + #[cfg(feature = "cosmwasm_1_3")] + pub fn query_delegator_withdraw_address( + &self, + delegator: impl Into, + ) -> StdResult { + let request = + DistributionQuery::DelegatorWithdrawAddress { delegator_address: delegator.into() } + .into(); + let res: DelegatorWithdrawAddressResponse = self.query(&request)?; + Ok(res.withdraw_address) + } + + #[cfg(feature = "cosmwasm_1_3")] + pub fn query_denom_metadata(&self, denom: impl Into) -> StdResult { + let request = BankQuery::DenomMetadata { denom: denom.into() }.into(); + let res: DenomMetadataResponse = self.query(&request)?; + Ok(res.metadata) + } + + #[cfg(feature = "cosmwasm_1_3")] + pub fn query_all_denom_metadata( + &self, + pagination: PageRequest, + ) -> StdResult { + let request = BankQuery::AllDenomMetadata { pagination: Some(pagination) }.into(); + self.query(&request) + } + + #[cfg(feature = "cosmwasm_1_4")] + pub fn query_delegation_rewards( + &self, + delegator: impl Into, + validator: impl Into, + ) -> StdResult> { + use crate::DelegationRewardsResponse; + + let request = DistributionQuery::DelegationRewards { + delegator_address: delegator.into(), + validator_address: validator.into(), + } + .into(); + let DelegationRewardsResponse { rewards } = self.query(&request)?; + + Ok(rewards) + } + + #[cfg(feature = "cosmwasm_1_4")] + pub fn query_delegation_total_rewards( + &self, + delegator: impl Into, + ) -> StdResult { + let request = + DistributionQuery::DelegationTotalRewards { delegator_address: delegator.into() } + .into(); + self.query(&request) + } + + #[cfg(feature = "cosmwasm_1_4")] + pub fn query_delegator_validators( + &self, + delegator: impl Into, + ) -> StdResult> { + use crate::DelegatorValidatorsResponse; + + let request = + DistributionQuery::DelegatorValidators { delegator_address: delegator.into() }.into(); + let res: DelegatorValidatorsResponse = self.query(&request)?; + Ok(res.validators) + } + + /// See [`GrpcQuery`](crate::GrpcQuery) for more information. + #[cfg(feature = "cosmwasm_2_0")] + pub fn query_grpc(&self, path: String, data: Binary) -> StdResult { + use crate::GrpcQuery; + self.query_raw(&QueryRequest::Grpc(GrpcQuery { path, data })) + } + + /// Queries another wasm contract. You should know a priori the proper types for T and U + /// (response and request) based on the contract API + pub fn query_wasm_smart( + &self, + contract_addr: impl Into, + msg: &impl Serialize, + ) -> StdResult { + let request = + WasmQuery::Smart { contract_addr: contract_addr.into(), msg: to_json_binary(msg)? } + .into(); + self.query(&request) + } + + /// Queries the raw storage from another wasm contract. + /// + /// You must know the exact layout and are implementation dependent + /// (not tied to an interface like query_wasm_smart). + /// That said, if you are building a few contracts together, this is a much cheaper approach + /// + /// Similar return value to [`Storage::get`]. Returns `Some(val)` or `None` if the data is + /// there. It only returns error on some runtime issue, not on any data cases. + pub fn query_wasm_raw( + &self, + contract_addr: impl Into, + key: impl Into, + ) -> StdResult>> { + let request: QueryRequest = + WasmQuery::Raw { contract_addr: contract_addr.into(), key: key.into() }.into(); + // we cannot use query, as it will try to parse the binary data, when we just want to return + // it, so a bit of code copy here... + let raw = to_json_vec(&request).map_err(|serialize_err| { + StdError::generic_err(format!("Serializing QueryRequest: {serialize_err}")) + })?; + match self.raw_query(&raw) { + SystemResult::Err(system_err) => + Err(StdError::generic_err(format!("Querier system error: {system_err}"))), + SystemResult::Ok(ContractResult::Err(contract_err)) => + Err(StdError::generic_err(format!("Querier contract error: {contract_err}"))), + SystemResult::Ok(ContractResult::Ok(value)) => + if value.is_empty() { + Ok(None) + } else { + Ok(Some(value.into())) + }, + } + } + + /// Given a contract address, query information about that contract. + pub fn query_wasm_contract_info( + &self, + contract_addr: impl Into, + ) -> StdResult { + let request = WasmQuery::ContractInfo { contract_addr: contract_addr.into() }.into(); + self.query(&request) + } + + /// Given a code ID, query information about that code. + #[cfg(feature = "cosmwasm_1_2")] + pub fn query_wasm_code_info(&self, code_id: u64) -> StdResult { + let request = WasmQuery::CodeInfo { code_id }.into(); + self.query(&request) + } + + #[cfg(feature = "staking")] + pub fn query_all_validators(&self) -> StdResult> { + let request = StakingQuery::AllValidators {}.into(); + let res: AllValidatorsResponse = self.query(&request)?; + Ok(res.validators) + } + + #[cfg(feature = "staking")] + pub fn query_validator(&self, address: impl Into) -> StdResult> { + let request = StakingQuery::Validator { address: address.into() }.into(); + let res: ValidatorResponse = self.query(&request)?; + Ok(res.validator) + } + + #[cfg(feature = "staking")] + pub fn query_bonded_denom(&self) -> StdResult { + let request = StakingQuery::BondedDenom {}.into(); + let res: BondedDenomResponse = self.query(&request)?; + Ok(res.denom) + } + + #[cfg(feature = "staking")] + pub fn query_all_delegations( + &self, + delegator: impl Into, + ) -> StdResult> { + let request = StakingQuery::AllDelegations { delegator: delegator.into() }.into(); + let res: AllDelegationsResponse = self.query(&request)?; + Ok(res.delegations) + } + + #[cfg(feature = "staking")] + pub fn query_delegation( + &self, + delegator: impl Into, + validator: impl Into, + ) -> StdResult> { + let request = + StakingQuery::Delegation { delegator: delegator.into(), validator: validator.into() } + .into(); + let res: DelegationResponse = self.query(&request)?; + Ok(res.delegation) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::{coins, testing::MockQuerier, Uint128}; + + // this is a simple demo helper to prove we can use it + fn demo_helper(_querier: &dyn Querier) -> u64 { + 2 + } + + // this just needs to compile to prove we can use it + #[test] + fn use_querier_wrapper_as_querier() { + let querier: MockQuerier = MockQuerier::new(&[]); + let wrapper = QuerierWrapper::::new(&querier); + + // call with deref shortcut + let res = demo_helper(&*wrapper); + assert_eq!(2, res); + + // call with explicit deref + let res = demo_helper(wrapper.deref()); + assert_eq!(2, res); + } + + #[test] + fn auto_deref_raw_query() { + let acct = String::from("foobar"); + let querier: MockQuerier = MockQuerier::new(&[(&acct, &coins(5, "BTC"))]); + let wrapper = QuerierWrapper::::new(&querier); + let query = QueryRequest::::Bank(BankQuery::Balance { + address: acct, + denom: "BTC".to_string(), + }); + + let raw = wrapper.raw_query(&to_json_vec(&query).unwrap()).unwrap().unwrap(); + let balance: BalanceResponse = from_json(raw).unwrap(); + assert_eq!(balance.amount.amount, Uint128::new(5)); + } + + #[cfg(feature = "cosmwasm_1_1")] + #[test] + fn bank_query_helpers_work() { + use crate::coin; + + let querier: MockQuerier = MockQuerier::new(&[ + ("foo", &[coin(123, "ELF"), coin(777, "FLY")]), + ("bar", &[coin(321, "ELF")]), + ]); + let wrapper = QuerierWrapper::::new(&querier); + + let supply = wrapper.query_supply("ELF").unwrap(); + assert_eq!(supply, coin(444, "ELF")); + + let balance = wrapper.query_balance("foo", "ELF").unwrap(); + assert_eq!(balance, coin(123, "ELF")); + + let all_balances = wrapper.query_all_balances("foo").unwrap(); + assert_eq!(all_balances, vec![coin(123, "ELF"), coin(777, "FLY")]); + } + + #[test] + fn contract_info() { + const ACCT: &str = "foobar"; + fn mock_resp() -> ContractInfoResponse { + ContractInfoResponse { + code_id: 0, + creator: Addr::unchecked("creator"), + admin: None, + pinned: false, + ibc_port: None, + } + } + + let mut querier: MockQuerier = MockQuerier::new(&[(ACCT, &coins(5, "BTC"))]); + querier.update_wasm(|q| -> QuerierResult { + if q == &(WasmQuery::ContractInfo { contract_addr: ACCT.to_string() }) { + SystemResult::Ok(ContractResult::Ok(to_json_binary(&mock_resp()).unwrap())) + } else { + SystemResult::Err(crate::SystemError::NoSuchContract { addr: ACCT.to_string() }) + } + }); + let wrapper = QuerierWrapper::::new(&querier); + + let contract_info = wrapper.query_wasm_contract_info(ACCT).unwrap(); + assert_eq!(contract_info, mock_resp()); + } + + #[test] + fn contract_info_err() { + const ACCT: &str = "foobar"; + fn mock_resp() -> ContractInfoResponse { + ContractInfoResponse { + code_id: 0, + creator: Addr::unchecked("creator"), + admin: None, + pinned: false, + ibc_port: None, + } + } + + let mut querier: MockQuerier = MockQuerier::new(&[(ACCT, &coins(5, "BTC"))]); + querier.update_wasm(|q| -> QuerierResult { + if q == &(WasmQuery::ContractInfo { contract_addr: ACCT.to_string() }) { + SystemResult::Ok(ContractResult::Ok(to_json_binary(&mock_resp()).unwrap())) + } else { + SystemResult::Err(crate::SystemError::NoSuchContract { addr: ACCT.to_string() }) + } + }); + let wrapper = QuerierWrapper::::new(&querier); + + let err = wrapper.query_wasm_contract_info("unknown").unwrap_err(); + assert!(matches!( + err, + StdError::GenericErr { + msg, + .. + } if msg == "Querier system error: No such contract: foobar" + )); + } + + #[test] + fn querier_into_empty() { + #[derive(Clone, Serialize, Deserialize)] + struct MyQuery; + impl CustomQuery for MyQuery {} + + let querier: MockQuerier = MockQuerier::new(&[]); + let wrapper = QuerierWrapper::::new(&querier); + + let _: QuerierWrapper = wrapper.into_empty(); + } +} diff --git a/cosmwasm/std/src/types.rs b/cosmwasm/std/src/types.rs new file mode 100644 index 00000000..cb89e7d0 --- /dev/null +++ b/cosmwasm/std/src/types.rs @@ -0,0 +1,110 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{coin::Coin, prelude::*, Addr, Timestamp}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Env { + pub block: BlockInfo, + /// Information on the transaction this message was executed in. + /// The field is unset when the + /// `MsgExecuteContract`/`MsgInstantiateContract`/`MsgMigrateContract` is not executed as part + /// of a transaction. + pub transaction: Option, + pub contract: ContractInfo, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct TransactionInfo { + /// The position of this transaction in the block. The first + /// transaction has index 0. + /// + /// This allows you to get a unique transaction indentifier in this chain + /// using the pair (`env.block.height`, `env.transaction.index`). + pub index: u32, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct BlockInfo { + /// The height of a block is the number of blocks preceding it in the blockchain. + pub height: u64, + /// Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 + /// UTC). + /// + /// The source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), + /// which has the same nanosecond precision as the `Timestamp` type. + /// + /// # Examples + /// + /// Using chrono: + /// + /// ``` + /// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; + /// # let env = Env { + /// # block: BlockInfo { + /// # height: 12_345, + /// # time: Timestamp::from_nanos(1_571_797_419_879_305_533), + /// # chain_id: "cosmos-testnet-14002".to_string(), + /// # }, + /// # transaction: Some(TransactionInfo { index: 3 }), + /// # contract: ContractInfo { + /// # address: Addr::unchecked("contract"), + /// # }, + /// # }; + /// # extern crate chrono; + /// use chrono::NaiveDateTime; + /// let seconds = env.block.time.seconds(); + /// let nsecs = env.block.time.subsec_nanos(); + /// let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); + /// ``` + /// + /// Creating a simple millisecond-precision timestamp (as used in JavaScript): + /// + /// ``` + /// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; + /// # let env = Env { + /// # block: BlockInfo { + /// # height: 12_345, + /// # time: Timestamp::from_nanos(1_571_797_419_879_305_533), + /// # chain_id: "cosmos-testnet-14002".to_string(), + /// # }, + /// # transaction: Some(TransactionInfo { index: 3 }), + /// # contract: ContractInfo { + /// # address: Addr::unchecked("contract"), + /// # }, + /// # }; + /// let millis = env.block.time.nanos() / 1_000_000; + /// ``` + pub time: Timestamp, + pub chain_id: String, +} + +/// Additional information from [MsgInstantiateContract] and [MsgExecuteContract], which is passed +/// along with the contract execution message into the `instantiate` and `execute` entry points. +/// +/// It contains the essential info for authorization - identity of the call, and payment. +/// +/// [MsgInstantiateContract]: https://github.com/CosmWasm/wasmd/blob/v0.15.0/x/wasm/internal/types/tx.proto#L47-L61 +/// [MsgExecuteContract]: https://github.com/CosmWasm/wasmd/blob/v0.15.0/x/wasm/internal/types/tx.proto#L68-L78 +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct MessageInfo { + /// The `sender` field from `MsgInstantiateContract` and `MsgExecuteContract`. + /// You can think of this as the address that initiated the action (i.e. the message). What + /// that means exactly heavily depends on the application. + /// + /// The x/wasm module ensures that the sender address signed the transaction or + /// is otherwise authorized to send the message. + /// + /// Additional signers of the transaction that are either needed for other messages or contain + /// unnecessary signatures are not propagated into the contract. + pub sender: Addr, + /// The funds that are sent to the contract as part of `MsgInstantiateContract` + /// or `MsgExecuteContract`. The transfer is processed in bank before the contract + /// is executed such that the new balance is visible during contract execution. + pub funds: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct ContractInfo { + pub address: Addr, +} diff --git a/cosmwasm/std/testdata/instantiate2_addresses.json b/cosmwasm/std/testdata/instantiate2_addresses.json new file mode 100644 index 00000000..a12ad695 --- /dev/null +++ b/cosmwasm/std/testdata/instantiate2_addresses.json @@ -0,0 +1,386 @@ +[ + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000", + "addressData": "5e865d3e45ad3e961f77fd77d46543417ced44d924dc3e079b5415ff6775f847" + }, + "out": { + "address": "purple1t6r960j945lfv8mhl4mage2rg97w63xeynwrupum2s2l7em4lprs9ce5hk" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d", + "addressData": "0995499608947a5281e2c7ebd71bdb26a1ad981946dad57f6c4d3ee35de77835" + }, + "out": { + "address": "purple1px25n9sgj3a99q0zcl4awx7my6s6mxqegmdd2lmvf5lwxh080q6suttktr" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "83326e554723b15bac664ceabc8a5887e27003abe9fbd992af8c7bcea4745167" + }, + "out": { + "address": "purple1svexu428ywc4htrxfn4tezjcsl38qqata8aany4033auafr529ns4v254c" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "9384c6248c0bb171e306fd7da0993ec1e20eba006452a3a9e078883eb3594564" + }, + "out": { + "address": "purple1jwzvvfyvpwchrccxl476pxf7c83qawsqv3f2820q0zyrav6eg4jqdcq7gc" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "9a8d5f98fb186825401a26206158e7a1213311a9b6a87944469913655af52ffb" + }, + "out": { + "address": "purple1n2x4lx8mrp5z2sq6ycsxzk885ysnxydfk658j3zxnyfk2kh49lasesxf6j" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "932f07bc53f7d0b0b43cb5a54ac3e245b205e6ae6f7c1d991dc6af4a2ff9ac18" + }, + "out": { + "address": "purple1jvhs00zn7lgtpdpukkj54slzgkeqte4wda7pmxgac6h55tle4svq8cmp60" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000", + "addressData": "9725e94f528d8b78d33c25f3dfcd60e6142d8be60ab36f6a5b59036fd51560db" + }, + "out": { + "address": "purple1juj7jn6j3k9h35euyhealntquc2zmzlxp2ek76jmtypkl4g4vrdsfwmwxk" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d", + "addressData": "b056e539bbaf447ba18f3f13b792970111fc78933eb6700f4d227b5216d63658" + }, + "out": { + "address": "purple1kptw2wdm4az8hgv08ufm0y5hqyglc7yn86m8qr6dyfa4y9kkxevqmkm9q3" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "6c98434180f052294ff89fb6d2dae34f9f4468b0b8e6e7c002b2a44adee39abd" + }, + "out": { + "address": "purple1djvyxsvq7pfzjnlcn7md9khrf705g69shrnw0sqzk2jy4hhrn27sjh2ysy" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "0aaf1c31c5d529d21d898775bc35b3416f47bfd99188c334c6c716102cbd3101" + }, + "out": { + "address": "purple1p2h3cvw9655ay8vfsa6mcddng9h5007ejxyvxdxxcutpqt9axyqsagmmay" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "36fe6ab732187cdfed46290b448b32eb7f4798e7a4968b0537de8a842cbf030e" + }, + "out": { + "address": "purple1xmlx4dejrp7dlm2x9y95fzejadl50x885jtgkpfhm69ggt9lqv8qk3vn4f" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "a0d0c942adac6f3e5e7c23116c4c42a24e96e0ab75f53690ec2d3de16067c751" + }, + "out": { + "address": "purple15rgvjs4d43hnuhnuyvgkcnzz5f8fdc9twh6ndy8v9577zcr8cags40l9dt" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000", + "addressData": "b95c467218d408a0f93046f227b6ece7fe18133ff30113db4d2a7becdfeca141" + }, + "out": { + "address": "purple1h9wyvusc6sy2p7fsgmez0dhvullpsyel7vq38k6d9fa7ehlv59qsvnyh36" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d", + "addressData": "23fe45dbbd45dc6cd25244a74b6e99e7a65bf0bac2f2842a05049d37555a3ae6" + }, + "out": { + "address": "purple1y0lytkaaghwxe5jjgjn5km5eu7n9hu96ctegg2s9qjwnw4268tnqxhg60a" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "6faea261ed63baa65b05726269e83b217fa6205dc7d9fb74f9667d004a69c082" + }, + "out": { + "address": "purple1d7h2yc0dvwa2vkc9wf3xn6pmy9l6vgzaclvlka8eve7sqjnfczpqqsdnwu" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "67a3ab6384729925fdb144574628ce96836fe098d2c6be4e84ac106b2728d96c" + }, + "out": { + "address": "purple1v736kcuyw2vjtld3g3t5v2xwj6pklcyc6trtun5y4sgxkfegm9kq7vgpnt" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "23a121263bfce05c144f4af86f3d8a9f87dc56f9dc48dbcffc8c5a614da4c661" + }, + "out": { + "address": "purple1ywsjzf3mlns9c9z0ftux70v2n7rac4hem3ydhnlu33dxzndycesssc7x2m" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "dd90dba6d6fcd5fb9c9c8f536314eb1bb29cb5aa084b633c5806b926a5636b58" + }, + "out": { + "address": "purple1mkgdhfkkln2lh8yu3afkx98trwefedd2pp9kx0zcq6ujdftrddvq50esay" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000", + "addressData": "547a743022f4f1af05b102f57bf1c1c7d7ee81bae427dc20d36b2c4ec57612ae" + }, + "out": { + "address": "purple123a8gvpz7nc67pd3qt6hhuwpclt7aqd6usnacgxndvkya3tkz2hq5hz38f" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d", + "addressData": "416e169110e4b411bc53162d7503b2bbf14d6b36b1413a4f4c9af622696e9665" + }, + "out": { + "address": "purple1g9hpdygsuj6pr0znzckh2qajh0c566ekk9qn5n6vntmzy6twjejsrl9alk" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "619a0988b92d8796cea91dea63cbb1f1aefa4a6b6ee5c5d1e937007252697220" + }, + "out": { + "address": "purple1vxdqnz9e9kredn4frh4x8ja37xh05jntdmjut50fxuq8y5nfwgsquu9mxh" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "d8af856a6a04852d19b647ad6d4336eb26e077f740aef1a0331db34d299a885a" + }, + "out": { + "address": "purple1mzhc26n2qjzj6xdkg7kk6sekavnwqalhgzh0rgpnrke562v63pdq8grp8q" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "c7fb7bea96daab23e416c4fcf328215303005e1d0d5424257335568e5381e33c" + }, + "out": { + "address": "purple1clahh65km24j8eqkcn70x2pp2vpsqhsap42zgftnx4tgu5upuv7q9ywjws" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "ccdf9dea141a6c2475870529ab38fae9dec30df28e005894fe6578b66133ab4a" + }, + "out": { + "address": "purple1en0em6s5rfkzgav8q556kw86a80vxr0j3cq93987v4utvcfn4d9q0tql4w" + } + } +] From 24fa5e76e2c68cc0a1bfb4adb070db9020990c67 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 23:09:21 +0900 Subject: [PATCH 017/133] feat: Add composable-support --- Cargo.toml | 2 + composable/composable-support/Cargo.toml | 30 +++ .../src/abstractions/counter/mod.rs | 215 ++++++++++++++++++ .../src/abstractions/mod.rs | 4 + .../src/abstractions/nonce/mod.rs | 143 ++++++++++++ .../src/abstractions/utils/decrement.rs | 105 +++++++++ .../src/abstractions/utils/increment.rs | 132 +++++++++++ .../src/abstractions/utils/mod.rs | 87 +++++++ .../src/abstractions/utils/start_at.rs | 46 ++++ composable/composable-support/src/lib.rs | 19 ++ composable/composable-support/src/math/mod.rs | 2 + .../composable-support/src/math/safe.rs | 76 +++++++ .../src/math/wrapping_next.rs | 19 ++ 13 files changed, 880 insertions(+) create mode 100644 composable/composable-support/Cargo.toml create mode 100644 composable/composable-support/src/abstractions/counter/mod.rs create mode 100644 composable/composable-support/src/abstractions/mod.rs create mode 100644 composable/composable-support/src/abstractions/nonce/mod.rs create mode 100644 composable/composable-support/src/abstractions/utils/decrement.rs create mode 100644 composable/composable-support/src/abstractions/utils/increment.rs create mode 100644 composable/composable-support/src/abstractions/utils/mod.rs create mode 100644 composable/composable-support/src/abstractions/utils/start_at.rs create mode 100644 composable/composable-support/src/lib.rs create mode 100644 composable/composable-support/src/math/mod.rs create mode 100644 composable/composable-support/src/math/safe.rs create mode 100644 composable/composable-support/src/math/wrapping_next.rs diff --git a/Cargo.toml b/Cargo.toml index 91bf1ff5..432d4397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "core-primitives", + "composable/composable-support", "cosmwasm/std", "frame/babel", "frame/cosmos/types", @@ -21,6 +22,7 @@ members = [ ] [workspace.dependencies] +composable-support = { path = "composable/composable-support", default-features = false } cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } cosmwasm-std = { path = "cosmwasm/std", default-features = false } diff --git a/composable/composable-support/Cargo.toml b/composable/composable-support/Cargo.toml new file mode 100644 index 00000000..60c496c3 --- /dev/null +++ b/composable/composable-support/Cargo.toml @@ -0,0 +1,30 @@ +[package] +authors = ["Composable Developers"] +edition = "2021" +homepage = "https://composable.finance" +name = "composable-support" +rust-version = "1.56" +version = "1.0.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { version = "3.6.0", package = "parity-scale-codec", default-features = false, features = [ + "derive", +] } +num-traits = { version = "0.2.14", default-features = false } + +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "num-traits/std", + "frame-support/std", + "sp-arithmetic/std", + "sp-runtime/std", +] diff --git a/composable/composable-support/src/abstractions/counter/mod.rs b/composable/composable-support/src/abstractions/counter/mod.rs new file mode 100644 index 00000000..7df5f0c5 --- /dev/null +++ b/composable/composable-support/src/abstractions/counter/mod.rs @@ -0,0 +1,215 @@ +use crate::abstractions::utils::{ + decrement::{Decrement, Decrementor}, + increment::{Increment, Incrementor}, + start_at::StartAtValue, + ValueQuery, +}; + +use codec::FullCodec; +use core::marker::PhantomData; +use frame_support::{ + pallet_prelude::StorageValue, + traits::{Get, StorageInstance}, +}; + +/// Defines what a counter should start at and how it should be incremented/ decremented. +/// +/// # Example +/// +/// A counter that starts at zero, incrementing using [`SafeNext`] and decrementing using +/// [`SafePrevious`]: +/// +/// ```rust,ignore +/// #[pallet::storage] +/// pub type SomeCounter = StorageValue< +/// _, +/// T::Something, +/// ValueQuery, +/// Counter +/// >; +/// ``` +/// +/// Increment the nonce in an extrinsic: +/// +/// ```rust,ignore +/// #[pallet::call] +/// impl Pallet { +/// pub fn extrinsic( +/// origin: OriginFor, +/// ) -> DispatchResultWithPostInfo { +/// let nonce_next = SomeNonce::increment()?; +/// } +/// } +/// ``` +pub struct Counter { + #[doc(hidden)] + _marker: PhantomData<(S, I, D)>, +} + +impl Get for Counter +where + T: 'static, + S: StartAtValue, + I: Incrementor, +{ + fn get() -> T { + S::value() + } +} + +// NOTE: Once chalk gets integrated and this limitation is fixed, this trait can be removed: +// https://github.com/rust-lang/rust/issues/20400 +// https://stackoverflow.com/questions/40392524/conflicting-trait-implementations-even-though-associated-types-differ/40408431#40408431use +pub trait CounterHelperTrait: 'static +where + T: FullCodec + Clone + Copy + 'static, + I: Incrementor, + D: Decrementor, +{ + type IOutput; + + type DOutput; + + fn increment_inner() -> Self::IOutput; + + fn decrement_inner() -> Self::DOutput; +} + +#[allow(clippy::disallowed_types)] +impl CounterHelperTrait + for (StorageValue>, T, T, S) +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + D: Decrementor, + Counter: Get, +{ + type IOutput = I::Output; + type DOutput = D::Output; + + fn increment_inner() -> Self::IOutput { + #[allow(clippy::disallowed_types)] + StorageValue::>::mutate(|x| { + let new_x = I::increment(*x); + *x = new_x; + new_x + }) + } + + fn decrement_inner() -> Self::DOutput { + #[allow(clippy::disallowed_types)] + StorageValue::>::mutate(|x| { + let new_x = D::decrement(*x); + *x = new_x; + new_x + }) + } +} + +#[allow(clippy::disallowed_types)] +impl CounterHelperTrait + for ( + StorageValue>, + Result, + Result, + S, + ) +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor>, + D: Decrementor>, + Counter: Get, + IncrementErr: 'static, + DecrementErr: 'static, +{ + type IOutput = I::Output; + type DOutput = D::Output; + + fn increment_inner() -> Self::IOutput { + #[allow(clippy::disallowed_types)] + StorageValue::>::try_mutate( + |x| -> Result { + match I::increment(*x) { + Ok(new_x) => { + *x = new_x; + Ok(new_x) + }, + Err(err) => Err(err), + } + }, + ) + } + + fn decrement_inner() -> Self::DOutput { + #[allow(clippy::disallowed_types)] + StorageValue::>::try_mutate( + |x| -> Result { + match D::decrement(*x) { + Ok(new_x) => { + *x = new_x; + Ok(new_x) + }, + Err(err) => Err(err), + } + }, + ) + } +} + +#[allow(clippy::disallowed_types)] +impl Increment for StorageValue> +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + D: Decrementor, + (StorageValue>, I::Output, D::Output, S): + CounterHelperTrait, +{ + type Output = + <(StorageValue>, I::Output, D::Output, S) as CounterHelperTrait< + T, + I, + D, + >>::IOutput; + + fn increment() -> Self::Output { + <(StorageValue>, I::Output, D::Output, S) as CounterHelperTrait< + T, + I, + D, + >>::increment_inner() + } +} + +#[allow(clippy::disallowed_types)] +impl Decrement for StorageValue> +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + D: Decrementor, + (StorageValue>, I::Output, D::Output, S): + CounterHelperTrait, +{ + type Output = + <(StorageValue>, I::Output, D::Output, S) as CounterHelperTrait< + T, + I, + D, + >>::DOutput; + + fn decrement() -> Self::Output { + <(StorageValue>, I::Output, D::Output, S) as CounterHelperTrait< + T, + I, + D, + >>::decrement_inner() + } +} diff --git a/composable/composable-support/src/abstractions/mod.rs b/composable/composable-support/src/abstractions/mod.rs new file mode 100644 index 00000000..c5aec39a --- /dev/null +++ b/composable/composable-support/src/abstractions/mod.rs @@ -0,0 +1,4 @@ +pub mod counter; +pub mod nonce; + +pub mod utils; diff --git a/composable/composable-support/src/abstractions/nonce/mod.rs b/composable/composable-support/src/abstractions/nonce/mod.rs new file mode 100644 index 00000000..a7192a71 --- /dev/null +++ b/composable/composable-support/src/abstractions/nonce/mod.rs @@ -0,0 +1,143 @@ +use crate::abstractions::utils::{ + increment::{Increment, Incrementor}, + start_at::StartAtValue, + ValueQuery, +}; + +use codec::FullCodec; +use core::marker::PhantomData; +use frame_support::{ + pallet_prelude::StorageValue, + traits::{Get, StorageInstance}, +}; + +/// Defines what a nonce should start at and how it should be incremented. +/// +/// # Example +/// +/// A nonce that starts at zero, incrementing using [`SafeAdd`]: +/// +/// ```rust,ignore +/// #[pallet::storage] +/// pub type SomeNonce = StorageValue< +/// _, +/// T::Something, +/// ValueQuery, +/// Nonce +/// >; +/// ``` +/// +/// Increment the nonce in an extrinsic: +/// +/// ```rust,ignore +/// #[pallet::call] +/// impl Pallet { +/// pub fn extrinsic( +/// origin: OriginFor, +/// ) -> DispatchResultWithPostInfo { +/// // notice the double ?; since SafeNext is fallible, increment() is also fallible +/// let nonce_next = SomeNonce::increment()??; +/// } +/// } +/// ``` +pub struct Nonce { + #[doc(hidden)] + _marker: PhantomData<(S, I)>, +} + +impl Get for Nonce +where + T: 'static, + S: StartAtValue, + I: Incrementor, +{ + fn get() -> T { + S::value() + } +} + +// NOTE: Once chalk gets integrated and this limitation is fixed, this trait can be removed: +// https://github.com/rust-lang/rust/issues/20400 +// https://stackoverflow.com/questions/40392524/conflicting-trait-implementations-even-though-associated-types-differ/40408431#40408431use +pub trait NonceHelperTrait: 'static +where + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, +{ + type Output; + + fn increment_inner() -> Self::Output; +} + +#[allow(clippy::disallowed_types)] +impl NonceHelperTrait for (StorageValue>, T, S) +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + Nonce: Get, +{ + type Output = I::Output; + + fn increment_inner() -> Self::Output { + #[allow(clippy::disallowed_types)] + StorageValue::>::mutate(|x| { + let new_x = I::increment(*x); + *x = new_x; + new_x + }) + } +} + +#[allow(clippy::disallowed_types)] +impl NonceHelperTrait + for (StorageValue>, Result, S) +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor>, + Nonce: Get, + IncrementErr: 'static, +{ + type Output = I::Output; + + fn increment_inner() -> Self::Output { + #[allow(clippy::disallowed_types)] + StorageValue::>::try_mutate(|x| -> Result { + match I::increment(*x) { + Ok(new_x) => { + *x = new_x; + Ok(new_x) + }, + Err(err) => Err(err), + } + }) + } +} + +impl Increment for StorageValue> +where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + (StorageValue>, I::Output, S): NonceHelperTrait, +{ + type Output = + <(StorageValue>, I::Output, S) as NonceHelperTrait< + T, + S, + I, + >>::Output; + + fn increment() -> Self::Output { + <(StorageValue>, I::Output, S) as NonceHelperTrait< + T, + S, + I, + >>::increment_inner() + } +} diff --git a/composable/composable-support/src/abstractions/utils/decrement.rs b/composable/composable-support/src/abstractions/utils/decrement.rs new file mode 100644 index 00000000..41629273 --- /dev/null +++ b/composable/composable-support/src/abstractions/utils/decrement.rs @@ -0,0 +1,105 @@ +use crate::{ + abstractions::utils::{decrement::sealed::Sealed, start_at::StartAtValue}, + math::safe::SafeSub, +}; + +use codec::FullCodec; +use core::fmt::Debug; +use sp_runtime::{traits::One, ArithmeticError}; + +/// An extension trait for [`StorageValue`]s that can be decreased. +pub trait Decrement: Sealed + 'static +where + T: FullCodec + Clone + Copy + 'static, + D: Decrementor, +{ + /// See [`Decrementor::Output`]. + type Output; + + /// Decrement the inner value. + fn decrement() -> Self::Output; +} + +/// Something that can decrement a value. +pub trait Decrementor: Sealed + 'static { + /// The result of decrementing the provided value `T`. + /// + /// Since decrementing a value is potentially a fallible operation, the return type of + /// [`Self::decrement`] is *not* just `T`; allowing for returning a Result, Option, or even a + /// completely new type. + type Output; + + fn decrement(value: T) -> Self::Output; +} + +/// An [`Decrementor`] that uses [`SafeSub`] to produce the next value. +pub struct SafeDecrement; + +impl Decrementor for SafeDecrement +where + T: Debug + SafeSub + One + 'static, +{ + type Output = Result; + + fn decrement(value: T) -> Self::Output { + value.safe_sub(&T::one()) + } +} + +// I'm not sure if this decrementer makes sense, or how it would be used in practice. Leaving it +// here in case we need/ want it. /// An [`Decrementor`] that decrements down to a minimum value. +// pub struct DecrementToMin { +// #[doc(hidden)] +// _marker: PhantomData<(Min, MinError, PalletError)>, +// } + +// impl Decrementor for DecrementToMin +// where +// T: Debug + Sub + One + PartialOrd + 'static, +// Min: Get + 'static, +// MinError: Debug + Default + Into + 'static, +// PalletError: 'static, +// { +// type Output = Result; + +// /// [`Sub`] is used safely here since `M::get()` must be `>` the lower limit for `T`. +// fn decrement(value: T) -> Self::Output { +// if value == Min::get() { +// Err(MinError::default()) +// } else { +// Ok(value.sub(T::one())) +// } +// } +// } + +mod sealed { + use frame_support::{pallet_prelude::StorageValue, traits::StorageInstance}; + + use crate::abstractions::{ + counter::{Counter, CounterHelperTrait}, + utils::{increment::Incrementor, ValueQuery}, + }; + + use super::*; + + /// Sealing trait for [`Decrement`][super::Decrement] and [`Decrementor`][super::Decrementor]. + /// If you want to add a new implementor, be sure to add it here and ensure it's tested. + pub trait Sealed {} + + impl Sealed for StorageValue> + where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + D: Decrementor, + (StorageValue>, I::Output, D::Output, S): + CounterHelperTrait, + { + } + + impl Sealed for SafeDecrement {} + + // See comment on commented-out DecrementToMin + // impl Sealed for DecrementToMin {} +} diff --git a/composable/composable-support/src/abstractions/utils/increment.rs b/composable/composable-support/src/abstractions/utils/increment.rs new file mode 100644 index 00000000..133a1063 --- /dev/null +++ b/composable/composable-support/src/abstractions/utils/increment.rs @@ -0,0 +1,132 @@ +use crate::{ + abstractions::utils::{increment::sealed::Sealed, start_at::StartAtValue}, + math::{safe::SafeAdd, wrapping_next::WrappingNext}, +}; + +use codec::FullCodec; +use core::{fmt::Debug, marker::PhantomData, ops::Add}; +use frame_support::pallet_prelude::Get; +use sp_runtime::{traits::One, ArithmeticError}; + +/// An extension trait for [`StorageValue`]s that are used as a [nonce](nonce). +/// +/// [nonce]: +pub trait Increment: Sealed + 'static +where + T: FullCodec + Clone + Copy + 'static, + I: Incrementor, +{ + /// See [`Incrementor::Output`]. + type Output; + + /// Increment the inner value. + fn increment() -> Self::Output; +} + +/// Something that can increment a value. +pub trait Incrementor: Sealed + 'static { + /// The result of incrementing the provided value `T`. + /// + /// Since incrementing a value is potentially a fallible operation, the return type of + /// [`Self::increment`] is *not* just `T`; allowing for returning a Result, Option, or even a + /// completely new type. + type Output; + + fn increment(value: T) -> Self::Output; +} + +/// An [`Incrementor`] that uses [`WrappingNext`] to produce the next value. +pub struct WrappingIncrement; + +impl Incrementor for WrappingIncrement +where + T: Debug + WrappingNext + 'static, +{ + type Output = T; + + fn increment(value: T) -> Self::Output { + value.next() + } +} + +/// An [`Incrementor`] that uses [`SafeAdd`] to produce the next value. +pub struct SafeIncrement; + +impl Incrementor for SafeIncrement +where + T: Debug + SafeAdd + One + 'static, +{ + type Output = Result; + + fn increment(value: T) -> Self::Output { + value.safe_add(&T::one()) + } +} + +/// An [`Incrementor`] that increments up to a maximum value. +pub struct IncrementToMax { + #[doc(hidden)] + _marker: PhantomData<(Max, MaxError, PalletError)>, +} + +impl Incrementor for IncrementToMax +where + T: Debug + Add + One + PartialOrd + 'static, + Max: Get + 'static, + MaxError: Debug + Default + Into + 'static, + PalletError: 'static, +{ + type Output = Result; + + /// [`Add`] is used safely here since `M::get()` must be `<=` the upper limit for `T`. + fn increment(value: T) -> Self::Output { + if value < Max::get() { + let new_value = value.add(T::one()); + Ok(new_value) + } else { + Err(MaxError::default().into()) + } + } +} + +mod sealed { + use frame_support::{pallet_prelude::StorageValue, traits::StorageInstance}; + + use crate::abstractions::{ + counter::{Counter, CounterHelperTrait}, + nonce::{Nonce, NonceHelperTrait}, + utils::{decrement::Decrementor, ValueQuery}, + }; + + use super::*; + + /// Sealing trait for [`Increment`][super::Increment] and [`Incrementor`][super::Incrementor]. + /// If you want to add a new implementor, be sure to add it here and ensure it's tested. + pub trait Sealed {} + + impl Sealed for StorageValue> + where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + (StorageValue>, I::Output, S): NonceHelperTrait, + { + } + + impl Sealed for StorageValue> + where + P: StorageInstance + 'static, + T: FullCodec + Clone + Copy + 'static, + S: StartAtValue, + I: Incrementor, + D: Decrementor, + (StorageValue>, I::Output, D::Output, S): + CounterHelperTrait, + { + } + + impl Sealed for SafeIncrement {} + impl Sealed for WrappingIncrement {} + impl Sealed for IncrementToMax {} +} diff --git a/composable/composable-support/src/abstractions/utils/mod.rs b/composable/composable-support/src/abstractions/utils/mod.rs new file mode 100644 index 00000000..1d7d771f --- /dev/null +++ b/composable/composable-support/src/abstractions/utils/mod.rs @@ -0,0 +1,87 @@ +pub mod decrement; +pub mod increment; +pub mod start_at; + +/// Helper macro to create a type that can be used in [`IncrementToMax`][increment::IncrementToMax]. +/// +/// # Usage +/// +/// ```rust,ignore +/// #[pallet::config] +/// pub trait Config: frame_system::Config { +/// type SomeType: Copy + Zero + SafeAdd + One + TypeInfo + Member + FullCodec; +/// +/// #[pallet::constant] +/// type SomeTypeMaxValue: Get; +/// } +/// +/// #[pallet::error] +/// #[derive(PartialEqNoBound)] +/// pub enum Error { +/// SomeTypeTooLarge +/// } +/// +/// #[pallet::storage] +/// #[allow(clippy::disallowed_type)] // counter +/// pub type Counter_ZeroInit_ToMax = StorageValue< +/// _, +/// T::SomeType, +/// ValueQuery, +/// Counter< +/// ZeroInit, +/// IncrementToMax< +/// T::SomeTypeMaxValue, +/// SomeTypeTooLarge, +/// Error, +/// >, +/// SafeDecrement, +/// >, +/// >; +/// +/// error_to_pallet_error!( +/// SomeTypeTooLarge, +/// ); +/// ``` +/// +/// Note that this assumes that the pallet's `Error` and `Config` types are in scope and not +/// renamed. +#[macro_export] +macro_rules! error_to_pallet_error { + ($($name:ident,)+) => { + $( + #[derive(::core::fmt::Debug, ::core::default::Default, ::core::cmp::PartialEq)] + pub struct $name; + + impl From<$name> for Error { + fn from(_: $name) -> Error { + Error::::$name + } + } + )+ + }; +} + +/// `#[allow(clippy::disallowed_types)]` on an import currently errors: +/// +/// ```rust,ignore +/// #[allow(clippy::disallowed_types)] +/// use frame_support::pallet_prelude::ValueQuery; +/// ``` +/// +/// Output: +/// +/// ```plaintext +/// error: useless lint attribute +/// --> frame/composable-support/src/abstractions/nonce/mod.rs:14:1 +/// | +/// 14 | #[allow(clippy::disallowed_types)] +/// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: if you just forgot a `!`, use: `#![allow(clippy::disallowed_types)]` +/// | +/// = note: `#[deny(clippy::useless_attribute)]` on by default +/// = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_attribute +/// ``` +/// +/// This type is a re-export to allow for importing it (as opposed to using a fully qualified +/// path) when using a nonce in a pallet that isn't importing `frame_support::pallet_prelude::*`. +#[allow(clippy::disallowed_types)] +pub type ValueQuery = frame_support::pallet_prelude::ValueQuery; diff --git a/composable/composable-support/src/abstractions/utils/start_at.rs b/composable/composable-support/src/abstractions/utils/start_at.rs new file mode 100644 index 00000000..85470bb2 --- /dev/null +++ b/composable/composable-support/src/abstractions/utils/start_at.rs @@ -0,0 +1,46 @@ +use sp_runtime::traits::{One, Zero}; + +use crate::abstractions::utils::start_at::sealed::Sealed; + +pub trait StartAtValue: Sealed + 'static { + fn value() -> T; +} + +/// Marker for nonces that should start at `::zero`. +pub struct ZeroInit; + +impl StartAtValue for ZeroInit { + fn value() -> T { + T::zero() + } +} + +/// Marker for nonces that should start at `::one`. +pub struct OneInit; + +impl StartAtValue for OneInit { + fn value() -> T { + T::one() + } +} + +/// Marker for nonces that should start at `::default`. +pub struct DefaultInit; + +impl StartAtValue for DefaultInit { + fn value() -> T { + T::default() + } +} + +mod sealed { + use super::*; + + /// Sealing trait for [`StartAtValue`][super::StartAtValue]. If you want to add a new + /// implementor, be sure to add it here and ensure it's tested. + pub trait Sealed {} + + impl Sealed for ZeroInit {} + impl Sealed for OneInit {} + impl Sealed for DefaultInit {} +} diff --git a/composable/composable-support/src/lib.rs b/composable/composable-support/src/lib.rs new file mode 100644 index 00000000..9dfed4e4 --- /dev/null +++ b/composable/composable-support/src/lib.rs @@ -0,0 +1,19 @@ +#![cfg_attr( + not(test), + warn( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::indexing_slicing, + clippy::todo, + clippy::unwrap_used, + clippy::panic + ) +)] // allow in tests +#![warn(clippy::unseparated_literal_suffix)] +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(generic_const_exprs)] +#![feature(const_trait_impl)] +#![allow(incomplete_features)] + +pub mod abstractions; +pub mod math; diff --git a/composable/composable-support/src/math/mod.rs b/composable/composable-support/src/math/mod.rs new file mode 100644 index 00000000..b089e83f --- /dev/null +++ b/composable/composable-support/src/math/mod.rs @@ -0,0 +1,2 @@ +pub mod safe; +pub mod wrapping_next; diff --git a/composable/composable-support/src/math/safe.rs b/composable/composable-support/src/math/safe.rs new file mode 100644 index 00000000..a99e63ab --- /dev/null +++ b/composable/composable-support/src/math/safe.rs @@ -0,0 +1,76 @@ +use sp_arithmetic::Rounding; +use sp_runtime::{ + helpers_128bit::multiply_by_rational_with_rounding, + traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Zero}, + ArithmeticError, +}; + +/// Thin wrapper around [`multiply_by_rational`], transforming any errors into [`ArithmeticError`] +/// for easier use in pallets and Dispatch-related contexts. See [`multiply_by_rational`] for more +/// information. +/// +/// Note: [`multiply_by_rational`] clamps `c` at a minimum of `1`, which can be confusing. +/// [`safe_multiply_by_rational`] instead returns a divide by zero error if `c == 0`. +pub fn safe_multiply_by_rational(a: u128, b: u128, c: u128) -> Result { + if c == 0 { + Err(ArithmeticError::DivisionByZero) + } else { + multiply_by_rational_with_rounding(a, b, c, Rounding::Down).ok_or(ArithmeticError::Overflow) + } +} + +// little bit slower than maximizing performance by knowing constraints. +// Example, you sum to negative numbers, can get underflow, so need to check on each add; but if +// you have positive number only, you cannot have underflow. Same for other constrains, like non +// zero divisor. + +pub trait SafeAdd: Sized { + fn safe_add(&self, rhs: &Self) -> Result; +} + +pub trait SafeDiv: Sized { + fn safe_div(&self, rhs: &Self) -> Result; +} + +pub trait SafeMul: Sized { + fn safe_mul(&self, rhs: &Self) -> Result; +} + +pub trait SafeSub: Sized { + fn safe_sub(&self, rhs: &Self) -> Result; +} + +pub trait SafeArithmetic: Sized + SafeAdd + SafeDiv + SafeMul + SafeSub {} + +impl SafeAdd for T { + #[inline(always)] + fn safe_add(&self, rhs: &Self) -> Result { + self.checked_add(rhs).ok_or(ArithmeticError::Overflow) + } +} + +impl SafeDiv for T { + #[inline(always)] + fn safe_div(&self, rhs: &Self) -> Result { + if rhs.is_zero() { + return Err(ArithmeticError::DivisionByZero) + } + self.checked_div(rhs).ok_or(ArithmeticError::Overflow) + } +} + +impl SafeMul for T { + #[inline(always)] + fn safe_mul(&self, rhs: &Self) -> Result { + self.checked_mul(rhs).ok_or(ArithmeticError::Overflow) + } +} + +impl SafeSub for T { + #[inline(always)] + fn safe_sub(&self, rhs: &Self) -> Result { + self.checked_sub(rhs).ok_or(ArithmeticError::Underflow) + } +} + +impl SafeArithmetic for T {} diff --git a/composable/composable-support/src/math/wrapping_next.rs b/composable/composable-support/src/math/wrapping_next.rs new file mode 100644 index 00000000..87ab132a --- /dev/null +++ b/composable/composable-support/src/math/wrapping_next.rs @@ -0,0 +1,19 @@ +use sp_runtime::traits::One; + +/// An object from which we can derive a second object of the same type. +/// This function cannot fail and might return the same object if a boundary is about to be crossed. +// This kind of function is usually called an Endomorphism. But let's keep it simple. +pub trait WrappingNext { + /// pallet must be coded that way that wrapping around does not do harm except of error + /// so additional check should be check on pallet level + fn next(&self) -> Self; +} + +impl WrappingNext for T +where + T: Copy + One + num_traits::WrappingAdd, +{ + fn next(&self) -> Self { + self.wrapping_add(&T::one()) + } +} From db82c971ebf10d572a6e4ce9769b6854be482d24 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 10 Sep 2024 23:28:31 +0900 Subject: [PATCH 018/133] feat: Add cosmwasm-vm --- Cargo.toml | 2 + composable/vm/Cargo.toml | 23 + composable/vm/src/executor.rs | 967 +++++++++++++++++++++++++++++++ composable/vm/src/has.rs | 5 + composable/vm/src/input.rs | 7 + composable/vm/src/lib.rs | 13 + composable/vm/src/memory.rs | 251 ++++++++ composable/vm/src/system.rs | 910 +++++++++++++++++++++++++++++ composable/vm/src/tagged.rs | 13 + composable/vm/src/transaction.rs | 10 + composable/vm/src/vm.rs | 416 +++++++++++++ 11 files changed, 2617 insertions(+) create mode 100644 composable/vm/Cargo.toml create mode 100644 composable/vm/src/executor.rs create mode 100644 composable/vm/src/has.rs create mode 100644 composable/vm/src/input.rs create mode 100644 composable/vm/src/lib.rs create mode 100644 composable/vm/src/memory.rs create mode 100644 composable/vm/src/system.rs create mode 100644 composable/vm/src/tagged.rs create mode 100644 composable/vm/src/transaction.rs create mode 100644 composable/vm/src/vm.rs diff --git a/Cargo.toml b/Cargo.toml index 432d4397..a37b0ada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "core-primitives", "composable/composable-support", + "composable/vm", "cosmwasm/std", "frame/babel", "frame/cosmos/types", @@ -26,6 +27,7 @@ composable-support = { path = "composable/composable-support", default-features cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } cosmwasm-std = { path = "cosmwasm/std", default-features = false } +cosmwasm-vm = { path = "composable/vm", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } diff --git a/composable/vm/Cargo.toml b/composable/vm/Cargo.toml new file mode 100644 index 00000000..e8ea84ae --- /dev/null +++ b/composable/vm/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cosmwasm-vm" +version = "0.2.0" +edition = "2021" + +[features] +default = ["iterator", "stargate"] +iterator = ["cosmwasm-std/iterator"] +stargate = ["cosmwasm-std/stargate"] +std = ["cosmwasm-std/std"] + +[dependencies] +cosmwasm-std = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", + "cosmwasm_1_4", +] } +log = { version = "0.4.21", default-features = false } +num = { version = "0.4.3", default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.127", default-features = false, features = [ + "alloc", +] } diff --git a/composable/vm/src/executor.rs b/composable/vm/src/executor.rs new file mode 100644 index 00000000..35b5818c --- /dev/null +++ b/composable/vm/src/executor.rs @@ -0,0 +1,967 @@ +use crate::{ + has::Has, + input::Input, + memory::{ + LimitedRead, RawFromRegion, RawIntoRegion, ReadWriteMemory, ReadableMemory, + ReadableMemoryErrorOf, WritableMemoryErrorOf, Write, + }, + tagged::Tagged, + vm::{VmErrorOf, VmInputOf, VmOutputOf, VM}, +}; +use alloc::vec::Vec; +use core::{fmt::Debug, marker::PhantomData}; +use cosmwasm_std::{Binary, ContractResult, Empty, Env, MessageInfo, QueryRequest, Response}; +use num::traits::Zero; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +pub mod read_limits { + /// A mibi (mega binary) + const MI: usize = 1024 * 1024; + /// Max length (in bytes) of the result data from an instantiate call. + pub const RESULT_INSTANTIATE: usize = 64 * MI; + /// Max length (in bytes) of the result data from an execute call. + pub const RESULT_EXECUTE: usize = 64 * MI; + /// Max length (in bytes) of the result data from a migrate call. + pub const RESULT_MIGRATE: usize = 64 * MI; + /// Max length (in bytes) of the result data from a sudo call. + pub const RESULT_SUDO: usize = 64 * MI; + /// Max length (in bytes) of the result data from a reply call. + pub const RESULT_REPLY: usize = 64 * MI; + /// Max length (in bytes) of the result data from a query call. + pub const RESULT_QUERY: usize = 64 * MI; + /// Max length (in bytes) of the query data from a `query_chain` call. + pub const REQUEST_QUERY: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_channel_open` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_OPEN: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_channel_connect` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_CONNECT: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_channel_close` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_CLOSE: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_packet_receive` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_RECEIVE: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_packet_ack` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_ACK: usize = 64 * MI; + /// Max length (in bytes) of the result data from a `ibc_packet_timeout` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_TIMEOUT: usize = 64 * MI; +} + +/// The limits for the JSON deserialization. +/// +/// Those limits are not used when the Rust JSON deserializer is bypassed by using the +/// public `call_*_raw` functions directly. +pub mod deserialization_limits { + /// A kibi (kilo binary) + const KI: usize = 1024; + /// Max length (in bytes) of the result data from an instantiate call. + pub const RESULT_INSTANTIATE: usize = 256 * KI; + /// Max length (in bytes) of the result data from an execute call. + pub const RESULT_EXECUTE: usize = 256 * KI; + /// Max length (in bytes) of the result data from a migrate call. + pub const RESULT_MIGRATE: usize = 256 * KI; + /// Max length (in bytes) of the result data from a sudo call. + pub const RESULT_SUDO: usize = 256 * KI; + /// Max length (in bytes) of the result data from a reply call. + pub const RESULT_REPLY: usize = 256 * KI; + /// Max length (in bytes) of the result data from a query call. + pub const RESULT_QUERY: usize = 256 * KI; + /// Max length (in bytes) of the query data from a `query_chain` call. + pub const REQUEST_QUERY: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_channel_open` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_OPEN: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_channel_connect` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_CONNECT: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_channel_close` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_CHANNEL_CLOSE: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_packet_receive` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_RECEIVE: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_packet_ack` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_ACK: usize = 256 * KI; + /// Max length (in bytes) of the result data from a `ibc_packet_timeout` call. + #[cfg(feature = "stargate")] + pub const RESULT_IBC_PACKET_TIMEOUT: usize = 256 * KI; +} + +pub type CosmwasmExecutionResult = ContractResult>; +pub type CosmwasmQueryResult = ContractResult; +pub type CosmwasmReplyResult = ContractResult>; +pub type CosmwasmMigrateResult = ContractResult>; + +pub type QueryResponse = Binary; + +pub trait DeserializeLimit { + fn deserialize_limit() -> usize; +} + +pub trait ReadLimit { + fn read_limit() -> usize; +} + +impl DeserializeLimit for QueryRequest { + fn deserialize_limit() -> usize { + deserialization_limits::REQUEST_QUERY + } +} + +impl ReadLimit for QueryRequest { + fn read_limit() -> usize { + read_limits::REQUEST_QUERY + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ReplyResult(pub CosmwasmExecutionResult); +impl DeserializeLimit for ReplyResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_REPLY + } +} +impl ReadLimit for ReplyResult { + fn read_limit() -> usize { + read_limits::RESULT_REPLY + } +} +impl From> for ContractResult> { + fn from(ReplyResult(result): ReplyResult) -> Self { + result + } +} +impl From>> for ReplyResult { + fn from(value: ContractResult>) -> Self { + ReplyResult(value) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct QueryResult(pub CosmwasmQueryResult); +impl DeserializeLimit for QueryResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_QUERY + } +} +impl ReadLimit for QueryResult { + fn read_limit() -> usize { + read_limits::RESULT_QUERY + } +} +impl From for QueryResult { + fn from(value: CosmwasmQueryResult) -> Self { + Self(value) + } +} +impl From for CosmwasmQueryResult { + fn from(QueryResult(value): QueryResult) -> Self { + value + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ExecuteResult(pub CosmwasmExecutionResult); +impl DeserializeLimit for ExecuteResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_EXECUTE + } +} +impl ReadLimit for ExecuteResult { + fn read_limit() -> usize { + read_limits::RESULT_EXECUTE + } +} +impl From> for ContractResult> { + fn from(ExecuteResult(result): ExecuteResult) -> Self { + result + } +} +impl From>> for ExecuteResult { + fn from(value: ContractResult>) -> Self { + ExecuteResult(value) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct InstantiateResult(pub CosmwasmExecutionResult); +impl DeserializeLimit for InstantiateResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_INSTANTIATE + } +} +impl ReadLimit for InstantiateResult { + fn read_limit() -> usize { + read_limits::RESULT_INSTANTIATE + } +} +impl From> for ContractResult> { + fn from(InstantiateResult(result): InstantiateResult) -> Self { + result + } +} +impl From>> for InstantiateResult { + fn from(value: ContractResult>) -> Self { + InstantiateResult(value) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct MigrateResult(pub CosmwasmExecutionResult); +impl DeserializeLimit for MigrateResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_MIGRATE + } +} +impl ReadLimit for MigrateResult { + fn read_limit() -> usize { + read_limits::RESULT_MIGRATE + } +} +impl From> for ContractResult> { + fn from(MigrateResult(result): MigrateResult) -> Self { + result + } +} +impl From>> for MigrateResult { + fn from(value: ContractResult>) -> Self { + MigrateResult(value) + } +} + +pub mod ibc { + #![cfg(feature = "stargate")] + + use super::{ + deserialization_limits, read_limits, AsFunctionName, ContractResult, Debug, Deserialize, + DeserializeLimit, Empty, HasInfo, Input, PhantomData, ReadLimit, Response, Serialize, + }; + use cosmwasm_std::{Ibc3ChannelOpenResponse, IbcBasicResponse, IbcReceiveResponse}; + + /// Response to the low level `ibc_channel_open` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelOpenResult(pub ContractResult>); + impl DeserializeLimit for IbcChannelOpenResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_CHANNEL_OPEN + } + } + impl ReadLimit for IbcChannelOpenResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_CHANNEL_OPEN + } + } + + /// Response to the low level `ibc_channel_connect` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelConnectResult(pub ContractResult>); + impl DeserializeLimit for IbcChannelConnectResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_CHANNEL_CONNECT + } + } + impl ReadLimit for IbcChannelConnectResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_CHANNEL_CONNECT + } + } + impl From> for ContractResult> { + fn from(IbcChannelConnectResult(result): IbcChannelConnectResult) -> Self { + match result { + ContractResult::Ok(IbcBasicResponse { messages, attributes, events, .. }) => + ContractResult::Ok( + Response::new() + .add_submessages(messages) + .add_attributes(attributes) + .add_events(events), + ), + ContractResult::Err(x) => ContractResult::Err(x), + } + } + } + impl From>> for IbcChannelConnectResult { + fn from(value: ContractResult>) -> Self { + IbcChannelConnectResult(match value { + ContractResult::Ok(response) => ContractResult::Ok( + IbcBasicResponse::new() + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + ), + ContractResult::Err(e) => ContractResult::Err(e), + }) + } + } + + /// Response to the low level `ibc_channel_close` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelCloseResult(pub ContractResult>); + impl DeserializeLimit for IbcChannelCloseResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_CHANNEL_CLOSE + } + } + impl ReadLimit for IbcChannelCloseResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_CHANNEL_CLOSE + } + } + impl From> for ContractResult> { + fn from(IbcChannelCloseResult(result): IbcChannelCloseResult) -> Self { + match result { + ContractResult::Ok(IbcBasicResponse { messages, attributes, events, .. }) => + ContractResult::Ok( + Response::new() + .add_submessages(messages) + .add_attributes(attributes) + .add_events(events), + ), + ContractResult::Err(x) => ContractResult::Err(x), + } + } + } + impl From>> for IbcChannelCloseResult { + fn from(value: ContractResult>) -> Self { + IbcChannelCloseResult(match value { + ContractResult::Ok(response) => ContractResult::Ok( + IbcBasicResponse::new() + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + ), + ContractResult::Err(e) => ContractResult::Err(e), + }) + } + } + + /// Response to the low level `ibc_packet_receive` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketReceiveResult(pub ContractResult>); + impl DeserializeLimit for IbcPacketReceiveResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_PACKET_RECEIVE + } + } + impl ReadLimit for IbcPacketReceiveResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_PACKET_RECEIVE + } + } + impl From> for ContractResult> { + fn from(IbcPacketReceiveResult(result): IbcPacketReceiveResult) -> Self { + match result { + ContractResult::Ok(IbcReceiveResponse { + messages, + attributes, + events, + acknowledgement, + .. + }) => { + let mut response = Response::new() + .add_submessages(messages) + .add_attributes(attributes) + .add_events(events); + + response.data = acknowledgement; + ContractResult::Ok(response) + }, + ContractResult::Err(x) => ContractResult::Err(x), + } + } + } + impl From>> for IbcPacketReceiveResult { + fn from(value: ContractResult>) -> Self { + IbcPacketReceiveResult(match value { + ContractResult::Ok(response) => ContractResult::Ok(match response.data { + Some(data) => IbcReceiveResponse::new(data) + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + None => IbcReceiveResponse::without_ack() + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + }), + ContractResult::Err(e) => ContractResult::Err(e), + }) + } + } + + /// Response to the low level `ibc_packet_ack` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketAckResult(pub ContractResult>); + impl DeserializeLimit for IbcPacketAckResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_PACKET_ACK + } + } + impl ReadLimit for IbcPacketAckResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_PACKET_ACK + } + } + impl From> for ContractResult> { + fn from(IbcPacketAckResult(result): IbcPacketAckResult) -> Self { + match result { + ContractResult::Ok(IbcBasicResponse { messages, attributes, events, .. }) => + ContractResult::Ok( + Response::new() + .add_submessages(messages) + .add_attributes(attributes) + .add_events(events), + ), + ContractResult::Err(x) => ContractResult::Err(x), + } + } + } + impl From>> for IbcPacketAckResult { + fn from(value: ContractResult>) -> Self { + IbcPacketAckResult(match value { + ContractResult::Ok(response) => ContractResult::Ok( + IbcBasicResponse::new() + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + ), + ContractResult::Err(e) => ContractResult::Err(e), + }) + } + } + + /// Response to the low level `ibc_packet_timeout` call. + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketTimeoutResult(pub ContractResult>); + impl DeserializeLimit for IbcPacketTimeoutResult { + fn deserialize_limit() -> usize { + deserialization_limits::RESULT_IBC_PACKET_TIMEOUT + } + } + impl ReadLimit for IbcPacketTimeoutResult { + fn read_limit() -> usize { + read_limits::RESULT_IBC_PACKET_TIMEOUT + } + } + impl From> for ContractResult> { + fn from(IbcPacketTimeoutResult(result): IbcPacketTimeoutResult) -> Self { + match result { + ContractResult::Ok(IbcBasicResponse { messages, attributes, events, .. }) => + ContractResult::Ok( + Response::new() + .add_submessages(messages) + .add_attributes(attributes) + .add_events(events), + ), + ContractResult::Err(x) => ContractResult::Err(x), + } + } + } + impl From>> for IbcPacketTimeoutResult { + fn from(value: ContractResult>) -> Self { + IbcPacketTimeoutResult(match value { + ContractResult::Ok(response) => ContractResult::Ok( + IbcBasicResponse::new() + .add_submessages(response.messages) + .add_attributes(response.attributes) + .add_events(response.events), + ), + ContractResult::Err(e) => ContractResult::Err(e), + }) + } + } + + /// Strong type representing a call to `ibc_channel_open` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelOpenCall; + impl Input for IbcChannelOpenCall { + type Output = IbcChannelOpenResult; + } + impl AsFunctionName for IbcChannelOpenCall { + const NAME: &'static str = "ibc_channel_open"; + } + impl HasInfo for IbcChannelOpenCall { + const HAS_INFO: bool = false; + } + + /// Strong type representing a call to `ibc_channel_connect` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelConnectCall(PhantomData); + impl Input for IbcChannelConnectCall { + type Output = IbcChannelConnectResult; + } + impl AsFunctionName for IbcChannelConnectCall { + const NAME: &'static str = "ibc_channel_connect"; + } + impl HasInfo for IbcChannelConnectCall { + const HAS_INFO: bool = false; + } + + /// Strong type representing a call to `ibc_channel_close` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcChannelCloseCall(PhantomData); + impl Input for IbcChannelCloseCall { + type Output = IbcChannelCloseResult; + } + impl AsFunctionName for IbcChannelCloseCall { + const NAME: &'static str = "ibc_channel_close"; + } + impl HasInfo for IbcChannelCloseCall { + const HAS_INFO: bool = false; + } + + /// Strong type representing a call to `ibc_packet_receive` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketReceiveCall(PhantomData); + impl Input for IbcPacketReceiveCall { + type Output = IbcPacketReceiveResult; + } + impl AsFunctionName for IbcPacketReceiveCall { + const NAME: &'static str = "ibc_packet_receive"; + } + impl HasInfo for IbcPacketReceiveCall { + const HAS_INFO: bool = false; + } + + /// Strong type representing a call to `ibc_packet_ack` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketAckCall(PhantomData); + impl Input for IbcPacketAckCall { + type Output = IbcPacketAckResult; + } + impl AsFunctionName for IbcPacketAckCall { + const NAME: &'static str = "ibc_packet_ack"; + } + impl HasInfo for IbcPacketAckCall { + const HAS_INFO: bool = false; + } + + /// Strong type representing a call to `ibc_packet_timeout` export. + #[allow(clippy::module_name_repetitions)] + pub struct IbcPacketTimeoutCall(PhantomData); + impl Input for IbcPacketTimeoutCall { + type Output = IbcPacketTimeoutResult; + } + impl AsFunctionName for IbcPacketTimeoutCall { + const NAME: &'static str = "ibc_packet_timeout"; + } + impl HasInfo for IbcPacketTimeoutCall { + const HAS_INFO: bool = false; + } +} + +/// The type representing a call to a contract `allocate` export. +pub struct AllocateCall(pub Pointer); +impl Input for AllocateCall { + type Output = Pointer; +} +impl AsFunctionName for AllocateCall { + const NAME: &'static str = "allocate"; +} + +pub struct Unit; + +/// The type representing a call to a contract `deallocate` export. +pub struct DeallocateCall(pub Pointer); +impl Input for DeallocateCall { + type Output = Unit; +} +impl AsFunctionName for DeallocateCall { + const NAME: &'static str = "deallocate"; +} + +/// The type representing a call to a contract `query` export. +pub struct QueryCall; +impl Input for QueryCall { + type Output = QueryResult; +} +impl AsFunctionName for QueryCall { + const NAME: &'static str = "query"; +} +impl HasInfo for QueryCall { + const HAS_INFO: bool = false; +} + +/// The type representing a call to a contract `instantiate` export. +pub struct InstantiateCall(PhantomData); +impl Input for InstantiateCall { + type Output = InstantiateResult; +} +impl AsFunctionName for InstantiateCall { + const NAME: &'static str = "instantiate"; +} +impl HasInfo for InstantiateCall { + const HAS_INFO: bool = true; +} + +/// The type representing a call to a contract `execute` export. +pub struct ExecuteCall(PhantomData); +impl Input for ExecuteCall { + type Output = ExecuteResult; +} +impl AsFunctionName for ExecuteCall { + const NAME: &'static str = "execute"; +} +impl HasInfo for ExecuteCall { + const HAS_INFO: bool = true; +} + +/// The type representing a call to a contract `reply` export. +pub struct ReplyCall(PhantomData); +impl Input for ReplyCall { + type Output = ReplyResult; +} +impl AsFunctionName for ReplyCall { + const NAME: &'static str = "reply"; +} +impl HasInfo for ReplyCall { + const HAS_INFO: bool = false; +} + +/// The type representing a call to a contract `migrate` export. +pub struct MigrateCall(PhantomData); +impl Input for MigrateCall { + type Output = MigrateResult; +} +impl AsFunctionName for MigrateCall { + const NAME: &'static str = "migrate"; +} +impl HasInfo for MigrateCall { + const HAS_INFO: bool = false; +} + +pub trait AsFunctionName { + const NAME: &'static str; +} + +/// Structure that hold the function inputs for `f(env, messageInfo, msg) -> X`. +pub struct CosmwasmCallInput<'a, Pointer, I>( + pub Tagged, + pub Tagged, + pub Tagged, + pub PhantomData, +); +impl<'a, Pointer, I: Input> Input for CosmwasmCallInput<'a, Pointer, I> { + type Output = Pointer; +} + +/// Structure that hold the function inputs for `f(env, msg) -> X`. +pub struct CosmwasmCallWithoutInfoInput<'a, Pointer, I>( + pub Tagged, + pub Tagged, + pub PhantomData, +); +impl<'a, Pointer, I: Input> Input for CosmwasmCallWithoutInfoInput<'a, Pointer, I> { + type Output = Pointer; +} + +/// Whether an input type require the `MessageInfo` message to be passed. +pub trait HasInfo { + const HAS_INFO: bool; +} + +/// Errors likely to happen while doing low level executor calls. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum ExecutorError { + /// Unable to serialize the structure to JSON. + FailedToSerialize, + /// Unable to deserialize the JSON payload to the given type. + FailedToDeserialize, + /// The requested allocation size is too big and would overflow the memory. + AllocationWouldOverflow, + /// The requrested deallocation size is too big and would overflow the memory (must be + /// impossible). + DeallocationWouldOverflow, + /// The read limit is too big and could not be converted to a pointer. + CallReadLimitWouldOverflow, + /// Pointer is invalid + InvalidPointer, +} + +pub mod constants { + /// A kibi (kilo binary) + pub const KI: usize = 1024; + /// A mibi (mega binary) + pub const MI: usize = 1024 * 1024; + /// Max key length for `db_write/db_read/db_remove/db_scan` (when VM reads the key argument from + /// Wasm memory) + pub const MAX_LENGTH_DB_KEY: usize = 64 * KI; + /// Max value length for `db_write` (when VM reads the value argument from Wasm memory) + pub const MAX_LENGTH_DB_VALUE: usize = 128 * KI; + /// Typically 20 (Cosmos SDK, Ethereum), 32 (Nano, Substrate) or 54 (`MockApi`) + pub const MAX_LENGTH_CANONICAL_ADDRESS: usize = 64; + /// The max length of human address inputs (in bytes). + /// The maximum allowed size for [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32) + /// is 90 characters and we're adding some safety margin around that for other formats. + pub const MAX_LENGTH_HUMAN_ADDRESS: usize = 256; + pub const MAX_LENGTH_QUERY_CHAIN_REQUEST: usize = 64 * KI; + /// Length of a serialized Ed25519 signature + pub const MAX_LENGTH_ED25519_SIGNATURE: usize = 64; + /// Max length of a Ed25519 message in bytes. + /// This is an arbitrary value, for performance / memory contraints. If you need to verify + /// larger messages, let us know. + pub const MAX_LENGTH_ED25519_MESSAGE: usize = 128 * 1024; + /// Max number of batch Ed25519 messages / signatures / `public_keys`. + /// This is an arbitrary value, for performance / memory contraints. If you need to batch-verify + /// a larger number of signatures, let us know. + pub const MAX_COUNT_ED25519_BATCH: usize = 256; + /// Max length for a debug message + pub const MAX_LENGTH_DEBUG: usize = 2 * MI; + /// Max length for an abort message + pub const MAX_LENGTH_ABORT: usize = 2 * MI; + /// Max length of a message hash + pub const MAX_LENGTH_MESSAGE_HASH: usize = 32; + /// Length of an edcsa signature + pub const EDCSA_SIGNATURE_LENGTH: usize = 64; + /// Max length for edcsa public key + pub const MAX_LENGTH_EDCSA_PUBKEY_LENGTH: usize = 65; + /// Length of an eddsa public key + pub const EDDSA_PUBKEY_LENGTH: usize = 32; +} + +/// Allow for untyped marshalling to specify a limit while extracting the bytes from a contract +/// memory. +pub struct ConstantReadLimit; +impl ReadLimit for ConstantReadLimit { + fn read_limit() -> usize { + K + } +} + +/// Allocate a chunk of bytes from the contract memory. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `len` - the len of the chunk to allocate. +/// +/// Returns the chunk pointer. +pub fn allocate(vm: &mut V, len: L) -> Result> +where + V: VM, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + P: Copy + TryFrom + Debug + for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: From, +{ + let len_value = P::try_from(len).map_err(|_| ExecutorError::AllocationWouldOverflow)?; + let result = vm.call(AllocateCall(len_value))?; + log::trace!("Allocate: size={:?}, pointer={:?}", len_value, result); + Ok(result) +} + +/// Deallocate a previously allocated chunk from a contract memory. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `pointer` - the pointer pointing the memory we will deallocate. +pub fn deallocate(vm: &mut V, pointer: V::Pointer) -> Result<(), VmErrorOf> +where + V: VM + ReadWriteMemory, + for<'x> Unit: TryFrom, Error = VmErrorOf>, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + VmErrorOf: From, +{ + log::trace!("Deallocate"); + vm.call(DeallocateCall(pointer))?; + Ok(()) +} + +pub fn passthrough_in_to( + vm: &mut V, + destination: V::Pointer, + data: &[u8], +) -> Result<(), VmErrorOf> +where + V: VM + ReadWriteMemory, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: + From> + From> + From, +{ + if destination.is_zero() { + return Err(ExecutorError::InvalidPointer.into()); + } + RawIntoRegion::try_from(Write(vm, destination, data))?; + Ok(()) +} + +/// Allocate memory in the contract and write raw bytes representing some value of type `T`. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `data` - the raw bytes that will be written in the contract memory. +/// +/// Returns either the tagged pointer or a `VmErrorOf`. +pub fn passthrough_in(vm: &mut V, data: &[u8]) -> Result, VmErrorOf> +where + V: VM + ReadWriteMemory, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: + From> + From> + From, +{ + log::trace!("PassthroughIn"); + let pointer = allocate::<_, _, usize>(vm, data.len())?; + passthrough_in_to(vm, pointer, data)?; + Ok(Tagged::new(pointer)) +} + +/// Extract the bytes held by the region identified with the `pointer`. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `pointer` - the region pointer from which we will read the bytes. +/// +/// Returns either the bytes read or a `VmErrorOf`. +pub fn passthrough_out(vm: &V, pointer: V::Pointer) -> Result, VmErrorOf> +where + V: VM + ReadableMemory, + T: ReadLimit, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: From> + From, +{ + log::trace!("PassthroughOut"); + let RawFromRegion(buffer) = RawFromRegion::try_from(LimitedRead( + vm, + pointer, + TryFrom::::try_from(T::read_limit()) + .map_err(|_| ExecutorError::CallReadLimitWouldOverflow)?, + ))?; + Ok(buffer) +} + +/// Allocate memory in the contract and write the type `T` serialized in JSON at the newly allocated +/// space. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `x` - the value the will be serialized to JSON and written in the contract memory. +/// +/// Returns either the tagged (type T) pointer to the allocated region or a `VmErrorOf`. +pub fn marshall_in(vm: &mut V, x: &T) -> Result, VmErrorOf> +where + V: VM + ReadWriteMemory, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: + From> + From> + From, + T: serde::ser::Serialize + Sized, +{ + log::trace!("MarshallIn"); + let serialized = serde_json::to_vec(x).map_err(|_| ExecutorError::FailedToSerialize)?; + passthrough_in(vm, &serialized) +} + +/// Read a JSON serialized value of type `T` from a region identified by the `pointer`. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `pointer` - the region pointer from which we will read and deserialized a JSON payload of type +/// `T`. +/// +/// Returns either the value `T` or a `VmErrorOf`. +pub fn marshall_out(vm: &V, pointer: V::Pointer) -> Result> +where + V: VM + ReadableMemory, + T: ReadLimit, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf>, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + VmErrorOf: From> + From, + T: serde::de::DeserializeOwned + ReadLimit + DeserializeLimit, +{ + log::trace!("MarshallOut"); + let output = passthrough_out::(vm, pointer)?; + Ok(serde_json::from_slice(&output).map_err(|_| ExecutorError::FailedToDeserialize)?) +} + +/// Execute a generic contract export (`instantiate`, `execute`, `migrate` etc...), providing the +/// custom raw `message` input. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `message` - the contract message passed to the export, usually specific to the contract +/// (`InstantiateMsg`, `ExecuteMsg` etc...). +/// +/// Returns either the associated `I::Output` or a `VmErrorOf`. +pub fn cosmwasm_call(vm: &mut V, message: &[u8]) -> Result> +where + V: VM + ReadWriteMemory + Has + Has, + I: Input + HasInfo, + I::Output: DeserializeOwned + ReadLimit + DeserializeLimit, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + for<'x> Unit: TryFrom, Error = VmErrorOf>, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf>, + VmErrorOf: + From> + From> + From, +{ + log::trace!("Call {}", alloc::string::String::from_utf8_lossy(message)); + let env = vm.get(); + let pointer = if I::HAS_INFO { + let info = vm.get(); + let input = CosmwasmCallInput( + marshall_in(vm, &env)?, + marshall_in(vm, &info)?, + passthrough_in(vm, message)?, + PhantomData, + ); + vm.call(input) + } else { + let input = CosmwasmCallWithoutInfoInput( + marshall_in(vm, &env)?, + passthrough_in(vm, message)?, + PhantomData, + ); + vm.call(input) + }?; + let result = marshall_out(vm, pointer)?; + deallocate(vm, pointer)?; + Ok(result) +} + +/// Execute a generic contract export (`instantiate`, `execute`, `migrate` etc...), providing the +/// high level message. +/// +/// # Arguments +/// +/// * `vm` - the virtual machine. +/// * `message` - the contract message passed to the export, usually specific to the contract +/// (`InstantiateMsg`, `ExecuteMsg` etc...). +/// +/// Returns either the associated `I::Output` or a `VmErrorOf`. +pub fn cosmwasm_call_serialize(vm: &mut V, message: &M) -> Result> +where + M: Serialize, + V: VM + ReadWriteMemory + Has + Has, + I: Input + HasInfo, + I::Output: DeserializeOwned + ReadLimit + DeserializeLimit, + V::Pointer: for<'x> TryFrom, Error = VmErrorOf>, + for<'x> Unit: TryFrom, Error = VmErrorOf>, + for<'x> VmInputOf<'x, V>: TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf> + + TryFrom, Error = VmErrorOf>, + VmErrorOf: + From> + From> + From, +{ + cosmwasm_call(vm, &serde_json::to_vec(message).map_err(|_| ExecutorError::FailedToSerialize)?) +} diff --git a/composable/vm/src/has.rs b/composable/vm/src/has.rs new file mode 100644 index 00000000..6335fda3 --- /dev/null +++ b/composable/vm/src/has.rs @@ -0,0 +1,5 @@ +// has.rs --- + +pub trait Has { + fn get(&self) -> T; +} diff --git a/composable/vm/src/input.rs b/composable/vm/src/input.rs new file mode 100644 index 00000000..b11fcbc2 --- /dev/null +++ b/composable/vm/src/input.rs @@ -0,0 +1,7 @@ +// input.rs --- + +pub type OutputOf = ::Output; + +pub trait Input { + type Output; +} diff --git a/composable/vm/src/lib.rs b/composable/vm/src/lib.rs new file mode 100644 index 00000000..6600feac --- /dev/null +++ b/composable/vm/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(trait_alias)] + +extern crate alloc; + +pub mod executor; +pub mod has; +pub mod input; +pub mod memory; +pub mod system; +pub mod tagged; +pub mod transaction; +pub mod vm; diff --git a/composable/vm/src/memory.rs b/composable/vm/src/memory.rs new file mode 100644 index 00000000..2184488b --- /dev/null +++ b/composable/vm/src/memory.rs @@ -0,0 +1,251 @@ +// memory.rs --- + +use alloc::{vec, vec::Vec}; +use core::{fmt::Debug, marker::PhantomData}; +use num::traits::Zero; + +pub type PointerOf = ::Pointer; +pub trait Pointable { + type Pointer: Debug + Ord + Copy + Zero + TryFrom + TryInto; +} + +#[allow(clippy::module_name_repetitions)] +pub trait ReadWriteMemory: + ReadableMemory::Error> + WritableMemory +{ +} + +impl ReadWriteMemory for T where + T: ReadableMemory::Error> + WritableMemory +{ +} + +pub type ReadableMemoryErrorOf = ::Error; +#[allow(clippy::module_name_repetitions)] +pub trait ReadableMemory: Pointable { + type Error: From; + fn read(&self, offset: Self::Pointer, buffer: &mut [u8]) -> Result<(), Self::Error>; +} + +pub type WritableMemoryErrorOf = ::Error; +#[allow(clippy::module_name_repetitions)] +pub trait WritableMemory: Pointable { + type Error: From; + fn write(&mut self, offset: Self::Pointer, buffer: &[u8]) -> Result<(), Self::Error>; +} + +pub struct Write<'a, 'b, M: Pointable>(pub &'a mut M, pub M::Pointer, pub &'b [u8]); +pub struct TypedWrite<'a, 'b, M: Pointable, T>(pub &'a mut M, pub M::Pointer, pub &'b T); +pub struct Read<'a, M: Pointable>(pub &'a M, pub M::Pointer); +pub struct LimitedRead<'a, M: Pointable>(pub &'a M, pub M::Pointer, pub M::Pointer); +pub struct LimitedTypedRead<'a, M: Pointable>(pub &'a M, pub M::Pointer, pub M::Pointer); + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum MemoryReadError { + InvalidTypeSize, + OverflowLimit, + InvalidPointer, + OutOfRange, + LengthExceedsCapacity, + ZeroOffset, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum MemoryWriteError { + RegionTooSmall, + BufferSizeOverflowPointer, + TypeSizeOverflow, +} + +/// +fn validate_region( + region: &Region, + limit: Option, +) -> Result<(), MemoryReadError> { + if region.offset.is_zero() { + return Err(MemoryReadError::LengthExceedsCapacity); + } + + if region.length > region.capacity { + return Err(MemoryReadError::OutOfRange); + } + + if let Some(limit) = limit { + if region.length > limit { + return Err(MemoryReadError::OverflowLimit); + } + } + + Ok(()) +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[repr(transparent)] +#[allow(clippy::module_name_repetitions)] +pub struct FromMemory(pub T); +impl<'a, M, T> TryFrom> for FromMemory +where + T: Sized, + M: ReadableMemory, +{ + type Error = M::Error; + fn try_from(Read(memory, offset): Read<'a, M>) -> Result { + log::trace!("FromMemory"); + let size = core::mem::size_of::(); + // see safety requirement of `from_raw_parts_mut` + if size == 0 || size > isize::MAX as usize { + Err(MemoryReadError::InvalidTypeSize.into()) + } else { + let mut t: T = unsafe { core::mem::zeroed() }; + let buffer = unsafe { + core::slice::from_raw_parts_mut(core::ptr::addr_of_mut!(t).cast::(), size) + }; + memory.read(offset, buffer)?; + Ok(FromMemory(t)) + } + } +} + +/// Private +/// +#[repr(C)] +#[derive(Default, Clone, Copy, Debug)] +struct Region { + offset: Pointer, + capacity: Pointer, + length: Pointer, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[repr(transparent)] +pub struct FromRegion(pub T); +impl<'a, M, T> TryFrom> for FromRegion +where + T: Sized, + M: ReadableMemory, +{ + type Error = M::Error; + fn try_from( + LimitedTypedRead(memory, pointer, limit): LimitedTypedRead<'a, M>, + ) -> Result { + log::trace!("FromRegion"); + let FromMemory(region) = FromMemory::>::try_from(Read(memory, pointer))?; + validate_region::(®ion, Some(limit))?; + let FromMemory(value) = FromMemory::::try_from(Read(memory, region.offset))?; + Ok(FromRegion(value)) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +#[repr(transparent)] +pub struct RawFromRegion(pub Vec); +impl<'a, M> TryFrom> for RawFromRegion +where + M: ReadableMemory, +{ + type Error = M::Error; + fn try_from( + LimitedRead(memory, pointer, limit): LimitedRead<'a, M>, + ) -> Result { + log::trace!("RawFromRegion: {:?}", pointer); + let FromMemory(region) = FromMemory::>::try_from(Read(memory, pointer))?; + validate_region::(®ion, Some(limit))?; + let mut buffer = + vec![0u8; region.length.try_into().map_err(|_| MemoryReadError::InvalidPointer)?]; + memory.read(region.offset, &mut buffer)?; + Ok(RawFromRegion(buffer)) + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct RawIntoMemory; +impl<'a, 'b, M> TryFrom> for RawIntoMemory +where + M: WritableMemory, +{ + type Error = M::Error; + fn try_from(Write(memory, offset, buffer): Write<'a, 'b, M>) -> Result { + log::trace!("RawIntoMemory"); + memory.write(offset, buffer)?; + Ok(RawIntoMemory) + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct IntoMemory(pub PhantomData); +impl<'a, 'b, M, T> TryFrom> for IntoMemory +where + T: Sized, + M: WritableMemory, +{ + type Error = M::Error; + fn try_from( + TypedWrite(memory, offset, value): TypedWrite<'a, 'b, M, T>, + ) -> Result { + log::trace!("IntoMemory"); + // safety requirement of `from_raw_parts` + if core::mem::size_of::() > isize::MAX as usize { + return Err(MemoryWriteError::TypeSizeOverflow.into()); + } + let buffer = unsafe { + core::slice::from_raw_parts((value as *const T).cast::(), core::mem::size_of::()) + }; + memory.write(offset, buffer)?; + Ok(IntoMemory(PhantomData)) + } +} + +pub struct IntoRegion(pub PhantomData); +impl<'a, 'b, M, T> TryFrom> for IntoRegion +where + T: Sized, + M: ReadWriteMemory, +{ + type Error = ::Error; + fn try_from( + TypedWrite(memory, pointer, value): TypedWrite<'a, 'b, M, T>, + ) -> Result { + log::trace!("IntoRegion"); + let FromMemory(mut region) = + FromMemory::>::try_from(Read(memory, pointer)) + .map_err(|_| MemoryWriteError::BufferSizeOverflowPointer)?; + log::trace!("Region: {:?}", region); + let len = M::Pointer::try_from(core::mem::size_of::()) + .map_err(|_| MemoryWriteError::BufferSizeOverflowPointer)?; + if region.capacity < len { + Err(::Error::from(MemoryWriteError::RegionTooSmall)) + } else { + let _ = IntoMemory::try_from(TypedWrite(memory, region.offset, value))?; + region.length = len; + let _ = IntoMemory::try_from(TypedWrite(memory, pointer, ®ion))?; + Ok(IntoRegion(PhantomData)) + } + } +} + +pub struct RawIntoRegion; +impl<'a, 'b, M> TryFrom> for RawIntoRegion +where + M: ReadWriteMemory, +{ + type Error = ::Error; + fn try_from(Write(memory, pointer, value): Write<'a, 'b, M>) -> Result { + log::trace!("RawIntoRegion"); + let FromMemory(mut region) = + FromMemory::>::try_from(Read(memory, pointer)) + .map_err(|_| MemoryWriteError::BufferSizeOverflowPointer)?; + log::trace!("Region: {:?}", region); + let len = M::Pointer::try_from(value.len()) + .map_err(|_| MemoryWriteError::BufferSizeOverflowPointer)?; + if region.capacity < len { + Err(::Error::from(MemoryWriteError::RegionTooSmall)) + } else { + let _ = RawIntoMemory::try_from(Write(memory, region.offset, value))?; + region.length = len; + let _ = IntoMemory::try_from(TypedWrite(memory, pointer, ®ion))?; + Ok(RawIntoRegion) + } + } +} diff --git a/composable/vm/src/system.rs b/composable/vm/src/system.rs new file mode 100644 index 00000000..2b4e569d --- /dev/null +++ b/composable/vm/src/system.rs @@ -0,0 +1,910 @@ +// system.rs --- + +#[cfg(feature = "stargate")] +use crate::executor::ibc::{ + IbcChannelCloseCall, IbcChannelConnectCall, IbcPacketAckCall, IbcPacketReceiveCall, + IbcPacketTimeoutCall, +}; +#[cfg(feature = "stargate")] +use crate::executor::AsFunctionName; +use crate::{ + executor::{ + cosmwasm_call, AllocateCall, CosmwasmCallInput, CosmwasmCallWithoutInfoInput, + CosmwasmQueryResult, DeallocateCall, DeserializeLimit, ExecuteCall, ExecutorError, HasInfo, + InstantiateCall, MigrateCall, QueryResult, ReadLimit, ReplyCall, Unit, + }, + has::Has, + input::{Input, OutputOf}, + memory::{PointerOf, ReadWriteMemory, ReadableMemoryErrorOf, WritableMemoryErrorOf}, + transaction::{Transactional, TransactionalErrorOf}, + vm::{ + VmAddressOf, VmErrorOf, VmGasCheckpoint, VmInputOf, VmMessageCustomOf, VmOutputOf, + VmQueryCustomOf, VM, + }, +}; +use alloc::{ + fmt::Display, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::fmt::Debug; +use cosmwasm_std::{ + Addr, AllBalanceResponse, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, + ContractResult, CosmosMsg, Env, Event, MessageInfo, QueryRequest, Reply, ReplyOn, Response, + SubMsgResponse, SubMsgResult, SupplyResponse, SystemResult, WasmMsg, WasmQuery, +}; +#[cfg(feature = "stargate")] +use cosmwasm_std::{Empty, IbcMsg}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +// WasmModuleEventType is stored with any contract TX that returns non empty EventAttributes +pub const WASM_MODULE_EVENT_TYPE: &str = "wasm"; + +// CustomContractEventPrefix contracts can create custom events. To not mix them with other system +// events they got the `wasm-` prefix. +pub const CUSTOM_CONTRACT_EVENT_PREFIX: &str = "wasm-"; + +// Minimum length of an event type +pub const CUSTOM_CONTRACT_EVENT_TYPE_MIN_LENGTH: usize = 2; + +pub const WASM_MODULE_EVENT_RESERVED_PREFIX: &str = "_"; + +#[allow(unused)] +#[allow(clippy::module_name_repetitions)] +pub enum SystemEventType { + StoreCode, + Instantiate, + Execute, + Migrate, + PinCode, + UnpinCode, + Sudo, + Reply, + #[cfg(feature = "stargate")] + IbcChannelConnect, + #[cfg(feature = "stargate")] + IbcChannelClose, + #[cfg(feature = "stargate")] + IbcPacketReceive, + #[cfg(feature = "stargate")] + IbcPacketAck, + #[cfg(feature = "stargate")] + IbcPacketTimeout, +} + +#[allow(clippy::module_name_repetitions)] +pub enum SystemAttributeKey { + ContractAddr, + CodeID, + ResultDataHex, + Feature, +} + +#[allow(clippy::module_name_repetitions)] +pub struct SystemAttribute { + key: SystemAttributeKey, + value: String, +} + +#[allow(clippy::module_name_repetitions)] +pub struct SystemEvent { + ty: SystemEventType, + attributes: Vec, +} + +impl From for Attribute { + fn from(SystemAttribute { key, value }: SystemAttribute) -> Self { + let attr_str = match key { + SystemAttributeKey::ContractAddr => "_contract_address", + SystemAttributeKey::CodeID => "code_id", + SystemAttributeKey::ResultDataHex => "result", + SystemAttributeKey::Feature => "feature", + }; + + Attribute { key: attr_str.into(), value } + } +} + +impl Display for SystemEventType { + fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + let event_str = match self { + SystemEventType::StoreCode => "store_code", + SystemEventType::Instantiate => "instantiate", + SystemEventType::Execute => "execute", + SystemEventType::Migrate => "migrate", + SystemEventType::PinCode => "pin_code", + SystemEventType::UnpinCode => "unpin_code", + SystemEventType::Sudo => "sudo", + SystemEventType::Reply => "reply", + #[cfg(feature = "stargate")] + SystemEventType::IbcChannelConnect => IbcChannelConnectCall::::NAME, + #[cfg(feature = "stargate")] + SystemEventType::IbcChannelClose => IbcChannelCloseCall::::NAME, + #[cfg(feature = "stargate")] + SystemEventType::IbcPacketReceive => IbcPacketReceiveCall::::NAME, + #[cfg(feature = "stargate")] + SystemEventType::IbcPacketAck => IbcPacketAckCall::::NAME, + #[cfg(feature = "stargate")] + SystemEventType::IbcPacketTimeout => IbcPacketTimeoutCall::::NAME, + }; + + write!(f, "{event_str}") + } +} + +impl From for Event { + fn from(sys_event: SystemEvent) -> Self { + Event::new(sys_event.ty.to_string()) + .add_attributes(sys_event.attributes.into_iter().map(Into::::into)) + } +} + +pub trait EventHasCodeId { + const HAS_CODE_ID: bool; +} + +impl EventHasCodeId for InstantiateCall { + const HAS_CODE_ID: bool = true; +} + +impl EventHasCodeId for ExecuteCall { + const HAS_CODE_ID: bool = false; +} + +impl EventHasCodeId for MigrateCall { + const HAS_CODE_ID: bool = true; +} + +impl EventHasCodeId for ReplyCall { + const HAS_CODE_ID: bool = false; +} + +#[cfg(feature = "stargate")] +impl EventHasCodeId for IbcChannelConnectCall { + const HAS_CODE_ID: bool = false; +} + +#[cfg(feature = "stargate")] +impl EventHasCodeId for IbcChannelCloseCall { + const HAS_CODE_ID: bool = false; +} + +#[cfg(feature = "stargate")] +impl EventHasCodeId for IbcPacketReceiveCall { + const HAS_CODE_ID: bool = false; +} + +#[cfg(feature = "stargate")] +impl EventHasCodeId for IbcPacketAckCall { + const HAS_CODE_ID: bool = false; +} + +#[cfg(feature = "stargate")] +impl EventHasCodeId for IbcPacketTimeoutCall { + const HAS_CODE_ID: bool = false; +} + +pub trait EventIsTyped { + const TYPE: SystemEventType; +} + +impl EventIsTyped for InstantiateCall { + const TYPE: SystemEventType = SystemEventType::Instantiate; +} + +impl EventIsTyped for ExecuteCall { + const TYPE: SystemEventType = SystemEventType::Execute; +} + +impl EventIsTyped for MigrateCall { + const TYPE: SystemEventType = SystemEventType::Migrate; +} + +impl EventIsTyped for ReplyCall { + const TYPE: SystemEventType = SystemEventType::Reply; +} + +#[cfg(feature = "stargate")] +impl EventIsTyped for IbcChannelConnectCall { + const TYPE: SystemEventType = SystemEventType::IbcChannelConnect; +} + +#[cfg(feature = "stargate")] +impl EventIsTyped for IbcChannelCloseCall { + const TYPE: SystemEventType = SystemEventType::IbcChannelClose; +} + +#[cfg(feature = "stargate")] +impl EventIsTyped for IbcPacketReceiveCall { + const TYPE: SystemEventType = SystemEventType::IbcPacketReceive; +} + +#[cfg(feature = "stargate")] +impl EventIsTyped for IbcPacketAckCall { + const TYPE: SystemEventType = SystemEventType::IbcPacketAck; +} + +#[cfg(feature = "stargate")] +impl EventIsTyped for IbcPacketTimeoutCall { + const TYPE: SystemEventType = SystemEventType::IbcPacketTimeout; +} + +pub trait HasEvent { + fn generate_event(address: String, code_id: CosmwasmCodeId) -> Event; +} + +impl HasEvent for I +where + I: Input + EventHasCodeId + EventIsTyped, +{ + fn generate_event(address: String, code_id: CosmwasmCodeId) -> Event { + let addr_attr = SystemAttribute { key: SystemAttributeKey::ContractAddr, value: address }; + let attributes = if I::HAS_CODE_ID { + vec![ + addr_attr, + SystemAttribute { key: SystemAttributeKey::CodeID, value: code_id.to_string() }, + ] + } else { + vec![addr_attr] + }; + SystemEvent { ty: I::TYPE, attributes }.into() + } +} + +/// Errors likely to happen while a VM is executing. +#[derive(Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum SystemError { + UnsupportedMessage, + FailedToSerialize, + ContractExecutionFailure(String), + ImmutableCantMigrate, + MustBeAdmin, + ReservedEventPrefixIsUsed, + EmptyEventKey, + EmptyEventValue, + EventTypeIsTooShort, +} + +#[derive(Debug)] +enum SubCallContinuation { + Continue(Option), + Reply(SubMsgResult), + Abort(E), +} + +pub type CosmwasmCodeId = u64; + +/// Minimum metadata associated to contracts. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct CosmwasmContractMeta { + pub code_id: CosmwasmCodeId, + pub admin: Option, + pub label: String, +} + +pub trait CosmwasmBaseVM = VM< + ContractMeta = CosmwasmContractMeta>, + StorageKey = Vec, + StorageValue = Vec, + > + ReadWriteMemory + + Transactional + + Has + + Has +where + VmMessageCustomOf: DeserializeOwned + Debug, + VmQueryCustomOf: DeserializeOwned + Debug, + VmAddressOf: Clone + TryFrom> + Into, + VmErrorOf: From> + + From> + + From + + From + + From> + + Debug, + PointerOf: for<'x> TryFrom, Error = VmErrorOf>; + +pub trait CosmwasmCallVM = CosmwasmBaseVM +where + I: Input + HasInfo + HasEvent, + OutputOf: Into>>> + + From>>>; + +pub trait CosmwasmDynamicVM = CosmwasmBaseVM +where + for<'x> Unit: TryFrom, Error = VmErrorOf>, + for<'x> VmInputOf<'x, Self>: TryFrom>, Error = VmErrorOf> + + TryFrom>, Error = VmErrorOf> + + TryFrom< + CosmwasmCallInput<'x, PointerOf, InstantiateCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, ExecuteCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, ReplyCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallWithoutInfoInput<'x, PointerOf, ReplyCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallWithoutInfoInput<'x, PointerOf, MigrateCall>>, + Error = VmErrorOf, + > + TryFrom, I>, Error = VmErrorOf> + + TryFrom, I>, Error = VmErrorOf>, + I: Input + HasInfo + HasEvent, + OutputOf: DeserializeOwned + ReadLimit + DeserializeLimit; + +#[cfg(feature = "stargate")] +/// Extra constraints required by stargate enabled `CosmWasm` VM (a.k.a. IBC capable). +pub trait StargateCosmwasmCallVM = CosmwasmBaseVM +where + for<'x> VmInputOf<'x, Self>: TryFrom< + CosmwasmCallInput<'x, PointerOf, IbcChannelConnectCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, IbcChannelCloseCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, IbcPacketReceiveCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, IbcPacketAckCall>>, + Error = VmErrorOf, + > + TryFrom< + CosmwasmCallInput<'x, PointerOf, IbcPacketTimeoutCall>>, + Error = VmErrorOf, + >; + +#[cfg(not(feature = "stargate"))] +pub trait StargateCosmwasmCallVM =; + +pub fn cosmwasm_system_entrypoint_serialize( + vm: &mut V, + message: &M, +) -> Result<(Option, Vec), VmErrorOf> +where + V: CosmwasmCallVM + CosmwasmDynamicVM + StargateCosmwasmCallVM, + M: Serialize, +{ + cosmwasm_system_entrypoint_serialize_hook(vm, message, |vm, msg| { + cosmwasm_call::(vm, msg).map(Into::into) + }) +} + +/// Extra helper to dispatch a typed message, serializing on the go. +pub fn cosmwasm_system_entrypoint_serialize_hook( + vm: &mut V, + message: &M, + hook: impl FnOnce( + &mut V, + &[u8], + ) -> Result>>, VmErrorOf>, +) -> Result<(Option, Vec), VmErrorOf> +where + V: CosmwasmCallVM + StargateCosmwasmCallVM, + M: Serialize, +{ + cosmwasm_system_entrypoint_hook( + vm, + &serde_json::to_vec(message).map_err(|_| SystemError::FailedToSerialize)?, + hook, + ) +} + +pub fn cosmwasm_system_entrypoint( + vm: &mut V, + message: &[u8], +) -> Result<(Option, Vec), VmErrorOf> +where + V: CosmwasmCallVM + CosmwasmDynamicVM + StargateCosmwasmCallVM, +{ + log::debug!("SystemEntrypoint"); + cosmwasm_system_entrypoint_hook(vm, message, |vm, msg| { + cosmwasm_call::(vm, msg).map(Into::into) + }) +} + +/// High level dispatch for a `CosmWasm` VM. +/// This call will manage and handle subcall as well as the transactions etc... +/// The implementation must be semantically valid w.r.t +/// +/// Returns either the value produced by the contract along the generated events or a `VmErrorOf` +pub fn cosmwasm_system_entrypoint_hook( + vm: &mut V, + message: &[u8], + hook: impl FnOnce( + &mut V, + &[u8], + ) -> Result>>, VmErrorOf>, +) -> Result<(Option, Vec), VmErrorOf> +where + V: CosmwasmCallVM + StargateCosmwasmCallVM, +{ + log::debug!("SystemEntrypointHook"); + let mut events = Vec::::new(); + let mut event_handler = |event: Event| { + events.push(event); + }; + vm.transaction_begin()?; + match cosmwasm_system_run_hook::(vm, message, &mut event_handler, hook) { + Ok(data) => { + vm.transaction_commit()?; + Ok((data, events)) + }, + Err(e) => { + vm.transaction_rollback()?; + Err(e) + }, + } +} + +/// Set `new_code_id` as the code id of the contract `contract_addr` +/// +/// Fails if the caller is not the admin of the contract +pub fn migrate( + vm: &mut V, + sender: VmAddressOf, + contract_addr: VmAddressOf, + new_code_id: CosmwasmCodeId, +) -> Result<(), VmErrorOf> { + let CosmwasmContractMeta { admin, label, .. } = vm.contract_meta(contract_addr.clone())?; + ensure_admin::(&sender.into(), admin.clone())?; + vm.set_contract_meta( + contract_addr, + CosmwasmContractMeta { code_id: new_code_id, admin, label }, + )?; + Ok(()) +} + +/// Set `new_admin` as the new admin of the contract `contract_addr` +/// +/// Fails if the caller is not currently admin of the target contract. +pub fn update_admin( + vm: &mut V, + sender: &Addr, + contract_addr: VmAddressOf, + new_admin: Option>, +) -> Result<(), VmErrorOf> { + let CosmwasmContractMeta { code_id, admin, label } = vm.contract_meta(contract_addr.clone())?; + ensure_admin::(sender, admin)?; + vm.set_contract_meta(contract_addr, CosmwasmContractMeta { code_id, admin: new_admin, label })?; + Ok(()) +} + +fn ensure_admin( + sender: &Addr, + contract_admin: Option>, +) -> Result<(), VmErrorOf> { + match contract_admin.map(Into::::into) { + None => Err(SystemError::ImmutableCantMigrate.into()), + Some(admin) if admin == *sender => Ok(()), + _ => Err(SystemError::MustBeAdmin.into()), + } +} + +fn sanitize_custom_attributes( + mut attributes: Vec, + contract_address: String, +) -> Result, SystemError> { + for attr in attributes.iter_mut() { + let new_key = attr.key.trim(); + if new_key.is_empty() { + return Err(SystemError::EmptyEventKey); + } + + let new_value = attr.value.trim(); + if new_value.is_empty() { + return Err(SystemError::EmptyEventValue); + } + + // this must be checked after being trimmed + if new_key.starts_with(WASM_MODULE_EVENT_RESERVED_PREFIX) { + return Err(SystemError::ReservedEventPrefixIsUsed); + } + + attr.key = new_key.into(); + attr.value = new_value.into(); + } + + // contract address attribute is added to every event + attributes.push( + SystemAttribute { key: SystemAttributeKey::ContractAddr, value: contract_address }.into(), + ); + + Ok(attributes) +} + +#[allow(clippy::too_many_lines)] +fn dispatch_submessage( + vm: &mut V, + caller_contract: &Addr, + msg: CosmosMsg>, + gas_limit: Option, + event_handler: &mut dyn FnMut(Event), +) -> Result<(Option, Vec), VmErrorOf> +where + V: CosmwasmCallVM + StargateCosmwasmCallVM, +{ + // Gas might be limited for the sub message execution. + vm.gas_checkpoint_push(match gas_limit { + Some(limit) => VmGasCheckpoint::Limited(limit), + None => VmGasCheckpoint::Unlimited, + })?; + + let mut sub_events = Vec::::new(); + + // Events dispatched by a submessage are added to both the + // submessage events and parent events. + let mut sub_event_handler = |event: Event| { + event_handler(event.clone()); + sub_events.push(event); + }; + + let sub_result = (|| match msg { + CosmosMsg::Custom(message) => + vm.message_custom(message, &mut sub_event_handler).map_err(Into::into), + CosmosMsg::Wasm(wasm_message) => match wasm_message { + WasmMsg::Execute { contract_addr, msg, funds } => { + let vm_contract_addr = contract_addr.try_into()?; + vm.continue_execute(vm_contract_addr, funds, &msg, &mut sub_event_handler) + }, + WasmMsg::Instantiate { admin, code_id, msg, funds, label } => vm + .continue_instantiate( + CosmwasmContractMeta { + code_id, + admin: match admin { + Some(admin) => Some(admin.try_into()?), + None => None, + }, + label, + }, + funds, + &msg, + &mut sub_event_handler, + ) + .map(|(_, data)| data), + WasmMsg::Instantiate2 { admin, code_id, label, msg, funds, salt } => vm + .continue_instantiate2( + CosmwasmContractMeta { + code_id, + admin: match admin { + Some(admin) => Some(admin.try_into()?), + None => None, + }, + label, + }, + funds, + &salt, + &msg, + &mut sub_event_handler, + ) + .map(|(_, data)| data), + WasmMsg::Migrate { contract_addr, new_code_id, msg } => { + let contract_addr = VmAddressOf::::try_from(contract_addr)?; + let sender = VmAddressOf::::try_from(caller_contract.clone().into_string())?; + migrate::(vm, sender, contract_addr.clone(), new_code_id)?; + vm.continue_migrate(contract_addr, &msg, &mut sub_event_handler) + }, + WasmMsg::UpdateAdmin { contract_addr, admin: new_admin } => { + let new_admin = new_admin.try_into()?; + let vm_contract_addr = VmAddressOf::::try_from(contract_addr)?; + update_admin::(vm, caller_contract, vm_contract_addr, Some(new_admin))?; + Ok(None) + }, + WasmMsg::ClearAdmin { contract_addr } => { + let vm_contract_addr = VmAddressOf::::try_from(contract_addr)?; + update_admin::(vm, caller_contract, vm_contract_addr, None)?; + Ok(None) + }, + _ => Err(SystemError::UnsupportedMessage.into()), + }, + CosmosMsg::Bank(bank_message) => match bank_message { + BankMsg::Send { to_address, amount } => { + let vm_contract_addr = to_address.try_into()?; + vm.transfer(&vm_contract_addr, &amount)?; + Ok(None) + }, + BankMsg::Burn { amount } => { + vm.burn(&amount)?; + Ok(None) + }, + _ => Err(SystemError::UnsupportedMessage.into()), + }, + #[cfg(feature = "stargate")] + CosmosMsg::Ibc(ibc_message) => match ibc_message { + IbcMsg::Transfer { channel_id, to_address, amount, timeout, memo: _ } => { + vm.ibc_transfer(channel_id, to_address, amount, timeout)?; + Ok(None) + }, + IbcMsg::SendPacket { channel_id, data, timeout } => { + vm.ibc_send_packet(channel_id, data, timeout)?; + Ok(None) + }, + IbcMsg::CloseChannel { channel_id } => { + vm.ibc_close_channel(channel_id)?; + Ok(None) + }, + _ => Err(SystemError::UnsupportedMessage.into()), + }, + // TODO(hussein-aitlahcen): determine whether we handle. + #[cfg(feature = "stargate")] + CosmosMsg::Stargate { .. } => Err(SystemError::UnsupportedMessage.into()), + // TODO(hussein-aitlahcen): determine whether we handle. + #[cfg(feature = "stargate")] + CosmosMsg::Gov(_) => Err(SystemError::UnsupportedMessage.into()), + _ => Err(SystemError::UnsupportedMessage.into()), + })(); + + // Make sure we remove the checkpoint. + vm.gas_checkpoint_pop()?; + + sub_result.map(|data| (data, sub_events)) +} + +#[allow(clippy::too_many_lines)] +pub fn cosmwasm_system_run( + vm: &mut V, + message: &[u8], + event_handler: &mut dyn FnMut(Event), +) -> Result, VmErrorOf> +where + V: CosmwasmCallVM + CosmwasmDynamicVM + StargateCosmwasmCallVM, +{ + cosmwasm_system_run_hook(vm, message, event_handler, |vm, msg| { + cosmwasm_call::(vm, msg).map(Into::into) + }) +} + +pub fn cosmwasm_system_run_hook( + vm: &mut V, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + hook: impl FnOnce( + &mut V, + &[u8], + ) -> Result>>, VmErrorOf>, +) -> Result, VmErrorOf> +where + V: CosmwasmCallVM + StargateCosmwasmCallVM, +{ + log::debug!("SystemRun"); + let info: MessageInfo = vm.get(); + let env: Env = vm.get(); + vm.transfer_from( + &info.sender.clone().into_string().try_into()?, + &env.contract.address.clone().into_string().try_into()?, + info.funds.as_slice(), + )?; + let output = hook(vm, message).map(Into::into); + log::debug!("Output: {:?}", output); + match output { + Ok(ContractResult::Ok(response)) => + handle_ok_run_response(vm, event_handler, env, response), + Ok(ContractResult::Err(e)) => Err(SystemError::ContractExecutionFailure(e).into()), + Err(e) => Err(e), + } +} + +fn handle_ok_run_response( + vm: &mut V, + event_handler: &mut dyn FnMut(Event), + env: Env, + response: Response>, +) -> Result, VmErrorOf> +where + V: CosmwasmCallVM + StargateCosmwasmCallVM, +{ + let CosmwasmContractMeta { code_id, .. } = vm.running_contract_meta()?; + let event = I::generate_event(env.contract.address.clone().into_string(), code_id); + event_handler(event); + + // https://github.com/CosmWasm/wasmd/blob/ac92fdcf37388cc8dc24535f301f64395f8fb3da/x/wasm/keeper/events.go#L16 + if !response.attributes.is_empty() { + let attributes = sanitize_custom_attributes( + response.attributes, + env.contract.address.clone().into_string(), + )?; + event_handler(Event::new(WASM_MODULE_EVENT_TYPE).add_attributes(attributes)); + } + + // Embed orphan attributes in a custom contract event. + // https://github.com/CosmWasm/wasmd/blob/ac92fdcf37388cc8dc24535f301f64395f8fb3da/x/wasm/keeper/events.go#L29 + for event in response.events { + let ty = event.ty.trim(); + if ty.len() < CUSTOM_CONTRACT_EVENT_TYPE_MIN_LENGTH { + return Err(SystemError::EventTypeIsTooShort.into()); + } + let attributes = sanitize_custom_attributes( + event.attributes, + env.contract.address.clone().into_string(), + )?; + let ty = [CUSTOM_CONTRACT_EVENT_PREFIX, ty].concat(); + event_handler(Event::new(ty).add_attributes(attributes)); + } + + // Fold dispatch over the submessages. If an exception occur (unless + // expected reply on error), we abort. Otherwise, it's up to the parent + // contract to decide in each case. + let mut data = response.data; + for submsg in response.messages { + log::debug!("Executing submessages"); + + // For each submessages, we might rollback and reply the failure to the + // parent contract. Hence, a new state tx must be created prior to the + // call. + vm.transaction_begin()?; + + // The result MUST be captured to determine whether we rollback or + // commit the local transaction. We MUST not return using something + // like the questionmark operator, as we want to catch both the success + // and failure branches here. Both branches may be used depending on + // the reply attached to the message. See reply_on. + let sub_res = dispatch_submessage( + vm, + &env.contract.address, + submsg.msg, + submsg.gas_limit, + event_handler, + ); + + log::debug!("Submessage result: {:?}", sub_res); + + let sub_cont = match (sub_res, submsg.reply_on) { + // If the submessage suceeded and no reply was asked or only on + // error, the call is considered successful and state change is + // comitted. + (Ok((data, _)), ReplyOn::Never | ReplyOn::Error) => { + log::debug!("Commit & Continue"); + vm.transaction_commit()?; + SubCallContinuation::Continue(data) + }, + // Similarly to previous case, if the submessage suceeded and we ask + // for a reply, the call is considered successful and we redispatch + // a reply to the parent contract. + (Ok((data, events)), ReplyOn::Always | ReplyOn::Success) => { + log::debug!("Commit & Reply"); + vm.transaction_commit()?; + // TODO: Handle msg_response + SubCallContinuation::Reply(SubMsgResult::Ok(SubMsgResponse { + events, + data, + msg_responses: Vec::new(), + })) + }, + // If the submessage failed and a reply is required, rollback the + // state change and dispatch a reply to the parent contract. The + // transaction is not aborted unless the reply also fails + // (cascading). + (Err(e), ReplyOn::Always | ReplyOn::Error) => { + log::debug!("Rollback & Reply"); + vm.transaction_rollback()?; + SubCallContinuation::Reply(SubMsgResult::Err(format!("{e:?}"))) + }, + // If an error happen and we did not expected it, abort the whole + // transaction. + (Err(e), ReplyOn::Never | ReplyOn::Success) => { + log::debug!("Rollback & Abort"); + vm.transaction_rollback()?; + SubCallContinuation::Abort(e) + }, + }; + + log::debug!("Submessage cont: {:?}", sub_cont); + + data = match sub_cont { + // If the submessage execution suceeded and we don't want to reply, + // proceed by overwritting the current `data` field if a new one has + // been yield by the current call. + SubCallContinuation::Continue(new_data) => new_data.or(data), + // An exception occured and we did not expected it (no reply on + // error), abort the current execution. + SubCallContinuation::Abort(e) => return Err(e), + // The parent contract is expected to get a reply, try to execute + // the reply and optionally overwrite the current data with with the + // one yield by the reply. + SubCallContinuation::Reply(response) => { + // TODO: Handle payload and gas_used + let new_data = vm.continue_reply( + Reply { + id: submsg.id, + result: response.clone(), + payload: Binary::default(), + gas_used: 0, + }, + event_handler, + )?; + // Tricky situation, either the reply provide a new value that + // we use, or we use the submessage value or we keep the current + // one. + new_data.or_else(|| Result::from(response).ok().and_then(|x| x.data)).or(data) + }, + } + } + + Ok(data) +} + +/// High level query for a `CosmWasm` VM. +/// +/// Returns either the value returned by the contract `query` export or a `VmErrorOf` +pub fn cosmwasm_system_query( + vm: &mut V, + request: QueryRequest>, +) -> Result, VmErrorOf> +where + V: CosmwasmBaseVM, +{ + log::debug!("SystemQuery"); + match request { + QueryRequest::Custom(query) => Ok(vm.query_custom(query)?), + QueryRequest::Bank(bank_query) => match bank_query { + BankQuery::Balance { address, denom } => { + let vm_account_addr = address.try_into()?; + let amount = vm.balance(&vm_account_addr, denom)?; + let serialized_info = serde_json::to_vec(&BalanceResponse::new(amount)) + .map_err(|_| SystemError::FailedToSerialize)?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(serialized_info)))) + }, + BankQuery::AllBalances { address } => { + let vm_account_addr = address.try_into()?; + let amount = vm.all_balance(&vm_account_addr)?; + let serialized_info = serde_json::to_vec(&AllBalanceResponse::new(amount)) + .map_err(|_| SystemError::FailedToSerialize)?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(serialized_info)))) + }, + BankQuery::Supply { denom } => { + // `SupplyResponse` is non-exhaustive, so can't define as `SupplyResponse { denom }` + let supply = SupplyResponse::new(vm.supply(denom)?); + let serialized_info = + serde_json::to_vec(&supply).map_err(|_| SystemError::FailedToSerialize)?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(serialized_info)))) + }, + _ => Err(SystemError::UnsupportedMessage.into()), + }, + QueryRequest::Wasm(wasm_query) => match wasm_query { + WasmQuery::Smart { contract_addr, msg: message } => { + let vm_contract_addr = contract_addr.try_into()?; + let QueryResult(output) = vm.continue_query(vm_contract_addr, &message)?; + Ok(SystemResult::Ok(output)) + }, + WasmQuery::Raw { contract_addr, key } => { + let vm_contract_addr = contract_addr.try_into()?; + let value = vm.query_raw(vm_contract_addr, key.to_vec())?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(value.unwrap_or_default())))) + }, + WasmQuery::ContractInfo { contract_addr } => { + let vm_contract_addr = contract_addr.try_into()?; + let info = vm.query_contract_info(vm_contract_addr)?; + let serialized_info = + serde_json::to_vec(&info).map_err(|_| SystemError::FailedToSerialize)?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(serialized_info)))) + }, + WasmQuery::CodeInfo { code_id } => { + let info = vm.query_code_info(code_id)?; + let serialized_info = + serde_json::to_vec(&info).map_err(|_| SystemError::FailedToSerialize)?; + Ok(SystemResult::Ok(ContractResult::Ok(Binary::new(serialized_info)))) + }, + _ => Err(SystemError::UnsupportedMessage.into()), + }, + _ => Err(SystemError::UnsupportedMessage.into()), + } +} + +/// High level query for a `CosmWasm` VM with remarshalling for contract execution continuation. +/// +/// Returns either the JSON serialized value returned by the contract `query` export or a +/// `VmErrorOf` +pub fn cosmwasm_system_query_raw( + vm: &mut V, + request: QueryRequest>, +) -> Result> +where + V: CosmwasmBaseVM, +{ + log::debug!("SystemQueryRaw"); + let output = cosmwasm_system_query(vm, request)?; + Ok(Binary::new(serde_json::to_vec(&output).map_err(|_| SystemError::FailedToSerialize)?)) +} diff --git a/composable/vm/src/tagged.rs b/composable/vm/src/tagged.rs new file mode 100644 index 00000000..0fdac9cd --- /dev/null +++ b/composable/vm/src/tagged.rs @@ -0,0 +1,13 @@ +// tagged.rs --- + +use core::marker::PhantomData; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[repr(transparent)] +pub struct Tagged(pub T, pub PhantomData); +impl Tagged { + #[inline] + pub fn new(t: T) -> Self { + Tagged(t, PhantomData) + } +} diff --git a/composable/vm/src/transaction.rs b/composable/vm/src/transaction.rs new file mode 100644 index 00000000..e7772f3c --- /dev/null +++ b/composable/vm/src/transaction.rs @@ -0,0 +1,10 @@ +// transaction.rs --- + +pub type TransactionalErrorOf = ::Error; + +pub trait Transactional { + type Error; + fn transaction_begin(&mut self) -> Result<(), Self::Error>; + fn transaction_commit(&mut self) -> Result<(), Self::Error>; + fn transaction_rollback(&mut self) -> Result<(), Self::Error>; +} diff --git a/composable/vm/src/vm.rs b/composable/vm/src/vm.rs new file mode 100644 index 00000000..0a5aa534 --- /dev/null +++ b/composable/vm/src/vm.rs @@ -0,0 +1,416 @@ +use crate::{ + executor::{CosmwasmQueryResult, QueryResult}, + input::Input, + system::CosmwasmCodeId, +}; +use alloc::{string::String, vec::Vec}; +use core::fmt::Debug; +#[cfg(feature = "stargate")] +use cosmwasm_std::IbcTimeout; +#[cfg(feature = "iterator")] +use cosmwasm_std::Order; +use cosmwasm_std::{ + Binary, CodeInfoResponse, Coin, ContractInfoResponse, Event, Reply, SystemResult, +}; + +use serde::de::DeserializeOwned; + +/// Gas checkpoint, used to meter sub-call gas usage. +#[allow(clippy::module_name_repetitions)] +pub enum VmGasCheckpoint { + /// Unlimited gas in a sub-call, the sub-call might exhaust the parent gas. + Unlimited, + /// Limited gas with fixed amount, the sub-call will only be able to execute under this gas + /// limit. + Limited(u64), +} + +/// Gasable VM calls. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum VmGas { + /// Instrumentation gas raised by the injected code. + Instrumentation { metered: u32 }, + /// Cost of `set_contract_meta`. + SetContractMeta, + /// Cost of `contract_meta`. + GetContractMeta, + /// Cost of `continue_query`. + ContinueQuery, + /// Cost of `continue_execute`. + ContinueExecute { nb_of_coins: u32 }, + /// Cost of `continue_instantiate`. + ContinueInstantiate { nb_of_coins: u32 }, + /// Cost of `continue_instantiate2` + ContinueInstantiate2 { nb_of_coins: u32 }, + /// Cost of `continue_migrate`. + ContinueMigrate, + /// Cost of `continue_reply` + ContinueReply, + /// Cost of `query_custom`. + QueryCustom, + /// Cost of `message_custom`. + MessageCustom, + /// Cost of `query_raw`. + QueryRaw, + /// Cost of `transfer`. + Transfer { nb_of_coins: u32 }, + /// Cost of `burn`. + Burn, + /// Cost of `balance`. + Balance, + /// Cost of `all_balance`. + AllBalance, + /// Cost of `supply` + Supply, + /// Cost of `query_contract_info`. + QueryContractInfo, + /// Cost of `query_code_info`. + QueryCodeInfo, + /// Cost of `db_read`. + DbRead, + /// Cost of `db_write`. + DbWrite, + /// Cost of `db_remove`. + DbRemove, + #[cfg(feature = "iterator")] + /// Cost of `db_scan`. + DbScan, + #[cfg(feature = "iterator")] + /// Cost of `db_next`. + DbNext, + /// Cost of `debug` + Debug, + /// Cost of `secp256k1_verify` + Secp256k1Verify, + /// Cost of `secp256k1_recover_pubkey` + Secp256k1RecoverPubkey, + /// Cost of `ed25519_verify` + Ed25519Verify, + /// Cost of `ed25519_batch_verify` + Ed25519BatchVerify, + /// Cost of `addr_validate` + AddrValidate, + /// Cost of `addr_canonicalize` + AddrCanonicalize, + /// Cost of `addr_humanize` + AddrHumanize, + #[cfg(feature = "stargate")] + /// Cost of `ibc_transfer`. + IbcTransfer, + #[cfg(feature = "stargate")] + /// Cost of `ibc_send_packet`. + IbcSendPacket, + #[cfg(feature = "stargate")] + /// Cost of `ibc_close_channel`. + IbcCloseChannel, +} + +#[allow(clippy::module_name_repetitions)] +pub type VmInputOf<'a, T> = ::Input<'a>; +#[allow(clippy::module_name_repetitions)] +pub type VmOutputOf<'a, T> = ::Output<'a>; +#[allow(clippy::module_name_repetitions)] +pub type VmErrorOf = ::Error; +#[allow(clippy::module_name_repetitions)] +pub type VmQueryCustomOf = ::QueryCustom; +#[allow(clippy::module_name_repetitions)] +pub type VmMessageCustomOf = ::MessageCustom; +#[allow(clippy::module_name_repetitions)] +pub type VmAddressOf = ::Address; +#[allow(clippy::module_name_repetitions)] +pub type VmCanonicalAddressOf = ::CanonicalAddress; +#[allow(clippy::module_name_repetitions)] +pub type VmStorageKeyOf = ::StorageKey; +#[allow(clippy::module_name_repetitions)] +pub type VmStorageValueOf = ::StorageValue; +#[allow(clippy::module_name_repetitions)] +pub type VmContractMetaOf = ::ContractMeta; + +/// A way of calling a VM. From the abstract `call` to `raw_call`. +pub trait VM: VMBase { + /// Execute an abstract call against the VM. + fn call<'a, I>(&mut self, input: I) -> Result + where + I: Input + TryInto, Error = Self::Error>, + I::Output: for<'x> TryFrom, Error = Self::Error>, + { + let input = input.try_into()?; + self.raw_call::(input) + } + + /// Execute a raw call against the VM. + fn raw_call<'a, O>(&mut self, input: Self::Input<'a>) -> Result + where + O: for<'x> TryFrom, Error = Self::Error>; +} + +/// Base functions required to be implemented by a VM to run `CosmWasm` contracts. +pub trait VMBase { + /// Input type, abstract type representing function inputs. + type Input<'a>; + /// Output type, abstract type representing function outputs. + type Output<'a>; + /// Custom query, also known as chain extension. + type QueryCustom: DeserializeOwned + Debug; + /// Custom message, also known as chain extension. + type MessageCustom: DeserializeOwned + Debug; + /// Metadata of a contract. + type ContractMeta; + /// Unique identifier for contract instances and users under the system. + type Address; + /// Binary representation of `Address`. + type CanonicalAddress; + /// Type of key used by the underlying DB. + type StorageKey; + /// Type of value used by the underlying DB. + type StorageValue; + /// Possible errors raised by this VM. + type Error; + + /// Get the contract metadata of the currently running contract. + fn running_contract_meta(&mut self) -> Result; + + #[cfg(feature = "iterator")] + /// Allows iteration over a set of key/value pairs, either forwards or backwards. + /// Returns an iterator ID that is unique within the Storage instance. + fn db_scan( + &mut self, + start: Option, + end: Option, + order: Order, + ) -> Result; + + #[cfg(feature = "iterator")] + /// Returns the next element of the iterator with the given ID. + fn db_next( + &mut self, + iterator_id: u32, + ) -> Result<(Self::StorageKey, Self::StorageValue), Self::Error>; + + /// Change the contract meta of a contract, actually migrating it. + fn set_contract_meta( + &mut self, + address: Self::Address, + new_contract_meta: Self::ContractMeta, + ) -> Result<(), Self::Error>; + + /// Get the contract metadata of a given contract. + fn contract_meta(&mut self, address: Self::Address) -> Result; + + /// Continue execution by calling query at the given contract address. + fn continue_query( + &mut self, + address: Self::Address, + message: &[u8], + ) -> Result; + + /// Continue execution by calling execute at the given contract address. + fn continue_execute( + &mut self, + address: Self::Address, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error>; + + /// Continue execution by instantiating the given contract `code_id`. + fn continue_instantiate( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error>; + + /// Continue execution by instantiating the given contract `code_id` with a predictable address. + /// Make sure to align with the implementation: https://github.com/CosmWasm/wasmd/blob/main/x/wasm/keeper/addresses.go + fn continue_instantiate2( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + salt: &[u8], + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error>; + + /// Continue execution by calling migrate at the given contract address. + fn continue_migrate( + &mut self, + address: Self::Address, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error>; + + /// Continue execution by calling reply at the given contract address. + fn continue_reply( + &mut self, + message: Reply, + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error>; + + /// Custom `CosmWasm` query. Usually a host extension. + fn query_custom( + &mut self, + query: Self::QueryCustom, + ) -> Result, Self::Error>; + + /// Custom `CosmWasm` message. Usually a host extension. + fn message_custom( + &mut self, + message: Self::MessageCustom, + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error>; + + /// Query raw value in a contract db. + fn query_raw( + &mut self, + address: Self::Address, + key: Self::StorageKey, + ) -> Result, Self::Error>; + + /// Transfer `funds` from `from` to `to` + fn transfer_from( + &mut self, + from: &Self::Address, + to: &Self::Address, + funds: &[Coin], + ) -> Result<(), Self::Error>; + + /// Transfer `funds` from the current contract to `to`. + fn transfer(&mut self, to: &Self::Address, funds: &[Coin]) -> Result<(), Self::Error>; + + /// Burn the `funds` from the current contract. + fn burn(&mut self, funds: &[Coin]) -> Result<(), Self::Error>; + + /// Query the balance of `denom` tokens. + fn balance(&mut self, account: &Self::Address, denom: String) -> Result; + + /// Query for the balance of all tokens. + fn all_balance(&mut self, account: &Self::Address) -> Result, Self::Error>; + + /// Query for the supply of a `denom`. + fn supply(&mut self, denom: String) -> Result; + + /// Query the contract info. + fn query_contract_info( + &mut self, + address: Self::Address, + ) -> Result; + + /// Query the code info. + fn query_code_info(&mut self, id: CosmwasmCodeId) -> Result; + + /// Log the message + fn debug(&mut self, message: Vec) -> Result<(), Self::Error>; + + /// Read an entry from the current contract db. + fn db_read(&mut self, key: Self::StorageKey) + -> Result, Self::Error>; + + /// Write into the current contract db. + fn db_write( + &mut self, + key: Self::StorageKey, + value: Self::StorageValue, + ) -> Result<(), Self::Error>; + + /// Remove an entry from the current contract db. + fn db_remove(&mut self, key: Self::StorageKey) -> Result<(), Self::Error>; + + /// Validates a human readable address. + /// NOTE: The return type is `Result, Self::Error>` but not + /// `Result<(), Self::Error>`, this is because errors that are related to address + /// validation are treated differently in wasmi vm. Any errors that are related to + /// address validation should be returned in the inner result like `Ok(Err(..))`. + fn addr_validate(&mut self, input: &str) -> Result, Self::Error>; + + /// Returns a canonical address from a human readable address. + /// see: [`Self::addr_validate`] + fn addr_canonicalize( + &mut self, + input: &str, + ) -> Result, Self::Error>; + + /// Returns a human readable address from a canonical address. + fn addr_humanize( + &mut self, + addr: &Self::CanonicalAddress, + ) -> Result, Self::Error>; + + /// Abort execution, called when the contract panic. + fn abort(&mut self, message: String) -> Result<(), Self::Error>; + + /// Charge gas value. + fn charge(&mut self, value: VmGas) -> Result<(), Self::Error>; + + /// Push a gas checkpoint, used to trap once the checkpoint is reached. + fn gas_checkpoint_push(&mut self, checkpoint: VmGasCheckpoint) -> Result<(), Self::Error>; + + /// Pop a previously pushed gas checkpoint. + fn gas_checkpoint_pop(&mut self) -> Result<(), Self::Error>; + + /// Ensure that some gas is available. + fn gas_ensure_available(&mut self) -> Result<(), Self::Error>; + + /// Verifies `message_hash` against a `signature` with a `public_key`, using the + /// secp256k1 ECDSA parametrization. + fn secp256k1_verify( + &mut self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result; + + /// Recovers a public key from a message hash and a signature. + /// + /// Returns the recovered pubkey in compressed form, which can be used + /// in `secp256k1_verify` directly. Any errors related to recovering the + /// public key should result in `Ok(Err(()))` + fn secp256k1_recover_pubkey( + &mut self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, ()>, Self::Error>; + + /// Verify `message` against a `signature`, with the `public_key` of the signer, using + /// the ed25519 elliptic curve digital signature parametrization / algorithm. + fn ed25519_verify( + &mut self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result; + + /// Performs batch Ed25519 signature verification. + fn ed25519_batch_verify( + &mut self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result; + + #[cfg(feature = "stargate")] + /// Transfer tokens over IBC. + fn ibc_transfer( + &mut self, + channel_id: String, + to_address: String, + amount: Coin, + timeout: IbcTimeout, + ) -> Result<(), Self::Error>; + + #[cfg(feature = "stargate")] + /// Send a packet over IBC. + fn ibc_send_packet( + &mut self, + channel_id: String, + data: Binary, + timeout: IbcTimeout, + ) -> Result<(), Self::Error>; + + #[cfg(feature = "stargate")] + /// Close an IBC channel. + fn ibc_close_channel(&mut self, channel_id: String) -> Result<(), Self::Error>; +} From 3df0758b2ebd9d0b816efef1b7ad721f04491526 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 11 Sep 2024 00:08:47 +0900 Subject: [PATCH 019/133] feat: Add cosmwasm-vm-wasmi --- Cargo.toml | 2 + composable/vm-wasmi/Cargo.toml | 41 + composable/vm-wasmi/LICENSE.md | 21 + .../vm-wasmi/fixtures/crypto_verify.wasm | Bin 0 -> 166358 bytes composable/vm-wasmi/fixtures/cw20_base.wasm | Bin 0 -> 272632 bytes composable/vm-wasmi/fixtures/cw20_ics20.wasm | Bin 0 -> 341462 bytes composable/vm-wasmi/fixtures/hackatom.wasm | Bin 0 -> 214650 bytes .../vm-wasmi/fixtures/recursion_test.wasm | Bin 0 -> 19234 bytes composable/vm-wasmi/fixtures/reflect.wasm | Bin 0 -> 262915 bytes .../vm-wasmi/fixtures/wyndex_factory.wasm | Bin 0 -> 415179 bytes composable/vm-wasmi/fixtures/wyndex_pair.wasm | Bin 0 -> 574819 bytes composable/vm-wasmi/src/code_gen.rs | 501 ++++++ composable/vm-wasmi/src/error.rs | 69 + composable/vm-wasmi/src/host_functions.rs | 765 ++++++++ composable/vm-wasmi/src/lib.rs | 263 +++ composable/vm-wasmi/src/semantic.rs | 1562 +++++++++++++++++ composable/vm-wasmi/src/validation.rs | 260 +++ composable/vm-wasmi/src/version.rs | 117 ++ composable/vm-wasmi/src/vm.rs | 580 ++++++ composable/vm/LICENSE.md | 21 + 20 files changed, 4202 insertions(+) create mode 100644 composable/vm-wasmi/Cargo.toml create mode 100644 composable/vm-wasmi/LICENSE.md create mode 100644 composable/vm-wasmi/fixtures/crypto_verify.wasm create mode 100644 composable/vm-wasmi/fixtures/cw20_base.wasm create mode 100644 composable/vm-wasmi/fixtures/cw20_ics20.wasm create mode 100644 composable/vm-wasmi/fixtures/hackatom.wasm create mode 100644 composable/vm-wasmi/fixtures/recursion_test.wasm create mode 100644 composable/vm-wasmi/fixtures/reflect.wasm create mode 100644 composable/vm-wasmi/fixtures/wyndex_factory.wasm create mode 100644 composable/vm-wasmi/fixtures/wyndex_pair.wasm create mode 100644 composable/vm-wasmi/src/code_gen.rs create mode 100644 composable/vm-wasmi/src/error.rs create mode 100644 composable/vm-wasmi/src/host_functions.rs create mode 100644 composable/vm-wasmi/src/lib.rs create mode 100644 composable/vm-wasmi/src/semantic.rs create mode 100644 composable/vm-wasmi/src/validation.rs create mode 100644 composable/vm-wasmi/src/version.rs create mode 100644 composable/vm-wasmi/src/vm.rs create mode 100644 composable/vm/LICENSE.md diff --git a/Cargo.toml b/Cargo.toml index a37b0ada..57119d22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "core-primitives", "composable/composable-support", "composable/vm", + "composable/vm-wasmi", "cosmwasm/std", "frame/babel", "frame/cosmos/types", @@ -28,6 +29,7 @@ cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } cosmwasm-std = { path = "cosmwasm/std", default-features = false } cosmwasm-vm = { path = "composable/vm", default-features = false } +cosmwasm-vm-wasmi = { path = "composable/vm-wasmi", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } diff --git a/composable/vm-wasmi/Cargo.toml b/composable/vm-wasmi/Cargo.toml new file mode 100644 index 00000000..849518c5 --- /dev/null +++ b/composable/vm-wasmi/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cosmwasm-vm-wasmi" +version = "0.2.0" +edition = "2021" + +[features] +default = ["iterator", "stargate"] +iterator = ["cosmwasm-vm/iterator"] +stargate = ["cosmwasm-vm/stargate"] +std = ["cosmwasm-std/std"] + +[dependencies] +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.127", default-features = false, features = [ + "alloc", +] } +either = { version = "1.8", default-features = false } +log = { version = "0.4.21", default-features = false } +wasmi = { version = "0.30.0", default-features = false } +wasmi-validation = { version = "0.5.0", default-features = false } +wasm-instrument = { version = "0.4.0", default-features = false } +cosmwasm-std = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", + "cosmwasm_1_4", +] } +cosmwasm-vm = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", +] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +rand_core = { version = "0.6.4", default-features = false, features = ["alloc"] } + +[dev-dependencies] +wat = "1.0" +env_logger = "0.9" +cosmwasm-crypto = { version = "2.1.3" } + +cw20-ics20 = { git = "https://github.com/CosmWasm/cw-plus", default-features = false, features = [ + "library", +] } diff --git a/composable/vm-wasmi/LICENSE.md b/composable/vm-wasmi/LICENSE.md new file mode 100644 index 00000000..ad426e3c --- /dev/null +++ b/composable/vm-wasmi/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Hussein Ait Lahcen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composable/vm-wasmi/fixtures/crypto_verify.wasm b/composable/vm-wasmi/fixtures/crypto_verify.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ff17295eafd4f1324d8e1bdb6cd5569821ad65ad GIT binary patch literal 166358 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%0DpOHqG4$Um^EBMCCaZl^H^c*+HTB?lrZ>8 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 f{77)MH#L7bUo>KuO$<6s4BcN)}|I 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Ć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 literal 0 HcmV?d00001 diff --git a/composable/vm-wasmi/fixtures/cw20_ics20.wasm b/composable/vm-wasmi/fixtures/cw20_ics20.wasm new file mode 100644 index 0000000000000000000000000000000000000000..488ec7d08ef559ba8c0cf825e58312ec9b283e68 GIT binary patch literal 341462 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≪bY(#5;vLch+DJwD= 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%0DkNo{E(M)TkX?NH?`xc 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 Y#qtdjXBahbeX+A#<4q5PyXN`-25n5(_y7O^ literal 0 HcmV?d00001 diff --git a/composable/vm-wasmi/fixtures/recursion_test.wasm b/composable/vm-wasmi/fixtures/recursion_test.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a5f7533110dfb5e690bf7536b28c2d75493be752 GIT binary patch literal 19234 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 literal 0 HcmV?d00001 diff --git a/composable/vm-wasmi/fixtures/reflect.wasm b/composable/vm-wasmi/fixtures/reflect.wasm new file mode 100644 index 0000000000000000000000000000000000000000..16a33deccb29f030d40ffd35a22cb7f8e8cd25d1 GIT binary patch literal 262915 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 CK?LFe literal 0 HcmV?d00001 diff --git a/composable/vm-wasmi/fixtures/wyndex_factory.wasm b/composable/vm-wasmi/fixtures/wyndex_factory.wasm new file mode 100644 index 0000000000000000000000000000000000000000..28fdbb85d59535525e0ba42231a481be2b2d54aa GIT binary patch literal 415179 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�Ru)UdlZ)1Y~=QaYua9UJa0 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 `j6ftWrV8Qq&X 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%0AytRQY-uv%y~^br;wGqCgo0T%ph+bTpGIlMLP3=6 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

)s!iAg{xC?wlS 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 D>JQT( literal 0 HcmV?d00001 diff --git a/composable/vm-wasmi/src/code_gen.rs b/composable/vm-wasmi/src/code_gen.rs new file mode 100644 index 00000000..a9869057 --- /dev/null +++ b/composable/vm-wasmi/src/code_gen.rs @@ -0,0 +1,501 @@ +use alloc::{string::String, vec, vec::Vec}; +use core::mem; +use cosmwasm_std::{ContractResult, Empty, Response}; +use cosmwasm_vm::executor::{ + AllocateCall, AsFunctionName, DeallocateCall, ExecuteCall, InstantiateCall, MigrateCall, + QueryCall, ReplyCall, +}; +use serde::Serialize; +use wasm_instrument::parity_wasm::{ + builder, + elements::{FuncBody, Instruction, Instructions, Local, ValueType}, +}; + +pub const INDEX_OF_USER_DEFINED_FNS: u32 = 9; +const INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR: u32 = 0; +// We know this won't truncate as it is being executed in a 32-bit wasm context +#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] +const SIZE_OF_I32: i32 = mem::size_of::() as i32; +/// Size of `CosmWasm` `Region` +/// `offset` + `capacity` + `length` +/// +const SIZE_OF_REGION: i32 = SIZE_OF_I32 * 3; + +pub struct InterfaceVersion8Call; + +impl AsFunctionName for InterfaceVersion8Call { + const NAME: &'static str = "interface_version_8"; +} + +pub struct DummyCall; + +impl AsFunctionName for DummyCall { + const NAME: &'static str = "dummy_fn"; +} + +/// Definition for the wasm code +#[derive(Debug)] +pub struct ModuleDefinition { + instantiate_call: InstantiateFn, + execute_call: ExecuteFn, + migrate_call: MigrateFn, + query_call: QueryFn, + reply_call: ReplyFn, + table: Option, + additional_functions: Vec, + additional_binary_size: usize, +} + +/// A wasm module ready to be put on chain. +#[derive(Clone)] +pub struct WasmModule { + pub code: Vec, +} + +#[derive(Debug)] +pub enum Error { + Internal, +} + +#[derive(Debug)] +pub struct Table(Vec); + +impl Table { + #[must_use] + pub fn new(table: Vec) -> Self { + Self(table) + } + + #[must_use] + pub fn fill(function_index: u32, n_elems: usize) -> Self { + Self(vec![function_index; n_elems]) + } +} + +impl ModuleDefinition { + pub fn new( + additional_functions: Vec, + additional_binary_size: usize, + table: Option
, + ) -> Result { + Ok(Self { + instantiate_call: InstantiateFn::new().map_err(|_| Error::Internal)?, + execute_call: ExecuteFn::new().map_err(|_| Error::Internal)?, + migrate_call: MigrateFn::new().map_err(|_| Error::Internal)?, + query_call: QueryFn::new().map_err(|_| Error::Internal)?, + reply_call: ReplyFn::new().map_err(|_| Error::Internal)?, + table, + additional_functions, + additional_binary_size, + }) + } + + pub fn with_instructions>( + fn_name: F, + mut instructions: Vec, + additional_binary_size: usize, + table: Option
, + ) -> Result { + instructions.push(Instruction::End); + Self::new( + vec![Function { + name: fn_name.into(), + params: Vec::new(), + result: None, + definition: FuncBody::new(Vec::new(), Instructions::new(instructions)), + }], + additional_binary_size, + table, + ) + } + + pub fn with_instantiate_response(response: S) -> Result { + Ok(Self { + instantiate_call: InstantiateFn( + InstantiateFn::plain(response).map_err(|_| Error::Internal)?, + ), + execute_call: ExecuteFn::new().map_err(|_| Error::Internal)?, + migrate_call: MigrateFn::new().map_err(|_| Error::Internal)?, + query_call: QueryFn::new().map_err(|_| Error::Internal)?, + reply_call: ReplyFn::new().map_err(|_| Error::Internal)?, + table: None, + additional_functions: Vec::new(), + additional_binary_size: 0, + }) + } +} + +#[derive(Debug)] +pub struct Function { + pub name: String, + pub params: Vec, + pub result: Option, + pub definition: FuncBody, +} + +impl Function { + #[must_use] + pub fn instructions(&self) -> &Instructions { + self.definition.code() + } +} + +pub struct FunctionBuilder(Function); + +impl FunctionBuilder { + pub fn new>(name: S) -> Self { + Self(Function { + name: name.into(), + params: Vec::new(), + result: None, + definition: FuncBody::empty(), + }) + } + + #[must_use] + pub fn param(mut self, param: ValueType) -> Self { + self.0.params.push(param); + self + } + + #[must_use] + pub fn result(mut self, result: ValueType) -> Self { + self.0.result = Some(result); + self + } + + #[must_use] + pub fn definition(mut self, func_body: FuncBody) -> Self { + self.0.definition = func_body; + self + } + + #[must_use] + pub fn build(self) -> Function { + self.0 + } + + #[must_use] + pub fn local(mut self, count: u32, value_type: ValueType) -> Self { + self.0.definition.locals_mut().push(Local::new(count, value_type)); + self + } + + #[must_use] + pub fn instructions(mut self, instructions: Vec) -> Self { + *self.0.definition.code_mut() = Instructions::new(instructions); + self + } +} + +trait EntrypointCall { + const MSG_PTR_INDEX: u32; + /// Plain entrypoint which just returns the response as is + /// `msg_ptr_index` is the index of the `msg_ptr` parameter in cosmwasm api + /// this index is 2 in `query` but 3 in `execute` + fn plain(response: T) -> Result { + let result = serde_json::to_string(&response)?; + + let instructions = vec![ + vec![ + // Allocate space for the response + // we target wasm32 so this will not truncate + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + Instruction::I32Const(result.len() as i32), + Instruction::Call(0), + // we save the ptr + Instruction::SetLocal(Self::MSG_PTR_INDEX + 1), + Instruction::GetLocal(Self::MSG_PTR_INDEX + 1), + // now we should set the length to response.len() + Instruction::I32Const(8), + Instruction::I32Add, + // we target wasm32 so this will not truncate + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + Instruction::I32Const(result.len() as i32), + Instruction::I32Store(0, 0), + Instruction::GetLocal(Self::MSG_PTR_INDEX + 1), + // returned ptr to { offset: i32, capacity: i32, length: i32 } + Instruction::I32Load(0, 0), + // now we load offset and save it to local_var_3 + Instruction::SetLocal(Self::MSG_PTR_INDEX), + Instruction::GetLocal(Self::MSG_PTR_INDEX), + ], + { + let mut instructions = Vec::new(); + for c in result.chars() { + instructions.extend(vec![ + Instruction::GetLocal(Self::MSG_PTR_INDEX), + Instruction::I32Const(c as i32), + Instruction::I32Store(0, 0), + Instruction::GetLocal(Self::MSG_PTR_INDEX), + Instruction::I32Const(1), + Instruction::I32Add, + Instruction::SetLocal(Self::MSG_PTR_INDEX), + ]); + } + instructions + }, + vec![ + Instruction::GetLocal(Self::MSG_PTR_INDEX + 1), + Instruction::Return, + Instruction::End, + ], + ] + .into_iter() + .flatten() + .collect::>(); + + Ok(FuncBody::new(vec![Local::new(2, ValueType::I32)], Instructions::new(instructions))) + } +} + +#[derive(Debug)] +struct ExecuteFn(FuncBody); + +impl EntrypointCall for ExecuteFn { + const MSG_PTR_INDEX: u32 = 3; +} + +impl ExecuteFn { + pub fn new() -> Result { + let response = Response::::default(); + Ok(ExecuteFn(Self::plain(ContractResult::Ok(response))?)) + } +} + +#[derive(Debug)] +struct InstantiateFn(FuncBody); + +impl EntrypointCall for InstantiateFn { + const MSG_PTR_INDEX: u32 = 3; +} + +impl InstantiateFn { + pub fn new() -> Result { + let response = Response::::default(); + Ok(InstantiateFn(Self::plain(ContractResult::Ok(response))?)) + } +} + +#[derive(Debug)] +struct MigrateFn(FuncBody); + +impl EntrypointCall for MigrateFn { + const MSG_PTR_INDEX: u32 = 2; +} + +impl MigrateFn { + pub fn new() -> Result { + let response = Response::::default(); + Ok(MigrateFn(Self::plain(ContractResult::Ok(response))?)) + } +} + +#[derive(Debug)] +struct QueryFn(FuncBody); + +impl EntrypointCall for QueryFn { + const MSG_PTR_INDEX: u32 = 2; +} + +impl QueryFn { + pub fn new() -> Result { + let encoded_result = hex::encode("{}"); + Ok(QueryFn(Self::plain(ContractResult::Ok(encoded_result))?)) + } +} + +#[derive(Debug)] +struct ReplyFn(FuncBody); + +impl EntrypointCall for ReplyFn { + const MSG_PTR_INDEX: u32 = 2; +} + +impl ReplyFn { + pub fn new() -> Result { + let response = Response::::default(); + Ok(ReplyFn(Self::plain(ContractResult::Ok(response))?)) + } +} + +impl From for WasmModule { + #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] + fn from(def: ModuleDefinition) -> Self { + let mut contract = builder::module() + // Generate memory + .memory() + .build() + // Add a global variable to be used in allocate function + .global() + .with_type(ValueType::I32) + .mutable() + // We don't want this to be zero since 0 is an invalid pointer + .init_expr(Instruction::I32Const(10)) + .build() + // Export memory + .export() + .field("memory") + .internal() + .memory(0) + .build(); + + // This is for indirect call table, we only support a single table + if let Some(table) = def.table { + contract = contract + .table() + .with_min(table.0.len() as u32) + .with_max(Some(table.0.len() as u32)) + .with_element(0, table.0) + .build(); + } + + let mut function_definitions = vec![ + // fn allocate(size: usize) -> u32; + Function { + name: AllocateCall::<()>::NAME.into(), + params: vec![ValueType::I32], // how much memory to allocate + result: Some(ValueType::I32), // ptr to the region of the new memory + definition: FuncBody::new( + Vec::new(), // We don't need any local variables + Instructions::new(vec![ + // Save original memory cursor in order to return it at the end + // Once we have allocated the memory for the new region + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + // reserve space + // save offset as global offset ptr + 12 + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + Instruction::I32Const(SIZE_OF_REGION), + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + Instruction::I32Add, + Instruction::I32Store(0, 0), + // set capacity to input reserve size + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + Instruction::I32Const(SIZE_OF_I32), + Instruction::I32Add, + Instruction::GetLocal(0), + Instruction::I32Store(0, 0), + // set length to 0 + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + Instruction::I32Const(SIZE_OF_I32 * 2), + Instruction::I32Add, + Instruction::I32Const(0), + Instruction::I32Store(0, 0), + // increase global offset ptr by (12 + capacity) + Instruction::GetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + Instruction::I32Const(SIZE_OF_REGION), + Instruction::I32Add, + Instruction::GetLocal(0), + Instruction::I32Add, + // increase global offset ptr by allocated size + Instruction::SetGlobal(INDEX_OF_LAST_UNRESERVED_MEMORY_CURSOR), + // Return the original memory cursor which we have cached at the + // beginning of this function + Instruction::Return, + Instruction::End, + ]), + ), + }, + // fn instantiate(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32; + Function { + name: ::NAME.into(), + params: vec![ValueType::I32, ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + definition: def.instantiate_call.0, + }, + // fn execute(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32; + Function { + name: ::NAME.into(), + params: vec![ValueType::I32, ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + definition: def.execute_call.0, + }, + // fn migrate(env_ptr: u32, msg_ptr: u32) -> u32; + Function { + name: ::NAME.into(), + params: vec![ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + definition: def.migrate_call.0, + }, + // fn deallocate(pointer: u32); + // NOTE: We are not deallocating memory because for our usecase it does + // not affect performance. + Function { + name: DeallocateCall::<()>::NAME.into(), + params: vec![ValueType::I32], + result: None, + definition: FuncBody::new(Vec::new(), Instructions::empty()), + }, + // fn query(env_ptr: u32, msg_ptr: u32) -> u32; + Function { + name: QueryCall::NAME.into(), + params: vec![ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + definition: def.query_call.0, + }, + // fn reply(env_ptr: u32, msg_ptr: u32) -> u32; + Function { + name: ::NAME.into(), + params: vec![ValueType::I32, ValueType::I32], + result: Some(ValueType::I32), + definition: def.reply_call.0, + }, + // dummy function to increase the binary size + // Used for increasing the total wasm's size. + // Useful when benchmarking for different binary sizes. + Function { + name: DummyCall::NAME.into(), + params: vec![], + result: None, + definition: FuncBody::new( + Vec::new(), + Instructions::new({ + let mut nops = vec![Instruction::Nop; def.additional_binary_size]; + nops.push(Instruction::End); + nops + }), + ), + }, + // fn interface_version_8() -> (); + // Required in order to signal compatibility with CosmWasm 1.0 + // + Function { + name: InterfaceVersion8Call::NAME.into(), + params: vec![], + result: None, + definition: FuncBody::new(Vec::new(), Instructions::empty()), + }, + ]; + + // Add functions definied by users. + // Useful for benchmarking + for function in def.additional_functions { + function_definitions.push(function); + } + + // we target wasm32 so this will not truncate + #[allow(clippy::cast_possible_wrap)] + for (i, func) in function_definitions.into_iter().enumerate() { + let mut signature_builder = contract.function().signature(); + if !func.params.is_empty() { + signature_builder = signature_builder.with_params(func.params); + } + if let Some(result) = func.result { + signature_builder = signature_builder.with_result(result); + } + + contract = signature_builder + .build() + .with_body(func.definition) + .build() + .export() + .field(func.name.as_str()) + .internal() + .func(i as u32) + .build(); + } + + let code = contract.build().into_bytes().unwrap(); + Self { code } + } +} diff --git a/composable/vm-wasmi/src/error.rs b/composable/vm-wasmi/src/error.rs new file mode 100644 index 00000000..5453e019 --- /dev/null +++ b/composable/vm-wasmi/src/error.rs @@ -0,0 +1,69 @@ +use core::{ + fmt::{Debug, Display}, + num::TryFromIntError, +}; +use cosmwasm_vm::{ + executor::ExecutorError, + memory::{MemoryReadError, MemoryWriteError}, + system::SystemError, +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum WasmiVMError { + ExecutorError(ExecutorError), + SystemError(SystemError), + MemoryReadError(MemoryReadError), + MemoryWriteError(MemoryWriteError), + MemoryNotExported, + MemoryExportedIsNotMemory, + LowLevelMemoryReadError, + LowLevelMemoryWriteError, + InvalidPointer, + UnexpectedUnit, + UnexpectedReturnType, + ExpectedUnit, + ExpectedPointer, + InvalidHostSignature, + InvalidValue, + MaxLimitExceeded, + NotADynamicModule, + FunctionNotFound, + MaxCallDepthExceeded, +} + +impl From for WasmiVMError { + fn from(e: ExecutorError) -> Self { + WasmiVMError::ExecutorError(e) + } +} + +impl From for WasmiVMError { + fn from(e: MemoryReadError) -> Self { + WasmiVMError::MemoryReadError(e) + } +} + +impl From for WasmiVMError { + fn from(e: MemoryWriteError) -> Self { + WasmiVMError::MemoryWriteError(e) + } +} + +impl From for WasmiVMError { + fn from(e: SystemError) -> Self { + WasmiVMError::SystemError(e) + } +} + +impl From for WasmiVMError { + fn from(_: TryFromIntError) -> Self { + WasmiVMError::InvalidPointer + } +} + +impl Display for WasmiVMError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{self:?}") + } +} diff --git a/composable/vm-wasmi/src/host_functions.rs b/composable/vm-wasmi/src/host_functions.rs new file mode 100644 index 00000000..4445944e --- /dev/null +++ b/composable/vm-wasmi/src/host_functions.rs @@ -0,0 +1,765 @@ +#![allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] +use super::{ + AsContextMut, String, Tagged, ToString, VMBase, Vec, VmErrorOf, VmGas, VmQueryCustomOf, + WasmiBaseVM, WasmiVM, +}; + +#[cfg(feature = "iterator")] +use super::WasmiVMError; + +#[cfg(feature = "iterator")] +use cosmwasm_std::Order; +use cosmwasm_std::QueryRequest; +use cosmwasm_vm::{ + executor::{ + constants, marshall_out, passthrough_in, passthrough_in_to, passthrough_out, + ConstantReadLimit, + }, + system::cosmwasm_system_query_raw, +}; +use wasmi::{core::Trap, Caller, Func, Linker}; + +/// Reads the value with the given key from the db. +/// +/// Returns the pointer to the read value if a value is successfully read. +/// Returns `0` if `None` is returned from the VM. +/// Propogates the error otherwise. +pub fn env_db_read(mut vm: WasmiVM, key_pointer: i32) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("db_read"); + + let key = passthrough_out::, ConstantReadLimit<{ constants::MAX_LENGTH_DB_KEY }>>( + &vm, + key_pointer as u32, + )?; + let value = vm.0.as_context_mut().data_mut().db_read(key); + match value { + Ok(Some(value)) => { + let Tagged(value_pointer, _) = passthrough_in::, ()>(&mut vm, &value)?; + Ok(value_pointer as i32) + }, + // None value is not treated as error but instead, `0` acting as nullptr is passed. + Ok(None) => Ok(0), + Err(e) => Err(e), + } +} + +/// Writes the given key-value pair to the db. +pub fn env_db_write( + mut vm: WasmiVM, + key_pointer: i32, + value_pointer: i32, +) -> Result<(), VmErrorOf> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("db_write"); + let key = passthrough_out::, ConstantReadLimit<{ constants::MAX_LENGTH_DB_KEY }>>( + &vm, + key_pointer as u32, + )?; + let value = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_DB_VALUE }>, + >(&vm, value_pointer as u32)?; + vm.db_write(key, value)?; + Ok(()) +} + +/// Removes the entry with the given key from the db. +pub fn env_db_remove(mut vm: WasmiVM, key_pointer: i32) -> Result<(), VmErrorOf> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("db_remove"); + let key = passthrough_out::, ConstantReadLimit<{ constants::MAX_LENGTH_DB_KEY }>>( + &vm, + key_pointer as u32, + )?; + vm.db_remove(key)?; + Ok(()) +} + +/// Creates an iterator over a storage with the given boundaries and order. +/// +/// Returns the iterator id if success, otherwise the error is propogated. +#[cfg(feature = "iterator")] +pub fn env_db_scan( + mut vm: WasmiVM, + start_ptr: i32, + end_ptr: i32, + order: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("db_scan"); + let start = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_DB_KEY }>, + >(&vm, start_ptr as u32)?; + let end = passthrough_out::, ConstantReadLimit<{ constants::MAX_LENGTH_DB_KEY }>>( + &vm, + end_ptr as u32, + )?; + let order: Order = TryInto::::try_into(order).map_err(|_| WasmiVMError::InvalidValue)?; + let value = vm.db_scan( + if start.is_empty() { None } else { Some(start) }, + if end.is_empty() { None } else { Some(end) }, + order, + )?; + Ok(value as i32) +} + +/// Reads the next item in an iterator with the given iterator id. +/// +/// TODO(aeryz): Check the `None` case here +#[cfg(feature = "iterator")] +pub fn env_db_next(mut vm: WasmiVM, iterator_id: i32) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("db_next"); + let next = vm.db_next(iterator_id as u32); + match next { + Ok((key, value)) => { + let out_data = encode_sections(&[key, value]).ok_or(WasmiVMError::InvalidValue)?; + let Tagged(value_pointer, _) = passthrough_in::, ()>(&mut vm, &out_data)?; + Ok(value_pointer as i32) + }, + Err(e) => Err(e), + } +} + +/// Validates the address. +/// +/// Returns `0` if the address is valid, otherwise pointer to the error string. +/// Returns `Err`, only if anything goes wrong internally. +pub fn env_addr_validate( + mut vm: WasmiVM, + address_pointer: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("addr_validate"); + let address = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_HUMAN_ADDRESS }>, + >(&vm, address_pointer as u32)?; + + let address = match String::from_utf8(address) { + Ok(address) => address, + Err(e) => { + let Tagged(value_pointer, _) = + passthrough_in::, ()>(&mut vm, e.to_string().as_bytes())?; + return Ok(value_pointer as i32); + }, + }; + + match vm.addr_validate(&address)? { + Ok(_) => Ok(0), + Err(e) => { + let Tagged(value_pointer, _) = + passthrough_in::, ()>(&mut vm, e.to_string().as_bytes())?; + Ok(value_pointer as i32) + }, + } +} + +/// Canonicalizes the address. +/// +/// Writes the canonicalized address to `destination_ptr`. +/// Returns `0` if the operation is successfull, otherwise pointer to the error string. +/// Returns `Err`, only if anything goes wrong internally. +pub fn env_addr_canonicalize( + mut vm: WasmiVM, + address_ptr: i32, + destination_ptr: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("addr_canonicalize"); + let address = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_HUMAN_ADDRESS }>, + >(&vm, address_ptr as u32)?; + + let address = match String::from_utf8(address) { + Ok(address) => address, + Err(e) => { + let Tagged(value_pointer, _) = + passthrough_in::, ()>(&mut vm, e.to_string().as_bytes())?; + return Ok(value_pointer as i32); + }, + }; + + match vm.addr_canonicalize(&address)? { + Ok(canonical_address) => { + passthrough_in_to::>( + &mut vm, + destination_ptr as u32, + &canonical_address.into(), + )?; + Ok(0) + }, + Err(e) => { + let Tagged(value_pointer, _) = + passthrough_in::, ()>(&mut vm, e.to_string().as_bytes())?; + Ok(value_pointer as i32) + }, + } +} + +/// Humanizes a canonical address. +/// +/// Writes the humanized address to `destination_ptr`. +/// Returns `0`, if the operation is successfull, otherwise pointer to the error string. +/// Returns `Err`, only if anything goes wrong internally. +pub fn env_addr_humanize( + mut vm: WasmiVM, + address_ptr: i32, + destination_ptr: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("addr_humanize"); + let address = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_CANONICAL_ADDRESS }>, + >(&vm, address_ptr as u32)?; + + // TODO(aeryz): check if `address.try_into` failure should return `Err` or `Ok(error str)` + match vm.addr_humanize(&address.try_into()?)? { + Ok(address) => { + passthrough_in_to::>( + &mut vm, + destination_ptr as u32, + address.into().as_bytes(), + )?; + Ok(0) + }, + Err(e) => { + let Tagged(value_pointer, _) = + passthrough_in::, ()>(&mut vm, e.to_string().as_bytes())?; + Ok(value_pointer as i32) + }, + } +} + +/// Verifies `secp256k1` signature with the given public key and message hash. +/// +/// Returns `0` if success, otherwise `1`. +pub fn env_secp256k1_verify( + mut vm: WasmiVM, + message_hash_ptr: i32, + signature_ptr: i32, + public_key_ptr: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + let message_hash = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_MESSAGE_HASH }>, + >(&vm, message_hash_ptr as u32)?; + let signature = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::EDCSA_SIGNATURE_LENGTH }>, + >(&vm, signature_ptr as u32)?; + let public_key = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_EDCSA_PUBKEY_LENGTH }>, + >(&vm, public_key_ptr as u32)?; + + let result = vm.secp256k1_verify(&message_hash, &signature, &public_key)?; + + Ok(i32::from(!result)) +} + +/// Recovers `secp256k1` public key from message hash, signature and recovery parameter. +/// +/// Returns 64 bit integer. +/// Returns the pointer to the recovered public key in the lower 32 bits, upper 32 bits MUST +/// be `0`. +/// Returns the error code in the upper 32 bits. Lower 32 bits MUST be `0`. +pub fn env_secp256k1_recover_pubkey( + mut vm: WasmiVM, + message_hash_ptr: i32, + signature_ptr: i32, + recovery_param: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("secp256k1_recover_pubkey"); + let message_hash = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_MESSAGE_HASH }>, + >(&vm, message_hash_ptr as u32)?; + let signature = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::EDCSA_SIGNATURE_LENGTH }>, + >(&vm, signature_ptr as u32)?; + + if let Ok(pubkey) = + vm.secp256k1_recover_pubkey(&message_hash, &signature, recovery_param as u8)? + { + // Note that if the call is success, the pointer is written to the lower + // 4-bytes. On failure, the error code is written to the upper 4-bytes, and + // we don't return an error. + let Tagged(value_pointer, _) = passthrough_in::, ()>(&mut vm, &pubkey)?; + Ok(i64::from(value_pointer)) + } else { + // TODO(aeryz): Check for the other error codes and see if it is doable. It's good to be + // consistent with the error codes. + const GENERIC_ERROR_CODE: i64 = 10; + Ok(GENERIC_ERROR_CODE << 32) + } +} + +/// Verifies `ed25519` signature with the given public key and message. +/// +/// Returns `0` if success, otherwise `1`. +pub fn env_ed25519_verify( + mut vm: WasmiVM, + message_ptr: i32, + signature_ptr: i32, + public_key_ptr: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("ed25519_verify"); + let message = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_ED25519_MESSAGE }>, + >(&vm, message_ptr as u32)?; + let signature = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_ED25519_SIGNATURE }>, + >(&vm, signature_ptr as u32)?; + let public_key = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::EDDSA_PUBKEY_LENGTH }>, + >(&vm, public_key_ptr as u32)?; + + vm.ed25519_verify(&message, &signature, &public_key) + .map(|result| i32::from(!result)) +} + +/// Does batch verification on `ed25519` signatures. +/// +/// Returns `0` if success, otherwise `1`. +pub fn env_ed25519_batch_verify( + mut vm: WasmiVM, + messages_ptr: i32, + signatures_ptr: i32, + public_keys_ptr: i32, +) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + // &[&[u8]]'s are written to the memory in an flattened encoded way. That's why we + // read a flat memory, not iterate through pointers and read arbitrary memory + // locations. + let messages = passthrough_out::< + WasmiVM, + ConstantReadLimit< + { (constants::MAX_LENGTH_ED25519_MESSAGE + 4) * constants::MAX_COUNT_ED25519_BATCH }, + >, + >(&vm, messages_ptr as u32)?; + let signatures = passthrough_out::< + WasmiVM, + ConstantReadLimit< + { (constants::MAX_LENGTH_ED25519_SIGNATURE + 4) * constants::MAX_COUNT_ED25519_BATCH }, + >, + >(&vm, signatures_ptr as u32)?; + let public_keys = passthrough_out::< + WasmiVM, + ConstantReadLimit< + { (constants::EDDSA_PUBKEY_LENGTH + 4) * constants::MAX_COUNT_ED25519_BATCH }, + >, + >(&vm, public_keys_ptr as u32)?; + + let (messages, signatures, public_keys) = + (decode_sections(&messages), decode_sections(&signatures), decode_sections(&public_keys)); + + vm.ed25519_batch_verify(&messages, &signatures, &public_keys) + .map(|result| i32::from(!result)) +} + +/// Logs the message. +pub fn env_debug(mut vm: WasmiVM, message_ptr: i32) -> Result<(), VmErrorOf> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + let message: Vec = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_ABORT }>, + >(&vm, message_ptr as u32)?; + vm.debug(message)?; + Ok(()) +} + +/// Reads a JSON query request and does a raw query. +/// +/// Returns the pointer to the result if success, otherwise propogates the error. +pub fn env_query_chain(mut vm: WasmiVM, query_ptr: i32) -> Result> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + log::debug!("query_chain"); + let request = + marshall_out::, QueryRequest>>(&vm, query_ptr as u32)?; + let value = cosmwasm_system_query_raw::>(&mut vm, request)?; + let Tagged(value_pointer, _) = passthrough_in::, ()>(&mut vm, &value)?; + Ok(value_pointer as i32) +} + +/// Aborts the execution with the given message. +pub fn env_abort(mut vm: WasmiVM, message_ptr: i32) -> Result<(), VmErrorOf> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + let message: Vec = passthrough_out::< + WasmiVM, + ConstantReadLimit<{ constants::MAX_LENGTH_ABORT }>, + >(&vm, message_ptr as u32)?; + vm.abort(String::from_utf8_lossy(&message).into())?; + Ok(()) +} + +/// Charges `value` amount of gas. +pub fn env_gas(mut vm: WasmiVM, value: i64) -> Result<(), VmErrorOf> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + vm.charge(VmGas::Instrumentation { metered: value as u32 })?; + Ok(()) +} + +/// Encodes multiple sections of data into one vector. +/// +/// Each section is suffixed by a section length encoded as big endian uint32. +/// Using suffixes instead of prefixes allows reading sections in reverse order, +/// such that the first element does not need to be re-allocated if the contract's +/// data structure supports truncation (such as a Rust vector). +/// +/// The resulting data looks like this: +/// +/// ```ignore +/// section1 || section1_len || section2 || section2_len || section3 || section3_len || … +/// ``` +#[cfg(feature = "iterator")] +#[must_use] +fn encode_sections(sections: &[Vec]) -> Option> { + let out_len: usize = + sections.iter().map(alloc::vec::Vec::len).sum::() + 4 * sections.len(); + sections.iter().try_fold(Vec::with_capacity(out_len), |mut acc, section| { + TryInto::::try_into(section.len()) + .map(|section_len| { + acc.extend(section); + acc.extend_from_slice(§ion_len.to_be_bytes()); + acc + }) + .ok() + }) +} + +/// Decodes sections of data into multiple slices. +/// +/// Each encoded section is suffixed by a section length, encoded as big endian uint32. +/// +/// See also: `encode_section`. +#[must_use] +fn decode_sections(data: &[u8]) -> Vec<&[u8]> { + let mut result: Vec<&[u8]> = Vec::new(); + let mut remaining_len = data.len(); + while remaining_len >= 4 { + let tail_len = u32::from_be_bytes([ + data[remaining_len - 4], + data[remaining_len - 3], + data[remaining_len - 2], + data[remaining_len - 1], + ]) as usize; + result.push(&data[remaining_len - 4 - tail_len..remaining_len - 4]); + remaining_len -= 4 + tail_len; + } + result.reverse(); + result +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn define( + mut ctx: impl AsContextMut, + linker: &mut Linker, +) -> Result<(), VmErrorOf> { + linker + .define( + "env", + "db_read", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, param: i32| -> Result { + env_db_read(WasmiVM(caller), param).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "db_write", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, key_pointer: i32, value_pointer: i32| -> Result<(), Trap> { + env_db_write(WasmiVM(caller), key_pointer, value_pointer).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "db_remove", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, key_pointer: i32| -> Result<(), Trap> { + env_db_remove(WasmiVM(caller), key_pointer).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + #[cfg(feature = "iterator")] + linker + .define( + "env", + "db_scan", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + start_ptr: i32, + end_ptr: i32, + order: i32| + -> Result { + env_db_scan(WasmiVM(caller), start_ptr, end_ptr, order).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + #[cfg(feature = "iterator")] + linker + .define( + "env", + "db_next", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, iterator_id: i32| -> Result { + env_db_next(WasmiVM(caller), iterator_id).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "addr_validate", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, address_ptr: i32| -> Result { + env_addr_validate(WasmiVM(caller), address_ptr).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "addr_canonicalize", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + address_ptr: i32, + destination_ptr: i32| + -> Result { + env_addr_canonicalize(WasmiVM(caller), address_ptr, destination_ptr) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "addr_humanize", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + address_ptr: i32, + destination_ptr: i32| + -> Result { + env_addr_humanize(WasmiVM(caller), address_ptr, destination_ptr) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "secp256k1_verify", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + message_hash_ptr: i32, + signature_ptr: i32, + public_key_ptr: i32| + -> Result { + env_secp256k1_verify( + WasmiVM(caller), + message_hash_ptr, + signature_ptr, + public_key_ptr, + ) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "secp256k1_recover_pubkey", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + message_hash_ptr: i32, + signature_ptr: i32, + recovery_param: i32| + -> Result { + env_secp256k1_recover_pubkey( + WasmiVM(caller), + message_hash_ptr, + signature_ptr, + recovery_param, + ) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "ed25519_verify", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + message_hash_ptr: i32, + signature_ptr: i32, + public_key_ptr: i32| + -> Result { + env_ed25519_verify( + WasmiVM(caller), + message_hash_ptr, + signature_ptr, + public_key_ptr, + ) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "ed25519_batch_verify", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, + messages_ptr: i32, + signatures_ptr: i32, + public_keys_ptr: i32| + -> Result { + env_ed25519_batch_verify( + WasmiVM(caller), + messages_ptr, + signatures_ptr, + public_keys_ptr, + ) + .map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "debug", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, message_ptr: i32| -> Result<(), Trap> { + env_debug(WasmiVM(caller), message_ptr).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "query_chain", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, query_ptr: i32| -> Result { + env_query_chain(WasmiVM(caller), query_ptr).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "abort", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, message_ptr: i32| -> Result<(), Trap> { + env_abort(WasmiVM(caller), message_ptr).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + linker + .define( + "env", + "gas", + Func::wrap( + ctx.as_context_mut(), + |caller: Caller<'_, V>, value: i64| -> Result<(), Trap> { + env_gas(WasmiVM(caller), value).map_err(Into::into) + }, + ), + ) + .map_err(Into::::into)?; + Ok(()) +} diff --git a/composable/vm-wasmi/src/lib.rs b/composable/vm-wasmi/src/lib.rs new file mode 100644 index 00000000..0c0fb812 --- /dev/null +++ b/composable/vm-wasmi/src/lib.rs @@ -0,0 +1,263 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(trait_alias)] +#![cfg_attr(test, feature(assert_matches))] +#![allow(soft_unstable)] +#![feature(test)] + +extern crate alloc; + +pub mod code_gen; +pub mod validation; +pub mod version; + +pub(crate) mod error; +pub(crate) mod host_functions; +pub(crate) mod vm; + +pub use error::*; +pub use vm::*; + +#[cfg(test)] +mod semantic; + +use alloc::{ + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::marker::PhantomData; +use cosmwasm_vm::{ + executor::{ + AllocateCall, AsFunctionName, CosmwasmCallInput, CosmwasmCallWithoutInfoInput, + DeallocateCall, Unit, + }, + tagged::Tagged, + vm::{VMBase, VmErrorOf, VmGas, VmQueryCustomOf}, +}; +use wasmi::{ + AsContextMut, Engine, Extern, Instance, Linker, Memory, Module, Store, Value as RuntimeValue, +}; + +/// A wasmi module reference +#[derive(Clone)] +pub struct WasmiModule { + /// Instance of a wasm module + pub instance: Instance, + /// Exported memory of the wasm module + pub memory: Memory, +} + +/// Api to `WasmiModule` in the inner VM. +/// This should be handled by the inner VM because of the current limitations +/// with `Wasmi` +pub trait WasmiContext { + /// Returns the module that is ready to be executed, if any + fn executing_module(&self) -> Option; + + /// Sets the executing module + fn set_wasmi_context(&mut self, instance: Instance, memory: Memory); + + /// Returns function call depth counter. + /// + /// Counts how many times we’ve moved from host code to inside of the WASM + /// code. This is tracked to avoid unbound recursion caused by malicious + /// contract calling host function which calls back into WASM code. See + /// https://github.com/JumpCrypto/security-research/blob/master/advisories/2023-003-cosmwasm.md + fn call_depth_mut(&mut self) -> &mut u32; +} + +/// Output of wasmi functions +pub struct WasmiOutput(WasmiFunctionResult, PhantomData); + +/// Name of wasm functions +pub struct WasmiFunctionName(String); + +/// Params of wasm functions +pub struct WasmiFunctionParams(Vec); + +/// Result of wasm functions +pub struct WasmiFunctionResult(Vec); + +/// Describes wasm functions +/// fn `WasmiFunctionName`(`WasmiFunctionParams`..) -> `WasmiFunctionResult`; +pub struct WasmiInput( + WasmiFunctionName, + WasmiFunctionParams, + /// This result will be overwritten by the wasm call. See `raw_call`. + WasmiFunctionResult, + PhantomData, +); + +impl WasmiInput { + #[must_use] + pub fn new(name: String, params: Vec, result: Vec) -> Self { + WasmiInput( + WasmiFunctionName(name), + WasmiFunctionParams(params), + WasmiFunctionResult(result), + PhantomData, + ) + } +} + +impl TryFrom>> for RuntimeValue +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + fn try_from( + WasmiOutput(WasmiFunctionResult(values), _): WasmiOutput>, + ) -> Result { + match values.as_slice() { + [ref run_val] => Ok(run_val.clone()), + _ => Err(WasmiVMError::UnexpectedReturnType.into()), + } + } +} + +impl TryFrom>> for Unit +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + fn try_from( + WasmiOutput(WasmiFunctionResult(values), _): WasmiOutput>, + ) -> Result { + if values.is_empty() { + Ok(Unit) + } else { + Err(WasmiVMError::ExpectedUnit.into()) + } + } +} + +impl TryFrom>> for u32 +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + fn try_from( + WasmiOutput(WasmiFunctionResult(values), _): WasmiOutput>, + ) -> Result { + // we target wasm32 so this will not truncate + #[allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss + )] + match values.as_slice() { + &[RuntimeValue::I32(ret_val)] => Ok(ret_val as u32), + _ => Err(WasmiVMError::ExpectedPointer.into()), + } + } +} + +impl TryFrom> for WasmiInput> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + // we target wasm32 so this will not truncate + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + fn try_from(AllocateCall(ptr): AllocateCall) -> Result { + Ok(WasmiInput::new( + AllocateCall::::NAME.into(), + vec![RuntimeValue::I32(ptr as i32)], + vec![RuntimeValue::I32(0)], + )) + } +} + +impl TryFrom> for WasmiInput> +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + fn try_from(DeallocateCall(ptr): DeallocateCall) -> Result { + Ok(WasmiInput::new( + DeallocateCall::::NAME.into(), + vec![RuntimeValue::I32(ptr as i32)], + Vec::new(), + )) + } +} + +impl<'a, I, V, S> TryFrom> for WasmiInput> +where + V: WasmiBaseVM, + S: AsContextMut, + I: AsFunctionName, +{ + type Error = VmErrorOf; + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] + fn try_from( + CosmwasmCallInput(Tagged(env_ptr, _), Tagged(info_ptr, _), Tagged(msg_ptr, _), _): CosmwasmCallInput<'a, u32, I>, + ) -> Result { + Ok(WasmiInput::new( + I::NAME.into(), + vec![ + RuntimeValue::I32(env_ptr as i32), + RuntimeValue::I32(info_ptr as i32), + RuntimeValue::I32(msg_ptr as i32), + ], + vec![RuntimeValue::I32(0)], + )) + } +} + +impl<'a, I, V, S> TryFrom> for WasmiInput> +where + V: WasmiBaseVM, + S: AsContextMut, + I: AsFunctionName, +{ + type Error = VmErrorOf; + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] + fn try_from( + CosmwasmCallWithoutInfoInput(Tagged(env_ptr, _), Tagged(msg_ptr, _), _): CosmwasmCallWithoutInfoInput< + 'a, + u32, + I, + >, + ) -> Result { + Ok(WasmiInput::new( + I::NAME.into(), + vec![RuntimeValue::I32(env_ptr as i32), RuntimeValue::I32(msg_ptr as i32)], + vec![RuntimeValue::I32(0)], + )) + } +} + +/// Note that validation is not done here since the implementers probably wouldn't want +/// to do an expensive validation on each time they load the same code. So DO NOT forget +/// to use `CodeValidation` to properly validate the wasm module. +pub fn new_wasmi_vm(code: &[u8], data: V) -> Result, VmErrorOf> { + let engine = Engine::default(); + let module = Module::new(&engine, code).map_err(Into::::into)?; + + let mut store = Store::new(&engine, data); + let mut linker = >::new(&engine); + + host_functions::define(store.as_context_mut(), &mut linker)?; + + let instance = linker + .instantiate(&mut store, &module) + .map_err(Into::::into)? + .start(&mut store) + .map_err(Into::::into)?; + + let memory = instance + .get_export(store.as_context_mut(), "memory") + .and_then(Extern::into_memory) + .ok_or(WasmiVMError::MemoryNotExported)?; + + store.data_mut().set_wasmi_context(instance, memory); + + Ok(WasmiVM(store)) +} diff --git a/composable/vm-wasmi/src/semantic.rs b/composable/vm-wasmi/src/semantic.rs new file mode 100644 index 00000000..4c051a88 --- /dev/null +++ b/composable/vm-wasmi/src/semantic.rs @@ -0,0 +1,1562 @@ +extern crate std; + +use crate::version::{Version, Version1x}; + +use super::{ + code_gen, new_wasmi_vm, OwnedWasmiVM, VMBase, WasmiContext, WasmiInput, WasmiModule, + WasmiOutput, WasmiVMError, +}; +use alloc::{ + collections::BTreeMap, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::{ + assert_matches::assert_matches, + fmt::{Debug, Display}, + num::NonZeroU32, + str::FromStr, +}; +#[cfg(feature = "stargate")] +use cosmwasm_std::IbcTimeout; +#[cfg(feature = "iterator")] +use cosmwasm_std::Order; +use cosmwasm_std::{ + Addr, Attribute, Binary, BlockInfo, CanonicalAddr, CodeInfoResponse, Coin, ContractInfo, + ContractInfoResponse, ContractResult, Empty, Env, Event, MessageInfo, Reply, SystemResult, + Timestamp, +}; +use cosmwasm_vm::{ + executor::{ + cosmwasm_call, CosmwasmExecutionResult, CosmwasmQueryResult, ExecuteCall, ExecuteResult, + ExecutorError, InstantiateCall, InstantiateResult, MigrateCall, QueryCall, QueryResult, + ReplyCall, + }, + has::Has, + memory::{MemoryReadError, MemoryWriteError}, + system::{ + cosmwasm_system_entrypoint, cosmwasm_system_entrypoint_hook, cosmwasm_system_run, + CosmwasmCodeId, CosmwasmContractMeta, SystemError, + }, + transaction::Transactional, + vm::{VmErrorOf, VmGas, VmGasCheckpoint}, +}; +use rand_core::OsRng; +use wasm_instrument::gas_metering::{host_function, Rules}; +use wasmi::core::HostError; + +const CANONICAL_LENGTH: usize = 54; +const SHUFFLES_ENCODE: usize = 18; +const SHUFFLES_DECODE: usize = 2; + +pub fn initialize() { + use std::sync::Once; + static INIT: Once = Once::new(); + INIT.call_once(|| { + env_logger::init(); + }); +} + +#[derive(PartialEq, Debug)] +enum SimpleVMError { + Interpreter, + VMError(WasmiVMError), + CodeNotFound(CosmwasmCodeId), + ContractNotFound(BankAccount), + InvalidAddress, + InvalidAccountFormat, + NoCustomQuery, + NoCustomMessage, + Unsupported, + OutOfGas, + #[cfg(feature = "iterator")] + IteratorDoesNotExist, + CannotDeserialize, + Crypto, +} + +impl HostError for SimpleVMError {} + +impl From for SimpleVMError { + fn from(_: wasmi::Error) -> Self { + Self::Interpreter + } +} +impl From for SimpleVMError { + fn from(e: WasmiVMError) -> Self { + SimpleVMError::VMError(e) + } +} +impl From for SimpleVMError { + fn from(e: SystemError) -> Self { + SimpleVMError::VMError(e.into()) + } +} +impl From for SimpleVMError { + fn from(e: ExecutorError) -> Self { + SimpleVMError::VMError(e.into()) + } +} +impl From for SimpleVMError { + fn from(e: MemoryReadError) -> Self { + SimpleVMError::VMError(e.into()) + } +} +impl From for SimpleVMError { + fn from(e: MemoryWriteError) -> Self { + SimpleVMError::VMError(e.into()) + } +} +impl Display for SimpleVMError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Default, Clone, PartialEq, Eq, Debug)] +struct Gas { + checkpoints: Vec, +} + +impl Gas { + fn new(initial_value: u64) -> Self { + Gas { checkpoints: vec![initial_value] } + } + fn current(&self) -> &u64 { + self.checkpoints.last().expect("impossible") + } + fn current_mut(&mut self) -> &mut u64 { + self.checkpoints.last_mut().expect("impossible") + } + fn push(&mut self, checkpoint: &VmGasCheckpoint) -> Result<(), SimpleVMError> { + match checkpoint { + VmGasCheckpoint::Unlimited => { + let parent = self.current_mut(); + let value = *parent; + *parent = 0; + self.checkpoints.push(value); + Ok(()) + }, + VmGasCheckpoint::Limited(limit) if limit <= self.current() => { + *self.current_mut() -= limit; + self.checkpoints.push(*limit); + Ok(()) + }, + VmGasCheckpoint::Limited(_) => Err(SimpleVMError::OutOfGas), + } + } + fn pop(&mut self) { + let child = self.checkpoints.pop().expect("impossible"); + let parent = self.current_mut(); + *parent += child; + } + fn charge(&mut self, value: u64) -> Result<(), SimpleVMError> { + let current = self.current_mut(); + if *current >= value { + *current -= value; + Ok(()) + } else { + Err(SimpleVMError::OutOfGas) + } + } +} + +#[cfg(feature = "iterator")] +#[derive(Default, Clone, Debug)] +struct Iter { + data: Vec<(Vec, Vec)>, + position: usize, +} + +#[derive(Default, Clone, Debug)] +struct SimpleWasmiVMStorage { + data: BTreeMap, Vec>, + #[cfg(feature = "iterator")] + iterators: BTreeMap, +} + +#[cfg(feature = "stargate")] +#[derive(Debug, Clone, PartialEq, Eq)] +struct SimpleIBCPacket { + channel_id: String, + data: Binary, + timeout: IbcTimeout, +} + +#[cfg(feature = "stargate")] +#[derive(Default, Clone, Debug, PartialEq, Eq)] +struct SimpleIBCState { + packets_sent: Vec, +} + +#[derive(Default, Clone)] +struct SimpleWasmiVMExtension { + #[cfg(feature = "stargate")] + ibc: BTreeMap, + storage: BTreeMap, + codes: BTreeMap>, + contracts: BTreeMap>, + next_account_id: BankAccount, + transaction_depth: u32, + gas: Gas, +} + +struct SimpleWasmiVM<'a> { + executing_module: Option, + env: Env, + info: MessageInfo, + extension: &'a mut SimpleWasmiVMExtension, + call_depth: u32, +} + +impl<'a> WasmiContext for SimpleWasmiVM<'a> { + fn executing_module(&self) -> Option { + self.executing_module.clone() + } + + fn set_wasmi_context(&mut self, instance: wasmi::Instance, memory: wasmi::Memory) { + self.executing_module = Some(WasmiModule { instance, memory }); + } + + fn call_depth_mut(&mut self) -> &mut u32 { + &mut self.call_depth + } +} + +impl<'a> SimpleWasmiVM<'a> { + fn load_subvm( + &mut self, + address: ::Address, + funds: Vec, + ) -> Result, VmErrorOf> { + log::debug!("Loading sub-vm, contract address: {:?}", address); + let code = (|| { + let CosmwasmContractMeta { code_id, .. } = self + .extension + .contracts + .get(&address) + .cloned() + .ok_or(SimpleVMError::ContractNotFound(address))?; + self.extension + .codes + .get(&code_id) + .ok_or(SimpleVMError::CodeNotFound(code_id)) + .cloned() + })()?; + let sub_vm = SimpleWasmiVM { + executing_module: None, + env: Env { + block: self.env.block.clone(), + transaction: self.env.transaction.clone(), + contract: ContractInfo { address: address.into() }, + }, + info: MessageInfo { sender: self.env.contract.address.clone(), funds }, + extension: self.extension, + call_depth: 0, + }; + let sub_vm = new_wasmi_vm::(&code, sub_vm)?; + Ok(sub_vm) + } +} + +#[derive(Debug, Clone)] +struct CanonicalAddress(pub CanonicalAddr); + +impl TryFrom> for CanonicalAddress { + type Error = SimpleVMError; + fn try_from(value: Vec) -> Result { + Ok(CanonicalAddress(CanonicalAddr::from(Binary::from(value)))) + } +} + +impl From for Vec { + fn from(addr: CanonicalAddress) -> Self { + addr.0.into() + } +} + +impl From for CanonicalAddr { + fn from(addr: CanonicalAddress) -> Self { + addr.0 + } +} + +impl<'a> VMBase for SimpleWasmiVM<'a> { + type Input<'x> = WasmiInput>; + type Output<'x> = WasmiOutput>; + type QueryCustom = Empty; + type MessageCustom = Empty; + type ContractMeta = CosmwasmContractMeta; + type Address = BankAccount; + type CanonicalAddress = CanonicalAddress; + type StorageKey = Vec; + type StorageValue = Vec; + type Error = SimpleVMError; + + fn running_contract_meta(&mut self) -> Result { + Ok(self + .extension + .contracts + .get( + &BankAccount::try_from(self.env.contract.address.clone()) + .expect("contract address is set by vm, this should never happen"), + ) + .cloned() + .expect("contract is inserted by vm, this should never happen")) + } + + fn set_contract_meta( + &mut self, + address: Self::Address, + contract_meta: Self::ContractMeta, + ) -> Result<(), Self::Error> { + let meta = self + .extension + .contracts + .get_mut(&address) + .ok_or(SimpleVMError::ContractNotFound(address))?; + + *meta = contract_meta; + + Ok(()) + } + + fn contract_meta(&mut self, address: Self::Address) -> Result { + self.extension + .contracts + .get_mut(&address) + .ok_or(SimpleVMError::ContractNotFound(address)) + .cloned() + } + + fn continue_query( + &mut self, + address: Self::Address, + message: &[u8], + ) -> Result { + let mut sub_vm = self.load_subvm(address, vec![])?; + cosmwasm_call::>(&mut sub_vm, message) + } + + fn continue_execute( + &mut self, + address: Self::Address, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + let mut sub_vm = self.load_subvm(address, funds)?; + cosmwasm_system_run::, _>( + &mut sub_vm, + message, + event_handler, + ) + } + + fn continue_instantiate( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error> { + let BankAccount(address) = self.extension.next_account_id; + self.extension.next_account_id = BankAccount(address + 1); + self.extension.contracts.insert(BankAccount(address), contract_meta); + + let mut sub_vm = self.load_subvm(BankAccount(address), funds)?; + cosmwasm_system_run::, _>( + &mut sub_vm, + message, + event_handler, + ) + .map(|data| (BankAccount(address), data)) + } + + fn continue_instantiate2( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + message: &[u8], + _salt: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error> { + let BankAccount(address) = self.extension.next_account_id; + self.extension.next_account_id = BankAccount(address + 1); + self.extension.contracts.insert(BankAccount(address), contract_meta); + + let mut sub_vm = self.load_subvm(BankAccount(address), funds)?; + cosmwasm_system_run::, _>( + &mut sub_vm, + message, + event_handler, + ) + .map(|data| (BankAccount(address), data)) + } + + fn continue_migrate( + &mut self, + address: Self::Address, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + let mut sub_vm = self.load_subvm(address, vec![])?; + cosmwasm_system_run::, _>( + &mut sub_vm, + message, + event_handler, + ) + } + + fn continue_reply( + &mut self, + message: Reply, + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + let mut sub_vm = + self.load_subvm(self.env.contract.address.clone().into_string().try_into()?, vec![])?; + + cosmwasm_system_run::, OwnedWasmiVM>( + &mut sub_vm, + &serde_json::to_vec(&message).map_err(|_| SimpleVMError::CannotDeserialize)?, + event_handler, + ) + } + + fn query_custom( + &mut self, + _: Self::QueryCustom, + ) -> Result, Self::Error> { + Err(SimpleVMError::NoCustomQuery) + } + + fn message_custom( + &mut self, + _: Self::MessageCustom, + _: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + Err(SimpleVMError::NoCustomMessage) + } + + fn query_raw( + &mut self, + address: Self::Address, + key: Self::StorageKey, + ) -> Result, Self::Error> { + Ok(self + .extension + .storage + .get(&address) + .unwrap_or(&SimpleWasmiVMStorage::default()) + .data + .get(&key) + .cloned()) + } + + fn transfer_from( + &mut self, + from: &Self::Address, + to: &Self::Address, + funds: &[Coin], + ) -> Result<(), Self::Error> { + log::debug!("Transfer from: {:?} -> {:?}\n{:?}", from, to, funds); + Ok(()) + } + + fn transfer(&mut self, to: &Self::Address, funds: &[Coin]) -> Result<(), Self::Error> { + log::debug!("Transfer: {:?} -> {:?}\n{:?}", self.env.contract.address, to, funds); + Ok(()) + } + + fn burn(&mut self, funds: &[Coin]) -> Result<(), Self::Error> { + log::debug!("Burn: {:?}\n{:?}", self.env.contract.address, funds); + Ok(()) + } + + fn balance(&mut self, _: &Self::Address, _: String) -> Result { + log::debug!("Query balance."); + Err(SimpleVMError::Unsupported) + } + + fn supply(&mut self, _: String) -> Result { + log::debug!("Supply."); + Err(SimpleVMError::Unsupported) + } + + fn all_balance(&mut self, _: &Self::Address) -> Result, Self::Error> { + log::debug!("Query all balance."); + Ok(vec![]) + } + + fn query_contract_info( + &mut self, + _: Self::Address, + ) -> Result { + Err(SimpleVMError::Unsupported) + } + + fn query_code_info(&mut self, _: CosmwasmCodeId) -> Result { + Err(SimpleVMError::Unsupported) + } + + fn debug(&mut self, message: Vec) -> Result<(), Self::Error> { + log::info!("[contract-debug] {}", String::from_utf8_lossy(&message)); + Ok(()) + } + + #[cfg(feature = "iterator")] + fn db_scan( + &mut self, + _start: Option, + _end: Option, + _order: Order, + ) -> Result { + let contract_addr = self.env.contract.address.clone().try_into()?; + let mut empty = SimpleWasmiVMStorage::default(); + let storage = self.extension.storage.get_mut(&contract_addr).unwrap_or(&mut empty); + + let data = storage.data.clone().into_iter().collect::>(); + // Exceeding u32 size is fatal + let last_id: u32 = storage + .iterators + .len() + .try_into() + .expect("Found more iterator IDs than supported"); + + let new_id = last_id + 1; + let iter = Iter { data, position: 0 }; + storage.iterators.insert(new_id, iter); + + Ok(new_id) + } + + #[cfg(feature = "iterator")] + fn db_next( + &mut self, + iterator_id: u32, + ) -> Result<(Self::StorageKey, Self::StorageValue), Self::Error> { + let contract_addr = self.env.contract.address.clone().try_into()?; + let storage = self + .extension + .storage + .get_mut(&contract_addr) + .ok_or(SimpleVMError::IteratorDoesNotExist)?; + + let iterator = storage + .iterators + .get_mut(&iterator_id) + .ok_or(SimpleVMError::IteratorDoesNotExist)?; + + let position = iterator.position; + if iterator.data.len() > position { + iterator.position += 1; + Ok(iterator.data[position].clone()) + } else { + // Empty data works like `None` in rust iterators + Ok((Vec::default(), Vec::default())) + } + } + + fn secp256k1_verify( + &mut self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + cosmwasm_crypto::secp256k1_verify(message_hash, signature, public_key) + .map_err(|_| SimpleVMError::Crypto) + } + + fn secp256k1_recover_pubkey( + &mut self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, ()>, Self::Error> { + Ok(cosmwasm_crypto::secp256k1_recover_pubkey(message_hash, signature, recovery_param) + .map_err(|_| ())) + } + + fn ed25519_verify( + &mut self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + cosmwasm_crypto::ed25519_verify(message, signature, public_key) + .map_err(|_| SimpleVMError::Crypto) + } + + fn ed25519_batch_verify( + &mut self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + cosmwasm_crypto::ed25519_batch_verify(&mut OsRng, messages, signatures, public_keys) + .map_err(|_| SimpleVMError::Crypto) + } + + fn addr_validate(&mut self, input: &str) -> Result, Self::Error> { + let canonical = match self.addr_canonicalize(input)? { + Ok(canonical) => canonical, + Err(e) => return Ok(Err(e)), + }; + let normalized = match self.addr_humanize(&canonical)? { + Ok(canonical) => canonical, + Err(e) => return Ok(Err(e)), + }; + let account = BankAccount::try_from(input.to_string())?; + Ok(if account == normalized { Ok(()) } else { Err(SimpleVMError::InvalidAddress) }) + } + + fn addr_canonicalize( + &mut self, + input: &str, + ) -> Result, Self::Error> { + // mimicks formats like hex or bech32 where different casings are valid for one address + let normalized = input.to_lowercase(); + + // Dummy input validation. This is more sophisticated for formats like bech32, where format + // and checksum are validated. + if normalized.len() < 3 { + return Ok(Err(SimpleVMError::InvalidAddress)); + } + + if normalized.len() > CANONICAL_LENGTH { + return Ok(Err(SimpleVMError::InvalidAddress)); + } + + let mut out = Vec::from(normalized); + // pad to canonical length with NULL bytes + out.resize(CANONICAL_LENGTH, 0x00); + // content-dependent rotate followed by shuffle to destroy + let rotate_by = digit_sum(&out) % CANONICAL_LENGTH; + out.rotate_left(rotate_by); + Ok(Ok(riffle_shuffle_n(out, SHUFFLES_ENCODE).try_into()?)) + } + + fn addr_humanize( + &mut self, + addr: &Self::CanonicalAddress, + ) -> Result, Self::Error> { + if addr.0.len() != CANONICAL_LENGTH { + return Ok(Err(SimpleVMError::InvalidAddress)); + } + + // Shuffle two more times which restored the original value (24 elements are back to + // original after 20 rounds) + let mut tmp = riffle_shuffle_n(addr.clone().into(), SHUFFLES_DECODE); + // Rotate back + let rotate_by = digit_sum(&tmp) % CANONICAL_LENGTH; + tmp.rotate_right(rotate_by); + // Remove NULL bytes (i.e. the padding) + let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect(); + // decode UTF-8 bytes into string + let Ok(human) = String::from_utf8(trimmed) else { + return Ok(Err(SimpleVMError::InvalidAddress)); + }; + Ok(BankAccount::try_from(Addr::unchecked(human)).map_err(|_| SimpleVMError::InvalidAddress)) + } + + fn db_read( + &mut self, + key: Self::StorageKey, + ) -> Result, Self::Error> { + let contract_addr = self.env.contract.address.clone().try_into()?; + let empty = SimpleWasmiVMStorage::default(); + Ok(self + .extension + .storage + .get(&contract_addr) + .unwrap_or(&empty) + .data + .get(&key) + .cloned()) + } + + fn db_write( + &mut self, + key: Self::StorageKey, + value: Self::StorageValue, + ) -> Result<(), Self::Error> { + let contract_addr = self.env.contract.address.clone().try_into()?; + self.extension + .storage + .entry(contract_addr) + .or_insert_with(SimpleWasmiVMStorage::default) + .data + .insert(key, value); + Ok(()) + } + + fn db_remove(&mut self, key: Self::StorageKey) -> Result<(), Self::Error> { + let contract_addr = self.env.contract.address.clone().try_into()?; + self.extension + .storage + .get_mut(&contract_addr) + .map(|contract_storage| contract_storage.data.remove(&key)); + Ok(()) + } + + fn abort(&mut self, message: String) -> Result<(), Self::Error> { + log::debug!("Contract aborted: {}", message); + Err(SimpleVMError::from(WasmiVMError::from(SystemError::ContractExecutionFailure(message)))) + } + + fn charge(&mut self, value: VmGas) -> Result<(), Self::Error> { + let gas_to_charge = match value { + VmGas::Instrumentation { metered } => u64::from(metered), + x => { + log::debug!("Charging gas: {:?}", x); + 1u64 + }, + }; + self.extension.gas.charge(gas_to_charge)?; + Ok(()) + } + + fn gas_checkpoint_push(&mut self, checkpoint: VmGasCheckpoint) -> Result<(), Self::Error> { + log::debug!("> Gas before: {:?}", self.extension.gas); + self.extension.gas.push(&checkpoint)?; + log::debug!("> Gas after: {:?}", self.extension.gas); + Ok(()) + } + + fn gas_checkpoint_pop(&mut self) -> Result<(), Self::Error> { + log::debug!("> Gas before: {:?}", self.extension.gas); + self.extension.gas.pop(); + log::debug!("> Gas after: {:?}", self.extension.gas); + Ok(()) + } + + fn gas_ensure_available(&mut self) -> Result<(), Self::Error> { + let checkpoint = + self.extension.gas.checkpoints.last().expect("invalis gas checkpoint state"); + if *checkpoint > 0 { + Ok(()) + } else { + Err(SimpleVMError::OutOfGas) + } + } + + #[cfg(feature = "stargate")] + fn ibc_transfer( + &mut self, + _channel_id: String, + _to_address: String, + _amount: Coin, + _timeout: IbcTimeout, + ) -> Result<(), Self::Error> { + todo!() + } + + #[cfg(feature = "stargate")] + fn ibc_send_packet( + &mut self, + channel_id: String, + data: Binary, + timeout: IbcTimeout, + ) -> Result<(), Self::Error> { + let contract_addr = self.env.contract.address.clone().try_into()?; + let entry = self.extension.ibc.entry(contract_addr).or_default(); + entry.packets_sent.push(SimpleIBCPacket { channel_id, data, timeout }); + Ok(()) + } + + #[cfg(feature = "stargate")] + fn ibc_close_channel(&mut self, _channel_id: String) -> Result<(), Self::Error> { + todo!() + } +} + +#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +struct BankAccount(u128); + +impl TryFrom for BankAccount { + type Error = SimpleVMError; + fn try_from(value: Addr) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for BankAccount { + type Error = SimpleVMError; + fn try_from(value: String) -> Result { + Ok(BankAccount(u128::from_str(&value).map_err(|_| SimpleVMError::InvalidAccountFormat)?)) + } +} + +impl From for Addr { + fn from(BankAccount(account): BankAccount) -> Self { + Addr::unchecked(account.to_string()) + } +} + +impl<'a> Has for SimpleWasmiVM<'a> { + fn get(&self) -> Env { + self.env.clone() + } +} +impl<'a> Has for SimpleWasmiVM<'a> { + fn get(&self) -> MessageInfo { + self.info.clone() + } +} + +impl<'a> Transactional for SimpleWasmiVM<'a> { + type Error = SimpleVMError; + fn transaction_begin(&mut self) -> Result<(), Self::Error> { + self.extension.transaction_depth += 1; + log::debug!("> Transaction begin: {}", self.extension.transaction_depth); + Ok(()) + } + fn transaction_commit(&mut self) -> Result<(), Self::Error> { + self.extension.transaction_depth -= 1; + log::debug!("< Transaction end: {}", self.extension.transaction_depth); + Ok(()) + } + fn transaction_rollback(&mut self) -> Result<(), Self::Error> { + self.extension.transaction_depth -= 1; + log::debug!("< Transaction abort: {}", self.extension.transaction_depth); + Ok(()) + } +} + +struct ConstantCostRules; +impl Rules for ConstantCostRules { + fn instruction_cost( + &self, + _: &wasm_instrument::parity_wasm::elements::Instruction, + ) -> Option { + Some(42) + } + + fn memory_grow_cost(&self) -> wasm_instrument::gas_metering::MemoryGrowCost { + wasm_instrument::gas_metering::MemoryGrowCost::Linear( + NonZeroU32::new(1024).expect("impossible"), + ) + } + + fn call_per_local_cost(&self) -> u32 { + 0 + } +} + +fn instrument_contract(code: &[u8]) -> Vec { + let module = + wasm_instrument::parity_wasm::elements::Module::from_bytes(code).expect("impossible"); + let instrumented_module = wasm_instrument::gas_metering::inject( + module, + host_function::Injector::new(Version1x::ENV_MODULE, Version1x::ENV_GAS), + &ConstantCostRules, + ) + .expect("impossible"); + instrumented_module.into_bytes().expect("impossible") +} + +fn create_vm( + extension: &mut SimpleWasmiVMExtension, + env: Env, + info: MessageInfo, +) -> Result, SimpleVMError> { + initialize(); + let code = extension + .codes + .get( + &extension + .contracts + .get(&env.clone().contract.address.try_into().unwrap()) + .expect("contract should have been uploaded") + .code_id, + ) + .expect("contract should have been uploaded") + .clone(); + let vm = SimpleWasmiVM { executing_module: None, env, info, extension, call_depth: 0 }; + let vm = new_wasmi_vm::(&code, vm)?; + Ok(vm) +} + +fn create_simple_vm( + sender: BankAccount, + contract: BankAccount, + funds: Vec, + extension: &mut SimpleWasmiVMExtension, +) -> Result, SimpleVMError> { + create_vm( + extension, + Env { + block: BlockInfo { + height: 0xDEAD_C0DE, + time: Timestamp::from_seconds(10000), + chain_id: "abstract-test".into(), + }, + transaction: None, + contract: ContractInfo { address: contract.into() }, + }, + MessageInfo { sender: sender.into(), funds }, + ) +} + +#[test] +fn test_bare() { + let code = instrument_contract(include_bytes!("../fixtures/cw20_base.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + assert_matches!( + cosmwasm_call::, OwnedWasmiVM>( + &mut vm, + r#"{ + "name": "Picasso", + "symbol": "PICA", + "decimals": 12, + "initial_balances": [], + "mint": null, + "marketing": null + }"# + .as_bytes(), + ) + .unwrap(), + InstantiateResult(CosmwasmExecutionResult::Ok(_)) + ); + assert_eq!( + cosmwasm_call::>( + &mut vm, + r#"{ "token_info": {} }"#.as_bytes(), + ) + .unwrap(), + QueryResult(CosmwasmQueryResult::Ok(Binary::new( + r#"{"name":"Picasso","symbol":"PICA","decimals":12,"total_supply":"0"}"# + .as_bytes() + .to_vec() + ))) + ); +} + +/// Tests behaviour of the runtime when malicious contract causes infinite +/// recursion. +/// +/// It’s possible for a contract to cause an infinite recursion within the +/// *host* stack. This is because host functions call contract’s `allocate` and +/// `deallocate` functions to manage memory within contract’s address space. +/// Unconditionally calling host function inside of an `allocate` can easily +/// lead to host recusing indefinitely and eventually crashing. +/// +/// This test checks whether recursion depth limit is correctly implemented. +#[test] +fn test_recursion() { + let code = instrument_contract(include_bytes!("../fixtures/recursion_test.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + assert_eq!( + cosmwasm_call::, OwnedWasmiVM>(&mut vm, b"{}"), + Err(SimpleVMError::Interpreter) + ); +} + +mod test_code_gen { + use cosmwasm_std::{ContractResult, Response}; + + use super::*; + #[test] + fn basic() { + let module: code_gen::WasmModule = + code_gen::ModuleDefinition::new(vec![], 10, None).unwrap().into(); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, module.code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + let result = cosmwasm_call::>( + &mut vm, + r#"{}"#.as_bytes(), + ) + .unwrap(); + assert_matches!(result, InstantiateResult(CosmwasmExecutionResult::Ok(_))); + let result = + cosmwasm_call::>(&mut vm, r#"{}"#.as_bytes()) + .unwrap(); + assert_matches!(result, ExecuteResult(CosmwasmExecutionResult::Ok(_))); + } + + #[test] + fn instantiate_response() { + let response = + ContractResult::Ok(Response::::new().add_attribute("Hello", "world!")); + let response_2 = + ContractResult::Ok(Response::::new().add_attribute("Hello", "mars!")); + let module: code_gen::WasmModule = + code_gen::ModuleDefinition::with_instantiate_response(response.clone()) + .unwrap() + .into(); + + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, module.code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + let result = cosmwasm_call::>( + &mut vm, + r#"{}"#.as_bytes(), + ) + .unwrap(); + assert_eq!(result, InstantiateResult(response)); + assert_ne!(result, InstantiateResult(response_2)); + } +} + +pub fn digit_sum(input: &[u8]) -> usize { + input.iter().map(|v| *v as usize).sum() +} + +fn riffle_shuffle_n(mut input: Vec, n: usize) -> Vec { + assert!(input.len() % 2 == 0, "Method only defined for even number of elements"); + let mid = input.len() / 2; + let mut tmp = Vec::with_capacity(input.len()); + for _ in 0..n { + tmp.clear(); + let (left, right) = input.split_at(mid); + for (l, r) in left.iter().zip(right.iter()) { + tmp.push(*r); + tmp.push(*l); + } + core::mem::swap(&mut input, &mut tmp); + } + input +} + +#[test] +fn test_riffle_shuffle() { + let input = [0, 1, 2, 3, 4, 5, 6, 7]; + let out1 = riffle_shuffle_n(input.to_vec(), 1); + let out2 = riffle_shuffle_n(input.to_vec(), 2); + let out3 = riffle_shuffle_n(input.to_vec(), 3); + assert_eq!(&[4, 0, 5, 1, 6, 2, 7, 3], out1.as_slice()); + assert_eq!(&[6, 4, 2, 0, 7, 5, 3, 1], out2.as_slice()); + assert_eq!(&[7, 6, 5, 4, 3, 2, 1, 0], out3.as_slice()); +} + +#[test] +fn test_riffle_shuffle_encode_decode() { + let input: Vec = (0..CANONICAL_LENGTH as u8).collect(); + let output = riffle_shuffle_n(input.clone(), SHUFFLES_ENCODE + SHUFFLES_DECODE); + assert_eq!(input, output); +} + +#[test] +fn test_orchestration_base() { + let code = instrument_contract(include_bytes!("../fixtures/cw20_base.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + let _ = cosmwasm_system_entrypoint::>( + &mut vm, + format!( + r#"{{ + "name": "Picasso", + "symbol": "PICA", + "decimals": 12, + "initial_balances": [], + "mint": {{ + "minter": "{}", + "cap": null + }}, + "marketing": null + }}"#, + sender.0 + ) + .as_bytes(), + ) + .unwrap(); + + let (_, events) = cosmwasm_system_entrypoint::>( + &mut vm, + r#"{ + "mint": { + "recipient": "10001", + "amount": "5555" + } + }"# + .as_bytes(), + ) + .unwrap(); + let attributes = vec![ + Attribute { key: "action".into(), value: "mint".into() }, + Attribute { key: "to".into(), value: "10001".into() }, + Attribute { key: "amount".into(), value: "5555".into() }, + ]; + + for attr in attributes { + assert!(events.iter().any(|e| e.attributes.contains(&attr))); + } +} + +#[test] +fn test_orchestration_advanced() { + let code = instrument_contract(include_bytes!("../fixtures/hackatom.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + assert_eq!( + cosmwasm_call::>( + &mut vm, + r#"{ "recurse": { "depth": 10, "work": 10 }}"#.as_bytes() + ) + .unwrap(), + QueryResult(CosmwasmQueryResult::Ok(Binary::new( + r#"{"hashed":"K4xL+Gub1930CJU6hdpwf0t3KNk27f5efqy9+YA6iio="}"#.as_bytes().to_vec() + ))) + ); +} + +#[test] +fn test_hook() { + let code = instrument_contract(include_bytes!("../fixtures/cw20_base.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + address, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + let _ = cosmwasm_system_entrypoint_hook::>( + &mut vm, + format!( + r#"{{ + "name": "Picasso", + "symbol": "PICA", + "decimals": 12, + "initial_balances": [], + "mint": {{ + "minter": "{}", + "cap": null + }}, + "marketing": null + }}"#, + sender.0 + ) + .as_bytes(), + |vm, msg| { + cosmwasm_call::>(vm, msg).map(Into::into) + }, + ) + .unwrap(); + let r = cosmwasm_system_entrypoint_hook::>( + &mut vm, + r#"{ + "mint": { + "recipient": "10001", + "amount": "5555" + } + }"# + .as_bytes(), + |_, _| Ok(ContractResult::Err("Bro".into())), + ); + assert_eq!( + r, + Err(SimpleVMError::VMError(WasmiVMError::SystemError( + SystemError::ContractExecutionFailure("Bro".into()) + ))) + ); +} + +#[test] +fn test_reply() { + let code = instrument_contract(include_bytes!("../fixtures/reflect.wasm")); + let code_hackatom = instrument_contract(include_bytes!("../fixtures/hackatom.wasm")); + let sender = BankAccount(100); + let address = BankAccount(10_000); + let hackatom_address = BankAccount(10_001); + let funds = vec![]; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code), (0x1338, code_hackatom)]), + contracts: BTreeMap::from([ + (address, CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }), + ( + hackatom_address, + CosmwasmContractMeta { code_id: 0x1338, admin: None, label: String::new() }, + ), + ]), + next_account_id: BankAccount(10_002), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + { + let mut vm = + create_simple_vm(address, hackatom_address, funds.clone(), &mut extension).unwrap(); + let (_, events) = cosmwasm_system_entrypoint::( + &mut vm, + r#"{"verifier": "10000", "beneficiary": "10000"}"#.as_bytes(), + ) + .unwrap(); + + assert!(events.iter().any(|e| e + .attributes + .contains(&Attribute { key: "Let the".into(), value: "hacking begin".into() }))); + } + log::debug!("{:?}", extension.storage); + { + let mut vm = create_simple_vm(sender, address, funds, &mut extension).unwrap(); + let _ = cosmwasm_system_entrypoint::>( + &mut vm, + r#"{}"#.as_bytes(), + ) + .unwrap(); + + let (_, events) = cosmwasm_system_entrypoint::>( + &mut vm, + r#"{ + "reflect_sub_msg": { + "msgs": [{ + "id": 10, + "msg": { + "wasm": { + "execute": { + "contract_addr": "10001", + "msg": "eyAicmVsZWFzZSI6IHt9IH0=", + "funds": [] + } + } + }, + "gas_limit": null, + "reply_on": "always" + }] + } + }"# + .as_bytes(), + ) + .unwrap(); + + let attributes = vec![ + Attribute { key: "action".into(), value: "release".into() }, + Attribute { key: "destination".into(), value: "10000".into() }, + Attribute { key: "action".into(), value: "reflect_subcall".into() }, + ]; + + for attr in attributes { + assert!(events.iter().any(|e| e.attributes.contains(&attr))); + } + } +} + +#[cfg(feature = "stargate")] +mod cw20_ics20 { + use super::{ + create_vm, instrument_contract, BankAccount, Gas, OwnedWasmiVM, SimpleIBCPacket, + SimpleIBCState, SimpleWasmiVM, SimpleWasmiVMExtension, + }; + + use super::{assert_matches, format, vec}; + use alloc::{collections::BTreeMap, string::String, vec::Vec}; + use cosmwasm_std::{ + Addr, Binary, BlockInfo, Coin, ContractInfo, ContractResult, Env, IbcChannel, + IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, + IbcPacketReceiveMsg, MessageInfo, Timestamp, Uint128, + }; + use cosmwasm_vm::{ + executor::{ + cosmwasm_call_serialize, + ibc::{ + IbcChannelConnectCall, IbcChannelOpenCall, IbcChannelOpenResult, + IbcPacketReceiveCall, + }, + ExecuteCall, InstantiateCall, + }, + system::{ + cosmwasm_system_entrypoint, cosmwasm_system_entrypoint_serialize, CosmwasmContractMeta, + }, + }; + use cw20_ics20::ibc::{Ics20Ack, Ics20Packet}; + + const DEFAULT_TIMEOUT: u64 = 3600; + const CONTRACT_PORT: &str = "ibc:wasm1234567890abcdef"; + const REMOTE_PORT: &str = "transfer"; + const CONNECTION_ID: &str = "connection-2"; + const ICS20_VERSION: &str = "ics20-1"; + + fn funded(funds: Vec, info: MessageInfo) -> MessageInfo { + MessageInfo { sender: info.sender, funds } + } + + fn create_channel(channel_id: &str) -> IbcChannel { + IbcChannel::new( + IbcEndpoint { port_id: CONTRACT_PORT.into(), channel_id: channel_id.into() }, + IbcEndpoint { port_id: REMOTE_PORT.into(), channel_id: channel_id.into() }, + IbcOrder::Unordered, + ICS20_VERSION, + CONNECTION_ID, + ) + } + + fn reverse_channel(channel: IbcChannel) -> IbcChannel { + IbcChannel::new( + channel.counterparty_endpoint, + channel.endpoint, + channel.order, + channel.version, + channel.connection_id, + ) + } + + fn reverse_packet( + channel: IbcChannel, + Ics20Packet { amount, denom, receiver, sender, memo }: Ics20Packet, + ) -> Ics20Packet { + let reversed_channel = reverse_channel(channel); + Ics20Packet { + amount, + denom: format!( + "{}/{}/{}", + reversed_channel.endpoint.port_id, reversed_channel.endpoint.channel_id, denom + ), + receiver: sender, + sender: receiver, + memo, + } + } + + #[test] + #[allow(clippy::too_many_lines)] + fn test_ics20_ibc_orchestration() { + // State setup + let code = instrument_contract(include_bytes!("../fixtures/cw20_ics20.wasm")); + let sender = BankAccount(100); + let contract = BankAccount(10_000); + let funds = vec![]; + let env = Env { + block: BlockInfo { + height: 0xDEAD_C0DE, + time: Timestamp::from_seconds(10000), + chain_id: "abstract-test".into(), + }, + transaction: None, + contract: ContractInfo { address: contract.into() }, + }; + let info = MessageInfo { sender: sender.into(), funds }; + let mut extension = SimpleWasmiVMExtension { + storage: BTreeMap::default(), + codes: BTreeMap::from([(0x1337, code)]), + contracts: BTreeMap::from([( + contract, + CosmwasmContractMeta { code_id: 0x1337, admin: None, label: String::new() }, + )]), + next_account_id: BankAccount(10_001), + transaction_depth: 0, + gas: Gas::new(100_000_000), + ..Default::default() + }; + + // IBC channel opening + let channel_name = "PicassoXTerra"; + let channel = create_channel(channel_name); + + { + let mut vm = create_vm(&mut extension, env.clone(), info.clone()).unwrap(); + + // Contract instantiation + assert_matches!( + cosmwasm_system_entrypoint::>( + &mut vm, + format!( + r#"{{ + "default_gas_limit": null, + "default_timeout": {}, + "gov_contract": "{}", + "allowlist": [] + }}"#, + DEFAULT_TIMEOUT, sender.0 + ) + .as_bytes(), + ), + Ok(_) + ); + + assert_matches!( + cosmwasm_call_serialize::, _>( + &mut vm, + &IbcChannelOpenMsg::OpenInit { channel: channel.clone() } + ) + .unwrap(), + IbcChannelOpenResult(ContractResult::Ok(None)) + ); + assert_matches!( + cosmwasm_system_entrypoint_serialize::< + IbcChannelConnectCall, + OwnedWasmiVM, + _, + >( + &mut vm, + &IbcChannelConnectMsg::OpenAck { + channel: channel.clone(), + counterparty_version: ICS20_VERSION.into(), + }, + ), + Ok(_) + ); + } + + { + // Actual cross-chain execution + let mut vm = create_vm( + &mut extension, + env.clone(), + funded( + vec![Coin { denom: "PICA".into(), amount: Uint128::new(1000) }], + info.clone(), + ), + ) + .unwrap(); + assert_matches!( + cosmwasm_system_entrypoint::>( + &mut vm, + format!( + r#"{{ + "transfer": {{ + "channel": "{channel_name}", + "remote_address": "0", + "timeout": null + }} + }}"# + ) + .as_bytes(), + ), + Ok(_) + ); + } + + // cw20-ics20 is symmetric, we should be able to forward sent packets + // back to the contract by reverting the channel/packets + let (_next_seq, packets_to_dispatch) = extension.ibc.iter().fold( + (0u64, Vec::with_capacity(extension.ibc.values().map(|x| x.packets_sent.len()).sum())), + |(next_seq, mut packets_to_dispatch), (_, SimpleIBCState { packets_sent })| { + packets_sent.iter().enumerate().for_each( + |(i, SimpleIBCPacket { data, timeout, .. })| { + let packet = serde_json::from_slice::(data.as_ref()).unwrap(); + packets_to_dispatch.push(IbcPacket::new( + Binary::from( + serde_json::to_vec(&reverse_packet(channel.clone(), packet)) + .unwrap(), + ), + channel.counterparty_endpoint.clone(), + channel.endpoint.clone(), + next_seq + i as u64, + timeout.clone(), + )); + }, + ); + (next_seq + packets_sent.len() as u64, packets_to_dispatch) + }, + ); + + let mut vm = create_vm(&mut extension, env, info).unwrap(); + let make_receive_msg = |packet| IbcPacketReceiveMsg::new(packet, Addr::unchecked("1337")); + for packet in packets_to_dispatch { + let (acknowledgment, _events) = cosmwasm_system_entrypoint_serialize::< + IbcPacketReceiveCall, + OwnedWasmiVM, + _, + >(&mut vm, &make_receive_msg(packet)) + .unwrap(); + let acknowledgment = + serde_json::from_slice::(acknowledgment.unwrap().as_ref()).unwrap(); + /* + See `ack_success` in cw20-ics20 ibs.rs + + // create a serialized success message + fn ack_success() -> Binary { + let res = Ics20Ack::Result(b"1".into()); + to_Binary::new(&res).unwrap() + } + */ + assert_eq!(acknowledgment, Ics20Ack::Result(b"1".into())); + } + } +} diff --git a/composable/vm-wasmi/src/validation.rs b/composable/vm-wasmi/src/validation.rs new file mode 100644 index 00000000..7f269876 --- /dev/null +++ b/composable/vm-wasmi/src/validation.rs @@ -0,0 +1,260 @@ +use super::version::Export; +use alloc::vec::Vec; +use wasm_instrument::parity_wasm::elements::{ + ExportSection, External, FunctionSection, ImportSection, Instruction, Internal, Module, Type, + TypeSection, ValueType, +}; +use wasmi_validation::Validator; + +#[derive(Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum ValidationError { + Validation(wasmi_validation::Error), + ExportMustBeAFunction(&'static str), + EntryPointPointToImport(&'static str), + ExportDoesNotExist(&'static str), + ExportWithoutSignature(&'static str), + ExportWithWrongSignature { + export_name: &'static str, + expected_signature: Vec, + actual_signature: Vec, + }, + MissingMandatoryExport(&'static str), + CannotImportTable, + CannotImportGlobal, + CannotImportMemory, + ImportWithoutSignature, + ImportIsBanned(&'static str, &'static str), + MustDeclareOneInternalMemory, + MustDeclareOneTable, + TableExceedLimit, + BrTableExceedLimit, + GlobalsExceedLimit, + GlobalFloatingPoint, + LocalFloatingPoint, + ParamFloatingPoint, + FunctionParameterExceedLimit, +} + +#[derive(PartialEq, Eq)] +pub enum ExportRequirement { + Mandatory, + Optional, +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeValidation<'a>(&'a Module); + +impl<'a> CodeValidation<'a> { + #[must_use] + pub fn new(module: &'a Module) -> Self { + CodeValidation(module) + } + + /// Middleware function for `wasmi_validation::validate_module` + /// + /// * `input`: Custom input to validator + pub fn validate_module( + self, + input: ::Input, + ) -> Result { + wasmi_validation::validate_module::(self.0, input) + .map_err(ValidationError::Validation)?; + Ok(self) + } + + /// Checks if the expected exports exist and correct. + /// + /// If the export is mandatory, then it has to be present in the wasm module it's signature must + /// be correct. In both cases, exports need to have the identical signatures as well. + pub fn validate_exports(self, expected_exports: &[Export]) -> Result { + let CodeValidation(module) = self; + let types = module.type_section().map_or(Default::default(), TypeSection::types); + let export_entries = + module.export_section().map_or(Default::default(), ExportSection::entries); + let func_entries = + module.function_section().map_or(Default::default(), FunctionSection::entries); + let fn_space_offset = module + .import_section() + .map_or(Default::default(), ImportSection::entries) + .iter() + .filter(|entry| matches!(*entry.external(), External::Function(_))) + .count(); + for (requirement, name, signature) in expected_exports { + match (requirement, export_entries.iter().find(|e| &e.field() == name)) { + (_, Some(export)) => { + let fn_idx = match export.internal() { + Internal::Function(ref fn_idx) => Ok(*fn_idx), + _ => Err(ValidationError::ExportMustBeAFunction(name)), + }?; + #[allow(clippy::cast_possible_truncation)] + let fn_idx = match fn_idx.checked_sub(fn_space_offset as u32) { + Some(fn_idx) => Ok(fn_idx), + None => Err(ValidationError::EntryPointPointToImport(name)), + }?; + let func_ty_idx = func_entries + .get(fn_idx as usize) + .ok_or(ValidationError::ExportDoesNotExist(name))? + .type_ref(); + let Type::Function(ref func_ty) = types + .get(func_ty_idx as usize) + .ok_or(ValidationError::ExportWithoutSignature(name))?; + if signature != &func_ty.params() { + return Err(ValidationError::ExportWithWrongSignature { + export_name: name, + expected_signature: signature.to_vec(), + actual_signature: func_ty.params().to_vec(), + }); + } + }, + (ExportRequirement::Mandatory, None) => + return Err(ValidationError::MissingMandatoryExport(name)), + (ExportRequirement::Optional, None) => {}, + } + } + Ok(self) + } + + /// Check if the module imports the correct externals and avoids importing + /// the banned imports. + /// + /// Currently only functions are imported, so it will fail if any other + /// external is imported. + pub fn validate_imports( + self, + import_banlist: &[(&'static str, &'static str)], + ) -> Result { + let CodeValidation(module) = self; + let types = module.type_section().map_or(Default::default(), TypeSection::types); + let import_entries = + module.import_section().map_or(Default::default(), ImportSection::entries); + for import in import_entries { + let type_idx = match import.external() { + External::Table(_) => Err(ValidationError::CannotImportTable), + External::Global(_) => Err(ValidationError::CannotImportGlobal), + External::Memory(_) => Err(ValidationError::CannotImportMemory), + External::Function(ref type_idx) => Ok(type_idx), + }?; + let import_name = import.field(); + let import_module = import.module(); + let Type::Function(_) = + types.get(*type_idx as usize).ok_or(ValidationError::ImportWithoutSignature)?; + if let Some((m, f)) = + import_banlist.iter().find(|(m, f)| m == &import_module && f == &import_name) + { + return Err(ValidationError::ImportIsBanned(m, f)); + } + } + Ok(self) + } + + /// Verify if the memory is setup correctly. + /// + /// Currently only a single memory section is supported. + pub fn validate_memory_limit(self) -> Result { + let CodeValidation(module) = self; + if module.memory_section().map_or(false, |ms| ms.entries().len() != 1) { + Err(ValidationError::MustDeclareOneInternalMemory) + } else { + Ok(self) + } + } + + /// Make sure that there is a table and the table's entries are smaller than the `limit`. + pub fn validate_table_size_limit(self, limit: u32) -> Result { + let CodeValidation(module) = self; + if let Some(table_section) = module.table_section() { + if table_section.entries().len() > 1 { + return Err(ValidationError::MustDeclareOneTable); + } + if let Some(table_type) = table_section.entries().first() { + if table_type.limits().initial() > limit { + return Err(ValidationError::TableExceedLimit); + } + } + } + Ok(self) + } + + /// Make sure that the br table length doesn't exceed the `limit`. + pub fn validate_br_table_size_limit(self, limit: u32) -> Result { + let CodeValidation(module) = self; + if let Some(code_section) = module.code_section() { + for instr in code_section.bodies().iter().flat_map(|body| body.code().elements()) { + if let Instruction::BrTable(table) = instr { + if table.table.len() > limit as usize { + return Err(ValidationError::BrTableExceedLimit); + } + } + } + }; + Ok(self) + } + + /// Make sure that the count of global variables don't exceed the `limit`. + pub fn validate_global_variable_limit(self, limit: u32) -> Result { + let CodeValidation(module) = self; + if let Some(global_section) = module.global_section() { + if global_section.entries().len() > limit as usize { + return Err(ValidationError::GlobalsExceedLimit); + } + } + Ok(self) + } + + /// Make sure that there is no floating types. Floating point types yield to undeterministic + /// wasm builds. That's why they are undesirable. + pub fn validate_no_floating_types(self) -> Result { + let CodeValidation(module) = self; + if let Some(global_section) = module.global_section() { + for global in global_section.entries() { + match global.global_type().content_type() { + ValueType::F32 | ValueType::F64 => + return Err(ValidationError::GlobalFloatingPoint), + _ => {}, + } + } + } + if let Some(code_section) = module.code_section() { + for func_body in code_section.bodies() { + for local in func_body.locals() { + match local.value_type() { + ValueType::F32 | ValueType::F64 => + return Err(ValidationError::LocalFloatingPoint), + _ => {}, + } + } + } + } + if let Some(type_section) = module.type_section() { + for wasm_type in type_section.types() { + match wasm_type { + Type::Function(func_type) => { + let return_type = func_type.results().first(); + for value_type in func_type.params().iter().chain(return_type) { + match value_type { + ValueType::F32 | ValueType::F64 => + return Err(ValidationError::ParamFloatingPoint), + _ => {}, + } + } + }, + } + } + } + Ok(self) + } + + /// Make sure that the count of parameters in functions do not exceed the `limit`. + pub fn validate_parameter_limit(self, limit: u32) -> Result { + let CodeValidation(module) = self; + if let Some(type_section) = module.type_section() { + for Type::Function(func) in type_section.types() { + if func.params().len() > limit as usize { + return Err(ValidationError::FunctionParameterExceedLimit); + } + } + } + Ok(self) + } +} diff --git a/composable/vm-wasmi/src/version.rs b/composable/vm-wasmi/src/version.rs new file mode 100644 index 00000000..56d6dcf6 --- /dev/null +++ b/composable/vm-wasmi/src/version.rs @@ -0,0 +1,117 @@ +use super::validation::ExportRequirement; +use cosmwasm_vm::executor::ibc::{ + IbcChannelCloseCall, IbcChannelConnectCall, IbcChannelOpenCall, IbcPacketAckCall, + IbcPacketReceiveCall, IbcPacketTimeoutCall, +}; + +use cosmwasm_vm::executor::{ + AllocateCall, AsFunctionName, DeallocateCall, ExecuteCall, InstantiateCall, MigrateCall, + QueryCall, ReplyCall, +}; +use wasm_instrument::parity_wasm::elements::ValueType; + +/// Requirement, Export name, Parameters +pub type Export = (ExportRequirement, &'static str, &'static [ValueType]); + +pub trait Version { + /// `ENV_MODULE.ENV_GAS` function import should be injected by the instrumentor. + const ENV_MODULE: &'static str = "env"; + const ENV_GAS: &'static str = "gas"; + const EXPORTS: &'static [Export]; + const IBC_EXPORTS: &'static [Export]; +} + +#[allow(clippy::module_name_repetitions)] +pub struct Version1x; + +impl Version for Version1x { + const EXPORTS: &'static [Export] = &[ + // We support v1+ + ( + ExportRequirement::Mandatory, + // extern "C" fn interface_version_8() -> () {} + "interface_version_8", + &[], + ), + // Memory related exports. + ( + ExportRequirement::Mandatory, + // extern "C" fn allocate(size: usize) -> u32; + AllocateCall::<()>::NAME, + &[ValueType::I32], + ), + ( + ExportRequirement::Mandatory, + // extern "C" fn deallocate(pointer: u32); + DeallocateCall::<()>::NAME, + &[ValueType::I32], + ), + // Contract execution exports. + ( + ExportRequirement::Mandatory, + // extern "C" fn instantiate(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32; + InstantiateCall::<()>::NAME, + &[ValueType::I32, ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Optional, + // extern "C" fn execute(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32; + ExecuteCall::<()>::NAME, + &[ValueType::I32, ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Optional, + // extern "C" fn query(env_ptr: u32, msg_ptr: u32) -> u32; + QueryCall::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Optional, + // extern "C" fn migrate(env_ptr: u32, msg_ptr: u32) -> u32; + MigrateCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Optional, + // extern "C" fn reply(env_ptr: u32, msg_ptr: u32) -> u32; + ReplyCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ]; + + // IBC callbacks that a contract must export to be considered IBC capable: + // extern "C" fn ibc_channel_open(env_ptr: u32, msg_ptr: u32) -> u32; + // extern "C" fn ibc_channel_connect(env_ptr: u32, msg_ptr: u32) -> u32; + // extern "C" fn ibc_channel_close(env_ptr: u32, msg_ptr: u32) -> u32; + // extern "C" fn ibc_packet_receive(env_ptr: u32, msg_ptr: u32) -> u32; + // extern "C" fn ibc_packet_ack(env_ptr: u32, msg_ptr: u32) -> u32; + // extern "C" fn ibc_packet_timeout(env_ptr: u32, msg_ptr: u32) -> u32; + const IBC_EXPORTS: &'static [Export] = &[ + (ExportRequirement::Mandatory, IbcChannelOpenCall::NAME, &[ValueType::I32, ValueType::I32]), + ( + ExportRequirement::Mandatory, + IbcChannelConnectCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Mandatory, + IbcChannelCloseCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Mandatory, + IbcPacketReceiveCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Mandatory, + IbcPacketAckCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ( + ExportRequirement::Mandatory, + IbcPacketTimeoutCall::<()>::NAME, + &[ValueType::I32, ValueType::I32], + ), + ]; +} diff --git a/composable/vm-wasmi/src/vm.rs b/composable/vm-wasmi/src/vm.rs new file mode 100644 index 00000000..757369c8 --- /dev/null +++ b/composable/vm-wasmi/src/vm.rs @@ -0,0 +1,580 @@ +use crate::{ + WasmiContext, WasmiFunctionName, WasmiFunctionParams, WasmiFunctionResult, WasmiInput, + WasmiModule, WasmiOutput, WasmiVMError, +}; +use alloc::{string::String, vec::Vec}; +use core::{ + fmt::{Debug, Display}, + marker::PhantomData, +}; +#[cfg(feature = "iterator")] +use cosmwasm_std::Order; +use cosmwasm_std::{ + Addr, Binary, CanonicalAddr, CodeInfoResponse, Coin, ContractInfoResponse, Env, Event, + MessageInfo, Reply, SystemResult, +}; +use cosmwasm_vm::{ + executor::{CosmwasmQueryResult, ExecutorError, QueryResult}, + has::Has, + memory::{MemoryReadError, MemoryWriteError, Pointable, ReadableMemory, WritableMemory}, + system::{CosmwasmCodeId, CosmwasmContractMeta, SystemError}, + transaction::{Transactional, TransactionalErrorOf}, + vm::{ + VMBase, VmAddressOf, VmCanonicalAddressOf, VmContractMetaOf, VmErrorOf, VmGas, + VmGasCheckpoint, VmMessageCustomOf, VmQueryCustomOf, VmStorageKeyOf, VmStorageValueOf, VM, + }, +}; +use wasmi::{AsContextMut, Extern, Store}; + +/// Maximum number of recursive calls from host to contract. +/// +/// To manage memory host calls `allocate` and `deallocate` contract functions. +/// If those functions are written incorrectly or maliciously they may invoke +/// host function which then tries to call `allocate` or `deallocate` again +/// leading to infinite recursion. +/// +/// This parameter limits how deep such recursion can be. Note that initial +/// invocation of a contract function is counted. +const MAX_CALL_DEPTH: u32 = 2; + +/// Base traits that are needed to be implemented to work with `WasmiVM`. +pub trait WasmiBaseVM = Sized + + VMBase< + ContractMeta = CosmwasmContractMeta>, + StorageKey = Vec, + StorageValue = Vec, + > + WasmiContext + + Transactional + + Has + + Has +where + VmAddressOf: Clone + TryFrom> + Into, + VmCanonicalAddressOf: + Clone + TryFrom, Error = VmErrorOf> + Into, + VmErrorOf: From + + From + + From + + From + + From + + From + + From> + + wasmi::core::HostError + + Debug + + Display; + +/// `WasmiVM` that owns the execution and the underlying VM. +pub type OwnedWasmiVM = WasmiVM>; + +/// Generic `WasmiVM` that handles wasm function calls, memory operations, charging gas, etc. +/// It handles the outer logic and forwards the execution to the underlying VM for VM +/// specific behavior. +pub struct WasmiVM>(pub S); + +impl> WasmiVM { + pub fn new(store: S) -> Self { + WasmiVM(store) + } +} + +/// Base VM implementation which basically, charges for gas and forwards +/// the execution to the underlying VM. +impl VMBase for WasmiVM +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Input<'x> = WasmiInput; + type Output<'x> = WasmiOutput; + type QueryCustom = VmQueryCustomOf; + type MessageCustom = VmMessageCustomOf; + type ContractMeta = VmContractMetaOf; + type Address = VmAddressOf; + type CanonicalAddress = VmCanonicalAddressOf; + type StorageKey = VmStorageKeyOf; + type StorageValue = VmStorageValueOf; + type Error = VmErrorOf; + + fn running_contract_meta(&mut self) -> Result { + self.charge(VmGas::GetContractMeta)?; + self.0.as_context_mut().data_mut().running_contract_meta() + } + + #[cfg(feature = "iterator")] + fn db_scan( + &mut self, + start: Option, + end: Option, + order: Order, + ) -> Result { + self.charge(VmGas::DbScan)?; + self.0.as_context_mut().data_mut().db_scan(start, end, order) + } + + #[cfg(feature = "iterator")] + fn db_next( + &mut self, + iterator_id: u32, + ) -> Result<(Self::StorageKey, Self::StorageValue), Self::Error> { + self.charge(VmGas::DbNext)?; + self.0.as_context_mut().data_mut().db_next(iterator_id) + } + + fn set_contract_meta( + &mut self, + address: Self::Address, + new_contract_meta: Self::ContractMeta, + ) -> Result<(), Self::Error> { + self.charge(VmGas::SetContractMeta)?; + self.0.as_context_mut().data_mut().set_contract_meta(address, new_contract_meta) + } + + fn contract_meta(&mut self, address: Self::Address) -> Result { + self.charge(VmGas::GetContractMeta)?; + self.0.as_context_mut().data_mut().contract_meta(address) + } + + fn continue_query( + &mut self, + address: Self::Address, + message: &[u8], + ) -> Result { + self.charge(VmGas::ContinueQuery)?; + self.0.as_context_mut().data_mut().continue_query(address, message) + } + + fn continue_execute( + &mut self, + address: Self::Address, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + self.charge(VmGas::ContinueExecute { + nb_of_coins: u32::try_from(funds.len()).map_err(|_| WasmiVMError::MaxLimitExceeded)?, + })?; + self.0 + .as_context_mut() + .data_mut() + .continue_execute(address, funds, message, event_handler) + } + + fn continue_instantiate( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error> { + self.charge(VmGas::ContinueInstantiate { + nb_of_coins: u32::try_from(funds.len()).map_err(|_| WasmiVMError::MaxLimitExceeded)?, + })?; + self.0.as_context_mut().data_mut().continue_instantiate( + contract_meta, + funds, + message, + event_handler, + ) + } + + fn continue_instantiate2( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + salt: &[u8], + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result<(Self::Address, Option), Self::Error> { + self.charge(VmGas::ContinueInstantiate2 { + nb_of_coins: u32::try_from(funds.len()).map_err(|_| WasmiVMError::MaxLimitExceeded)?, + })?; + self.0.as_context_mut().data_mut().continue_instantiate2( + contract_meta, + funds, + salt, + message, + event_handler, + ) + } + + fn continue_migrate( + &mut self, + address: Self::Address, + message: &[u8], + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + self.charge(VmGas::ContinueMigrate)?; + self.0 + .as_context_mut() + .data_mut() + .continue_migrate(address, message, event_handler) + } + + fn continue_reply( + &mut self, + message: Reply, + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + self.charge(VmGas::ContinueReply)?; + self.0.as_context_mut().data_mut().continue_reply(message, event_handler) + } + + fn query_custom( + &mut self, + query: Self::QueryCustom, + ) -> Result, Self::Error> { + self.charge(VmGas::QueryCustom)?; + self.0.as_context_mut().data_mut().query_custom(query) + } + + fn message_custom( + &mut self, + message: Self::MessageCustom, + event_handler: &mut dyn FnMut(Event), + ) -> Result, Self::Error> { + self.charge(VmGas::MessageCustom)?; + self.0.as_context_mut().data_mut().message_custom(message, event_handler) + } + + fn query_raw( + &mut self, + address: Self::Address, + key: Self::StorageKey, + ) -> Result, Self::Error> { + self.charge(VmGas::QueryRaw)?; + self.0.as_context_mut().data_mut().query_raw(address, key) + } + + fn transfer_from( + &mut self, + from: &Self::Address, + to: &Self::Address, + funds: &[Coin], + ) -> Result<(), Self::Error> { + self.charge(VmGas::Transfer { + nb_of_coins: u32::try_from(funds.len()).map_err(|_| WasmiVMError::MaxLimitExceeded)?, + })?; + self.0.as_context_mut().data_mut().transfer_from(from, to, funds) + } + + fn transfer(&mut self, to: &Self::Address, funds: &[Coin]) -> Result<(), Self::Error> { + self.charge(VmGas::Transfer { + nb_of_coins: u32::try_from(funds.len()).map_err(|_| WasmiVMError::MaxLimitExceeded)?, + })?; + self.0.as_context_mut().data_mut().transfer(to, funds) + } + + fn burn(&mut self, funds: &[Coin]) -> Result<(), Self::Error> { + self.charge(VmGas::Burn)?; + self.0.as_context_mut().data_mut().burn(funds) + } + + fn balance(&mut self, account: &Self::Address, denom: String) -> Result { + self.charge(VmGas::Balance)?; + self.0.as_context_mut().data_mut().balance(account, denom) + } + + fn all_balance(&mut self, account: &Self::Address) -> Result, Self::Error> { + self.charge(VmGas::AllBalance)?; + self.0.as_context_mut().data_mut().all_balance(account) + } + + fn supply(&mut self, denom: String) -> Result { + self.charge(VmGas::Supply)?; + self.0.as_context_mut().data_mut().supply(denom) + } + + fn query_contract_info( + &mut self, + address: Self::Address, + ) -> Result { + self.charge(VmGas::QueryContractInfo)?; + self.0.as_context_mut().data_mut().query_contract_info(address) + } + + fn query_code_info(&mut self, id: CosmwasmCodeId) -> Result { + self.charge(VmGas::QueryCodeInfo)?; + self.0.as_context_mut().data_mut().query_code_info(id) + } + + fn debug(&mut self, message: Vec) -> Result<(), Self::Error> { + self.charge(VmGas::Debug)?; + self.0.as_context_mut().data_mut().debug(message) + } + + fn db_read( + &mut self, + key: Self::StorageKey, + ) -> Result, Self::Error> { + self.charge(VmGas::DbRead)?; + self.0.as_context_mut().data_mut().db_read(key) + } + + fn db_write( + &mut self, + key: Self::StorageKey, + value: Self::StorageValue, + ) -> Result<(), Self::Error> { + self.charge(VmGas::DbWrite)?; + self.0.as_context_mut().data_mut().db_write(key, value) + } + + fn db_remove(&mut self, key: Self::StorageKey) -> Result<(), Self::Error> { + self.charge(VmGas::DbRemove)?; + self.0.as_context_mut().data_mut().db_remove(key) + } + + fn addr_validate(&mut self, input: &str) -> Result, Self::Error> { + self.charge(VmGas::AddrValidate)?; + self.0.as_context_mut().data_mut().addr_validate(input) + } + + fn addr_canonicalize( + &mut self, + input: &str, + ) -> Result, Self::Error> { + self.charge(VmGas::AddrCanonicalize)?; + self.0.as_context_mut().data_mut().addr_canonicalize(input) + } + + fn addr_humanize( + &mut self, + addr: &Self::CanonicalAddress, + ) -> Result, Self::Error> { + self.charge(VmGas::AddrHumanize)?; + self.0.as_context_mut().data_mut().addr_humanize(addr) + } + + fn abort(&mut self, message: String) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().abort(message) + } + + fn charge(&mut self, value: VmGas) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().charge(value) + } + + fn gas_checkpoint_push(&mut self, checkpoint: VmGasCheckpoint) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().gas_checkpoint_push(checkpoint) + } + + fn gas_checkpoint_pop(&mut self) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().gas_checkpoint_pop() + } + + fn gas_ensure_available(&mut self) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().gas_ensure_available() + } + + fn secp256k1_verify( + &mut self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.0.as_context_mut().data_mut().charge(VmGas::Secp256k1Verify)?; + self.0 + .as_context_mut() + .data_mut() + .secp256k1_verify(message_hash, signature, public_key) + } + + fn secp256k1_recover_pubkey( + &mut self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, ()>, Self::Error> { + self.charge(VmGas::Secp256k1RecoverPubkey)?; + self.0.as_context_mut().data_mut().secp256k1_recover_pubkey( + message_hash, + signature, + recovery_param, + ) + } + + fn ed25519_verify( + &mut self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.charge(VmGas::Ed25519Verify)?; + self.0 + .as_context_mut() + .data_mut() + .ed25519_verify(message, signature, public_key) + } + + fn ed25519_batch_verify( + &mut self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + self.charge(VmGas::Ed25519BatchVerify)?; + self.0 + .as_context_mut() + .data_mut() + .ed25519_batch_verify(messages, signatures, public_keys) + } + + #[cfg(feature = "stargate")] + fn ibc_transfer( + &mut self, + channel_id: String, + to_address: String, + amount: Coin, + timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), Self::Error> { + self.charge(VmGas::IbcTransfer)?; + self.0 + .as_context_mut() + .data_mut() + .ibc_transfer(channel_id, to_address, amount, timeout) + } + + #[cfg(feature = "stargate")] + fn ibc_send_packet( + &mut self, + channel_id: String, + data: Binary, + timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), Self::Error> { + self.charge(VmGas::IbcSendPacket)?; + self.0.as_context_mut().data_mut().ibc_send_packet(channel_id, data, timeout) + } + + #[cfg(feature = "stargate")] + fn ibc_close_channel(&mut self, channel_id: String) -> Result<(), Self::Error> { + self.charge(VmGas::IbcCloseChannel)?; + self.0.as_context_mut().data_mut().ibc_close_channel(channel_id) + } +} + +impl VM for WasmiVM +where + V: WasmiBaseVM, + S: AsContextMut, +{ + fn raw_call<'a, O>( + &mut self, + WasmiInput( + WasmiFunctionName(function_name), + WasmiFunctionParams(params), + WasmiFunctionResult(mut result), + _, + ): Self::Input<'a>, + ) -> Result + where + O: for<'x> TryFrom, Error = VmErrorOf>, + { + log::trace!("Function name: {}", function_name); + let WasmiModule { instance, .. } = self + .0 + .as_context() + .data() + .executing_module() + .ok_or(WasmiVMError::NotADynamicModule)?; + + // TODO(aeryz): Investigate typed calls, they avoid type checks so + // could be beneficial for the performance. Also, it returns the + // output instead of writing to a mutable reference. + let export = instance + .get_export(self.0.as_context(), &function_name) + .and_then(Extern::into_func) + .ok_or(WasmiVMError::FunctionNotFound)?; + + // Avoid DOS caused by infinite recursion. + let mut ctx = self.0.as_context_mut(); + let depth = ctx.data_mut().call_depth_mut(); + if *depth >= MAX_CALL_DEPTH { + return Err(WasmiVMError::MaxCallDepthExceeded.into()); + } + *depth += 1; + + let res = export.call(ctx, ¶ms, &mut result); + + let mut ctx = self.0.as_context_mut(); + let depth = ctx.data_mut().call_depth_mut(); + *depth = depth.checked_sub(1).expect("depth to be positive"); + + res?; + + O::try_from(WasmiOutput(WasmiFunctionResult(result), PhantomData)) + } +} + +impl Pointable for WasmiVM +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Pointer = u32; +} + +impl ReadableMemory for WasmiVM +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + fn read(&self, offset: Self::Pointer, buffer: &mut [u8]) -> Result<(), Self::Error> { + let WasmiModule { memory, .. } = self + .0 + .as_context() + .data() + .executing_module() + .ok_or(WasmiVMError::NotADynamicModule)?; + memory + .read(self.0.as_context(), offset as usize, buffer) + .map_err(|_| WasmiVMError::LowLevelMemoryReadError.into()) + } +} + +impl WritableMemory for WasmiVM +where + V: WasmiBaseVM, + S: AsContextMut, +{ + type Error = VmErrorOf; + fn write(&mut self, offset: Self::Pointer, buffer: &[u8]) -> Result<(), Self::Error> { + let WasmiModule { memory, .. } = self + .0 + .as_context() + .data() + .executing_module() + .ok_or(WasmiVMError::NotADynamicModule)?; + memory + .write(self.0.as_context_mut(), offset as usize, buffer) + .map_err(|_| WasmiVMError::LowLevelMemoryWriteError.into()) + } +} + +impl Transactional for WasmiVM +where + V: Transactional + WasmiBaseVM, + S: AsContextMut, +{ + type Error = TransactionalErrorOf; + + fn transaction_begin(&mut self) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().transaction_begin() + } + + fn transaction_commit(&mut self) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().transaction_commit() + } + + fn transaction_rollback(&mut self) -> Result<(), Self::Error> { + self.0.as_context_mut().data_mut().transaction_rollback() + } +} + +impl, S, U> Has for WasmiVM +where + V: Transactional + WasmiBaseVM, + S: AsContextMut, +{ + fn get(&self) -> U { + self.0.as_context().data().get() + } +} diff --git a/composable/vm/LICENSE.md b/composable/vm/LICENSE.md new file mode 100644 index 00000000..ad426e3c --- /dev/null +++ b/composable/vm/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Hussein Ait Lahcen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From a1a710ab48bf3bfcf5da35fc16037d6e94b0f6e3 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 11 Sep 2024 17:02:59 +0900 Subject: [PATCH 020/133] refactor: Refactor context in pallet-cosmos-types --- frame/cosmos/types/src/context/mod.rs | 45 +++++++++++++++++++ .../src/{context.rs => context/traits.rs} | 4 +- .../types/src/{events.rs => events/mod.rs} | 28 +++++++++--- frame/cosmos/types/src/events/traits.rs | 25 +++++++++++ frame/cosmos/types/src/{gas.rs => gas/mod.rs} | 12 ++--- frame/cosmos/types/src/gas/traits.rs | 26 +++++++++++ 6 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 frame/cosmos/types/src/context/mod.rs rename frame/cosmos/types/src/{context.rs => context/traits.rs} (93%) rename frame/cosmos/types/src/{events.rs => events/mod.rs} (78%) create mode 100644 frame/cosmos/types/src/events/traits.rs rename frame/cosmos/types/src/{gas.rs => gas/mod.rs} (84%) create mode 100644 frame/cosmos/types/src/gas/traits.rs diff --git a/frame/cosmos/types/src/context/mod.rs b/frame/cosmos/types/src/context/mod.rs new file mode 100644 index 00000000..68a5ece4 --- /dev/null +++ b/frame/cosmos/types/src/context/mod.rs @@ -0,0 +1,45 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod traits; + +use crate::{ + events::{traits::EventManager as _, EventManager}, + gas::{traits::GasMeter, BasicGasMeter, Gas}, +}; + +pub struct Context { + pub gas_meter: BasicGasMeter, + pub event_manager: EventManager, +} + +impl traits::Context for Context { + type GasMeter = BasicGasMeter; + type EventManager = EventManager; + + fn new(limit: Gas) -> Self { + Self { gas_meter: Self::GasMeter::new(limit), event_manager: Self::EventManager::new() } + } + + fn gas_meter(&mut self) -> &mut Self::GasMeter { + &mut self.gas_meter + } + + fn event_manager(&mut self) -> &mut Self::EventManager { + &mut self.event_manager + } +} diff --git a/frame/cosmos/types/src/context.rs b/frame/cosmos/types/src/context/traits.rs similarity index 93% rename from frame/cosmos/types/src/context.rs rename to frame/cosmos/types/src/context/traits.rs index 1accfe77..bad4b1f3 100644 --- a/frame/cosmos/types/src/context.rs +++ b/frame/cosmos/types/src/context/traits.rs @@ -16,8 +16,8 @@ // limitations under the License. use crate::{ - events::EventManager, - gas::{Gas, GasMeter}, + events::traits::EventManager, + gas::{traits::GasMeter, Gas}, }; pub trait Context { diff --git a/frame/cosmos/types/src/events.rs b/frame/cosmos/types/src/events/mod.rs similarity index 78% rename from frame/cosmos/types/src/events.rs rename to frame/cosmos/types/src/events/mod.rs index da4e2ede..87c00544 100644 --- a/frame/cosmos/types/src/events.rs +++ b/frame/cosmos/types/src/events/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod traits; + use alloc::vec::Vec; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -45,9 +47,25 @@ pub struct EventAttribute { pub type CosmosEvents = Vec; -pub trait EventManager { - fn new() -> Self; - fn events(&self) -> CosmosEvents; - fn emit_event(&mut self, event: CosmosEvent); - fn emit_events(&mut self, events: CosmosEvents); +#[derive(Clone, Debug, Default)] +pub struct EventManager { + events: CosmosEvents, +} + +impl traits::EventManager for EventManager { + fn new() -> Self { + Self::default() + } + + fn events(&self) -> CosmosEvents { + self.events.clone() + } + + fn emit_event(&mut self, event: CosmosEvent) { + self.events.push(event); + } + + fn emit_events(&mut self, events: CosmosEvents) { + self.events.extend(events); + } } diff --git a/frame/cosmos/types/src/events/traits.rs b/frame/cosmos/types/src/events/traits.rs new file mode 100644 index 00000000..553c2218 --- /dev/null +++ b/frame/cosmos/types/src/events/traits.rs @@ -0,0 +1,25 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{CosmosEvent, CosmosEvents}; + +pub trait EventManager { + fn new() -> Self; + fn events(&self) -> CosmosEvents; + fn emit_event(&mut self, event: CosmosEvent); + fn emit_events(&mut self, events: CosmosEvents); +} diff --git a/frame/cosmos/types/src/gas.rs b/frame/cosmos/types/src/gas/mod.rs similarity index 84% rename from frame/cosmos/types/src/gas.rs rename to frame/cosmos/types/src/gas/mod.rs index 86a372a5..e85ee1a4 100644 --- a/frame/cosmos/types/src/gas.rs +++ b/frame/cosmos/types/src/gas/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod traits; + pub type Gas = u64; #[derive(Clone, Debug, Eq, PartialEq)] @@ -23,21 +25,13 @@ pub enum Error { OutOfGas, } -pub trait GasMeter { - fn new(limit: Gas) -> Self; - fn consumed_gas(&self) -> Gas; - fn gas_remaining(&self) -> Gas; - fn limit(&self) -> Gas; - fn consume_gas(&mut self, amount: Gas, descriptor: &str) -> Result; -} - #[derive(Clone, Debug)] pub struct BasicGasMeter { pub limit: Gas, pub consumed: Gas, } -impl GasMeter for BasicGasMeter { +impl traits::GasMeter for BasicGasMeter { fn new(limit: Gas) -> Self { Self { limit, consumed: 0 } } diff --git a/frame/cosmos/types/src/gas/traits.rs b/frame/cosmos/types/src/gas/traits.rs new file mode 100644 index 00000000..01c70e6f --- /dev/null +++ b/frame/cosmos/types/src/gas/traits.rs @@ -0,0 +1,26 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{Error, Gas}; + +pub trait GasMeter { + fn new(limit: Gas) -> Self; + fn consumed_gas(&self) -> Gas; + fn gas_remaining(&self) -> Gas; + fn limit(&self) -> Gas; + fn consume_gas(&mut self, amount: Gas, descriptor: &str) -> Result; +} From 2b55a1b87ee64c6a5d467eaa7c712326c92f4735 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 11 Sep 2024 20:23:08 +0900 Subject: [PATCH 021/133] feat: Add pallet-cosmos weights --- Cargo.toml | 2 ++ frame/cosmos/Cargo.toml | 17 +++++++++++++++++ frame/cosmos/src/lib.rs | 18 ++++++++++++++++++ frame/cosmos/src/weights.rs | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 frame/cosmos/Cargo.toml create mode 100644 frame/cosmos/src/lib.rs create mode 100644 frame/cosmos/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index 57119d22..9b8d213d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "composable/vm-wasmi", "cosmwasm/std", "frame/babel", + "frame/cosmos", "frame/cosmos/types", "frame/cosmos/rpc", "frame/cosmos/runtime-api", @@ -36,6 +37,7 @@ noir-runtime-common = { path = "runtime/common", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } +pallet-cosmos = { path = "frame/cosmos", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-features = false } diff --git a/frame/cosmos/Cargo.toml b/frame/cosmos/Cargo.toml new file mode 100644 index 00000000..9154cc3a --- /dev/null +++ b/frame/cosmos/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pallet-cosmos" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = ["frame-support/std", "frame-system/std", "sp-core/std"] diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs new file mode 100644 index 00000000..deb2772a --- /dev/null +++ b/frame/cosmos/src/lib.rs @@ -0,0 +1,18 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod weights; diff --git a/frame/cosmos/src/weights.rs b/frame/cosmos/src/weights.rs new file mode 100644 index 00000000..a8b97b8e --- /dev/null +++ b/frame/cosmos/src/weights.rs @@ -0,0 +1,34 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; +use frame_support::{dispatch::DispatchClass, weights::Weight}; +use sp_core::Get; + +pub trait WeightInfo { + fn default_weight() -> Weight; +} + +pub struct CosmosWeight(PhantomData); +impl WeightInfo for CosmosWeight +where + T: frame_system::Config, +{ + fn default_weight() -> Weight { + T::BlockWeights::get().get(DispatchClass::Normal).base_extrinsic + } +} From 44285b382d8ebedf682fd8bc9696ab985d0b081f Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 12 Sep 2024 14:58:52 +0900 Subject: [PATCH 022/133] feat: Add pallet-cosmos Config --- frame/cosmos/Cargo.toml | 25 +++++++- frame/cosmos/src/lib.rs | 134 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/frame/cosmos/Cargo.toml b/frame/cosmos/Cargo.toml index 9154cc3a..11bc0130 100644 --- a/frame/cosmos/Cargo.toml +++ b/frame/cosmos/Cargo.toml @@ -8,10 +8,33 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +parity-scale-codec = { version = "3.6", default-features = false, features = [ + "derive", +] } +np-cosmos = { workspace = true, default-features = false } +scale-info = { version = "2.11", default-features = false, features = [ + "derive", +] } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [features] default = ["std"] -std = ["frame-support/std", "frame-system/std", "sp-core/std"] +std = [ + "cosmos-sdk-proto/std", + "frame-support/std", + "frame-system/std", + "pallet-cosmos-x-auth-signing/std", + "pallet-cosmos-types/std", + "parity-scale-codec/std", + "np-cosmos/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", +] +try-runtime = [] diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index deb2772a..6103e01d 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -15,4 +15,138 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + pub mod weights; + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::H160; +use sp_runtime::RuntimeDebug; + +#[derive(Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum RawOrigin { + CosmosTransaction(H160), +} + +pub fn ensure_cosmos_transaction(o: OuterOrigin) -> Result +where + OuterOrigin: Into>, +{ + match o.into() { + Ok(RawOrigin::CosmosTransaction(n)) => Ok(n), + _ => Err("bad origin: expected to be a Cosmos transaction"), + } +} + +pub trait AddressMapping { + fn into_account_id(address: H160) -> A; +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use cosmos_sdk_proto::Any; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungibles, + tokens::{AssetId, Balance}, + Contains, Currency, + }, + }; + use frame_system::WeightInfo; + use np_cosmos::traits::ChainInfo; + use pallet_cosmos_types::{ + context::traits::Context, events::CosmosEvent, gas::Gas, handler::AnteDecorator, + msgservice::MsgServiceRouter, + }; + use pallet_cosmos_x_auth_signing::{ + sign_mode_handler::traits::SignModeHandler, sign_verifiable_tx::traits::SigVerifiableTx, + }; + use sp_runtime::traits::Convert; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::event] + #[pallet::generate_deposit(pub fn deposit_event)] + pub enum Event { + AnteHandled(Vec), + Executed { gas_wanted: u64, gas_used: u64, events: Vec }, + } + + #[pallet::config(with_default)] + pub trait Config: frame_system::Config { + /// Mapping from address to account id. + #[pallet::no_default] + type AddressMapping: AddressMapping; + + type Balance: Balance + Into; + + /// Identifier for the class of asset. + type AssetId: AssetId + Ord + MaybeSerializeDeserialize; + + #[pallet::no_default] + type NativeAsset: Currency; + + #[pallet::no_default] + type Assets: fungibles::metadata::Inspect< + Self::AccountId, + Balance = Self::Balance, + AssetId = Self::AssetId, + > + fungibles::Mutate + + fungibles::Balanced; + + #[pallet::no_default_bounds] + type RuntimeEvent: From + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + #[pallet::no_default] + type WeightInfo: WeightInfo; + + /// Converter between Weight and Gas. + type WeightToGas: Convert + Convert; + + /// Converter between AssetId and Denom. + #[pallet::no_default] + type AssetToDenom: Convert> + + Convert; + + /// Context for executing transactions. + type Context: Context; + + type ChainInfo: ChainInfo; + + /// Ante handler for fee and auth. + type AnteHandler: AnteDecorator; + + /// Message filter for allowed messages. + type MsgFilter: Contains; + + /// Router for redirecting messages. + #[pallet::no_default] + type MsgServiceRouter: MsgServiceRouter; + + #[pallet::no_default] + type SigVerifiableTx: SigVerifiableTx; + + /// Generate sign bytes according to the sign mode. + #[pallet::no_default] + type SignModeHandler: SignModeHandler; + + /// The maximum number of characters allowed in a memo. + #[pallet::constant] + type MaxMemoCharacters: Get; + + /// The maximum number of signatures for a transaction. + #[pallet::constant] + type TxSigLimit: Get; + + /// The maximum length of a denomination for an asset. + #[pallet::constant] + type MaxDenomLimit: Get; + } +} From 7d4bfb1e3bc6cb6961725aff3f8d802b43614361 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 12 Sep 2024 15:16:59 +0900 Subject: [PATCH 023/133] feat: Add Error to pallet-cosmos --- frame/cosmos/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 6103e01d..a2086d78 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -60,8 +60,8 @@ pub mod pallet { use frame_system::WeightInfo; use np_cosmos::traits::ChainInfo; use pallet_cosmos_types::{ - context::traits::Context, events::CosmosEvent, gas::Gas, handler::AnteDecorator, - msgservice::MsgServiceRouter, + context::traits::Context, errors::CosmosError, events::CosmosEvent, gas::Gas, + handler::AnteDecorator, msgservice::MsgServiceRouter, }; use pallet_cosmos_x_auth_signing::{ sign_mode_handler::traits::SignModeHandler, sign_verifiable_tx::traits::SigVerifiableTx, @@ -78,6 +78,11 @@ pub mod pallet { Executed { gas_wanted: u64, gas_used: u64, events: Vec }, } + #[pallet::error] + pub enum Error { + CosmosError(CosmosError), + } + #[pallet::config(with_default)] pub trait Config: frame_system::Config { /// Mapping from address to account id. From 63e88cd91f5721e906bcfa96180500ec63cc03f9 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 12 Sep 2024 22:48:35 +0900 Subject: [PATCH 024/133] feat: Add pallet-cosmos Call --- frame/cosmos/src/lib.rs | 243 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 226 insertions(+), 17 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index a2086d78..c7562794 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -21,10 +21,28 @@ extern crate alloc; pub mod weights; +pub use self::pallet::*; +use crate::weights::WeightInfo; +use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchInfo, PostDispatchInfo}, + pallet_prelude::*, +}; +use frame_system::{pallet_prelude::*, CheckWeight}; +use pallet_cosmos_types::{ + address::acc_address_from_bech32, context::traits::Context, errors::RootError, + events::traits::EventManager, gas::traits::GasMeter, handler::AnteDecorator, + msgservice::MsgServiceRouter, +}; +use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::H160; -use sp_runtime::RuntimeDebug; +use sp_runtime::{ + traits::{Convert, DispatchInfoOf, Dispatchable}, + transaction_validity::ValidTransactionBuilder, + RuntimeDebug, SaturatedConversion, +}; #[derive(Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] pub enum RawOrigin { @@ -41,6 +59,74 @@ where } } +impl Call +where + OriginFor: Into>>, + T: Send + Sync + Config, + T::RuntimeCall: Dispatchable, +{ + pub fn is_self_contained(&self) -> bool { + matches!(self, Call::transact { .. }) + } + + pub fn check_self_contained(&self) -> Option> { + if let Call::transact { tx_bytes } = self { + let check = || { + let tx = Tx::decode(&mut &tx_bytes[..]) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let fee_payer = T::SigVerifiableTx::fee_payer(&tx) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let (_hrp, address_raw) = acc_address_from_bech32(&fee_payer).map_err(|_| { + TransactionValidityError::Invalid(InvalidTransaction::BadSigner) + })?; + + if address_raw.len() != 20 { + return Err(TransactionValidityError::Invalid(InvalidTransaction::BadSigner)); + } + + Ok(H160::from_slice(&address_raw)) + }; + + Some(check()) + } else { + None + } + } + + pub fn validate_self_contained( + &self, + origin: &H160, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option { + if let Call::transact { tx_bytes } = self { + if let Err(e) = CheckWeight::::do_validate(dispatch_info, len) { + return Some(Err(e)); + } + + Some(Pallet::::validate_transaction_in_pool(*origin, tx_bytes)) + } else { + None + } + } + + pub fn pre_dispatch_self_contained( + &self, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option> { + if let Call::transact { tx_bytes } = self { + if let Err(e) = CheckWeight::::do_pre_dispatch(dispatch_info, len) { + return Some(Err(e)); + } + + Some(Pallet::::validate_transaction_in_block(tx_bytes)) + } else { + None + } + } +} + pub trait AddressMapping { fn into_account_id(address: H160) -> A; } @@ -49,24 +135,14 @@ pub trait AddressMapping { pub mod pallet { use super::*; use cosmos_sdk_proto::Any; - use frame_support::{ - pallet_prelude::*, - traits::{ - fungibles, - tokens::{AssetId, Balance}, - Contains, Currency, - }, + use frame_support::traits::{ + fungibles, + tokens::{AssetId, Balance}, + Contains, Currency, }; - use frame_system::WeightInfo; use np_cosmos::traits::ChainInfo; - use pallet_cosmos_types::{ - context::traits::Context, errors::CosmosError, events::CosmosEvent, gas::Gas, - handler::AnteDecorator, msgservice::MsgServiceRouter, - }; - use pallet_cosmos_x_auth_signing::{ - sign_mode_handler::traits::SignModeHandler, sign_verifiable_tx::traits::SigVerifiableTx, - }; - use sp_runtime::traits::Convert; + use pallet_cosmos_types::{errors::CosmosError, events::CosmosEvent, gas::Gas}; + use pallet_cosmos_x_auth_signing::sign_mode_handler::traits::SignModeHandler; #[pallet::pallet] pub struct Pallet(PhantomData); @@ -154,4 +230,137 @@ pub mod pallet { #[pallet::constant] type MaxDenomLimit: Get; } + + #[pallet::call] + impl Pallet + where + OriginFor: Into>>, + { + #[pallet::call_index(0)] + #[pallet::weight({ + use cosmos_sdk_proto::traits::Message; + use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; + + Tx::decode(&mut &tx_bytes[..]) + .ok() + .and_then(|tx| tx.auth_info) + .and_then(|auth_info| auth_info.fee) + .map_or(T::WeightInfo::default_weight(), |fee| { + T::WeightToGas::convert(fee.gas_limit) + }) + })] + pub fn transact(origin: OriginFor, tx_bytes: Vec) -> DispatchResultWithPostInfo { + let _source = ensure_cosmos_transaction(origin)?; + + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(T::WeightInfo::default_weight()), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), + })?; + + Self::apply_validated_transaction(tx) + } + } +} + +impl Pallet { + fn validate_transaction_in_pool(origin: H160, tx_bytes: &[u8]) -> TransactionValidity { + let tx = Tx::decode(&mut &*tx_bytes) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + + T::AnteHandler::ante_handle(&tx, true)?; + + let transaction_nonce = T::SigVerifiableTx::sequence(&tx) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + + let mut builder = + ValidTransactionBuilder::default().and_provides((origin, transaction_nonce)); + + let who = T::AddressMapping::into_account_id(origin); + let sequence = frame_system::Pallet::::account_nonce(&who).saturated_into(); + + if transaction_nonce > sequence { + if let Some(prev_nonce) = transaction_nonce.checked_sub(1) { + builder = builder.and_requires((origin, prev_nonce)) + } + } + + builder.build() + } + + pub fn validate_transaction_in_block(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { + let tx = Tx::decode(&mut &*tx_bytes) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + + T::AnteHandler::ante_handle(&tx, false)?; + + Ok(()) + } + + pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { + let body = tx.body.ok_or(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(T::WeightInfo::default_weight()), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), + })?; + let gas_limit = tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(T::WeightInfo::default_weight()), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), + })? + .gas_limit; + + let mut ctx = T::Context::new(gas_limit); + ctx.gas_meter() + .consume_gas(T::WeightInfo::default_weight().ref_time(), "") + .map_err(|_| DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(RootError::OutOfGas.into()).into(), + })?; + + for msg in body.messages.iter() { + let handler = T::MsgServiceRouter::route(msg).ok_or(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(RootError::UnknownRequest.into()).into(), + })?; + + handler.handle(msg, &mut ctx).map_err(|e| DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), + pays_fee: Pays::Yes, + }, + error: Error::::CosmosError(e).into(), + })?; + } + + Self::deposit_event(Event::Executed { + gas_wanted: gas_limit, + gas_used: T::WeightToGas::convert(Weight::from_parts( + ctx.gas_meter().consumed_gas(), + 0, + )), + events: ctx.event_manager().events(), + }); + + Ok(PostDispatchInfo { + actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), + pays_fee: Pays::Yes, + }) + } } From 31b94c8fa121ccc8571f9af88f1e1df50fa5a657 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 13 Sep 2024 00:17:00 +0900 Subject: [PATCH 025/133] refactor: Refactor handling error in pallet-cosmos call --- frame/cosmos/src/lib.rs | 106 ++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index c7562794..14110555 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -25,7 +25,7 @@ pub use self::pallet::*; use crate::weights::WeightInfo; use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; use frame_support::{ - dispatch::{DispatchErrorWithPostInfo, DispatchInfo, PostDispatchInfo}, + dispatch::{DispatchInfo, PostDispatchInfo, WithPostDispatchInfo}, pallet_prelude::*, }; use frame_system::{pallet_prelude::*, CheckWeight}; @@ -72,16 +72,14 @@ where pub fn check_self_contained(&self) -> Option> { if let Call::transact { tx_bytes } = self { let check = || { - let tx = Tx::decode(&mut &tx_bytes[..]) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; - let fee_payer = T::SigVerifiableTx::fee_payer(&tx) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; - let (_hrp, address_raw) = acc_address_from_bech32(&fee_payer).map_err(|_| { - TransactionValidityError::Invalid(InvalidTransaction::BadSigner) - })?; + let tx = Tx::decode(&mut &tx_bytes[..]).map_err(|_| InvalidTransaction::Call)?; + let fee_payer = + T::SigVerifiableTx::fee_payer(&tx).map_err(|_| InvalidTransaction::Call)?; + let (_hrp, address_raw) = acc_address_from_bech32(&fee_payer) + .map_err(|_| InvalidTransaction::BadSigner)?; if address_raw.len() != 20 { - return Err(TransactionValidityError::Invalid(InvalidTransaction::BadSigner)); + return Err(InvalidTransaction::BadSigner.into()); } Ok(H160::from_slice(&address_raw)) @@ -135,10 +133,13 @@ pub trait AddressMapping { pub mod pallet { use super::*; use cosmos_sdk_proto::Any; - use frame_support::traits::{ - fungibles, - tokens::{AssetId, Balance}, - Contains, Currency, + use frame_support::{ + dispatch::WithPostDispatchInfo, + traits::{ + fungibles, + tokens::{AssetId, Balance}, + Contains, Currency, + }, }; use np_cosmos::traits::ChainInfo; use pallet_cosmos_types::{errors::CosmosError, events::CosmosEvent, gas::Gas}; @@ -252,12 +253,9 @@ pub mod pallet { pub fn transact(origin: OriginFor, tx_bytes: Vec) -> DispatchResultWithPostInfo { let _source = ensure_cosmos_transaction(origin)?; - let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(T::WeightInfo::default_weight()), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| { + Error::::CosmosError(RootError::TxDecodeError.into()) + .with_weight(T::WeightInfo::default_weight()) })?; Self::apply_validated_transaction(tx) @@ -267,13 +265,12 @@ pub mod pallet { impl Pallet { fn validate_transaction_in_pool(origin: H160, tx_bytes: &[u8]) -> TransactionValidity { - let tx = Tx::decode(&mut &*tx_bytes) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; T::AnteHandler::ante_handle(&tx, true)?; - let transaction_nonce = T::SigVerifiableTx::sequence(&tx) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let transaction_nonce = + T::SigVerifiableTx::sequence(&tx).map_err(|_| InvalidTransaction::Call)?; let mut builder = ValidTransactionBuilder::default().and_provides((origin, transaction_nonce)); @@ -291,8 +288,7 @@ impl Pallet { } pub fn validate_transaction_in_block(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { - let tx = Tx::decode(&mut &*tx_bytes) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; T::AnteHandler::ante_handle(&tx, false)?; @@ -300,66 +296,48 @@ impl Pallet { } pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { - let body = tx.body.ok_or(DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(T::WeightInfo::default_weight()), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), - })?; let gas_limit = tx .auth_info .as_ref() .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(T::WeightInfo::default_weight()), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(RootError::TxDecodeError.into()).into(), - })? + .ok_or( + Error::::CosmosError(RootError::TxDecodeError.into()) + .with_weight(T::WeightInfo::default_weight()), + )? .gas_limit; let mut ctx = T::Context::new(gas_limit); ctx.gas_meter() .consume_gas(T::WeightInfo::default_weight().ref_time(), "") - .map_err(|_| DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(RootError::OutOfGas.into()).into(), + .map_err(|_| { + Error::::CosmosError(RootError::OutOfGas.into()) + .with_weight(T::WeightInfo::default_weight()) })?; + let body = tx.body.ok_or( + Error::::CosmosError(RootError::TxDecodeError.into()) + .with_weight(T::WeightInfo::default_weight()), + )?; for msg in body.messages.iter() { - let handler = T::MsgServiceRouter::route(msg).ok_or(DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(RootError::UnknownRequest.into()).into(), - })?; - - handler.handle(msg, &mut ctx).map_err(|e| DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), - pays_fee: Pays::Yes, - }, - error: Error::::CosmosError(e).into(), + let handler = T::MsgServiceRouter::route(msg).ok_or( + Error::::CosmosError(RootError::UnknownRequest.into()) + .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())), + )?; + + handler.handle(msg, &mut ctx).map_err(|e| { + Error::::CosmosError(e) + .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())) })?; } Self::deposit_event(Event::Executed { gas_wanted: gas_limit, - gas_used: T::WeightToGas::convert(Weight::from_parts( - ctx.gas_meter().consumed_gas(), - 0, - )), + gas_used: ctx.gas_meter().consumed_gas(), events: ctx.event_manager().events(), }); Ok(PostDispatchInfo { - actual_weight: Some(Weight::from_parts(ctx.gas_meter().consumed_gas(), 0)), + actual_weight: Some(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())), pays_fee: Pays::Yes, }) } From deeb7e48448632519b4491cf180e0accbfbea4ac Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 13 Sep 2024 23:06:12 +0900 Subject: [PATCH 026/133] refactor: Refactor get_sign_bytes in pallet-cosmos-x-auth-signing --- .../auth/signing/src/sign_mode_handler/mod.rs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs index ce1d547d..9a1c7a96 100644 --- a/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs +++ b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs @@ -24,9 +24,12 @@ use alloc::{ use cosmos_sdk_proto::{ cosmos::{ bank, - tx::v1beta1::{ - mode_info::{Single, Sum}, - ModeInfo, SignDoc, Tx, TxRaw, + tx::{ + signing::v1beta1::SignMode, + v1beta1::{ + mode_info::{Single, Sum}, + ModeInfo, SignDoc, Tx, TxRaw, + }, }, }, cosmwasm::wasm, @@ -63,6 +66,9 @@ pub enum SignModeHandlerError { UnsupportedMode, } +const SIGN_MODE_DIRECT: i32 = SignMode::Direct as i32; +const SIGN_MODE_LEGACY_AMINO_JSON: i32 = SignMode::LegacyAminoJson as i32; + pub struct SignModeHandler; impl traits::SignModeHandler for SignModeHandler { fn get_sign_bytes( @@ -72,17 +78,19 @@ impl traits::SignModeHandler for SignModeHandler { ) -> Result, SignModeHandlerError> { let sum = mode.sum.as_ref().ok_or(SignModeHandlerError::EmptyModeInfo)?; let sign_bytes = match sum { - Sum::Single(Single { mode }) => match mode { - 1 /* SIGN_MODE_DIRECT */ => { - let tx_raw = TxRaw::decode(&mut &*tx.encode_to_vec()).map_err(|_| SignModeHandlerError::DecodeTxError)?; + Sum::Single(Single { mode }) => match *mode { + SIGN_MODE_DIRECT => { + let tx_raw = TxRaw::decode(&mut &*tx.encode_to_vec()) + .map_err(|_| SignModeHandlerError::DecodeTxError)?; SignDoc { body_bytes: tx_raw.body_bytes, auth_info_bytes: tx_raw.auth_info_bytes, chain_id: data.chain_id.clone(), account_number: data.account_number, - }.encode_to_vec() + } + .encode_to_vec() }, - 127 /* SIGN_MODE_LEGACY_AMINO_JSON */ => { + SIGN_MODE_LEGACY_AMINO_JSON => { let body = tx.body.as_ref().ok_or(SignModeHandlerError::EmptyTxBody)?; let mut msgs = Vec::::new(); for msg in body.messages.iter() { @@ -99,7 +107,11 @@ impl traits::SignModeHandler for SignModeHandler { msgs.push(legacy_msg); } - let fee = tx.auth_info.as_ref().and_then(|auth_info| auth_info.fee.as_ref()).ok_or(SignModeHandlerError::EmptyFee)?; + let fee = tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(SignModeHandlerError::EmptyFee)?; let sign_doc = StdSignDoc { account_number: data.account_number.to_string(), chain_id: data.chain_id.clone(), @@ -109,7 +121,8 @@ impl traits::SignModeHandler for SignModeHandler { sequence: data.sequence.to_string(), }; - serde_json::to_vec(&sign_doc).map_err(|_| SignModeHandlerError::SerializeError)? + serde_json::to_vec(&sign_doc) + .map_err(|_| SignModeHandlerError::SerializeError)? }, _ => return Err(SignModeHandlerError::UnsupportedMode), }, From a1f384c4e21f06427532d2db9d70305b0aa7a6bb Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 13 Sep 2024 23:10:58 +0900 Subject: [PATCH 027/133] test: Refactor any_match_test --- frame/cosmos/types/src/macros.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frame/cosmos/types/src/macros.rs b/frame/cosmos/types/src/macros.rs index acd55374..af412d9b 100644 --- a/frame/cosmos/types/src/macros.rs +++ b/frame/cosmos/types/src/macros.rs @@ -38,13 +38,12 @@ mod tests { cosmos::bank::v1beta1::MsgSend, cosmwasm::wasm::v1::{MsgExecuteContract, MsgStoreCode}, prost::Name, - traits::Message, Any, }; #[test] fn any_match_test() { - let any = Any { type_url: MsgSend::type_url(), value: MsgSend::default().encode_to_vec() }; + let any = Any::from_msg(&MsgSend::default()).unwrap(); let result = any_match!( any, { MsgSend => any.type_url, @@ -54,10 +53,7 @@ mod tests { ); assert_eq!(result, MsgSend::type_url()); - let any = Any { - type_url: MsgExecuteContract::type_url(), - value: MsgExecuteContract::default().encode_to_vec(), - }; + let any = Any::from_msg(&MsgExecuteContract::default()).unwrap(); let result = any_match!( any, { MsgSend => any.type_url, From 94ed909323835c23aec235434731e0036e2fa884 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 14 Sep 2024 14:57:03 +0900 Subject: [PATCH 028/133] feat: Add NativeDenom and NativeAssetId to pallet-cosmos --- frame/cosmos/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 14110555..db094486 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -182,6 +182,10 @@ pub mod pallet { > + fungibles::Mutate + fungibles::Balanced; + type NativeDenom: Get<&'static str>; + + type NativeAssetId: Get; + #[pallet::no_default_bounds] type RuntimeEvent: From + IsType<::RuntimeEvent>; From 95d89a0e5ab56bb215d2ad4315f33f86abcdac57 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 15 Sep 2024 21:49:55 +0900 Subject: [PATCH 029/133] feat: Add pallet-cosmos-x-bank --- Cargo.toml | 2 + frame/cosmos/x/bank/Cargo.toml | 28 ++++++++ frame/cosmos/x/bank/src/lib.rs | 120 +++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 frame/cosmos/x/bank/Cargo.toml create mode 100644 frame/cosmos/x/bank/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 9b8d213d..d5092e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "frame/cosmos/runtime-api", "frame/cosmos/x/auth/migrations", "frame/cosmos/x/auth/signing", + "frame/cosmos/x/bank", "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm/types", "frame/multimap", @@ -41,6 +42,7 @@ pallet-cosmos = { path = "frame/cosmos", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-features = false } +pallet-cosmos-x-bank = { path = "frame/cosmos/x/bank", default-features = false } pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/cosmos/x/bank/Cargo.toml b/frame/cosmos/x/bank/Cargo.toml new file mode 100644 index 00000000..e18a7dfa --- /dev/null +++ b/frame/cosmos/x/bank/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pallet-cosmos-x-bank" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-cosmos = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-bank-types = { workspace = true, default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "frame-support/std", + "pallet-cosmos/std", + "pallet-cosmos-types/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs new file mode 100644 index 00000000..cd0dff70 --- /dev/null +++ b/frame/cosmos/x/bank/src/lib.rs @@ -0,0 +1,120 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use core::marker::PhantomData; +use cosmos_sdk_proto::{cosmos::bank::v1beta1::MsgSend, prost::Message, Any}; +use frame_support::traits::{ + fungibles::Mutate, + tokens::{currency::Currency, Preservation}, + ExistenceRequirement, +}; +use pallet_cosmos::AddressMapping; +use pallet_cosmos_types::{ + address::acc_address_from_bech32, + coin::amount_to_string, + context, + errors::RootError, + events::{ + traits::EventManager, CosmosEvent, EventAttribute, ATTRIBUTE_KEY_AMOUNT, + ATTRIBUTE_KEY_SENDER, + }, +}; +use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; +use sp_core::{Get, H160}; +use sp_runtime::{traits::Convert, SaturatedConversion}; + +pub struct MsgSendHandler(PhantomData); + +impl Default for MsgSendHandler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl pallet_cosmos_types::msgservice::MsgHandler for MsgSendHandler +where + T: pallet_cosmos::Config, + Context: context::traits::Context, +{ + fn handle( + &self, + msg: &Any, + ctx: &mut Context, + ) -> Result<(), pallet_cosmos_types::errors::CosmosError> { + // TODO: Add gas metering + let MsgSend { from_address, to_address, amount } = + MsgSend::decode(&mut &*msg.value).map_err(|_| RootError::UnpackAnyError)?; + + let (_hrp, from_address_raw) = + acc_address_from_bech32(&from_address).map_err(|_| RootError::InvalidAddress)?; + if from_address_raw.len() != 20 { + return Err(RootError::InvalidAddress.into()); + } + let from_account = T::AddressMapping::into_account_id(H160::from_slice(&from_address_raw)); + + let (_hrp, to_address_raw) = + acc_address_from_bech32(&to_address).map_err(|_| RootError::InvalidAddress)?; + if to_address_raw.len() != 20 { + return Err(RootError::InvalidAddress.into()); + } + let to_account = T::AddressMapping::into_account_id(H160::from_slice(&to_address_raw)); + + for amt in amount.iter() { + let amount = amt.amount.parse::().map_err(|_| RootError::InvalidCoins)?; + + if T::NativeDenom::get() == amt.denom { + T::NativeAsset::transfer( + &from_account, + &to_account, + amount.saturated_into(), + ExistenceRequirement::KeepAlive, + ) + .map_err(|_| RootError::InsufficientFunds)?; + } else { + let asset_id = T::AssetToDenom::convert(amt.denom.clone()) + .map_err(|_| RootError::InvalidCoins)?; + T::Assets::transfer( + asset_id, + &from_account, + &to_account, + amount.saturated_into(), + Preservation::Preserve, + ) + .map_err(|_| RootError::InsufficientFunds)?; + } + } + + let event = CosmosEvent { + r#type: EVENT_TYPE_TRANSFER.into(), + attributes: vec![ + EventAttribute { key: ATTRIBUTE_KEY_SENDER.into(), value: from_address.into() }, + EventAttribute { key: ATTRIBUTE_KEY_RECIPIENT.into(), value: to_address.into() }, + EventAttribute { + key: ATTRIBUTE_KEY_AMOUNT.into(), + value: amount_to_string(&amount).into(), + }, + ], + }; + ctx.event_manager().emit_event(event); + + Ok(()) + } +} From 5441e6dd5b6a699c7789432495c22f2a86a426cb Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 15 Sep 2024 22:16:58 +0900 Subject: [PATCH 030/133] refactor: Use ensure to validation --- frame/cosmos/x/bank/src/lib.rs | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index cd0dff70..9f7d027b 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -21,21 +21,25 @@ extern crate alloc; use core::marker::PhantomData; use cosmos_sdk_proto::{cosmos::bank::v1beta1::MsgSend, prost::Message, Any}; -use frame_support::traits::{ - fungibles::Mutate, - tokens::{currency::Currency, Preservation}, - ExistenceRequirement, +use frame_support::{ + ensure, + traits::{ + fungibles::Mutate, + tokens::{currency::Currency, Preservation}, + ExistenceRequirement, + }, }; use pallet_cosmos::AddressMapping; use pallet_cosmos_types::{ address::acc_address_from_bech32, coin::amount_to_string, context, - errors::RootError, + errors::{CosmosError, RootError}, events::{ traits::EventManager, CosmosEvent, EventAttribute, ATTRIBUTE_KEY_AMOUNT, ATTRIBUTE_KEY_SENDER, }, + msgservice::MsgHandler, }; use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; use sp_core::{Get, H160}; @@ -49,32 +53,26 @@ impl Default for MsgSendHandler { } } -impl pallet_cosmos_types::msgservice::MsgHandler for MsgSendHandler +impl MsgHandler for MsgSendHandler where T: pallet_cosmos::Config, Context: context::traits::Context, { - fn handle( - &self, - msg: &Any, - ctx: &mut Context, - ) -> Result<(), pallet_cosmos_types::errors::CosmosError> { + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgSend { from_address, to_address, amount } = MsgSend::decode(&mut &*msg.value).map_err(|_| RootError::UnpackAnyError)?; let (_hrp, from_address_raw) = acc_address_from_bech32(&from_address).map_err(|_| RootError::InvalidAddress)?; - if from_address_raw.len() != 20 { - return Err(RootError::InvalidAddress.into()); - } + + ensure!(from_address_raw.len() == 20, RootError::InvalidAddress); let from_account = T::AddressMapping::into_account_id(H160::from_slice(&from_address_raw)); let (_hrp, to_address_raw) = acc_address_from_bech32(&to_address).map_err(|_| RootError::InvalidAddress)?; - if to_address_raw.len() != 20 { - return Err(RootError::InvalidAddress.into()); - } + + ensure!(to_address_raw.len() == 20, RootError::InvalidAddress); let to_account = T::AddressMapping::into_account_id(H160::from_slice(&to_address_raw)); for amt in amount.iter() { From cedc55ada065cc6d8dc3ef3249a14bc6090bcf3d Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 01:00:09 +0900 Subject: [PATCH 031/133] feat: Add pallet-cosmos-x-auth --- Cargo.toml | 2 + frame/cosmos/x/auth/Cargo.toml | 41 +++++ frame/cosmos/x/auth/src/basic.rs | 72 ++++++++ frame/cosmos/x/auth/src/fee.rs | 145 +++++++++++++++++ frame/cosmos/x/auth/src/lib.rs | 25 +++ frame/cosmos/x/auth/src/msg.rs | 40 +++++ frame/cosmos/x/auth/src/sigverify.rs | 235 +++++++++++++++++++++++++++ primitives/cosmos/src/traits.rs | 4 + 8 files changed, 564 insertions(+) create mode 100644 frame/cosmos/x/auth/Cargo.toml create mode 100644 frame/cosmos/x/auth/src/basic.rs create mode 100644 frame/cosmos/x/auth/src/fee.rs create mode 100644 frame/cosmos/x/auth/src/lib.rs create mode 100644 frame/cosmos/x/auth/src/msg.rs create mode 100644 frame/cosmos/x/auth/src/sigverify.rs diff --git a/Cargo.toml b/Cargo.toml index d5092e83..210167e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "frame/cosmos/types", "frame/cosmos/rpc", "frame/cosmos/runtime-api", + "frame/cosmos/x/auth", "frame/cosmos/x/auth/migrations", "frame/cosmos/x/auth/signing", "frame/cosmos/x/bank", @@ -40,6 +41,7 @@ np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } pallet-cosmos = { path = "frame/cosmos", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } +pallet-cosmos-x-auth = { path = "frame/cosmos/x/auth", default-features = false } pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", default-features = false } pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-features = false } pallet-cosmos-x-bank = { path = "frame/cosmos/x/bank", default-features = false } diff --git a/frame/cosmos/x/auth/Cargo.toml b/frame/cosmos/x/auth/Cargo.toml new file mode 100644 index 00000000..3e5c9f91 --- /dev/null +++ b/frame/cosmos/x/auth/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pallet-cosmos-x-auth" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +cosmos-sdk-proto = { version = "0.24.0", default-features = false } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +np-cosmos = { workspace = true, default-features = false } +pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } +pallet-cosmos = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +ripemd = { version = "0.1.3", default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[dev-dependencies] +hex = { version = "0.4.3", default-features = false } + +[features] +default = ["std"] +std = [ + "cosmos-sdk-proto/std", + "frame-support/std", + "frame-system/std", + "np-cosmos/std", + "pallet-cosmos-x-auth-signing/std", + "pallet-cosmos/std", + "pallet-cosmos-types/std", + "ripemd/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "hex/std", +] diff --git a/frame/cosmos/x/auth/src/basic.rs b/frame/cosmos/x/auth/src/basic.rs new file mode 100644 index 00000000..ab7c2290 --- /dev/null +++ b/frame/cosmos/x/auth/src/basic.rs @@ -0,0 +1,72 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; +use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; +use frame_support::{ensure, pallet_prelude::ValidTransaction, traits::Get}; +use pallet_cosmos_types::handler::AnteDecorator; +use sp_runtime::{ + transaction_validity::{InvalidTransaction, TransactionValidity}, + SaturatedConversion, +}; + +pub struct ValidateBasicDecorator(PhantomData); + +impl AnteDecorator for ValidateBasicDecorator +where + T: frame_system::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + ensure!(!tx.signatures.is_empty(), InvalidTransaction::Call); + let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + ensure!(auth_info.signer_infos.len() == tx.signatures.len(), InvalidTransaction::Call); + + Ok(ValidTransaction::default()) + } +} + +pub struct TxTimeoutHeightDecorator(PhantomData); + +impl AnteDecorator for TxTimeoutHeightDecorator +where + T: frame_system::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; + + let block_number: u64 = frame_system::Pallet::::block_number().saturated_into(); + if body.timeout_height > 0 && block_number > body.timeout_height { + return Err(InvalidTransaction::Stale.into()); + } + + Ok(ValidTransaction::default()) + } +} + +pub struct ValidateMemoDecorator(PhantomData); + +impl AnteDecorator for ValidateMemoDecorator +where + T: pallet_cosmos::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; + ensure!(body.memo.len() <= T::MaxMemoCharacters::get() as usize, InvalidTransaction::Call); + + Ok(ValidTransaction::default()) + } +} diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs new file mode 100644 index 00000000..a224b74d --- /dev/null +++ b/frame/cosmos/x/auth/src/fee.rs @@ -0,0 +1,145 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; +use cosmos_sdk_proto::cosmos::tx::v1beta1::{Fee, Tx}; +use frame_support::{ + ensure, + pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, + traits::{ + fungibles::Balanced, + tokens::{Fortitude, Precision, Preservation}, + Currency, ExistenceRequirement, WithdrawReasons, + }, +}; +use pallet_cosmos::AddressMapping; +use pallet_cosmos_types::{ + address::acc_address_from_bech32, + coin::amount_to_string, + events::{ + CosmosEvent, EventAttribute, ATTRIBUTE_KEY_FEE, ATTRIBUTE_KEY_FEE_PAYER, EVENT_TYPE_TX, + }, + handler::AnteDecorator, +}; +use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; +use sp_core::{Get, H160}; +use sp_runtime::{ + traits::{Convert, Zero}, + SaturatedConversion, +}; + +pub struct DeductFeeDecorator(PhantomData); + +impl AnteDecorator for DeductFeeDecorator +where + T: frame_system::Config + pallet_cosmos::Config, +{ + fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { + let fee = tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(InvalidTransaction::Call)?; + + if !simulate && !frame_system::Pallet::::block_number().is_zero() && fee.gas_limit == 0 { + return Err(InvalidTransaction::Call.into()); + } + + // TODO: Implements txFeeChecker + + Self::check_deduct_fee(tx)?; + + Ok(ValidTransaction::default()) + } +} + +impl DeductFeeDecorator +where + T: pallet_cosmos::Config, +{ + fn check_deduct_fee(tx: &Tx) -> TransactionValidity { + let fee_payer = T::SigVerifiableTx::fee_payer(tx).map_err(|_| InvalidTransaction::Call)?; + + let fee = tx + .auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .ok_or(InvalidTransaction::Call)?; + + // TODO: Fee granter support + ensure!(fee.granter.is_empty(), InvalidTransaction::Call); + + let (_hrp, address_raw) = + acc_address_from_bech32(&fee_payer).map_err(|_| InvalidTransaction::BadSigner)?; + ensure!(address_raw.len() == 20, InvalidTransaction::BadSigner); + let deduct_fees_from = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + // TODO: Check fee is zero + if !fee.amount.is_empty() { + Self::deduct_fees(&deduct_fees_from, fee)?; + } + + pallet_cosmos::Pallet::::deposit_event(pallet_cosmos::Event::AnteHandled(vec![ + CosmosEvent { + r#type: EVENT_TYPE_TX.into(), + attributes: vec![ + EventAttribute { + key: ATTRIBUTE_KEY_FEE.into(), + value: amount_to_string(&fee.amount).into(), + }, + EventAttribute { key: ATTRIBUTE_KEY_FEE_PAYER.into(), value: fee_payer.into() }, + ], + }, + ])); + + Ok(ValidTransaction::default()) + } + + fn deduct_fees(acc: &T::AccountId, fee: &Fee) -> TransactionValidity { + for amt in fee.amount.iter() { + let amount = amt.amount.parse::().map_err(|_| InvalidTransaction::Call)?; + + if amt.denom == T::NativeDenom::get() { + let _imbalance = T::NativeAsset::withdraw( + acc, + amount.saturated_into(), + WithdrawReasons::TRANSACTION_PAYMENT, + ExistenceRequirement::KeepAlive, + ) + .map_err(|_| InvalidTransaction::Payment)?; + + // TODO: Resolve imbalance + } else { + let asset_id = T::AssetToDenom::convert(amt.denom.clone()) + .map_err(|_| InvalidTransaction::Call)?; + let _imbalance = T::Assets::withdraw( + asset_id, + acc, + amount.saturated_into(), + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) + .map_err(|_| InvalidTransaction::Payment)?; + + // TODO: Resolve imbalance + } + } + + Ok(ValidTransaction::default()) + } +} diff --git a/frame/cosmos/x/auth/src/lib.rs b/frame/cosmos/x/auth/src/lib.rs new file mode 100644 index 00000000..de3e311f --- /dev/null +++ b/frame/cosmos/x/auth/src/lib.rs @@ -0,0 +1,25 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod basic; +pub mod fee; +pub mod msg; +pub mod sigverify; diff --git a/frame/cosmos/x/auth/src/msg.rs b/frame/cosmos/x/auth/src/msg.rs new file mode 100644 index 00000000..85e57d6f --- /dev/null +++ b/frame/cosmos/x/auth/src/msg.rs @@ -0,0 +1,40 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; +use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; +use frame_support::{ + ensure, + pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, + traits::Contains, +}; +use pallet_cosmos_types::handler::AnteDecorator; + +pub struct KnownMsgDecorator(PhantomData); + +impl AnteDecorator for KnownMsgDecorator +where + T: pallet_cosmos::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; + + ensure!(body.messages.iter().all(T::MsgFilter::contains), InvalidTransaction::Call); + + Ok(ValidTransaction::default()) + } +} diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs new file mode 100644 index 00000000..81a94f43 --- /dev/null +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -0,0 +1,235 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::{cmp::Ordering, marker::PhantomData}; +use cosmos_sdk_proto::{ + cosmos::{ + crypto::{multisig::LegacyAminoPubKey, secp256k1}, + tx::v1beta1::{ModeInfo, SignerInfo, Tx}, + }, + prost::Message, + Any, +}; +use frame_support::{ + ensure, + pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, +}; +use np_cosmos::traits::ChainInfo; +use pallet_cosmos::AddressMapping; +use pallet_cosmos_types::{address::acc_address_from_bech32, any_match, handler::AnteDecorator}; +use pallet_cosmos_x_auth_signing::{ + sign_mode_handler::{traits::SignModeHandler, SignerData}, + sign_verifiable_tx::traits::SigVerifiableTx, +}; +use ripemd::Digest; +use sp_core::{ecdsa, sha2_256, ByteArray, Get, H160}; +use sp_runtime::{transaction_validity::TransactionValidityError, SaturatedConversion}; + +pub struct SigVerificationDecorator(PhantomData); + +impl AnteDecorator for SigVerificationDecorator +where + T: pallet_cosmos::Config + frame_system::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let signatures = &tx.signatures; + let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| InvalidTransaction::Call)?; + + let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + + ensure!(signatures.len() == signers.len(), InvalidTransaction::Call); + ensure!(signatures.len() == auth_info.signer_infos.len(), InvalidTransaction::Call); + + for (i, sig) in signatures.iter().enumerate() { + let signer = signers.get(i).ok_or(InvalidTransaction::Call)?; + let signer_info = auth_info.signer_infos.get(i).ok_or(InvalidTransaction::Call)?; + + let (_hrp, signer_addr_raw) = + acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; + ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); + + let who = T::AddressMapping::into_account_id(H160::from_slice(&signer_addr_raw)); + let sequence = frame_system::Pallet::::account_nonce(&who).saturated_into(); + + match signer_info.sequence.cmp(&sequence) { + Ordering::Greater => Err(InvalidTransaction::Future), + Ordering::Less => Err(InvalidTransaction::Stale), + Ordering::Equal => Ok(()), + }?; + + let public_key = + signer_info.public_key.as_ref().ok_or(InvalidTransaction::BadSigner)?; + let chain_id = T::ChainInfo::chain_id().to_string(); + let signer_data = SignerData { + address: signer.clone(), + chain_id, + account_number: 0, + sequence: signer_info.sequence, + pub_key: public_key.clone(), + }; + let sign_mode = signer_info.mode_info.as_ref().ok_or(InvalidTransaction::BadSigner)?; + + Self::verify_signature(public_key, &signer_data, sign_mode, sig, tx)?; + } + + Ok(ValidTransaction::default()) + } +} + +impl SigVerificationDecorator +where + T: pallet_cosmos::Config, +{ + fn verify_signature( + public_key: &Any, + signer_data: &SignerData, + sign_mode: &ModeInfo, + signature: &[u8], + tx: &Tx, + ) -> Result<(), TransactionValidityError> { + any_match!( + public_key, { + secp256k1::PubKey => { + let public_key = + secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| { + InvalidTransaction::BadSigner + })?; + + let mut hasher = ripemd::Ripemd160::new(); + hasher.update(sha2_256(&public_key.key)); + let address = H160::from_slice(&hasher.finalize()); + + let (_hrp, signer_addr_raw) = + acc_address_from_bech32(&signer_data.address).map_err(|_| { + InvalidTransaction::BadSigner + })?; + + ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); + ensure!(H160::from_slice(&signer_addr_raw) == address, InvalidTransaction::BadSigner); + + let sign_bytes = T::SignModeHandler::get_sign_bytes(sign_mode, signer_data, tx) + .map_err(|_| InvalidTransaction::Call)?; + + if !ecdsa_verify(signature, &sign_bytes, &public_key.key) { + return Err(InvalidTransaction::BadProof.into()); + } + + Ok(()) + } + }, + Err(InvalidTransaction::BadSigner.into()) + ) + } +} + +pub fn ecdsa_verify(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool { + let pub_key = match ecdsa::Public::from_slice(public_key) { + Ok(pub_key) => pub_key, + Err(_) => return false, + }; + let msg = sha2_256(message); + + if signature.len() == 64 { + for rec_id in 0..=3 { + let mut rec_sig = [0u8; 65]; + rec_sig[0..signature.len()].copy_from_slice(signature); + rec_sig[64] = rec_id; + let sig = ecdsa::Signature::from(rec_sig); + + if sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key) { + return true; + } + } + false + } else if signature.len() == 65 { + match ecdsa::Signature::try_from(signature) { + Ok(sig) => sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key), + Err(_) => false, + } + } else { + false + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn ecdsa_verify_test() { + let sig = hex::decode("f7e0d198c62821cc5817c8e935f523308301e29819f5d882f3249b9e173a614f38000ddbff446c0abfa7c7d019dbb17072b28933fc8187c973fbf03d0459f76e").unwrap(); + let message = hex::decode("0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331716436396e75776a393567746134616b6a677978746a39756a6d7a34773865646d7179737177122d636f736d6f7331676d6a32657861673033747467616670726b6463337438383067726d61396e776566636432771a100a057561746f6d12073130303030303012710a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a112040a020801121f0a150a057561746f6d120c3838363838303030303030301080c0f1c59495141a1174686574612d746573746e65742d30303120ad8a2e").unwrap(); + let public_key = + hex::decode("020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1") + .unwrap(); + + assert!(ecdsa_verify(&sig, &message, &public_key)); + } +} + +pub struct ValidateSigCountDecorator(PhantomData); + +impl AnteDecorator for ValidateSigCountDecorator +where + T: pallet_cosmos::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let mut sig_count = 0u64; + + let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + for SignerInfo { public_key, .. } in auth_info.signer_infos.iter() { + let public_key = public_key.as_ref().ok_or(InvalidTransaction::BadSigner)?; + sig_count = sig_count.saturating_add(Self::count_sub_keys(public_key)?); + + ensure!(sig_count <= T::TxSigLimit::get(), InvalidTransaction::Call); + } + + Ok(ValidTransaction::default()) + } +} + +impl ValidateSigCountDecorator { + fn count_sub_keys(pubkey: &Any) -> Result { + // TODO: Support legacy multi signatures. + if LegacyAminoPubKey::decode(&mut &*pubkey.value).is_ok() { + Err(InvalidTransaction::BadSigner.into()) + } else { + Ok(1) + } + } +} + +pub struct IncrementSequenceDecorator(PhantomData); + +impl AnteDecorator for IncrementSequenceDecorator +where + T: frame_system::Config + pallet_cosmos::Config, +{ + fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| InvalidTransaction::Call)?; + for signer in signers.iter() { + let (_hrp, address_raw) = + acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; + ensure!(address_raw.len() == 20, InvalidTransaction::BadSigner); + + let account = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + frame_system::pallet::Pallet::::inc_account_nonce(account); + } + + Ok(ValidTransaction::default()) + } +} diff --git a/primitives/cosmos/src/traits.rs b/primitives/cosmos/src/traits.rs index e95475c5..8f54e740 100644 --- a/primitives/cosmos/src/traits.rs +++ b/primitives/cosmos/src/traits.rs @@ -18,6 +18,7 @@ /// Chain information trait. pub trait ChainInfo { fn bech32_prefix() -> &'static str; + fn chain_id() -> &'static str; } /// Cosmos Hub chain information. @@ -27,4 +28,7 @@ impl ChainInfo for CosmosHub { fn bech32_prefix() -> &'static str { "cosmos" } + fn chain_id() -> &'static str { + "dev" + } } From 73fc23fe14c825ff6311bd9a51bed0e99daf6a0a Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 11:36:31 +0900 Subject: [PATCH 032/133] refactor: Refactor sigverify in pallet-cosmos-x-auth --- frame/cosmos/x/auth/src/sigverify.rs | 43 +++++++++++----------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index 81a94f43..819cb7bd 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -48,15 +48,14 @@ where fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { let signatures = &tx.signatures; let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| InvalidTransaction::Call)?; - - let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + let signer_infos = &tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?.signer_infos; ensure!(signatures.len() == signers.len(), InvalidTransaction::Call); - ensure!(signatures.len() == auth_info.signer_infos.len(), InvalidTransaction::Call); + ensure!(signatures.len() == signer_infos.len(), InvalidTransaction::Call); for (i, sig) in signatures.iter().enumerate() { let signer = signers.get(i).ok_or(InvalidTransaction::Call)?; - let signer_info = auth_info.signer_infos.get(i).ok_or(InvalidTransaction::Call)?; + let signer_info = signer_infos.get(i).ok_or(InvalidTransaction::Call)?; let (_hrp, signer_addr_raw) = acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; @@ -72,7 +71,7 @@ where }?; let public_key = - signer_info.public_key.as_ref().ok_or(InvalidTransaction::BadSigner)?; + signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; let chain_id = T::ChainInfo::chain_id().to_string(); let signer_data = SignerData { address: signer.clone(), @@ -81,7 +80,7 @@ where sequence: signer_info.sequence, pub_key: public_key.clone(), }; - let sign_mode = signer_info.mode_info.as_ref().ok_or(InvalidTransaction::BadSigner)?; + let sign_mode = signer_info.mode_info.as_ref().ok_or(InvalidTransaction::Call)?; Self::verify_signature(public_key, &signer_data, sign_mode, sig, tx)?; } @@ -117,14 +116,14 @@ where acc_address_from_bech32(&signer_data.address).map_err(|_| { InvalidTransaction::BadSigner })?; - ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); + ensure!(H160::from_slice(&signer_addr_raw) == address, InvalidTransaction::BadSigner); let sign_bytes = T::SignModeHandler::get_sign_bytes(sign_mode, signer_data, tx) .map_err(|_| InvalidTransaction::Call)?; - if !ecdsa_verify(signature, &sign_bytes, &public_key.key) { + if !ecdsa_verify_prehashed(signature, &sign_bytes, &public_key.key) { return Err(InvalidTransaction::BadProof.into()); } @@ -136,32 +135,24 @@ where } } -pub fn ecdsa_verify(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool { +pub fn ecdsa_verify_prehashed(signature: &[u8], message: &[u8], public_key: &[u8]) -> bool { let pub_key = match ecdsa::Public::from_slice(public_key) { Ok(pub_key) => pub_key, Err(_) => return false, }; let msg = sha2_256(message); - if signature.len() == 64 { - for rec_id in 0..=3 { + match signature.len() { + 64 => (0..=3).any(|rec_id| { let mut rec_sig = [0u8; 65]; - rec_sig[0..signature.len()].copy_from_slice(signature); + rec_sig[..64].copy_from_slice(signature); rec_sig[64] = rec_id; let sig = ecdsa::Signature::from(rec_sig); - - if sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key) { - return true; - } - } - false - } else if signature.len() == 65 { - match ecdsa::Signature::try_from(signature) { - Ok(sig) => sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key), - Err(_) => false, - } - } else { - false + sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key) + }), + 65 => ecdsa::Signature::try_from(signature) + .map_or(false, |sig| sp_io::crypto::ecdsa_verify_prehashed(&sig, &msg, &pub_key)), + _ => false, } } @@ -177,7 +168,7 @@ pub mod tests { hex::decode("020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1") .unwrap(); - assert!(ecdsa_verify(&sig, &message, &public_key)); + assert!(ecdsa_verify_prehashed(&sig, &message, &public_key)); } } From c5d126dd92361d82f7762f96a7a954aed6f6ac8f Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 19:58:55 +0900 Subject: [PATCH 033/133] feat: Add cosmwasm-runtime-api --- Cargo.toml | 2 ++ frame/cosmos/x/auth/src/sigverify.rs | 3 +-- frame/cosmwasm/runtime-api/Cargo.toml | 20 ++++++++++++++++++++ frame/cosmwasm/runtime-api/src/lib.rs | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 frame/cosmwasm/runtime-api/Cargo.toml create mode 100644 frame/cosmwasm/runtime-api/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 210167e5..b9c8fe19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "frame/cosmos/x/bank", "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm/types", + "frame/cosmwasm/runtime-api", "frame/multimap", "primitives/cosmos", "primitives/ethereum", @@ -30,6 +31,7 @@ members = [ composable-support = { path = "composable/composable-support", default-features = false } cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } +cosmwasm-runtime-api = { path = "frame/cosmwasm/runtime-api", default-features = false } cosmwasm-std = { path = "cosmwasm/std", default-features = false } cosmwasm-vm = { path = "composable/vm", default-features = false } cosmwasm-vm-wasmi = { path = "composable/vm-wasmi", default-features = false } diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index 819cb7bd..5a154a48 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -70,8 +70,7 @@ where Ordering::Equal => Ok(()), }?; - let public_key = - signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; + let public_key = signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; let chain_id = T::ChainInfo::chain_id().to_string(); let signer_data = SignerData { address: signer.clone(), diff --git a/frame/cosmwasm/runtime-api/Cargo.toml b/frame/cosmwasm/runtime-api/Cargo.toml new file mode 100644 index 00000000..bb636e0a --- /dev/null +++ b/frame/cosmwasm/runtime-api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +authors = ["Composable Developers"] +edition = "2021" +homepage = "https://composable.finance" +name = "cosmwasm-runtime-api" +rust-version = "1.56" +version = "1.0.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.0", default-features = false, features = [ + "derive", +] } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = ["codec/std", "sp-api/std"] diff --git a/frame/cosmwasm/runtime-api/src/lib.rs b/frame/cosmwasm/runtime-api/src/lib.rs new file mode 100644 index 00000000..49dc923c --- /dev/null +++ b/frame/cosmwasm/runtime-api/src/lib.rs @@ -0,0 +1,20 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::{string::String, vec::Vec}; +use codec::Codec; + +// Cosmwasm Runtime API declaration. +sp_api::decl_runtime_apis! { + pub trait CosmwasmRuntimeApi + where + Error: Codec + { + fn query( + contract: String, + gas: u64, + query_request: Vec, + ) -> Result, Error>; + } +} From 1d341c58a47664beee6c862b44e8bbb78d4a9f2a Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 20:16:41 +0900 Subject: [PATCH 034/133] feat: Add cosmwasm-rpc --- Cargo.toml | 2 + frame/cosmwasm/rpc/Cargo.toml | 28 ++++++++++ frame/cosmwasm/rpc/src/lib.rs | 75 +++++++++++++++++++++++++++ frame/cosmwasm/runtime-api/Cargo.toml | 2 +- 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 frame/cosmwasm/rpc/Cargo.toml create mode 100644 frame/cosmwasm/rpc/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index b9c8fe19..1648db1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "frame/cosmos/x/bank", "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm/types", + "frame/cosmwasm/rpc", "frame/cosmwasm/runtime-api", "frame/multimap", "primitives/cosmos", @@ -31,6 +32,7 @@ members = [ composable-support = { path = "composable/composable-support", default-features = false } cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } +cosmwasm-rpc = { path = "frame/cosmwasm/rpc" } cosmwasm-runtime-api = { path = "frame/cosmwasm/runtime-api", default-features = false } cosmwasm-std = { path = "cosmwasm/std", default-features = false } cosmwasm-vm = { path = "composable/vm", default-features = false } diff --git a/frame/cosmwasm/rpc/Cargo.toml b/frame/cosmwasm/rpc/Cargo.toml new file mode 100644 index 00000000..0e9aedf2 --- /dev/null +++ b/frame/cosmwasm/rpc/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = ["Composable Developers"] +edition = "2021" +homepage = "https://composable.finance" +name = "cosmwasm-rpc" +rust-version = "1.56" +version = "1.0.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# substrate primitives +sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } + +# local +cosmwasm-runtime-api = { workspace = true } + +# SCALE +codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = [ + "derive", +] } + +# rpc +jsonrpsee = { version = "0.23", features = ["server", "macros"] } diff --git a/frame/cosmwasm/rpc/src/lib.rs b/frame/cosmwasm/rpc/src/lib.rs new file mode 100644 index 00000000..631ccd83 --- /dev/null +++ b/frame/cosmwasm/rpc/src/lib.rs @@ -0,0 +1,75 @@ +use codec::Codec; +pub use cosmwasm_api::CosmwasmApiServer; +use cosmwasm_runtime_api::CosmwasmRuntimeApi; +use jsonrpsee::{ + core::RpcResult, + proc_macros::rpc, + types::{ErrorObject, ErrorObjectOwned}, +}; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_runtime::traits::Block as BlockT; +use std::{marker::PhantomData, sync::Arc}; + +mod cosmwasm_api { + use super::*; + #[rpc(client, server)] + pub trait CosmwasmApi { + #[method(name = "cosmwasm_query")] + fn query( + &self, + contract: String, + gas: u64, + query_request: Bytes, + at: Option, + ) -> RpcResult; + } +} + +pub struct Cosmwasm { + client: Arc, + _marker: PhantomData, +} + +impl Cosmwasm { + pub fn new(client: Arc) -> Self { + Self { client, _marker: Default::default() } + } +} + +fn runtime_error_into_rpc_error(e: E) -> ErrorObjectOwned { + ErrorObject::owned( + 9876, // no real reason for this value + e.to_string(), + None::<()>, + ) +} + +impl CosmwasmApiServer<::Hash, Error> + for Cosmwasm +where + Block: BlockT, + Error: Send + Sync + 'static + Codec + AsRef<[u8]>, + C: Send + Sync + 'static, + C: ProvideRuntimeApi, + C: HeaderBackend, + C::Api: CosmwasmRuntimeApi, +{ + fn query( + &self, + contract: String, + gas: u64, + query_request: Bytes, + at: Option<::Hash>, + ) -> RpcResult { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + let runtime_api_result = api + .query(at, contract, gas, query_request.to_vec()) + .map_err(runtime_error_into_rpc_error)?; + runtime_api_result + .map(Bytes::from) + .map_err(|e| runtime_error_into_rpc_error(String::from_utf8_lossy(e.as_ref()))) + } +} diff --git a/frame/cosmwasm/runtime-api/Cargo.toml b/frame/cosmwasm/runtime-api/Cargo.toml index bb636e0a..fe87c0a4 100644 --- a/frame/cosmwasm/runtime-api/Cargo.toml +++ b/frame/cosmwasm/runtime-api/Cargo.toml @@ -10,7 +10,7 @@ version = "1.0.0" targets = ["x86_64-unknown-linux-gnu"] [dependencies] -codec = { package = "parity-scale-codec", version = "3.2.0", default-features = false, features = [ +codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = [ "derive", ] } sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } From a68041757cd681026b1b0dc50329a7cb95953d0f Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 20:29:27 +0900 Subject: [PATCH 035/133] feat: Add pallet-cosmwasm --- Cargo.toml | 2 + frame/cosmwasm/Cargo.toml | 89 ++ frame/cosmwasm/License.md | 21 + frame/cosmwasm/README.md | 176 +++ frame/cosmwasm/src/crypto.rs | 109 ++ frame/cosmwasm/src/dispatchable_call.rs | 116 ++ frame/cosmwasm/src/entrypoint.rs | 130 ++ frame/cosmwasm/src/ibc.rs | 43 + frame/cosmwasm/src/instrument.rs | 351 +++++ frame/cosmwasm/src/lib.rs | 1467 ++++++++++++++++++++ frame/cosmwasm/src/pallet_hook.rs | 107 ++ frame/cosmwasm/src/prelude.rs | 6 + frame/cosmwasm/src/runtimes/abstraction.rs | 223 +++ frame/cosmwasm/src/runtimes/mod.rs | 2 + frame/cosmwasm/src/runtimes/vm.rs | 673 +++++++++ frame/cosmwasm/src/types.rs | 87 ++ frame/cosmwasm/src/utils.rs | 129 ++ frame/cosmwasm/src/weights.rs | 1371 ++++++++++++++++++ 18 files changed, 5102 insertions(+) create mode 100644 frame/cosmwasm/Cargo.toml create mode 100644 frame/cosmwasm/License.md create mode 100644 frame/cosmwasm/README.md create mode 100644 frame/cosmwasm/src/crypto.rs create mode 100644 frame/cosmwasm/src/dispatchable_call.rs create mode 100644 frame/cosmwasm/src/entrypoint.rs create mode 100644 frame/cosmwasm/src/ibc.rs create mode 100644 frame/cosmwasm/src/instrument.rs create mode 100644 frame/cosmwasm/src/lib.rs create mode 100644 frame/cosmwasm/src/pallet_hook.rs create mode 100644 frame/cosmwasm/src/prelude.rs create mode 100644 frame/cosmwasm/src/runtimes/abstraction.rs create mode 100644 frame/cosmwasm/src/runtimes/mod.rs create mode 100644 frame/cosmwasm/src/runtimes/vm.rs create mode 100644 frame/cosmwasm/src/types.rs create mode 100644 frame/cosmwasm/src/utils.rs create mode 100644 frame/cosmwasm/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index 1648db1c..03bd7cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "frame/cosmos/x/bank", "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm/types", + "frame/cosmwasm", "frame/cosmwasm/rpc", "frame/cosmwasm/runtime-api", "frame/multimap", @@ -51,6 +52,7 @@ pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-f pallet-cosmos-x-bank = { path = "frame/cosmos/x/bank", default-features = false } pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } +pallet-cosmwasm = { path = "frame/cosmwasm", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } [profile.release] diff --git a/frame/cosmwasm/Cargo.toml b/frame/cosmwasm/Cargo.toml new file mode 100644 index 00000000..7d3aa82a --- /dev/null +++ b/frame/cosmwasm/Cargo.toml @@ -0,0 +1,89 @@ +[package] +authors = ["Composable Developers"] +edition = "2021" +homepage = "https://composable.finance" +name = "pallet-cosmwasm" +version = "1.0.0" + +[lib] +doctest = false + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6", default-features = false } +scale-info = { default-features = false, version = "2.11", features = [ + "derive", +] } + +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +hex-literal = "0.4.1" +ibc = { version = "0.54.0", default-features = false } +libsecp256k1 = { version = "0.7.0", default-features = false } +log = { version = "0.4.21", default-features = false } +parity-wasm = { version = "0.45.0", default-features = false } +serde = { version = "1.0.210", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.127", default-features = false, features = [ + "alloc", +] } +sha2 = { version = "0.10", default-features = false } +sha3 = { version = "0.10", default-features = false } +vec1 = { version = "*", default-features = false, features = ["smallvec-v1"] } +wasmi = { version = "0.30.0", default-features = false } +wasm-instrument = { version = "0.4.0", default-features = false } +wasmi-validation = { version = "0.5.0", default-features = false } + +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-io ={ git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +composable-support = { workspace = true, default-features = false } +cosmwasm-std = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", + "cosmwasm_1_4", +] } +cosmwasm-vm = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", +] } +cosmwasm-vm-wasmi = { workspace = true, default-features = false, features = [ + "iterator", + "stargate", +] } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "hex/std", + "ibc/std", + "libsecp256k1/std", + "log/std", + "parity-wasm/std", + "serde/std", + "serde_json/std", + "sha2/std", + "sha3/std", + "vec1/std", + "wasmi/std", + "wasm-instrument/std", + "wasmi-validation/std", + "frame-support/std", + "frame-system/std", + "pallet-assets/std", + "pallet-balances/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "composable-support/std", + "cosmwasm-std/std", + "cosmwasm-vm/std", + "cosmwasm-vm-wasmi/std", +] +try-runtime = [] diff --git a/frame/cosmwasm/License.md b/frame/cosmwasm/License.md new file mode 100644 index 00000000..f0058185 --- /dev/null +++ b/frame/cosmwasm/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [Composable Finance LTD] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frame/cosmwasm/README.md b/frame/cosmwasm/README.md new file mode 100644 index 00000000..d5181975 --- /dev/null +++ b/frame/cosmwasm/README.md @@ -0,0 +1,176 @@ +# pallet-cosmwasm +In this section we provide actual implementation details of the CosmWasm specification as a Substrate pallet. +We cover some of the theoretical topics that were presented before, and provide real code that exemplifies how they can be developed in a pallet. + +We are deeply grateful to Ethan Frey and the talented team at Confio for their visionary work in creating CosmWasm. Their dedication, innovation, and commitment to advancing the field of blockchain technology have been a driving force in our journey. + + +## 0. Module Extensions + +Module extensions are part of ambient host "contract" (like Bank or Ibc) which receive query and execute messages,potentially encoded in ProtoBuf, against modules interfaces. + +We do not have such. +Instead we have pallet precompiles which are "contracts" with pallet account as address which receive and execute contract messages, against pallet interfaces. + +## 1. Gas Metering +When a contract is uploaded through the `upload` extrinsic, its code is instrumented. +This instrumentation process adds both gas metering and stack height limit to the uploaded code. + +Proper gas metering is achieved in two steps: +1. **Benchmarking**: Each instruction is benchmarked to determine the cost of its execution. +2. **Injecting Gas Metering**: Gas metering is injected into the code by using [wasm_instrument](https://github.com/paritytech/wasm-instrument). This process injects a call into the gas function for every execution or path/code block (function call, if, else, etc.) with the associated execution cost parameter. Therefore, it computes the overall total cost and ensures that every code block is paid before getting executed. + +Then, whenever a contract entrypoint is called, the pallet checks if the instrumentation version is up-to-date. +In case not, the code gets re-instrumented to ensure proper gas metering. + +## 2. Uploading Contracts +The `upload` extrinsic is used to upload smart contracts´ `code` to the pallet. + +### 2.1. Definition + +```rust,ignore +#[pallet::weight(T::WeightInfo::upload(code.len() as u32))] +pub fn upload(origin: OriginFor, code: ContractCodeOf) -> DispatchResultWithPostInfo; +``` + +### 2.2. Execution Flow +1. Check if the `code` is already uploaded. +2. Reserve `length(code) * deposit_per_byte` amount of native asset. +3. Check if the wasm code is valid. +4. Do the instrumentation by injecting gas metering and stack height limit. +5. Assign a code id to the `code`. This `code id ` is incremented on each upload. +6. Deposit the upload event. + +```rust,ignore +pub enum Event { + Uploaded { + code_hash: CodeHashOf, + code_id: CosmwasmCodeId, + } +``` + +### 2.3. Fee +Fees depend linearly on the size of the code. + +## 3. Instantiating a Contract +The `instantiate` extrinsic is used to instantiate a smart contract. + +### 3.1 Definition + +```rust,ignore +#[pallet::weight(T::WeightInfo::instantiate(funds.len() as u32).saturating_add(*gas))] +pub fn instantiate( + origin: OriginFor, + code_identifier: CodeIdentifier, + salt: ContractSaltOf, + admin: Option>, + label: ContractLabelOf, + funds: FundsOf, + gas: u64, + message: ContractMessageOf, +) -> DispatchResultWithPostInfo; +``` + +`origin` will be the `sender` from the contracts' perspective. + +Our goal is to create a deterministic smart contract environment. +Hence, we are not only using `code id` to identify a code. +Since `code id` depends on the current state of the chain, users won't be able to deterministically identify their code. +So we created `CodeIdentifier` which makes users able to also identify their code by using `CodeHash`. +And the corresponding `code id` is fetched internally. +This feature comes in handy when users want to batch their `upload + instantiate` calls, or they do any kind of scripting to upload and run the contracts. + +```rust,ignore +pub enum CodeIdentifier { + CodeId(CosmwasmCodeId), + CodeHash(CodeHashOf), +} +``` + +`admin` is the optional owner of the contract. Note that if it is set to `None`, the contract cannot ever be migrated or do any `admin` operations. +Therefore, it will become an immutable contract. +The `label` field is used as a human-readable `String` for the instantiated contract. +`salt` is used when a user wants to instantiate the same contract with the same parameters twice. +This ensures that during the contract address generation, addresses remain unique. +`funds` are transferred to the contract prior to instantiation. +Then, new balances will be visible to the contract. +`gas` represents the gas limit, and, `message` field is passed to the contract as the `InstantiateMsg`. + +### 3.2 Execution Flow +First, the contract address MUST be derived. +As stated previously, one of our goals is determinism. +Then, the contract addresses are also deterministic as opposed to other CosmWasm-running chains. +The algorithm is based on `instantiator`, `salt`, `code_hash` and `message` which is: + +```ignore +hash(instantiator + salt + code_hash + hash(message)) +``` + +This gives users opportunity to know the contract address prior to creation, which becomes really handy when it comes to XCVM. +Because this will provide a way to know the `interpreter` address prior to the creation for example so that users can add `Transfer` instruction which transfers some funds to the `interpreter` without paying for late-binding. + +Then the necessary setup is done like deriving a contract trie id for storage, increasing the `refcount` of the code. +Finally, instrumentation version is checked, and re-instrumentation happens if necessary. +Then, the `instantiate` entrypoint of the contract is called and `Instantiated` event is yielded. + +```rust,ignore +pub enum Event { + Instantiated { + contract: AccountIdOf, + info: ContractInfoOf, + } +} +``` + +### 3.3 Fee +Total fees depend on three factors: +- Instructions to be run. +- Base cost of instantiate call +- Funds to be transferred. + +The total fee can be computed as follows. + +```ignore +base_instantiate_fee + (fee_per_fund * length(funds)) + executed_instruction_costs +``` + +The remaining gas is refunded after execution. + +## 4. Executing a Contract +The `execute` extrinsic is used for executing a smart contract. + +### 4.1. Definition + +```rust,ignore +#[pallet::weight(T::WeightInfo::execute(funds.len() as u32).saturating_add(*gas))] +pub fn execute( + origin: OriginFor, + contract: AccountIdOf, + funds: FundsOf, + gas: Gas, + message: ContractMessageOf, +) -> DispatchResultWithPostInfo; +``` + +The `contract` field contains the contract address to execute. +The `funds` are transferred to the contract prior to instantiation. +So that the new balances will be visible to the contract. +`gas` represents the gas limit, and, `message` is passed to the contract as the `ExecuteMsg`. + +### 4.2 Execution Flow +The execution flow is reduced to a minimum for contract execution. +Only a check for re-instrumentation is performed, and then the execution of the `execute` entrypoint of the contract is triggered. + +### 4.3 Fee +Total fees depend on three factors: +- Instructions to be run. +- Base cost of instantiate call +- Funds to be transferred. + +The total fee can be computed as follows. + +```ignore +base_instantiate_fee + (fee_per_fund * length(funds)) + executed_instruction_costs +``` + +The remaining gas is refunded after execution. diff --git a/frame/cosmwasm/src/crypto.rs b/frame/cosmwasm/src/crypto.rs new file mode 100644 index 00000000..a60e333b --- /dev/null +++ b/frame/cosmwasm/src/crypto.rs @@ -0,0 +1,109 @@ +use crate::{Config, Pallet, SUBSTRATE_ECDSA_SIGNATURE_LEN}; +use alloc::{vec, vec::Vec}; + +use sp_core::{ecdsa, ed25519}; +impl Pallet { + pub(crate) fn do_secp256k1_recover_pubkey( + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, ()> { + // `recovery_param` must be 0 or 1. Other values are not supported from CosmWasm. + if recovery_param >= 2 { + return Err(()) + } + + if signature.len() != SUBSTRATE_ECDSA_SIGNATURE_LEN - 1 { + return Err(()) + } + + // Try into a [u8; 32] + let message_hash = message_hash.try_into().map_err(|_| ())?; + + let signature = { + // Since we fill `signature_inner` with `recovery_param`, when 64 bytes are written + // the final byte will be the `recovery_param`. + let mut signature_inner = [recovery_param; SUBSTRATE_ECDSA_SIGNATURE_LEN]; + signature_inner[..SUBSTRATE_ECDSA_SIGNATURE_LEN - 1].copy_from_slice(signature); + signature_inner + }; + + sp_io::crypto::secp256k1_ecdsa_recover(&signature, &message_hash) + .map(|without_tag| { + let mut with_tag = vec![0x04_u8]; + with_tag.extend_from_slice(&without_tag[..]); + with_tag + }) + .map_err(|_| ()) + } + + pub(crate) fn do_secp256k1_verify( + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> bool { + let message_hash = match message_hash.try_into() { + Ok(message_hash) => message_hash, + Err(_) => return false, + }; + + // We are expecting 64 bytes long public keys but the substrate function use an + // additional byte for recovery id. So we insert a dummy byte. + let signature = { + let mut signature_inner = [0_u8; SUBSTRATE_ECDSA_SIGNATURE_LEN]; + signature_inner[..SUBSTRATE_ECDSA_SIGNATURE_LEN - 1].copy_from_slice(signature); + ecdsa::Signature::from(signature_inner) + }; + + let public_key = match libsecp256k1::PublicKey::parse_slice(public_key, None) { + Ok(public_key) => ecdsa::Public::from_raw(public_key.serialize_compressed()), + Err(_) => return false, + }; + + sp_io::crypto::ecdsa_verify_prehashed(&signature, &message_hash, &public_key) + } + + pub(crate) fn do_ed25519_batch_verify( + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> bool { + // https://substrate.stackexchange.com/questions/9754/sp-iocryptoed25519-batch-verify-was-removed-amid-polkadot-0-9-39-and-0-9-4 + + let mut messages = messages.to_vec(); + let mut public_keys = public_keys.to_vec(); + + if messages.len() == signatures.len() && messages.len() == public_keys.len() { + // Nothing needs to be done + } else if messages.len() == 1 && signatures.len() == public_keys.len() { + // There can be a single message signed with different signature-public key pairs + messages = messages.repeat(signatures.len()); + } else if public_keys.len() == 1 && messages.len() == signatures.len() { + // Single entity(with a public key) might wanna verify different messages + public_keys = public_keys.repeat(signatures.len()); + } else { + // Any other case is wrong + return false + } + + messages + .iter() + .zip(signatures) + .zip(public_keys) + .all(|((m, s), p)| Self::do_ed25519_verify(m, s, p)) + } + + pub(crate) fn do_ed25519_verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> bool { + let signature: ed25519::Signature = match signature.try_into() { + Ok(signature) => signature, + Err(_) => return false, + }; + + let public_key: ed25519::Public = match public_key.try_into() { + Ok(public_key) => public_key, + Err(_) => return false, + }; + + sp_io::crypto::ed25519_verify(&signature, message, &public_key) + } +} diff --git a/frame/cosmwasm/src/dispatchable_call.rs b/frame/cosmwasm/src/dispatchable_call.rs new file mode 100644 index 00000000..e3461016 --- /dev/null +++ b/frame/cosmwasm/src/dispatchable_call.rs @@ -0,0 +1,116 @@ +use crate::{ + pallet_hook::PalletHook, + runtimes::vm::{ContractBackend, CosmwasmVMError, CosmwasmVMShared}, + types::*, + Config, Pallet, +}; +use alloc::vec::Vec; +use core::marker::PhantomData; +use cosmwasm_std::Coin; +use cosmwasm_vm::{ + executor::{cosmwasm_call, AsFunctionName}, + system::{ + cosmwasm_system_entrypoint_hook, cosmwasm_system_run_hook, CosmwasmCallVM, + CosmwasmDynamicVM, StargateCosmwasmCallVM, + }, + vm::VmErrorOf, +}; +use cosmwasm_vm_wasmi::OwnedWasmiVM; +use wasmi::AsContext; + +/// Generic ready-to-call state for all input types +pub struct DispatchableCall { + pub sender: AccountIdOf, + pub contract: AccountIdOf, + pub entrypoint: EntryPoint, + pub output: O, + pub marker: PhantomData, +} + +/// Dispatch state for all `Input`s +impl DispatchableCall { + /// Start a cosmwasm transaction by calling an entrypoint. + /// This is used for running the top level messages and will return output O + /// when succesful. + /// + /// * `shared` - Shared state of the Cosmwasm VM. + /// * `funds` - Funds to be transferred before execution. + /// * `message` - Message to be passed to the entrypoint. + pub(crate) fn top_level_call( + self, + shared: &mut CosmwasmVMShared, + funds: FundsOf, + message: ContractMessageOf, + ) -> Result> + where + for<'x> OwnedWasmiVM>: + CosmwasmCallVM + CosmwasmDynamicVM + StargateCosmwasmCallVM, + for<'x> VmErrorOf>>: + From> + Into>, + I: AsFunctionName, + { + Pallet::::top_level_dispatch( + shared, + self.entrypoint, + self.sender, + self.contract, + funds, + |mut vm| { + cosmwasm_system_entrypoint_hook::(&mut vm, &message, |vm, message| { + match vm.0.as_context().data().contract_runtime { + ContractBackend::CosmWasm { .. } => + cosmwasm_call::(vm, message).map(Into::into), + ContractBackend::Pallet { .. } => + T::PalletHook::execute(vm, self.entrypoint, message), + } + }) + .map_err(Into::into) + }, + )?; + Ok(self.output) + } + + /// Continue the execution by running an entrypoint. This is used for running + /// submessages. + /// + /// * `shared` - Shared state of the Cosmwasm VM. + /// * `funds` - Funds to be transferred before execution. + /// * `message` - Message to be passed to the entrypoint. + /// * `event_handler` - Event handler that is passed by the VM. + pub(crate) fn sub_call( + self, + shared: &mut CosmwasmVMShared, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, CosmwasmVMError> + where + for<'x> OwnedWasmiVM>: + CosmwasmCallVM + CosmwasmDynamicVM + StargateCosmwasmCallVM, + for<'x> VmErrorOf>>: + From> + Into>, + { + // Call `cosmwasm_call` to transfer funds and create the vm instance before + // calling the callback. + Pallet::::sub_level_dispatch( + shared, + self.sender, + self.contract, + funds, + // `cosmwasm_system_run` is called instead of `cosmwasm_system_entrypoint` here + // because here, we want to continue running the transaction with the given + // entrypoint + |mut vm| { + cosmwasm_system_run_hook::(&mut vm, message, event_handler, |vm, message| { + match vm.0.as_context().data().contract_runtime { + ContractBackend::CosmWasm { .. } => + cosmwasm_call::(vm, message).map(Into::into), + ContractBackend::Pallet { .. } => + T::PalletHook::execute(vm, self.entrypoint, message), + } + }) + .map_err(Into::into) + }, + ) + } +} diff --git a/frame/cosmwasm/src/entrypoint.rs b/frame/cosmwasm/src/entrypoint.rs new file mode 100644 index 00000000..aafd7e11 --- /dev/null +++ b/frame/cosmwasm/src/entrypoint.rs @@ -0,0 +1,130 @@ +use crate::{ + dispatchable_call::DispatchableCall, + runtimes::{abstraction::CosmwasmAccount, vm::CosmwasmVMShared}, + types::*, + CodeIdToInfo, Config, ContractToInfo, CurrentNonce, Error, Event, Pallet, +}; + +use composable_support::abstractions::utils::increment::Increment; +use core::marker::PhantomData; +use cosmwasm_vm::{ + executor::{ExecuteCall, InstantiateCall, MigrateCall, ReplyCall}, + system::CosmwasmCodeId, +}; + +use frame_support::ensure; +/// Prepares for `instantiate` entrypoint call. +/// +/// * `instantiator` - Address of the account that calls this entrypoint. +pub(crate) fn setup_instantiate_call( + instantiator: AccountIdOf, + code_id: CosmwasmCodeId, + salt: &[u8], + admin: Option>, + label: ContractLabelOf, +) -> Result, T>, Error> { + let code_hash = CodeIdToInfo::::get(code_id) + .ok_or(Error::::CodeNotFound)? + .pristine_code_hash; + let contract = Pallet::::derive_contract_address(&instantiator, salt, &code_hash)?; + // Make sure that contract address does not already exist + ensure!(Pallet::::contract_exists(&contract).is_err(), Error::::ContractAlreadyExists); + let nonce = CurrentNonce::::increment().map_err(|_| Error::::NonceOverflow)?; + let trie_id = Pallet::::derive_contract_trie_id(&contract, nonce); + let contract_info = + ContractInfoOf:: { instantiator: instantiator.clone(), code_id, trie_id, admin, label }; + ContractToInfo::::insert(&contract, &contract_info); + CodeIdToInfo::::try_mutate(code_id, |entry| -> Result<(), Error> { + let code_info = entry.as_mut().ok_or(Error::::CodeNotFound)?; + code_info.refcount = + code_info.refcount.checked_add(1).ok_or(Error::::RefcountOverflow)?; + Ok(()) + })?; + Pallet::::deposit_event(Event::::Instantiated { + contract: contract.clone(), + info: contract_info, + }); + Ok(DispatchableCall { + sender: instantiator, + contract: contract.clone(), + entrypoint: EntryPoint::Instantiate, + output: contract, + marker: PhantomData, + }) +} + +/// Prepares for `execute` entrypoint call. +/// +/// * `executor` - Address of the account that calls this entrypoint. +/// * `contract` - Address of the contract to be called. +pub(crate) fn setup_execute_call( + executor: AccountIdOf, + contract: AccountIdOf, +) -> Result, Error> { + Ok(DispatchableCall { + entrypoint: EntryPoint::Execute, + sender: executor, + contract, + output: (), + marker: PhantomData, + }) +} + +/// Prepares for `reply` entrypoint call. +/// +/// * `executor` - Address of the account that calls this entrypoint. +/// * `contract` - Address of the contract to be called. +pub(crate) fn setup_reply_call( + executor: AccountIdOf, + contract: AccountIdOf, +) -> Result, Error> { + Ok(DispatchableCall { + entrypoint: EntryPoint::Reply, + sender: executor, + contract, + output: (), + marker: PhantomData, + }) +} + +/// Prepares for `migrate` entrypoint call. +/// +/// * `migrator` - Address of the account that calls this entrypoint. +/// * `contract` - Address of the contract to be called. +/// * `new_code_id` - New code id that the contract will point to (or use). +pub(crate) fn setup_migrate_call( + shared: &mut CosmwasmVMShared, + migrator: AccountIdOf, + contract: AccountIdOf, + new_code_id: CosmwasmCodeId, + call_migrate: bool, +) -> Result, Error> { + // If the migrate already happened, no need to do that again. + // This is the case for sub-message execution where `migrate` is + // called by the VM. + if call_migrate { + Pallet::::sub_level_dispatch( + shared, + migrator.clone(), + contract.clone(), + Default::default(), + |mut vm| { + cosmwasm_vm::system::migrate( + &mut vm, + CosmwasmAccount::new(migrator.clone()), + CosmwasmAccount::new(contract.clone()), + new_code_id, + ) + }, + ) + .map_err(|_| Error::::NotAuthorized)?; + } + + Ok(DispatchableCall { + sender: migrator, + contract, + entrypoint: EntryPoint::Migrate, + output: (), + marker: PhantomData, + }) +} diff --git a/frame/cosmwasm/src/ibc.rs b/frame/cosmwasm/src/ibc.rs new file mode 100644 index 00000000..7ab2e295 --- /dev/null +++ b/frame/cosmwasm/src/ibc.rs @@ -0,0 +1,43 @@ +use crate::{ + prelude::*, + runtimes::vm::CosmwasmVMError, + types::{AccountIdOf, DefaultCosmwasmVM}, + Config, Pallet, +}; + +impl Pallet { + /// Check whether a contract export the mandatory IBC functions and is consequently IBC capable. + pub(crate) fn do_check_ibc_capability(_module: &parity_wasm::elements::Module) -> bool { + false + } + + pub fn do_ibc_transfer( + _vm: &mut DefaultCosmwasmVM, + _channel_id: String, + _to_address: String, + _amount: cosmwasm_std::Coin, + _timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), CosmwasmVMError> { + Err(CosmwasmVMError::::Unsupported) + } + + pub(crate) fn do_ibc_send_packet( + _vm: &mut DefaultCosmwasmVM, + _channel_id: String, + _data: cosmwasm_std::Binary, + _timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), CosmwasmVMError> { + Err(CosmwasmVMError::::Unsupported) + } + + pub(crate) fn do_ibc_close_channel( + _vm: &mut DefaultCosmwasmVM, + _channel_id: String, + ) -> Result<(), CosmwasmVMError> { + Err(CosmwasmVMError::::Unsupported) + } + + pub(crate) fn do_compute_ibc_contract_port(address: AccountIdOf) -> String { + format!("wasm.{}", Pallet::::account_to_cosmwasm_addr(address)) + } +} diff --git a/frame/cosmwasm/src/instrument.rs b/frame/cosmwasm/src/instrument.rs new file mode 100644 index 00000000..7abc6635 --- /dev/null +++ b/frame/cosmwasm/src/instrument.rs @@ -0,0 +1,351 @@ +use crate::{weights::WeightInfo, Config}; +use codec::{Decode, Encode}; +use core::marker::PhantomData; +use cosmwasm_vm_wasmi::version::{Version, Version1x}; +use frame_support::weights::Weight; +use parity_wasm::elements::{Instruction, Module}; +use scale_info::TypeInfo; +use wasm_instrument::gas_metering::{self, host_function, MemoryGrowCost, Rules}; + +pub const INSTRUCTIONS_MULTIPLIER: u32 = 100; + +/// Current instrumentation version +/// Must be incremented whenever the instrumentation is updated. +pub const INSTRUMENTATION_VERSION: u16 = 1; + +/// Errors that can possibly happen while instrumenting a code. +#[derive(Clone, Debug, Encode, Decode, TypeInfo)] +pub enum InstrumentationError { + GasMeteringInjection, + StackHeightLimitingInjection, +} + +/// Instrument a code for gas metering and stack height limiting. +pub fn gas_and_stack_instrumentation( + module: Module, + _gas_module_name: &str, + stack_limit: u32, + cost_rules: &impl Rules, +) -> Result { + let gas_instrumented_module = gas_metering::inject( + module, + host_function::Injector::new(Version1x::ENV_MODULE, Version1x::ENV_GAS), + cost_rules, + ) + .map_err(|e| { + log::debug!(target: "runtime::contracts", "gas_and_stack_instrumentation: {:?}", e); + InstrumentationError::GasMeteringInjection + })?; + let stack_and_gas_instrumented_module = + wasm_instrument::inject_stack_limiter(gas_instrumented_module, stack_limit).map_err( + |e| { + log::debug!(target: "runtime::contracts", "gas_and_stack_instrumentation: {:?}", e); + InstrumentationError::StackHeightLimitingInjection + }, + )?; + Ok(stack_and_gas_instrumented_module) +} + +type WeightFn = fn(n: u32) -> Weight; + +/// Calculates and returns the weight of a single instruction +/// +/// * weight_fn: Generated weight function of an instruction +/// * additional_instrs: Count of instructions that are used to be able to generate a valid program +/// but should be included in the weight calculation +fn calculate_weight(weight_fn: WeightFn, n_additional_instrs: u32) -> u32 { + (weight_fn(1).saturating_sub(weight_fn(0)).saturating_sub( + T::WeightInfo::instruction_I64Const(1) + .saturating_sub(T::WeightInfo::instruction_I64Const(0)) + .saturating_div(2), + )) + .saturating_mul(n_additional_instrs.into()) + .saturating_div(INSTRUCTIONS_MULTIPLIER.into()) + .ref_time() as u32 +} + +/// Calculates a weight that is dependent on other weight. Eg. `else` because it cannot +/// exist without an `if` +fn calculate_weight_custom(weight_fn: WeightFn, custom_fn: WeightFn) -> u32 { + (weight_fn(1) + .saturating_sub(weight_fn(0)) + .saturating_sub(custom_fn(1).saturating_sub(custom_fn(0)).saturating_div(2))) + .saturating_div(INSTRUCTIONS_MULTIPLIER.into()) + .ref_time() as u32 +} + +#[derive(Encode, Decode, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct CostRules { + i64const: u32, + f64const: u32, + i64load: u32, + f64load: u32, + i64store: u32, + f64store: u32, + i64eq: u32, + i64eqz: u32, + i64ne: u32, + i64lts: u32, + i64gts: u32, + i64les: u32, + i64ges: u32, + i64clz: u32, + i64ctz: u32, + i64popcnt: u32, + i64add: u32, + i64sub: u32, + i64mul: u32, + i64divs: u32, + i64divu: u32, + i64rems: u32, + i64and: u32, + i64or: u32, + i64xor: u32, + i64shl: u32, + i64shrs: u32, + i64rotl: u32, + i64rotr: u32, + i32wrapi64: u32, + i64extendsi32: u32, + f64eq: u32, + f64ne: u32, + f64lt: u32, + f64gt: u32, + f64le: u32, + f64ge: u32, + f64abs: u32, + f64neg: u32, + f64ceil: u32, + f64floor: u32, + f64trunc: u32, + f64nearest: u32, + f64sqrt: u32, + f64add: u32, + f64sub: u32, + f64mul: u32, + f64div: u32, + f64min: u32, + f64max: u32, + f64copysign: u32, + select: u32, + if_: u32, + else_: u32, + getlocal: u32, + setlocal: u32, + teelocal: u32, + setglobal: u32, + getglobal: u32, + currentmemory: u32, + growmemory: u32, + br: u32, + brif: u32, + brtable: u32, + brtable_per_elem: u32, + call: u32, + call_indirect: u32, + #[codec(skip)] + _marker: PhantomData, +} + +impl Default for CostRules { + fn default() -> Self { + Self { + i64const: calculate_weight::(T::WeightInfo::instruction_I64Const, 1), + f64const: calculate_weight::(T::WeightInfo::instruction_F64Const, 1), + i64load: calculate_weight::(T::WeightInfo::instruction_I64Load, 2), + f64load: calculate_weight::(T::WeightInfo::instruction_F64Load, 2), + i64store: calculate_weight::(T::WeightInfo::instruction_I64Store, 2), + f64store: calculate_weight::(T::WeightInfo::instruction_F64Store, 2), + i64eq: calculate_weight::(T::WeightInfo::instruction_I64Eq, 3), + i64eqz: calculate_weight::(T::WeightInfo::instruction_I64Eqz, 2), + i64ne: calculate_weight::(T::WeightInfo::instruction_I64Ne, 3), + i64lts: calculate_weight::(T::WeightInfo::instruction_I64LtS, 3), + i64gts: calculate_weight::(T::WeightInfo::instruction_I64GtS, 3), + i64les: calculate_weight::(T::WeightInfo::instruction_I64LeS, 3), + i64ges: calculate_weight::(T::WeightInfo::instruction_I64GeS, 3), + i64clz: calculate_weight::(T::WeightInfo::instruction_I64Clz, 3), + i64ctz: calculate_weight::(T::WeightInfo::instruction_I64Ctz, 2), + i64popcnt: calculate_weight::(T::WeightInfo::instruction_I64Popcnt, 2), + i64add: calculate_weight::(T::WeightInfo::instruction_I64Add, 3), + i64sub: calculate_weight::(T::WeightInfo::instruction_I64Sub, 3), + i64mul: calculate_weight::(T::WeightInfo::instruction_I64Mul, 3), + i64divs: calculate_weight::(T::WeightInfo::instruction_I64DivS, 3), + i64divu: calculate_weight::(T::WeightInfo::instruction_I64DivU, 3), + i64rems: calculate_weight::(T::WeightInfo::instruction_I64RemS, 3), + i64and: calculate_weight::(T::WeightInfo::instruction_I64And, 3), + i64or: calculate_weight::(T::WeightInfo::instruction_I64Or, 3), + i64xor: calculate_weight::(T::WeightInfo::instruction_I64Xor, 3), + i64shl: calculate_weight::(T::WeightInfo::instruction_I64Shl, 3), + i64shrs: calculate_weight::(T::WeightInfo::instruction_I64ShrS, 3), + i64rotl: calculate_weight::(T::WeightInfo::instruction_I64Rotl, 3), + i64rotr: calculate_weight::(T::WeightInfo::instruction_I64Rotr, 3), + i32wrapi64: calculate_weight::(T::WeightInfo::instruction_I32WrapI64, 3), + i64extendsi32: calculate_weight::(T::WeightInfo::instruction_I64ExtendSI32, 3), + f64eq: calculate_weight::(T::WeightInfo::instruction_F64Eq, 3), + f64ne: calculate_weight::(T::WeightInfo::instruction_F64Ne, 3), + f64lt: calculate_weight::(T::WeightInfo::instruction_F64Lt, 3), + f64gt: calculate_weight::(T::WeightInfo::instruction_F64Gt, 3), + f64le: calculate_weight::(T::WeightInfo::instruction_F64Le, 3), + f64ge: calculate_weight::(T::WeightInfo::instruction_F64Ge, 3), + f64abs: calculate_weight::(T::WeightInfo::instruction_F64Abs, 2), + f64neg: calculate_weight::(T::WeightInfo::instruction_F64Neg, 2), + f64ceil: calculate_weight::(T::WeightInfo::instruction_F64Ceil, 2), + f64floor: calculate_weight::(T::WeightInfo::instruction_F64Floor, 2), + f64trunc: calculate_weight::(T::WeightInfo::instruction_F64Trunc, 2), + f64nearest: calculate_weight::(T::WeightInfo::instruction_F64Nearest, 2), + f64sqrt: calculate_weight::(T::WeightInfo::instruction_F64Sqrt, 2), + f64add: calculate_weight::(T::WeightInfo::instruction_F64Add, 3), + f64sub: calculate_weight::(T::WeightInfo::instruction_F64Sub, 3), + f64mul: calculate_weight::(T::WeightInfo::instruction_F64Mul, 3), + f64div: calculate_weight::(T::WeightInfo::instruction_F64Div, 3), + f64min: calculate_weight::(T::WeightInfo::instruction_F64Min, 3), + f64max: calculate_weight::(T::WeightInfo::instruction_F64Max, 3), + f64copysign: calculate_weight::(T::WeightInfo::instruction_F64Copysign, 3), + select: calculate_weight::(T::WeightInfo::instruction_Select, 4), + if_: calculate_weight::(T::WeightInfo::instruction_If, 2), + else_: calculate_weight_custom( + T::WeightInfo::instruction_Else, + T::WeightInfo::instruction_If, + ), + getlocal: calculate_weight::(T::WeightInfo::instruction_GetLocal, 1), + setlocal: calculate_weight::(T::WeightInfo::instruction_SetLocal, 1), + teelocal: calculate_weight::(T::WeightInfo::instruction_TeeLocal, 2), + getglobal: calculate_weight::(T::WeightInfo::instruction_GetGlobal, 1), + setglobal: calculate_weight::(T::WeightInfo::instruction_SetGlobal, 2), + currentmemory: calculate_weight::(T::WeightInfo::instruction_CurrentMemory, 2), + growmemory: calculate_weight::(T::WeightInfo::instruction_GrowMemory, 2), + br: calculate_weight::(T::WeightInfo::instruction_Br, 1), + brif: calculate_weight::(T::WeightInfo::instruction_BrIf, 1), + brtable: calculate_weight::(T::WeightInfo::instruction_BrTable, 1), + brtable_per_elem: calculate_weight::(T::WeightInfo::instruction_BrTable_per_elem, 0), + call: calculate_weight::(T::WeightInfo::instruction_Call, 2), + call_indirect: calculate_weight::(T::WeightInfo::instruction_CallIndirect, 3), + _marker: PhantomData, + } + } +} + +impl Rules for CostRules { + /// Returns the cost for the passed `instruction`. + fn instruction_cost(&self, instruction: &Instruction) -> Option { + let weight = match instruction { + Instruction::I64Const(_) | Instruction::I32Const(_) => self.i64const, + Instruction::F64Const(_) | Instruction::F32Const(_) => self.f64const, + Instruction::I64Load(_, _) | + Instruction::I32Load(_, _) | + Instruction::I32Load8S(_, _) | + Instruction::I32Load8U(_, _) | + Instruction::I32Load16S(_, _) | + Instruction::I32Load16U(_, _) | + Instruction::I64Load8S(_, _) | + Instruction::I64Load8U(_, _) | + Instruction::I64Load16S(_, _) | + Instruction::I64Load16U(_, _) | + Instruction::I64Load32S(_, _) | + Instruction::I64Load32U(_, _) => self.i64load, + Instruction::F64Load(_, _) | Instruction::F32Load(_, _) => self.f64load, + Instruction::I64Store(_, _) | + Instruction::I32Store(_, _) | + Instruction::I32Store8(_, _) | + Instruction::I32Store16(_, _) | + Instruction::I64Store8(_, _) | + Instruction::I64Store16(_, _) | + Instruction::I64Store32(_, _) => self.i64store, + Instruction::F64Store(_, _) | Instruction::F32Store(_, _) => self.f64store, + Instruction::I64Eq | Instruction::I32Eq => self.i64eq, + Instruction::I64Eqz | Instruction::I32Eqz => self.i64eqz, + Instruction::I64Ne | Instruction::I32Ne => self.i64ne, + Instruction::I64LtS | + Instruction::I32LtS | + Instruction::I64LtU | + Instruction::I32LtU => self.i64lts, + Instruction::I64GtS | + Instruction::I32GtS | + Instruction::I64GtU | + Instruction::I32GtU => self.i64gts, + Instruction::I64LeS | + Instruction::I32LeS | + Instruction::I64LeU | + Instruction::I32LeU => self.i64les, + Instruction::I64GeS | + Instruction::I32GeS | + Instruction::I64GeU | + Instruction::I32GeU => self.i64ges, + Instruction::I64Clz | Instruction::I32Clz => self.i64clz, + Instruction::I64Ctz | Instruction::I32Ctz => self.i64ctz, + Instruction::I64Popcnt | Instruction::I32Popcnt => self.i64popcnt, + Instruction::I64Add | Instruction::I32Add => self.i64add, + Instruction::I64Sub | Instruction::I32Sub => self.i64sub, + Instruction::I64Mul | Instruction::I32Mul => self.i64mul, + Instruction::I64DivS | Instruction::I32DivS => self.i64divs, + Instruction::I64DivU | Instruction::I32DivU => self.i64divu, + Instruction::I64RemU | + Instruction::I32RemU | + Instruction::I64RemS | + Instruction::I32RemS => self.i64rems, + Instruction::I64And | Instruction::I32And => self.i64and, + Instruction::I64Or | Instruction::I32Or => self.i64or, + Instruction::I64Xor | Instruction::I32Xor => self.i64xor, + Instruction::I64Shl | Instruction::I32Shl => self.i64shl, + Instruction::I64ShrU | + Instruction::I32ShrU | + Instruction::I64ShrS | + Instruction::I32ShrS => self.i64shrs, + Instruction::I64Rotl | Instruction::I32Rotl => self.i64rotl, + Instruction::I64Rotr | Instruction::I32Rotr => self.i64rotr, + Instruction::I32WrapI64 => self.i32wrapi64, + Instruction::I64ExtendSI32 | Instruction::I64ExtendUI32 => self.i64extendsi32, + Instruction::F64Eq | Instruction::F32Eq => self.f64eq, + Instruction::F64Ne | Instruction::F32Ne => self.f64ne, + Instruction::F64Lt | Instruction::F32Lt => self.f64lt, + Instruction::F64Gt | Instruction::F32Gt => self.f64gt, + Instruction::F64Le | Instruction::F32Le => self.f64le, + Instruction::F64Ge | Instruction::F32Ge => self.f64ge, + Instruction::F64Abs | Instruction::F32Abs => self.f64abs, + Instruction::F64Neg | Instruction::F32Neg => self.f64neg, + Instruction::F64Ceil | Instruction::F32Ceil => self.f64ceil, + Instruction::F64Floor | Instruction::F32Floor => self.f64floor, + Instruction::F64Trunc | Instruction::F32Trunc => self.f64trunc, + Instruction::F64Nearest | Instruction::F32Nearest => self.f64nearest, + Instruction::F64Sqrt | Instruction::F32Sqrt => self.f64sqrt, + Instruction::F64Add | Instruction::F32Add => self.f64add, + Instruction::F64Sub | Instruction::F32Sub => self.f64sub, + Instruction::F64Mul | Instruction::F32Mul => self.f64mul, + Instruction::F64Div | Instruction::F32Div => self.f64div, + Instruction::F64Min | Instruction::F32Min => self.f64min, + Instruction::F64Max | Instruction::F32Max => self.f64max, + Instruction::F64Copysign | Instruction::F32Copysign => self.f64copysign, + Instruction::Drop => self.i64const, + Instruction::Select => self.select, + Instruction::If(_) => self.if_, + Instruction::Else => self.else_, + Instruction::GetLocal(_) => self.getlocal, + Instruction::SetLocal(_) => self.setlocal, + Instruction::TeeLocal(_) => self.teelocal, + Instruction::GetGlobal(_) => self.getglobal, + Instruction::SetGlobal(_) => self.setglobal, + Instruction::CurrentMemory(_) => self.currentmemory, + Instruction::GrowMemory(_) => self.growmemory, + Instruction::Br(_) => self.br, + Instruction::BrIf(_) => self.brif, + Instruction::BrTable(table) => self + .brtable + .saturating_add(self.brtable_per_elem.saturating_mul(table.table.len() as u32)), + Instruction::Call(_) => self.call, + Instruction::CallIndirect(_, _) => self.call_indirect, + _ => 1_000, + }; + Some(weight) + } + + /// Returns the costs for growing the memory using the `memory.grow` instruction. + fn memory_grow_cost(&self) -> MemoryGrowCost { + // GrowMemory is already benchmarked + MemoryGrowCost::Free + } + + fn call_per_local_cost(&self) -> u32 { + 0 + } +} diff --git a/frame/cosmwasm/src/lib.rs b/frame/cosmwasm/src/lib.rs new file mode 100644 index 00000000..5644b3bb --- /dev/null +++ b/frame/cosmwasm/src/lib.rs @@ -0,0 +1,1467 @@ +#![recursion_limit = "256"] +#![feature(sync_unsafe_cell)] +#![feature(trait_alias)] +#![cfg_attr( + not(test), + deny( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::indexing_slicing, + clippy::todo, + clippy::unwrap_used, + clippy::panic, + clippy::unseparated_literal_suffix, + clippy::disallowed_types + ) +)] +#![cfg_attr(not(feature = "std"), no_std)] +#![deny( + bad_style, + bare_trait_objects, + improper_ctypes, + non_shorthand_field_patterns, + no_mangle_generic_items, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + unconditional_recursion, + unused_allocation, + unused_comparisons, + unused_parens, + while_true, + trivial_casts, + trivial_numeric_casts, + unused_extern_crates +)] +#![allow(clippy::type_complexity)] +#![doc = include_str!("../README.md")] + +extern crate alloc; + +use alloc::string::ToString; + +pub use pallet::*; +use sp_core::H256; +pub mod crypto; +pub mod dispatchable_call; +pub mod entrypoint; +pub mod ibc; +pub mod instrument; +pub mod pallet_hook; +mod prelude; +pub mod runtimes; +pub mod types; +pub mod utils; +pub mod weights; + +const SUBSTRATE_ECDSA_SIGNATURE_LEN: usize = 65; +use crate::{ + entrypoint::*, + instrument::{gas_and_stack_instrumentation, INSTRUMENTATION_VERSION}, + pallet_hook::PalletHook, + runtimes::{ + abstraction::{CosmwasmAccount, Gas, VMPallet}, + vm::{ + ContractBackend, CosmwasmVM, CosmwasmVMCache, CosmwasmVMError, CosmwasmVMShared, + InitialStorageMutability, + }, + }, + types::*, +}; +use alloc::{ + collections::{btree_map::Entry, BTreeMap}, + format, + string::String, + vec::Vec, +}; +use composable_support::abstractions::utils::increment::Increment; +use cosmwasm_std::{ + Addr, Attribute as CosmwasmEventAttribute, Binary as CosmwasmBinary, BlockInfo, + CodeInfoResponse, Coin, ContractInfo as CosmwasmContractInfo, ContractInfoResponse, Env, + Event as CosmwasmEvent, MessageInfo, Timestamp, TransactionInfo, +}; +use cosmwasm_vm::{ + executor::{cosmwasm_call, QueryCall, QueryResponse}, + system::{cosmwasm_system_query, CosmwasmCodeId, CosmwasmContractMeta}, +}; +use cosmwasm_vm_wasmi::{ + new_wasmi_vm, + validation::{CodeValidation, ValidationError}, + version::{Version, Version1x}, + OwnedWasmiVM, +}; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo}, + pallet_prelude::*, + storage::child::ChildInfo, + traits::{ + fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate}, + tokens::Preservation, + Currency, ExistenceRequirement, Get, ReservableCurrency, UnixTime, + }, + ReversibleStorageHasher, StorageHasher, +}; +use sp_runtime::traits::SaturatedConversion; +use wasmi::AsContext; +use wasmi_validation::PlainValidator; + +#[allow(clippy::too_many_arguments)] +#[frame_support::pallet] +pub mod pallet { + use crate::{ + instrument::CostRules, pallet_hook::PalletHook, runtimes::vm::InitialStorageMutability, + types::*, weights::WeightInfo, + }; + use alloc::{string::String, vec::Vec}; + use composable_support::abstractions::{ + nonce::Nonce, + utils::{increment::SafeIncrement, start_at::ZeroInit}, + }; + use core::fmt::Debug; + use cosmwasm_vm::system::CosmwasmCodeId; + + use frame_support::{ + dispatch::DispatchResultWithPostInfo, + pallet_prelude::*, + traits::{ + fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate}, + tokens::{AssetId, Balance}, + BuildGenesisConfig, Currency, Get, ReservableCurrency, UnixTime, + }, + transactional, PalletId, Twox64Concat, + }; + use frame_system::{ensure_signed, pallet_prelude::OriginFor}; + use sp_core::crypto::UncheckedFrom; + use sp_runtime::traits::{Convert, MaybeDisplay}; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Uploaded { code_hash: [u8; 32], code_id: CosmwasmCodeId }, + Instantiated { contract: AccountIdOf, info: ContractInfoOf }, + Executed { contract: AccountIdOf, entrypoint: EntryPoint, data: Option> }, + ExecutionFailed { contract: AccountIdOf, entrypoint: EntryPoint, error: Vec }, + Emitted { contract: AccountIdOf, ty: Vec, attributes: Vec<(Vec, Vec)> }, + Migrated { contract: AccountIdOf, to: CosmwasmCodeId }, + AdminUpdated { contract: AccountIdOf, new_admin: Option> }, + } + + #[pallet::error] + pub enum Error { + Instrumentation, + VmCreation, + ContractHasNoInfo, + CodeDecoding, + CodeValidation, + CodeEncoding, + CodeInstrumentation, + InstrumentedCodeIsTooBig, + CodeAlreadyExists, + CodeNotFound, + ContractAlreadyExists, + ContractNotFound, + SubstrateDispatch, + AssetConversion, + TransferFailed, + LabelTooBig, + UnknownDenom, + StackOverflow, + NotEnoughFundsForUpload, + NonceOverflow, + RefcountOverflow, + VMDepthOverflow, + SignatureVerificationError, + IteratorIdOverflow, + IteratorNotFound, + IteratorValueNotFound, + NotAuthorized, + NotImplemented, + Unsupported, + ExecuteDeserialize, + Ibc, + FailedToSerialize, + OutOfGas, + InvalidGasCheckpoint, + InvalidSalt, + InvalidAccount, + Interpreter, + VirtualMachine, + AccountConversionFailure, + Aborted, + ReadOnlyViolation, + Rpc, + Precompile, + QueryDeserialize, + ExecuteSerialize, + Xcm, + IncrementFailed, + } + + #[pallet::config] + pub trait Config: + frame_system::Config> + Send + Sync + Debug + { + /// Max number of frames a contract is able to push, a.k.a recursive calls. + const MAX_FRAMES: u8; + + #[pallet::event] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type AccountIdExtended: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen + + UncheckedFrom + + AsRef<[u8]>; + + /// Pallet unique ID. + #[pallet::constant] + type PalletId: Get; + + /// Current chain ID. Provided to the contract via the [`Env`]. + #[pallet::constant] + type ChainId: Get<&'static str>; + + /// Max accepted code size in bytes. + #[pallet::constant] + type MaxCodeSize: Get; + + /// Max code size after gas instrumentation. + #[pallet::constant] + type MaxInstrumentedCodeSize: Get; + + /// Max message size in bytes. + #[pallet::constant] + type MaxMessageSize: Get; + + /// Max contract label size. + #[pallet::constant] + type MaxContractLabelSize: Get; + + /// Max contract trie id size. + #[pallet::constant] + type MaxContractTrieIdSize: Get; + + /// Max instantiate salt. + #[pallet::constant] + type MaxInstantiateSaltSize: Get; + + /// Max assets in a [`FundsOf`] batch. + #[pallet::constant] + type MaxFundsAssets: Get; + + /// Max wasm table size. + #[pallet::constant] + type CodeTableSizeLimit: Get; + + /// Max wasm globals limit. + #[pallet::constant] + type CodeGlobalVariableLimit: Get; + + /// Max wasm functions parameters limit. + #[pallet::constant] + type CodeParameterLimit: Get; + + /// Max wasm branch table size limit. + #[pallet::constant] + type CodeBranchTableSizeLimit: Get; + + /// Max wasm stack size limit. + #[pallet::constant] + type CodeStackLimit: Get; + + /// Price of a byte when uploading new code. + /// The price is expressed in [`Self::NativeAsset`]. + /// This amount is reserved from the owner and released when the code is destroyed. + #[pallet::constant] + type CodeStorageByteDeposit: Get; + + /// Price of writing a byte in the storage. + #[pallet::constant] + type ContractStorageByteWritePrice: Get; + + /// Price of extracting a byte from the storage. + #[pallet::constant] + type ContractStorageByteReadPrice: Get; + + #[pallet::constant] + type WasmCostRules: Get>; + + /// A way to convert from our native account to cosmwasm `Addr`. + type AccountToAddr: Convert, String> + + Convert, ()>> + + Convert, Result, ()>>; + + /// Type of an account balance. + type Balance: Balance + Into; + + /// Type of a tradable asset id. + /// + /// The [`Ord`] constraint is required for [`BoundedBTreeMap`]. + type AssetId: AssetId + Ord; + + /// A way to convert from our native currency to cosmwasm `Denom`. + type AssetToDenom: Convert, String> + + Convert, ()>>; + + /// Interface used to pay when uploading code. + type NativeAsset: ReservableCurrency, Balance = BalanceOf> + + Currency>; + + /// Interface from which we are going to execute assets operations. + type Assets: FungiblesInspect< + AccountIdOf, + Balance = BalanceOf, + AssetId = AssetIdOf, + > + FungiblesMutate, Balance = BalanceOf, AssetId = AssetIdOf>; + + /// Source of time. + type UnixTime: UnixTime; + + /// Weight implementation. + type WeightInfo: WeightInfo; + + /// A hook into the VM execution semantic, allowing the runtime to hook into a contract + /// execution. + type PalletHook: PalletHook; + + /// Origin to upload a WASM code + type UploadWasmOrigin: EnsureOrigin; + + type ExecuteWasmOrigin: EnsureOrigin; + + type NativeDenom: Get<&'static str>; + + type NativeAssetId: Get; + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// A mapping from an original code id to the original code, untouched by instrumentation. + #[pallet::storage] + pub(crate) type PristineCode = + StorageMap<_, Twox64Concat, CosmwasmCodeId, BoundedVec>>; + + /// A mapping between an original code id and instrumented wasm code, ready for execution. + #[pallet::storage] + pub(crate) type InstrumentedCode = + StorageMap<_, Twox64Concat, CosmwasmCodeId, ContractInstrumentedCodeOf>; + + /// Monotonic counter incremented on code creation. + #[allow(clippy::disallowed_types)] + #[pallet::storage] + pub(crate) type CurrentCodeId = + StorageValue<_, CosmwasmCodeId, ValueQuery, Nonce>; + + /// A mapping between an original code hash and its metadata. + #[pallet::storage] + pub(crate) type CodeIdToInfo = + StorageMap<_, Twox64Concat, CosmwasmCodeId, CodeInfoOf>; + + /// A mapping between a code hash and it's unique ID. + #[pallet::storage] + pub(crate) type CodeHashToId = StorageMap<_, Identity, [u8; 32], CosmwasmCodeId>; + + /// This is a **monotonic** counter incremented on contract instantiation. + /// The purpose of this nonce is just to make sure that contract trie are unique. + #[allow(clippy::disallowed_types)] + #[pallet::storage] + pub(crate) type CurrentNonce = + StorageValue<_, u64, ValueQuery, Nonce>; + + /// A mapping between a contract's account id and it's metadata. + #[pallet::storage] + pub(crate) type ContractToInfo = + StorageMap<_, Identity, AccountIdOf, ContractInfoOf>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub contracts: Vec<(T::AccountIdExtended, ContractCodeOf)>, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { contracts: Default::default() } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for (who, code) in self.contracts.clone() { + >::do_upload(&who, code).expect("contracts in genesis are valid"); + } + } + } + + #[pallet::call] + impl Pallet { + /// Upload a CosmWasm contract. + /// The function will ensure that the wasm module is well formed and that it fits the + /// according limits. The module exports are going to be checked against the expected + /// CosmWasm export signatures. + /// + /// * Emits an `Uploaded` event on success. + /// + /// # Arguments + /// + /// - `origin` the original dispatching the extrinsic. + /// - `code` the actual wasm code. + #[transactional] + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::upload(code.len() as u32))] + pub fn upload(origin: OriginFor, code: ContractCodeOf) -> DispatchResult { + T::UploadWasmOrigin::ensure_origin(origin.clone())?; + let who = ensure_signed(origin)?; + Self::do_upload(&who, code)?; + + Ok(()) + } + + /// Instantiate a previously uploaded code resulting in a new contract being generated. + /// + /// * Emits an `Instantiated` event on success. + /// * Emits an `Executed` event. + /// * Possibly emit `Emitted` events. + /// + /// # Arguments + /// + /// * `origin` the origin dispatching the extrinsic. + /// * `code_identifier` the unique code id generated when the code has been uploaded via + /// [`upload`]. + /// * `salt` the salt, usually used to instantiate the same contract multiple times. + /// * `funds` the assets transferred to the contract prior to calling it's `instantiate` + /// export. + /// * `gas` the maximum gas to use, the remaining is refunded at the end of the transaction. + #[pallet::call_index(1)] + #[transactional] + // must depend on message too + #[pallet::weight(T::WeightInfo::instantiate(funds.len() as u32).saturating_add(Weight::from_parts(*gas, 0)))] + pub fn instantiate( + origin: OriginFor, + code_identifier: CodeIdentifier, + salt: ContractSaltOf, + admin: Option>, + label: ContractLabelOf, + funds: FundsOf, + gas: u64, + message: ContractMessageOf, + ) -> DispatchResultWithPostInfo { + T::ExecuteWasmOrigin::ensure_origin(origin.clone())?; + let who = ensure_signed(origin)?; + let mut shared = Self::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + let initial_gas = T::WeightInfo::instantiate(funds.len() as u32) + .saturating_add(Weight::from_parts(gas, 0)) + .ref_time(); + let outcome = Self::do_instantiate( + &mut shared, + who, + code_identifier, + salt, + admin, + label, + funds, + message, + ) + .map(|_| ()); + Self::refund_gas(outcome, initial_gas, shared.gas.remaining()) + } + + /// Execute a previously instantiated contract. + /// + /// * Emits an `Executed` event. + /// * Possibly emit `Emitted` events. + /// + /// # Arguments + /// + /// * `origin` the origin dispatching the extrinsic. + /// * `code_id` the unique code id generated when the code has been uploaded via [`upload`]. + /// * `salt` the salt, usually used to instantiate the same contract multiple times. + /// * `funds` the assets transferred to the contract prior to calling it's `instantiate` + /// export. + /// * `gas` the maximum gas to use, the remaining is refunded at the end of the transaction. + #[pallet::call_index(2)] + #[transactional] + #[pallet::weight(T::WeightInfo::execute(funds.len() as u32).saturating_add(Weight::from_parts(*gas, 0)))] + pub fn execute( + origin: OriginFor, + contract: AccountIdOf, + funds: FundsOf, + gas: u64, + message: ContractMessageOf, + ) -> DispatchResultWithPostInfo { + T::ExecuteWasmOrigin::ensure_origin(origin.clone())?; + let who = ensure_signed(origin)?; + let mut shared = Self::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + let initial_gas = T::WeightInfo::execute(funds.len() as u32) + .saturating_add(Weight::from_parts(gas, 0)) + .ref_time(); + let outcome = Self::do_execute(&mut shared, who, contract, funds, message); + Self::refund_gas(outcome, initial_gas, shared.gas.remaining()) + } + + /// Migrate a previously instantiated contract. + /// + /// * Emits a `Migrated` event on success. + /// * Emits an `Executed` event. + /// * Possibly emit `Emitted` events. + /// + /// # Arguments + /// + /// * `origin` the origin dispatching the extrinsic. + /// * `contract` the address of the contract that we want to migrate + /// * `new_code_identifier` the code identifier that we want to switch to. + /// * `gas` the maximum gas to use, the remaining is refunded at the end of the transaction. + /// * `message` MigrateMsg, that will be passed to the contract. + #[pallet::call_index(3)] + #[transactional] + #[pallet::weight(T::WeightInfo::migrate().saturating_add(Weight::from_parts(*gas, 0)))] + pub fn migrate( + origin: OriginFor, + contract: AccountIdOf, + new_code_identifier: CodeIdentifier, + gas: u64, + message: ContractMessageOf, + ) -> DispatchResultWithPostInfo { + T::ExecuteWasmOrigin::ensure_origin(origin.clone())?; + let who = ensure_signed(origin)?; + let mut shared = Self::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + let initial_gas = + T::WeightInfo::migrate().saturating_add(Weight::from_parts(gas, 0)).ref_time(); + let outcome = + Self::do_migrate(&mut shared, who, contract, new_code_identifier, message); + Self::refund_gas(outcome, initial_gas, shared.gas.remaining()) + } + + /// Update the admin of a contract. + /// + /// * Emits a `AdminUpdated` event on success. + /// + /// # Arguments + /// + /// * `origin` the origin dispatching the extrinsic. + /// * `contract` the address of the contract that we want to migrate. + /// * `new_admin` new admin of the contract that we want to update to. + /// * `gas` the maximum gas to use, the remaining is refunded at the end of the transaction. + #[pallet::call_index(4)] + #[transactional] + #[pallet::weight(T::WeightInfo::update_admin().saturating_add(Weight::from_parts(*gas, 0)))] + pub fn update_admin( + origin: OriginFor, + contract: AccountIdOf, + new_admin: Option>, + gas: u64, + ) -> DispatchResultWithPostInfo { + T::ExecuteWasmOrigin::ensure_origin(origin.clone())?; + let who = ensure_signed(origin)?; + let mut shared = Self::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + let initial_gas = T::WeightInfo::update_admin() + .saturating_add(Weight::from_parts(gas, 0)) + .ref_time(); + let outcome = + Self::do_update_admin(&mut shared, who, contract.clone(), new_admin.clone()); + Self::deposit_event(Event::::AdminUpdated { contract, new_admin }); + Self::refund_gas(outcome, initial_gas, shared.gas.remaining()) + } + } +} + +/// Query cosmwasm contracts +/// +/// * `contract` the address of contract to query. +/// * `gas` the maximum gas to use, the remaining is refunded at the end of the transaction. +/// * `query_request` the binary query, which should be deserializable to `QueryRequest`. +pub fn query( + contract: AccountIdOf, + gas: u64, + query_request: Vec, +) -> Result> { + let mut shared = Pallet::::do_create_vm_shared(gas, InitialStorageMutability::ReadOnly); + let query_request = serde_json::from_slice(&query_request) + .map_err(|e| CosmwasmVMError::::Rpc(e.to_string()))?; + Pallet::::sub_level_dispatch( + &mut shared, + contract.clone(), + contract, + Default::default(), + |mut vm| { + cosmwasm_system_query(&mut vm, query_request)? + .into_result() + .map_err(|e| CosmwasmVMError::::Rpc(format!("{:?}", e)))? + .into_result() + .map_err(|e| CosmwasmVMError::::Rpc(e)) + }, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn instantiate( + instantiator: AccountIdOf, + code_id: CosmwasmCodeId, + salt: Vec, + admin: Option>, + label: Vec, + funds: BTreeMap, (BalanceOf, KeepAlive)>, + gas: u64, + message: Vec, +) -> Result, CosmwasmVMError> { + let salt: ContractSaltOf = salt + .try_into() + .map_err(|_| CosmwasmVMError::::Rpc(String::from("'salt' is too large")))?; + let label: ContractLabelOf = label + .try_into() + .map_err(|_| CosmwasmVMError::::Rpc(String::from("'label' is too large")))?; + let funds: FundsOf = funds + .try_into() + .map_err(|_| CosmwasmVMError::::Rpc(String::from("'funds' is too large")))?; + let message: ContractMessageOf = message + .try_into() + .map_err(|_| CosmwasmVMError::::Rpc(String::from("'message' is too large")))?; + let mut shared = Pallet::::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + setup_instantiate_call(instantiator, code_id, &salt, admin, label)?.top_level_call( + &mut shared, + funds, + message, + ) +} + +pub fn execute( + executor: AccountIdOf, + contract: AccountIdOf, + funds: FundsOf, + gas: u64, + message: ContractMessageOf, +) -> Result<(), CosmwasmVMError> { + let mut shared = Pallet::::do_create_vm_shared(gas, InitialStorageMutability::ReadWrite); + setup_execute_call(executor, contract)?.top_level_call(&mut shared, funds, message) +} + +impl VMPallet for T { + type VmError = CosmwasmVMError; +} + +impl Pallet { + /// Create the shared VM state. Including readonly stack, VM depth, gas metering limits and + /// code cache. + /// + /// This state is shared across all VMs (all contracts loaded within a single call) and is + /// used to optimize some operations as well as track shared state (readonly storage while + /// doing a `query` etc...) + pub fn do_create_vm_shared( + gas: u64, + storage_mutability: InitialStorageMutability, + ) -> CosmwasmVMShared { + CosmwasmVMShared { + storage_readonly_depth: match storage_mutability { + InitialStorageMutability::ReadOnly => 1, + InitialStorageMutability::ReadWrite => 0, + }, + depth: 0, + gas: Gas::new(T::MAX_FRAMES, gas), + cache: CosmwasmVMCache { code: Default::default() }, + } + } + + /// Wrapper around [`Pallet::::cosmwasm_call`] for extrinsics. + /// It's purpose is converting the input and emit events based on the result. + pub(crate) fn top_level_dispatch( + shared: &mut CosmwasmVMShared, + entrypoint: EntryPoint, + sender: AccountIdOf, + contract: AccountIdOf, + funds: FundsOf, + call: F, + ) -> Result<(), CosmwasmVMError> + where + F: for<'x> FnOnce( + OwnedWasmiVM>, + ) -> Result< + (Option, Vec), + CosmwasmVMError, + >, + { + let cosmwasm_funds = funds + .into_iter() + .map(|(asset, (amount, _))| Self::native_asset_to_cosmwasm_asset(asset, amount)) + .collect::>(); + + Self::sub_level_dispatch(shared, sender, contract.clone(), cosmwasm_funds, call).map( + |(data, events)| { + for CosmwasmEvent { ty, attributes, .. } in events { + Self::deposit_event(Event::::Emitted { + contract: contract.clone(), + ty: ty.into(), + attributes: attributes + .into_iter() + .map(|CosmwasmEventAttribute { key, value }| (key.into(), value.into())) + .collect::>(), + }); + } + Self::deposit_event(Event::::Executed { + contract, + entrypoint, + data: data.map(Into::into), + }); + }, + ) + } + + /// Low-level cosmwasm call over the VM. Transfers the `funds` before calling the callback. + pub(crate) fn sub_level_dispatch( + shared: &mut CosmwasmVMShared, + sender: AccountIdOf, + contract: AccountIdOf, + funds: Vec, + call: F, + ) -> Result> + where + F: for<'x> FnOnce(OwnedWasmiVM>) -> Result>, + { + let vm = Self::cosmwasm_new_vm(shared, sender, contract, funds)?; + call(vm) + } + + /// Refund the remaining gas regardless of a contract outcome. + pub(crate) fn refund_gas( + outcome: Result<(), CosmwasmVMError>, + initial_gas: u64, + remaining_gas: u64, + ) -> DispatchResultWithPostInfo { + log::debug!(target: "runtime::contracts", "outcome: {:?}", outcome); + let post_info = PostDispatchInfo { + actual_weight: Some(Weight::from_parts(initial_gas.saturating_sub(remaining_gas), 0)), + pays_fee: Pays::Yes, + }; + match outcome { + Ok(()) => Ok(post_info), + Err(error) => { + log::info!(target: "runtime::contracts", "executing contract error with {}", &error); + let error = match error { + CosmwasmVMError::Pallet(e) => e, + CosmwasmVMError::InvalidGasCheckpoint => Error::::OutOfGas, + CosmwasmVMError::OutOfGas => Error::::OutOfGas, + CosmwasmVMError::Interpreter(_) => Error::::Interpreter, + CosmwasmVMError::VirtualMachine(_) => Error::::VirtualMachine, + CosmwasmVMError::AccountConvert => Error::::AccountConversionFailure, + CosmwasmVMError::Aborted(_) => Error::::Aborted, + CosmwasmVMError::ReadOnlyViolation => Error::::ReadOnlyViolation, + CosmwasmVMError::Unsupported => Error::::Unsupported, + CosmwasmVMError::ExecuteDeserialize => Error::::ExecuteDeserialize, + CosmwasmVMError::ContractNotFound => Error::::ContractNotFound, + CosmwasmVMError::Rpc(_) => Error::::Rpc, + CosmwasmVMError::Ibc(_) => Error::::Ibc, + CosmwasmVMError::SubstrateDispatch(_) => Error::::SubstrateDispatch, + CosmwasmVMError::AssetConversion => Error::::AssetConversion, + CosmwasmVMError::QuerySerialize => Error::::FailedToSerialize, + CosmwasmVMError::Precompile => Error::::Precompile, + CosmwasmVMError::QueryDeserialize => Error::::QueryDeserialize, + CosmwasmVMError::ExecuteSerialize => Error::::ExecuteSerialize, + CosmwasmVMError::NotImplemented => Error::::NotAuthorized, + CosmwasmVMError::Xcm(_) => Error::::Xcm, + }; + Err(DispatchErrorWithPostInfo { error: error.into(), post_info }) + }, + } + } + + /// Set the contract info and update the state accordingly. + /// + /// This function will update the state if the `code_id` is changing: + /// 1. Refcount of the new `code_id` is incremented. + /// 2. Refcount of the old `code_id` is decremented. + /// 3. Delete every entry related to old `code_id` if the refcount is 0. And unreserve the + /// bonded funds. + pub(crate) fn do_set_contract_meta( + contract: &AccountIdOf, + code_id: CosmwasmCodeId, + admin: Option>, + label: String, + ) -> Result<(), Error> { + let mut info = Self::contract_info(contract)?; + + if info.code_id != code_id { + // Increase the refcount of `new_code_id`. + CodeIdToInfo::::try_mutate_exists(code_id, |entry| -> Result<(), Error> { + let code_info = entry.as_mut().ok_or(Error::::CodeNotFound)?; + code_info.refcount = + code_info.refcount.checked_add(1).ok_or(Error::::RefcountOverflow)?; + Ok(()) + })?; + + // Modify the existing `code_id`'s states and unreserve the bonded funds. + CodeIdToInfo::::try_mutate_exists(info.code_id, |entry| -> Result<(), Error> { + // Decrement the refcount + let code_info = entry.as_mut().ok_or(Error::::CodeNotFound)?; + code_info.refcount = + code_info.refcount.checked_sub(1).ok_or(Error::::RefcountOverflow)?; + if code_info.refcount == 0 { + // Unreserve the bonded funds for this code + let code = PristineCode::::try_get(info.code_id) + .map_err(|_| Error::::CodeNotFound)?; + let deposit = code.len().saturating_mul(T::CodeStorageByteDeposit::get() as _); + let _ = T::NativeAsset::unreserve(&code_info.creator, deposit.saturated_into()); + PristineCode::::remove(info.code_id); + InstrumentedCode::::remove(info.code_id); + CodeHashToId::::remove(code_info.pristine_code_hash); + // Code is unused after this point, so it can be removed + *entry = None; + } + Ok(()) + })?; + } + + info.code_id = code_id; + info.admin = admin; + info.label = label + .as_bytes() + .to_vec() + .try_into() + .map_err(|_| crate::Error::::LabelTooBig)?; + + Self::set_contract_info(contract, info); + Ok(()) + } + + /// Ensure that a contract exists. + pub(crate) fn contract_exists(contract: &AccountIdOf) -> Result<(), Error> { + match T::PalletHook::info(contract) { + Some(_) => Ok(()), + None if ContractToInfo::::contains_key(contract) => Ok(()), + _ => Err(Error::::ContractNotFound), + } + } + + /// Validate a wasm module against the defined limitations. + /// + /// Notably + /// + /// - whether it is well formed and follow the spec. + /// - memory limit. + /// - table size limit. + /// - global variables limit. + /// - function parameters limit. + /// - branch table limit. + /// - ensuring no floating type are used. + /// - ensuring mandatory exports are present and that their signature matches. + /// - ensuring that forbidden imports are not imported. + pub(crate) fn do_validate_code(module: &parity_wasm::elements::Module) -> Result<(), Error> { + let validation: Result<(), ValidationError> = (|| { + let _ = CodeValidation::new(module) + .validate_module::(())? + .validate_memory_limit()? + .validate_table_size_limit(T::CodeTableSizeLimit::get())? + .validate_global_variable_limit(T::CodeGlobalVariableLimit::get())? + .validate_parameter_limit(T::CodeParameterLimit::get())? + .validate_br_table_size_limit(T::CodeBranchTableSizeLimit::get())? + .validate_no_floating_types()? + .validate_exports(Version1x::EXPORTS)? + // env.gas is banned as injected by instrumentation + .validate_imports(&[(Version1x::ENV_MODULE, Version1x::ENV_GAS)])?; + Ok(()) + })(); + validation.map_err(|e| { + log::debug!(target: "runtime::contracts", "do_validate_code: {:#?}", e); + Error::::CodeValidation + }) + } + + /// Instrument the wasm module by injecting stack height limitation along gas metering. + pub(crate) fn do_instrument_code( + module: parity_wasm::elements::Module, + ) -> Result, Error> { + Self::do_validate_code(&module)?; + let instrumented_module = gas_and_stack_instrumentation( + module, + Version1x::ENV_MODULE, + T::CodeStackLimit::get(), + &T::WasmCostRules::get(), + ); + instrumented_module + .map_err(|e| { + log::debug!(target: "runtime::contracts", "do_instrument_code: {:#?}", e); + Error::::CodeInstrumentation + })? + .into_bytes() + .map_err(|e| { + log::debug!(target: "runtime::contracts", "do_instrument_code: {:#?}", e); + Error::::CodeEncoding + })? + .try_into() + .map_err(|_| Error::::InstrumentedCodeIsTooBig) + } + + /// Check whether the instrumented code of a contract is up to date. + /// If the instrumentation is outdated, re-instrument the pristine code. + pub(crate) fn do_check_for_reinstrumentation( + code_id: CosmwasmCodeId, + ) -> Result>, Error> { + CodeIdToInfo::::try_mutate(code_id, |entry| { + let code_info = entry.as_mut().ok_or(Error::::CodeNotFound)?; + if code_info.instrumentation_version != INSTRUMENTATION_VERSION { + log::debug!(target: "runtime::contracts", "do_check_for_reinstrumentation: required"); + let code = PristineCode::::get(code_id).ok_or(Error::::CodeNotFound)?; + let module = Self::do_load_module(&code)?; + let instrumented_code = Self::do_instrument_code(module)?; + InstrumentedCode::::insert(code_id, instrumented_code.clone()); + code_info.instrumentation_version = INSTRUMENTATION_VERSION; + Ok(Some(instrumented_code.into())) + } else { + log::debug!(target: "runtime::contracts", "do_check_for_reinstrumentation: not required"); + Ok(None) + } + }) + } + + pub(crate) fn do_load_module( + code: &ContractCodeOf, + ) -> Result> { + parity_wasm::elements::Module::from_bytes(code).map_err(|e| { + log::debug!(target: "runtime::contracts", "do_load_module: {:#?}", e); + Error::::CodeDecoding + }) + } + + /// Extract the current environment from the pallet. + pub(crate) fn cosmwasm_env(cosmwasm_contract_address: CosmwasmAccount) -> Env { + Env { + block: Self::block_env(), + transaction: frame_system::Pallet::::extrinsic_index() + .map(|index| TransactionInfo { index }), + contract: CosmwasmContractInfo { + address: Addr::unchecked(String::from(cosmwasm_contract_address)), + }, + } + } + + pub fn do_upload( + who: &AccountIdOf, + code: ContractCodeOf, + ) -> Result<(H256, u64), Error> { + let code_hash = sp_io::hashing::sha2_256(&code); + ensure!(!CodeHashToId::::contains_key(code_hash), Error::::CodeAlreadyExists); + let deposit = code.len().saturating_mul(T::CodeStorageByteDeposit::get() as _); + T::NativeAsset::reserve(who, deposit.saturated_into()) + .map_err(|_| Error::::NotEnoughFundsForUpload)?; + let module = Self::do_load_module(&code)?; + let ibc_capable = Self::do_check_ibc_capability(&module); + let instrumented_code = Self::do_instrument_code(module)?; + let code_id = CurrentCodeId::::increment().map_err(|_| Error::::IncrementFailed)?; + CodeHashToId::::insert(code_hash, code_id); + PristineCode::::insert(code_id, code); + InstrumentedCode::::insert(code_id, instrumented_code); + CodeIdToInfo::::insert( + code_id, + CodeInfoOf:: { + creator: who.clone(), + pristine_code_hash: code_hash, + instrumentation_version: INSTRUMENTATION_VERSION, + ibc_capable, + refcount: 0, + }, + ); + Self::deposit_event(Event::::Uploaded { code_hash, code_id }); + Ok((H256::from(code_hash), code_id)) + } + + #[allow(clippy::too_many_arguments)] + pub fn do_instantiate( + shared: &mut CosmwasmVMShared, + who: AccountIdOf, + code_identifier: CodeIdentifier, + salt: ContractSaltOf, + admin: Option>, + label: ContractLabelOf, + funds: FundsOf, + message: ContractMessageOf, + ) -> Result, CosmwasmVMError> { + let code_id = match code_identifier { + CodeIdentifier::CodeId(code_id) => code_id, + CodeIdentifier::CodeHash(code_hash) => + CodeHashToId::::try_get(code_hash).map_err(|_| Error::::CodeNotFound)?, + }; + setup_instantiate_call(who, code_id, &salt, admin, label)? + .top_level_call(shared, funds, message) + } + + pub fn do_execute( + shared: &mut CosmwasmVMShared, + who: AccountIdOf, + contract: AccountIdOf, + funds: FundsOf, + message: ContractMessageOf, + ) -> Result<(), CosmwasmVMError> { + setup_execute_call(who, contract)?.top_level_call(shared, funds, message) + } + + pub fn do_migrate( + shared: &mut CosmwasmVMShared, + who: AccountIdOf, + contract: AccountIdOf, + new_code_identifier: CodeIdentifier, + message: ContractMessageOf, + ) -> Result<(), CosmwasmVMError> { + let new_code_id = match new_code_identifier { + CodeIdentifier::CodeId(code_id) => code_id, + CodeIdentifier::CodeHash(code_hash) => + CodeHashToId::::try_get(code_hash).map_err(|_| Error::::CodeNotFound)?, + }; + + setup_migrate_call(shared, who, contract, new_code_id, true)?.top_level_call( + shared, + Default::default(), + message, + ) + } + + pub fn do_update_admin( + shared: &mut CosmwasmVMShared, + who: AccountIdOf, + contract: AccountIdOf, + new_admin: Option>, + ) -> Result<(), CosmwasmVMError> { + Self::sub_level_dispatch( + shared, + who.clone(), + contract.clone(), + Default::default(), + |mut vm| { + cosmwasm_vm::system::update_admin( + &mut vm, + &Addr::unchecked(Self::account_to_cosmwasm_addr(who)), + CosmwasmAccount::new(contract), + new_admin.map(CosmwasmAccount::new), + ) + .map_err(Into::into) + }, + ) + } + + fn block_env() -> BlockInfo { + BlockInfo { + height: frame_system::Pallet::::block_number().saturated_into(), + time: Timestamp::from_seconds(T::UnixTime::now().as_secs()), + chain_id: T::ChainId::get().into(), + } + } + + /// Create a new CosmWasm VM. One instance is created per contract but all of them share the + /// same [`CosmwasmVMShared<'a, T>`] structure. If the [`contract`] is a PalletHook, the + /// CosmwasmVM with a [`ContractBackend::Pallet`] is returned instead of + /// [`ContractBackend::CosmWasm`] + /// + /// Prior to instantiating the VM. The depth is checked against [`T::MaxFrames`] and the + /// contract code is loaded from the shared state if cached. If the code is not in cache, we + /// check whether reinstrumentation is required and cache the code. + pub(crate) fn cosmwasm_new_vm( + shared: &mut CosmwasmVMShared, + sender: AccountIdOf, + contract: AccountIdOf, + funds: Vec, + ) -> Result>, CosmwasmVMError> { + shared.depth = shared.depth.checked_add(1).ok_or(Error::::VMDepthOverflow)?; + ensure!(shared.depth <= T::MAX_FRAMES, Error::::StackOverflow); + + let contract_address: CosmwasmAccount = CosmwasmAccount::new(contract.clone()); + let env = Self::cosmwasm_env(contract_address.clone()); + let cosmwasm_message_info = { + let cosmwasm_sender_address: CosmwasmAccount = CosmwasmAccount::new(sender); + MessageInfo { sender: Addr::unchecked(String::from(cosmwasm_sender_address)), funds } + }; + + // If the [`contract`] is actually a pallet that is exposed as a cosmwasm contract, + // then we use the pallet instead of setting up a ContractBackend::CosmWasm + if let Some(precompiled_info) = T::PalletHook::info(&contract) { + let engine = wasmi::Engine::default(); + let store = wasmi::Store::new( + &engine, + CosmwasmVM { + cosmwasm_env: env, + cosmwasm_message_info, + contract_address, + contract_info: precompiled_info.contract, + shared, + iterators: Default::default(), + contract_runtime: ContractBackend::Pallet { call_depth_mut: 0 }, + }, + ); + + return Ok(OwnedWasmiVM::new(store)); + } + + // Else, the contract is not a pallet. We continue with the normal wasmi vm creation + // process: + let info = Self::contract_info(&contract)?; + let code = Self::get_code_from_cache(shared, info.code_id)?.clone(); + + log::debug!(target: "runtime::contracts", "env : {:#?}", env); + log::debug!(target: "runtime::contracts", "info : {:#?}", cosmwasm_message_info); + + let vm = CosmwasmVM { + cosmwasm_env: env, + cosmwasm_message_info, + contract_address, + contract_info: info, + shared, + iterators: Default::default(), + contract_runtime: ContractBackend::CosmWasm { + executing_module: None, + call_depth_mut: 0, + }, + }; + + let wasmi_vm = new_wasmi_vm(code.as_slice(), vm).map_err(|_| Error::::VmCreation)?; + Ok(wasmi_vm) + } + + /// Get the code for `code_id` from the `shared_vm`'s code cache. If the code isn't in the cache + /// yet, then we reinstrument the code if needed and insert it into the cache. + /// + /// Note that it may look like we could have an invalid cache if reinstrumentation just + /// happend, but the `shared.cache.code` is only shared for a single tx, so it is + /// impossible for non-reinstrumented code to be in the `shared.code.cache` + pub(crate) fn get_code_from_cache( + shared_vm: &mut CosmwasmVMShared, + code_id: CosmwasmCodeId, + ) -> Result<&mut Vec, CosmwasmVMError> { + log::debug!(target: "runtime::contracts", "Getting code for: {:?} from shared cache", code_id); + match shared_vm.cache.code.entry(code_id) { + Entry::Vacant(v) => { + log::debug!(target: "runtime::contracts", "Code cache miss: {:?}", code_id); + // Reinstrument the code if needed + let code = Self::do_check_for_reinstrumentation(code_id)?.map_or_else( + || { + // map_or_else instead of map_or to prevent the code from getting loaded + Ok(InstrumentedCode::::get(code_id) + .ok_or(Error::::CodeNotFound)? + .into_inner()) + }, + Ok::<_, Error>, + )?; + Ok(v.insert(code)) + }, + Entry::Occupied(o) => { + log::debug!(target: "runtime::contracts", "Code cache hit: {:?}", code_id); + Ok(o.into_mut()) + }, + } + } + + /// Abstract function to operate on a contract child trie entry. + pub(crate) fn with_db_entry( + trie_id: &ContractTrieIdOf, + key: &[u8], + f: impl FnOnce(ChildInfo, Vec) -> R, + ) -> R { + let child_trie = Self::contract_child_trie(trie_id.as_ref()); + f(child_trie, Blake2_128Concat::hash(key)) + } + + /// Compute the gas required to read the given entry. + /// + /// Equation: len(entry(trie, key)) x [`T::ContractStorageByteReadPrice`] + pub(crate) fn do_db_read_gas(trie_id: &ContractTrieIdOf, key: &[u8]) -> u64 { + Self::with_db_entry(trie_id, key, |child_trie, entry| { + let bytes_to_read = storage::child::len(&child_trie, &entry).unwrap_or(0); + u64::from(bytes_to_read).saturating_mul(T::ContractStorageByteReadPrice::get().into()) + }) + } + + /// Read an entry from the executing contract storage, charging the according gas prior to + /// actually reading the entry. + pub(crate) fn do_db_read( + vm: &mut DefaultCosmwasmVM, + key: &[u8], + ) -> Result>, CosmwasmVMError> { + let price = Self::do_db_read_gas(&vm.contract_info.trie_id, key); + vm.charge_raw(price)?; + Ok(Self::with_db_entry(&vm.contract_info.trie_id, key, |child_trie, entry| { + storage::child::get_raw(&child_trie, &entry) + })) + } + + /// Read an entry from an arbitrary contract, charging the according gas prior to actually + /// reading the entry. + pub(crate) fn do_db_read_other_contract( + vm: &mut DefaultCosmwasmVM, + trie_id: &ContractTrieIdOf, + key: &[u8], + ) -> Result>, CosmwasmVMError> { + let price = Self::do_db_read_gas(trie_id, key); + vm.charge_raw(price)?; + Ok(Self::with_db_entry(trie_id, key, |child_trie, entry| { + storage::child::get_raw(&child_trie, &entry) + })) + } + + /// Compute the gas required to overwrite the given entry. + /// + /// Equation: len(entry(trie, key)) - len(value) x [`T::ContractStorageByteWritePrice`] + /// With minus saturating. + pub(crate) fn do_db_write_gas(trie_id: &ContractTrieIdOf, key: &[u8], value: &[u8]) -> u64 { + Self::with_db_entry(trie_id, key, |child_trie, entry| { + let bytes_to_write = match storage::child::len(&child_trie, &entry) { + Some(current_len) => (value.len() as u32).saturating_sub(current_len), + None => value.len() as u32, + }; + u64::from(bytes_to_write).saturating_mul(T::ContractStorageByteWritePrice::get().into()) + }) + } + + /// Write an entry from the executing contract, charging the according gas prior to actually + /// writing the entry. + pub(crate) fn do_db_write( + vm: &mut DefaultCosmwasmVM, + key: &[u8], + value: &[u8], + ) -> Result<(), CosmwasmVMError> { + let price = Self::do_db_write_gas(&vm.contract_info.trie_id, key, value); + vm.charge_raw(price)?; + Self::with_db_entry(&vm.contract_info.trie_id, key, |child_trie, entry| { + storage::child::put_raw(&child_trie, &entry, value) + }); + Ok(()) + } + + /// Create an empty iterator. + pub(crate) fn do_db_scan(vm: &mut DefaultCosmwasmVM) -> Result> { + let iterator_id = vm.iterators.len() as u32; + // let child_info = Self::contract_child_trie(vm.contract_info.trie_id.as_ref()); + vm.iterators.insert(iterator_id, Vec::new()); + Ok(iterator_id) + } + + /// Return the next (reversed-key, value) pair and save the state. If the next key + /// is `None`, the iterator is removed from the storage. + pub(crate) fn do_db_next( + vm: &mut DefaultCosmwasmVM, + iterator_id: u32, + ) -> Result, Vec)>, CosmwasmVMError> { + // Get the next value from the vm's iterators, based on the iterator id. + // Error if the iterator with id [`iterator_id`] + let child_info = Self::contract_child_trie(vm.contract_info.trie_id.as_ref()); + let key_entry = vm.iterators.get_mut(&iterator_id).ok_or(Error::::IteratorNotFound)?; + match sp_io::default_child_storage::next_key(child_info.storage_key(), key_entry) { + Some(key) => { + let reversed_key = Blake2_128Concat::reverse(&key).to_vec(); + *key_entry = key; + Ok(Some(( + reversed_key.clone(), + Self::do_db_read(vm, reversed_key.as_slice())? + .ok_or(Error::::IteratorValueNotFound)?, + ))) + }, + None => Ok(None), + } + } + + /// Remove an entry from the executing contract, no gas is charged for this operation. + pub(crate) fn do_db_remove(vm: &mut DefaultCosmwasmVM, key: &[u8]) { + let trie_id = &vm.contract_info.trie_id; + Self::with_db_entry(trie_id, key, |child_trie, entry| { + storage::child::kill(&child_trie, &entry) + }) + } + + pub(crate) fn do_running_contract_meta( + vm: &mut DefaultCosmwasmVM, + ) -> CosmwasmContractMeta> { + CosmwasmContractMeta { + code_id: vm.contract_info.code_id, + admin: vm.contract_info.admin.clone().map(CosmwasmAccount::new), + label: String::from_utf8_lossy(&vm.contract_info.label).into(), + } + } + + /// Retrieve an account balance. + pub(crate) fn do_balance(account: &AccountIdOf, denom: String) -> Result> { + let asset = Self::cosmwasm_asset_to_native_asset(denom)?; + if asset == T::NativeAssetId::get() { + Ok(T::NativeAsset::free_balance(account).into()) + } else { + Ok(T::Assets::balance(asset, account).into()) + } + } + + pub(crate) fn do_supply(denom: String) -> Result> { + if denom == T::NativeDenom::get() { + Ok(T::NativeAsset::total_issuance().into()) + } else { + let asset = Self::cosmwasm_asset_to_native_asset(denom)?; + Ok(T::Assets::total_issuance(asset).into()) + } + } + + /// Execute a transfer of funds between two accounts. + pub(crate) fn do_transfer( + from: &AccountIdOf, + to: &AccountIdOf, + funds: &[Coin], + preservation: Preservation, + ) -> Result<(), Error> { + for Coin { denom, amount } in funds { + let amount = amount.u128().saturated_into(); + + if denom == T::NativeDenom::get() { + T::NativeAsset::transfer(from, to, amount, ExistenceRequirement::KeepAlive) + .map_err(|_| Error::::TransferFailed)?; + } else { + let asset = Self::cosmwasm_asset_to_native_asset(denom.clone())?; + T::Assets::transfer(asset, from, to, amount, preservation) + .map_err(|_| Error::::TransferFailed)?; + } + } + Ok(()) + } + pub(crate) fn do_continue_instantiate( + vm: &mut DefaultCosmwasmVM, + CosmwasmContractMeta { code_id, admin, label }: CosmwasmContractMeta>, + funds: Vec, + salt: &[u8], + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, CosmwasmVMError> { + let label = label + .as_bytes() + .to_vec() + .try_into() + .map_err(|_| crate::Error::::LabelTooBig)?; + setup_instantiate_call( + vm.contract_address.clone().into_inner(), + code_id, + salt, + admin.map(|admin| admin.into_inner()), + label, + )? + .sub_call(vm.shared, funds, message, event_handler) + } + + pub(crate) fn do_continue_execute( + vm: &mut DefaultCosmwasmVM, + contract: AccountIdOf, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, CosmwasmVMError> { + setup_execute_call(vm.contract_address.clone().into_inner(), contract)?.sub_call( + vm.shared, + funds, + message, + event_handler, + ) + } + + pub(crate) fn do_continue_reply( + vm: &mut DefaultCosmwasmVM, + reply: cosmwasm_std::Reply, + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, CosmwasmVMError> { + setup_reply_call( + vm.contract_address.clone().into_inner(), + vm.contract_address.clone().into_inner(), + )? + .sub_call( + vm.shared, + Vec::default(), + &serde_json::to_vec(&reply).map_err(|_| Error::::FailedToSerialize)?, + event_handler, + ) + } + + pub(crate) fn do_continue_migrate( + vm: &mut DefaultCosmwasmVM, + contract: AccountIdOf, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, CosmwasmVMError> { + let CosmwasmContractMeta { code_id, .. } = Self::do_running_contract_meta(vm); + setup_migrate_call( + vm.shared, + vm.contract_address.clone().into_inner(), + contract, + code_id, + false, + )? + .sub_call(vm.shared, Default::default(), message, event_handler) + } + + pub(crate) fn do_query_contract_info( + vm: &mut DefaultCosmwasmVM, + address: AccountIdOf, + ) -> Result> { + // TODO: cache or at least check if its current contract and use `self.contract_info` + let (contract_info, code_info, pinned) = match T::PalletHook::info(&address) { + Some(precompiled_info) => (precompiled_info.contract, precompiled_info.code, true), + None => { + let contract_info = if &address == vm.contract_address.as_ref() { + vm.contract_info.clone() + } else { + Pallet::::contract_info(&address)? + }; + let code_info = CodeIdToInfo::::get(contract_info.code_id) + .ok_or(Error::::CodeNotFound)?; + let pinned = vm.shared.cache.code.contains_key(&contract_info.code_id); + (contract_info, code_info, pinned) + }, + }; + let ibc_port = if code_info.ibc_capable { + Some(Pallet::::do_compute_ibc_contract_port(address)) + } else { + None + }; + + let creator = CosmwasmAccount::::new(contract_info.instantiator.clone()); + Ok(ContractInfoResponse::new( + contract_info.code_id, + creator.into(), + contract_info.admin.map(|admin| CosmwasmAccount::::new(admin).into()), + pinned, + ibc_port, + )) + } + + pub(crate) fn do_query_code_info(code_id: u64) -> Result> { + let code_info = CodeIdToInfo::::get(code_id).ok_or(Error::::CodeNotFound)?; + Ok(CodeInfoResponse::new( + code_id, + Addr::unchecked(Pallet::::account_to_cosmwasm_addr(code_info.creator)), + code_info.pristine_code_hash.into(), + )) + } + + pub(crate) fn do_continue_query( + vm: &mut DefaultCosmwasmVM, + contract: AccountIdOf, + message: &[u8], + ) -> Result> { + let sender = vm.contract_address.clone().into_inner(); + vm.shared.push_readonly(); + let result = Pallet::::sub_level_dispatch( + vm.shared, + sender, + contract, + Default::default(), + |mut vm| match vm.0.as_context().data().contract_runtime { + ContractBackend::CosmWasm { .. } => + cosmwasm_call::>>(&mut vm, message), + ContractBackend::Pallet { .. } => + T::PalletHook::query(&mut vm, message).map(Into::into), + }, + ); + vm.shared.pop_readonly(); + result + } + + pub(crate) fn do_query_raw( + vm: &mut DefaultCosmwasmVM, + address: AccountIdOf, + key: &[u8], + ) -> Result>, CosmwasmVMError> { + // TODO(hussein-aitlahcen): allow to raw query precompiled contracts? + let info = Pallet::::contract_info(&address)?; + Pallet::::do_db_read_other_contract(vm, &info.trie_id, key) + } +} diff --git a/frame/cosmwasm/src/pallet_hook.rs b/frame/cosmwasm/src/pallet_hook.rs new file mode 100644 index 00000000..ef102ff3 --- /dev/null +++ b/frame/cosmwasm/src/pallet_hook.rs @@ -0,0 +1,107 @@ +use crate::{prelude::*, runtimes::vm::CosmwasmVM, types::*, Config, Error}; +use cosmwasm_std::{ContractResult, QueryResponse, Response}; +use cosmwasm_vm::vm::{VMBase, VmErrorOf}; +use cosmwasm_vm_wasmi::OwnedWasmiVM; + +/// A hook for pallets into the VM. Used to call substrate pallets from a CosmWasm contract. +pub trait PalletHook { + /// Return hardcoded contract informations for a precompiled contract. + fn info( + contract_address: &AccountIdOf, + ) -> Option, ContractLabelOf, ContractTrieIdOf>>; + + /// Hook into a contract call. + fn execute<'a>( + vm: &mut OwnedWasmiVM>, + entrypoint: EntryPoint, + message: &[u8], + ) -> Result< + ContractResult> as VMBase>::MessageCustom>>, + VmErrorOf>>, + >; + + fn run<'a>( + vm: &mut OwnedWasmiVM>, + entrypoint: EntryPoint, + message: &[u8], + ) -> Result, VmErrorOf>>>; + + /// Hook into a contract query. + fn query<'a>( + vm: &mut OwnedWasmiVM>, + message: &[u8], + ) -> Result, VmErrorOf>>>; +} + +/// Default implementation, acting as identity (unhooked). +impl PalletHook for () { + fn info( + _: &AccountIdOf, + ) -> Option, ContractLabelOf, ContractTrieIdOf>> { + None + } + + fn execute<'a>( + _vm: &mut OwnedWasmiVM>, + _entrypoint: EntryPoint, + _message: &[u8], + ) -> Result< + ContractResult> as VMBase>::MessageCustom>>, + VmErrorOf>>, + > +where { + Err(Error::::Unsupported.into()) + } + + fn query<'a>( + _vm: &mut OwnedWasmiVM>, + _: &[u8], + ) -> Result, VmErrorOf>>> { + Err(Error::::Unsupported.into()) + } + + fn run<'a>( + _vm: &mut OwnedWasmiVM>, + _entrypoint: EntryPoint, + _message: &[u8], + ) -> Result, VmErrorOf>>> { + Err(Error::::Unsupported.into()) + } +} + +impl PalletContractCodeInfo +where + AccountId: Clone, + TrieId: Default, +{ + pub fn new(account_id: AccountId, ibc_capable: bool, label: Label) -> Self { + PalletContractCodeInfo { + code: CodeInfo:: { + // When this is used for an actual Pallet, we would use the Pallet's AccountId + creator: account_id.clone(), + // Not applicable to Pallet, so we use default() + pristine_code_hash: Default::default(), + // Not applicable since we use native gas metering + instrumentation_version: u16::MAX, + // Not applicable to Pallet, so we use the max + refcount: u32::MAX, + // A pallet can choose wether to be IBC capable + ibc_capable, + }, + contract: ContractInfo { + // Pallets don't need a code ID, but we do not want to clash with CosmWasm + // contracts so we pick u64::MAX + code_id: u64::MAX, + // We have no storage + trie_id: Default::default(), + // When this is used for an actual Pallet, we would use the Pallet's AccountId + instantiator: account_id.clone(), + // When this is used for an actual Pallet, we would use Some(the Pallet's + // AccountId) + admin: Some(account_id), + // When this is used for an actual Pallet, we would use "pallet-PALLET_NAME" + label, + }, + } + } +} diff --git a/frame/cosmwasm/src/prelude.rs b/frame/cosmwasm/src/prelude.rs new file mode 100644 index 00000000..82eb7862 --- /dev/null +++ b/frame/cosmwasm/src/prelude.rs @@ -0,0 +1,6 @@ +pub use alloc::{ + format, + str::FromStr, + string::{String, ToString}, + vec::Vec, +}; diff --git a/frame/cosmwasm/src/runtimes/abstraction.rs b/frame/cosmwasm/src/runtimes/abstraction.rs new file mode 100644 index 00000000..83c99c82 --- /dev/null +++ b/frame/cosmwasm/src/runtimes/abstraction.rs @@ -0,0 +1,223 @@ +use crate::{types::AccountIdOf, Config, Pallet}; +use alloc::{string::String, vec::Vec}; +use codec::Encode; +use core::marker::PhantomData; +use cosmwasm_std::{Addr, CanonicalAddr}; +use cosmwasm_vm::vm::VmGasCheckpoint; +use scale_info::TypeInfo; +use vec1::Vec1; + +pub trait VMPallet { + type VmError; +} + +#[derive(Clone, Debug)] +pub struct CanonicalCosmwasmAccount(pub CosmwasmAccount); + +impl From> for CanonicalCosmwasmAccount { + fn from(from: CosmwasmAccount) -> Self { + CanonicalCosmwasmAccount(from) + } +} + +impl TryFrom> for CanonicalCosmwasmAccount { + type Error = T::VmError; + fn try_from(source: Vec) -> Result { + Ok(CanonicalCosmwasmAccount(CosmwasmAccount::new(Pallet::::canonical_addr_to_account( + source, + )?))) + } +} + +#[allow(clippy::from_over_into)] +impl Into for CanonicalCosmwasmAccount { + fn into(self) -> CanonicalAddr { + let cosmwasm_account = &self.0; + CanonicalAddr::from(cosmwasm_account.1.as_ref()) + } +} + +#[derive(Clone, Debug, Encode, TypeInfo, PartialEq)] +pub struct CosmwasmAccount(PhantomData, AccountIdOf); + +impl CosmwasmAccount { + pub fn into_inner(self) -> AccountIdOf { + self.1 + } +} + +impl AsRef> for CosmwasmAccount { + fn as_ref(&self) -> &AccountIdOf { + &self.1 + } +} + +impl CosmwasmAccount { + pub const fn new(x: AccountIdOf) -> Self { + CosmwasmAccount(PhantomData, x) + } +} + +#[allow(clippy::from_over_into)] +impl From> for String { + fn from(account: CosmwasmAccount) -> Self { + Pallet::::account_to_cosmwasm_addr(account.1) + } +} + +impl TryFrom for CosmwasmAccount { + type Error = T::VmError; + fn try_from(value: String) -> Result { + Pallet::::cosmwasm_addr_to_account(value).map(CosmwasmAccount::new) + } +} + +#[allow(clippy::from_over_into)] +impl From> for Addr { + fn from(account: CosmwasmAccount) -> Self { + Self::unchecked(String::from(account)) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Gas { + /// Stack of checkpoints. + /// + /// Always non-empty with last entry corresponding to the latest checkpoint. + /// When a new checkpoint is created, gas from the current one is moved to + /// the new one (see [`Self::push`]) such that total remaining gas is the + /// sum of gas on all checkpoints. + checkpoints: Vec1, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum GasOutcome { + Halt, + Continue, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct TooManyCheckpoints; +#[derive(Debug, PartialEq, Eq)] +pub struct NoCheckpointToPop; + +impl Gas { + pub fn new(max_frames: u8, initial_value: u64) -> Self { + let max_frames = usize::from(max_frames).max(1); + Gas { checkpoints: Vec1::with_capacity(initial_value, max_frames) } + } + + fn current_mut(&mut self) -> &mut u64 { + self.checkpoints.last_mut() + } + + /// Pushes a new gas checkpoint. + /// + /// If `max_frames` number of checkpoints have been reached, returns + /// [`TooManyCheckpoints`] error. Otherwise, checks if there’s enough gas + /// at the current checkpoint and if so creates a new checkpoint with + /// requested limit. + pub fn push(&mut self, checkpoint: VmGasCheckpoint) -> Result { + if self.checkpoints.len() >= self.checkpoints.capacity() { + return Err(TooManyCheckpoints) + } + let parent = self.current_mut(); + Ok(match checkpoint { + VmGasCheckpoint::Unlimited => { + let value = *parent; + *parent = 0; + self.checkpoints.push(value); + GasOutcome::Continue + }, + VmGasCheckpoint::Limited(limit) if limit <= *parent => { + *parent -= limit; + self.checkpoints.push(limit); + GasOutcome::Continue + }, + _ => GasOutcome::Halt, + }) + } + /// Pops the last gas checkpoint. + /// + /// Any gas limit remaining in the checkpoint is added back to the parent + /// checkpoint. Returns an error if function tries to pop the final + /// checkpoint. + pub fn pop(&mut self) -> Result<(), NoCheckpointToPop> { + self.checkpoints + .pop() + .map(|child| { + *self.current_mut() += child; + }) + .or(Err(NoCheckpointToPop)) + } + + pub fn charge(&mut self, value: u64) -> GasOutcome { + let current = self.current_mut(); + if let Some(left) = current.checked_sub(value) { + *current = left; + GasOutcome::Continue + } else { + *current = 0; + GasOutcome::Halt + } + } + + pub fn remaining(&self) -> u64 { + // always less than `initial_value`, it will not panic + self.checkpoints.iter().sum() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_checkpoint_gas_limits() { + let total_gas = 100_000u64; + let max_frames = 100; + let mut gas = Gas::new(max_frames, total_gas); + assert_eq!(gas.push(VmGasCheckpoint::Limited(50_000)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(30_000)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(20_000)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(10_000)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(5_000)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(5_001)), Ok(GasOutcome::Halt)); + assert_eq!(gas.checkpoints, [50000, 20000, 10000, 10000, 5000, 5000]); + assert_eq!(gas.pop(), Ok(())); + assert_eq!(gas.checkpoints, [50000, 20000, 10000, 10000, 10000]); + + assert_eq!(gas.remaining(), total_gas); + + assert_eq!(gas.charge(5000), GasOutcome::Continue); + assert_eq!(gas.charge(10000), GasOutcome::Halt); + assert_eq!(gas.checkpoints, [50000, 20000, 10000, 10000, 0]); + assert_eq!(gas.pop(), Ok(())); + assert_eq!(gas.charge(10000), GasOutcome::Continue); + assert_eq!(gas.checkpoints, [50000, 20000, 10000, 0]); + + assert_eq!(gas.remaining(), total_gas - 20000); + } + + #[test] + fn test_invalid_checkpoints() { + const TOTAL_GAS: u64 = 100; + let mut gas = Gas::new(3, TOTAL_GAS); + assert_eq!(gas.pop(), Err(NoCheckpointToPop)); + + for _ in 0..2 { + assert_eq!(gas.push(VmGasCheckpoint::Limited(50)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(50)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(50)), Err(TooManyCheckpoints)); + + assert_eq!(gas.pop(), Ok(())); + assert_eq!(gas.pop(), Ok(())); + assert_eq!(gas.pop(), Err(NoCheckpointToPop)); + + assert_eq!(gas.remaining(), TOTAL_GAS); + } + + assert_eq!(gas.push(VmGasCheckpoint::Limited(50)), Ok(GasOutcome::Continue)); + assert_eq!(gas.push(VmGasCheckpoint::Limited(60)), Ok(GasOutcome::Halt)); + } +} diff --git a/frame/cosmwasm/src/runtimes/mod.rs b/frame/cosmwasm/src/runtimes/mod.rs new file mode 100644 index 00000000..b1c023fe --- /dev/null +++ b/frame/cosmwasm/src/runtimes/mod.rs @@ -0,0 +1,2 @@ +pub mod abstraction; +pub mod vm; diff --git a/frame/cosmwasm/src/runtimes/vm.rs b/frame/cosmwasm/src/runtimes/vm.rs new file mode 100644 index 00000000..4619620c --- /dev/null +++ b/frame/cosmwasm/src/runtimes/vm.rs @@ -0,0 +1,673 @@ +use super::abstraction::{CanonicalCosmwasmAccount, CosmwasmAccount, Gas}; +use crate::{runtimes::abstraction::GasOutcome, types::*, weights::WeightInfo, Config, Pallet}; +use alloc::{borrow::ToOwned, collections::btree_map::BTreeMap, string::String, vec::Vec}; +use core::marker::{Send, Sync}; +use cosmwasm_std::{CodeInfoResponse, Coin, ContractInfoResponse, Empty, Env, MessageInfo}; +use cosmwasm_vm::{ + executor::ExecutorError, + has::Has, + memory::{MemoryReadError, MemoryWriteError}, + system::{CosmwasmCodeId, CosmwasmContractMeta, SystemError}, + transaction::Transactional, + vm::{VMBase, VmGas}, +}; +use cosmwasm_vm_wasmi::{ + OwnedWasmiVM, WasmiContext, WasmiInput, WasmiModule, WasmiOutput, WasmiVMError, +}; +use frame_support::traits::tokens::Preservation; +use sp_runtime::DispatchError; +use wasmi::{core::HostError, Instance, Memory}; + +/// Different type of contract runtimes. A contract might either be dynamically loaded or statically +/// invoked (precompiled). +pub enum ContractBackend { + /// A dynamically loaded CosmWasmbased contract. This code has previously been uploaded by a + /// user. + CosmWasm { + /// The wasmi module instantiated for the CosmWasm contract. + executing_module: Option, + call_depth_mut: u32, + }, + /// A substrate pallet, which is a precompiled contract that is included in the runtime. + Pallet { call_depth_mut: u32 }, +} + +impl WasmiContext for ContractBackend { + fn executing_module(&self) -> Option { + match self { + ContractBackend::CosmWasm { executing_module, .. } => executing_module.clone(), + ContractBackend::Pallet { .. } => None, + } + } + + fn set_wasmi_context(&mut self, instance: Instance, memory: Memory) { + *self = ContractBackend::CosmWasm { + executing_module: Some(WasmiModule { instance, memory }), + call_depth_mut: 0, + }; + } + + fn call_depth_mut(&mut self) -> &mut u32 { + match self { + ContractBackend::CosmWasm { call_depth_mut, .. } => call_depth_mut, + ContractBackend::Pallet { call_depth_mut } => call_depth_mut, + } + } +} + +#[derive(Debug)] +pub enum CosmwasmVMError { + Interpreter(wasmi::Error), + VirtualMachine(WasmiVMError), + Pallet(crate::Error), + SubstrateDispatch(DispatchError), + AccountConvert, + Aborted(String), + ReadOnlyViolation, + OutOfGas, + InvalidGasCheckpoint, + Unsupported, + NotImplemented, + ContractNotFound, + ExecuteDeserialize, + QuerySerialize, + QueryDeserialize, + ExecuteSerialize, + Rpc(String), + Ibc(String), + Xcm(String), + AssetConversion, + Precompile, +} + +impl HostError + for CosmwasmVMError +{ +} + +impl core::fmt::Display for CosmwasmVMError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From> for CosmwasmVMError { + fn from(e: crate::Error) -> Self { + Self::Pallet(e) + } +} + +impl From for CosmwasmVMError { + fn from(e: wasmi::Error) -> Self { + Self::Interpreter(e) + } +} + +impl From for CosmwasmVMError { + fn from(e: WasmiVMError) -> Self { + Self::VirtualMachine(e) + } +} + +impl From for CosmwasmVMError { + fn from(e: SystemError) -> Self { + Self::VirtualMachine(e.into()) + } +} + +impl From for CosmwasmVMError { + fn from(e: ExecutorError) -> Self { + Self::VirtualMachine(e.into()) + } +} + +impl From for CosmwasmVMError { + fn from(e: MemoryReadError) -> Self { + Self::VirtualMachine(e.into()) + } +} + +impl From for CosmwasmVMError { + fn from(e: MemoryWriteError) -> Self { + Self::VirtualMachine(e.into()) + } +} + +/// Initial storage mutability. +pub enum InitialStorageMutability { + /// The storage is currently readonly, any operation trying to mutate it will result in a + /// [`CosmwasmVMError::ReadOnlyViolation`] + ReadOnly, + /// Mutable operations on the storage are currently allowed. + ReadWrite, +} + +/// VM shared cache +#[derive(Clone, Default)] +pub struct CosmwasmVMCache { + /// Code cache, a mapping from an identifier to it's code. + pub code: BTreeMap>, +} + +/// VM shared state +#[derive(Clone)] +pub struct CosmwasmVMShared { + /// Readonly depth, used to determine whether the storage is currently readonly or not. + /// Whenever a contract call `query`, we increment this counter before the call and decrement + /// after the call. A value of 0 mean that the contract is able to mutate the storage. + /// A value > 0 mean that the storage is readonly. + pub storage_readonly_depth: u32, + /// VM depth, i.e. how many contracts has been loaded and are currently running. + pub depth: u8, + /// Shared Gas metering. + pub gas: Gas, + /// Shared cache. + pub cache: CosmwasmVMCache, +} + +impl CosmwasmVMShared { + /// Constructs a VM with given gas limits and other fields set to defaults. + /// + /// The arguments have the same meaning as in [`Gas::new`] constructor. + pub fn with_gas(max_frames: u8, initial_value: u64) -> Self { + Self { + storage_readonly_depth: 0, + depth: 0, + gas: Gas::new(max_frames, initial_value), + cache: CosmwasmVMCache::default(), + } + } + + /// Whether the storage is currently readonly. + pub fn storage_is_readonly(&self) -> bool { + self.storage_readonly_depth > 0 + } + /// Increase storage readonly depth. + /// Happening when a contract call the querier. + pub fn push_readonly(&mut self) { + self.storage_readonly_depth += 1; + } + /// Decrease storage readonly depth. + /// Happening when a querier exit. + pub fn pop_readonly(&mut self) { + self.storage_readonly_depth -= 1; + } +} + +/// Cosmwasm VM instance data. +/// This structure hold the state for a single contract. +/// Note that all [`CosmwasmVM`] share the same [`CosmWasmVMShared`] structure. +pub struct CosmwasmVM<'a, T: Config> { + /// CosmWasm [`Env`] for the executing contract. + pub cosmwasm_env: Env, + /// CosmWasm [`MessageInfo`] for the executing contract. + pub cosmwasm_message_info: MessageInfo, + /// Executing contract address. + pub contract_address: CosmwasmAccount, + /// Executing contract metadata. + pub contract_info: ContractInfoOf, + /// State shared across all contracts within a single transaction. + pub shared: &'a mut CosmwasmVMShared, + /// Iterator id's to corresponding keys. Keys are used to get the next key. + pub iterators: BTreeMap>, + /// Actual contract runtime + pub contract_runtime: ContractBackend, +} + +impl<'a, T: Config> Has for CosmwasmVM<'a, T> { + fn get(&self) -> Env { + self.cosmwasm_env.clone() + } +} + +impl<'a, T: Config> Has for CosmwasmVM<'a, T> { + fn get(&self) -> MessageInfo { + self.cosmwasm_message_info.clone() + } +} + +impl<'a, T: Config> WasmiContext for CosmwasmVM<'a, T> { + fn executing_module(&self) -> Option { + self.contract_runtime.executing_module() + } + + fn set_wasmi_context(&mut self, instance: Instance, memory: Memory) { + self.contract_runtime.set_wasmi_context(instance, memory) + } + + fn call_depth_mut(&mut self) -> &mut u32 { + match &mut self.contract_runtime { + ContractBackend::CosmWasm { call_depth_mut, .. } => call_depth_mut, + ContractBackend::Pallet { call_depth_mut } => call_depth_mut, + } + } +} + +impl<'a, T: Config> CosmwasmVM<'a, T> { + pub fn charge_raw(&mut self, gas: u64) -> Result<(), ::Error> { + match self.shared.gas.charge(gas) { + GasOutcome::Halt => Err(CosmwasmVMError::OutOfGas), + GasOutcome::Continue => Ok(()), + } + } +} + +impl<'a, T: Config + Send + Sync> VMBase for CosmwasmVM<'a, T> { + type Input<'x> = WasmiInput>; + type Output<'x> = WasmiOutput>; + type QueryCustom = Empty; + type MessageCustom = Empty; + type ContractMeta = CosmwasmContractMeta>; + type Address = CosmwasmAccount; + type CanonicalAddress = CanonicalCosmwasmAccount; + type StorageKey = Vec; + type StorageValue = Vec; + type Error = CosmwasmVMError; + + fn running_contract_meta(&mut self) -> Result { + log::debug!(target: "runtime::contracts", "contract_meta"); + Ok(Pallet::::do_running_contract_meta(self)) + } + + fn db_scan( + &mut self, + _start: Option, + _end: Option, + _order: cosmwasm_std::Order, + ) -> Result { + log::debug!(target: "runtime::contracts", "db_scan"); + Pallet::::do_db_scan(self) + } + + fn db_next( + &mut self, + iterator_id: u32, + ) -> Result<(Self::StorageKey, Self::StorageValue), Self::Error> { + log::debug!(target: "runtime::contracts", "db_next"); + match Pallet::::do_db_next(self, iterator_id)? { + Some(kv_pair) => Ok(kv_pair), + None => Ok((Vec::new(), Vec::new())), + } + } + + fn set_contract_meta( + &mut self, + address: Self::Address, + CosmwasmContractMeta { code_id: new_code_id, admin, label }: Self::ContractMeta, + ) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "set_contract_meta"); + Pallet::::do_set_contract_meta( + &address.into_inner(), + new_code_id, + admin.map(|admin| admin.into_inner()), + label, + ) + .map_err(Into::into) + } + + fn contract_meta(&mut self, address: Self::Address) -> Result { + log::debug!(target: "runtime::contracts", "code_id"); + Pallet::::do_contract_meta(address.into_inner()) + } + + fn continue_query( + &mut self, + address: Self::Address, + message: &[u8], + ) -> Result { + log::debug!(target: "runtime::contracts", "continue_query"); + Pallet::::do_continue_query(self, address.into_inner(), message) + } + + fn continue_execute( + &mut self, + address: Self::Address, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "continue_execute"); + Pallet::::do_continue_execute(self, address.into_inner(), funds, message, event_handler) + } + + fn continue_reply( + &mut self, + message: cosmwasm_std::Reply, + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "continue_reply"); + Pallet::::do_continue_reply(self, message, event_handler) + } + + fn continue_instantiate( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result<(Self::Address, Option), Self::Error> { + log::debug!(target: "runtime::contracts", "continue_instantiate"); + self.continue_instantiate2(contract_meta, funds, b"salt", message, event_handler) + } + + fn continue_instantiate2( + &mut self, + contract_meta: Self::ContractMeta, + funds: Vec, + salt: &[u8], + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result<(Self::Address, Option), Self::Error> { + log::debug!(target: "runtime::contracts", "continue_instantiate2"); + Pallet::::do_continue_instantiate( + self, + contract_meta, + funds, + salt, + message, + event_handler, + ) + .map(|r| (self.contract_address.clone(), r)) + } + + fn continue_migrate( + &mut self, + address: Self::Address, + message: &[u8], + event_handler: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "continue_migrate"); + Pallet::::do_continue_migrate(self, address.into_inner(), message, event_handler) + } + + fn query_custom( + &mut self, + _: Self::QueryCustom, + ) -> Result, Self::Error> + { + log::debug!(target: "runtime::contracts", "query_custom"); + Err(CosmwasmVMError::Unsupported) + } + + fn message_custom( + &mut self, + _: Self::MessageCustom, + _: &mut dyn FnMut(cosmwasm_std::Event), + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "message_custom"); + Err(CosmwasmVMError::Unsupported) + } + + fn query_raw( + &mut self, + address: Self::Address, + key: Self::StorageKey, + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "query_raw"); + Pallet::::do_query_raw(self, address.into_inner(), &key) + } + + fn transfer(&mut self, to: &Self::Address, funds: &[Coin]) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "transfer: {:#?}", funds); + let from = self.contract_address.as_ref(); + Pallet::::do_transfer(from, to.as_ref(), funds, Preservation::Expendable)?; + Ok(()) + } + + fn burn(&mut self, funds: &[Coin]) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "burn: {:#?}", funds); + Err(CosmwasmVMError::NotImplemented) + } + + fn balance(&mut self, account: &Self::Address, denom: String) -> Result { + log::debug!(target: "runtime::contracts", "balance: {} => {:#?}", String::from(account.clone()), denom); + let amount = Pallet::::do_balance(account.as_ref(), denom.clone())?; + Ok(Coin { denom, amount: amount.into() }) + } + + fn all_balance(&mut self, account: &Self::Address) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "all balance: {}", String::from(account.clone())); + Err(CosmwasmVMError::Unsupported) + } + + fn supply(&mut self, denom: String) -> Result { + let amount = Pallet::::do_supply(denom.clone())?; + Ok(Coin { denom, amount: amount.into() }) + } + + fn query_contract_info( + &mut self, + address: Self::Address, + ) -> Result { + log::trace!(target: "runtime::contracts", "query_contract_info"); + Pallet::::do_query_contract_info(self, address.into_inner()) + } + + fn query_code_info( + &mut self, + code_id: CosmwasmCodeId, + ) -> Result { + log::debug!(target: "runtime::contracts", "query_code_info"); + Pallet::::do_query_code_info(code_id) + } + + fn debug(&mut self, message: Vec) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "[CONTRACT-LOG] {}", String::from_utf8_lossy(&message)); + Ok(()) + } + + fn db_read( + &mut self, + key: Self::StorageKey, + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "db_read"); + Pallet::::do_db_read(self, &key) + } + + fn db_write( + &mut self, + key: Self::StorageKey, + value: Self::StorageValue, + ) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "db_write"); + if self.shared.storage_is_readonly() { + Err(CosmwasmVMError::ReadOnlyViolation) + } else { + Pallet::::do_db_write(self, &key, &value)?; + Ok(()) + } + } + + fn db_remove(&mut self, key: Self::StorageKey) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "db_remove"); + if self.shared.storage_is_readonly() { + Err(CosmwasmVMError::ReadOnlyViolation) + } else { + Pallet::::do_db_remove(self, &key); + Ok(()) + } + } + + fn addr_validate(&mut self, input: &str) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "addr_validate"); + match Pallet::::do_addr_validate(input.to_owned()) { + Ok(_) => Ok(Ok(())), + Err(e) => Ok(Err(e)), + } + } + + fn addr_canonicalize( + &mut self, + input: &str, + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "addr_canonicalize"); + let account = match Pallet::::do_addr_canonicalize(input.to_owned()) { + Ok(account) => account, + Err(e) => return Ok(Err(e)), + }; + + Ok(Ok(CosmwasmAccount::new(account).into())) + } + + fn addr_humanize( + &mut self, + addr: &Self::CanonicalAddress, + ) -> Result, Self::Error> { + log::debug!(target: "runtime::contracts", "addr_humanize"); + Ok(Ok(Pallet::::do_addr_humanize(addr))) + } + + fn abort(&mut self, message: String) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "db_abort"); + Err(CosmwasmVMError::Aborted(message)) + } + + fn charge(&mut self, value: VmGas) -> Result<(), Self::Error> { + let gas_to_charge = match value { + VmGas::Instrumentation { metered } => metered as u64, + VmGas::DbRead => T::WeightInfo::db_read().ref_time(), + VmGas::DbWrite => T::WeightInfo::db_write().ref_time(), + VmGas::DbRemove => T::WeightInfo::db_remove().ref_time(), + VmGas::DbScan => T::WeightInfo::db_scan().ref_time(), + VmGas::DbNext => T::WeightInfo::db_next().ref_time(), + VmGas::Balance => T::WeightInfo::balance().ref_time(), + VmGas::Secp256k1Verify => T::WeightInfo::secp256k1_verify().ref_time(), + VmGas::Secp256k1RecoverPubkey => T::WeightInfo::secp256k1_recover_pubkey().ref_time(), + VmGas::Ed25519Verify => T::WeightInfo::ed25519_verify().ref_time(), + VmGas::Ed25519BatchVerify => T::WeightInfo::ed25519_batch_verify().ref_time(), + VmGas::AddrValidate => T::WeightInfo::addr_validate().ref_time(), + VmGas::AddrCanonicalize => T::WeightInfo::addr_canonicalize().ref_time(), + VmGas::AddrHumanize => T::WeightInfo::addr_humanize().ref_time(), + VmGas::GetContractMeta => T::WeightInfo::contract_meta().ref_time(), + VmGas::SetContractMeta => T::WeightInfo::set_contract_meta().ref_time(), + VmGas::Transfer { nb_of_coins } => T::WeightInfo::transfer(nb_of_coins).ref_time(), + VmGas::ContinueExecute { nb_of_coins } => + T::WeightInfo::continue_execute(nb_of_coins).ref_time(), + VmGas::ContinueInstantiate { nb_of_coins } => + T::WeightInfo::continue_instantiate(nb_of_coins).ref_time(), + VmGas::ContinueMigrate => T::WeightInfo::continue_migrate().ref_time(), + VmGas::ContinueQuery => T::WeightInfo::continue_query().ref_time(), + VmGas::ContinueReply => T::WeightInfo::continue_reply().ref_time(), + VmGas::QueryRaw => T::WeightInfo::query_raw().ref_time(), + VmGas::QueryContractInfo => T::WeightInfo::query_contract_info().ref_time(), + VmGas::QueryCodeInfo => T::WeightInfo::query_code_info().ref_time(), + _ => 1_u64, + }; + self.charge_raw(gas_to_charge) + } + + fn gas_checkpoint_push( + &mut self, + checkpoint: cosmwasm_vm::vm::VmGasCheckpoint, + ) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "gas_checkpoint_push"); + match self.shared.gas.push(checkpoint) { + Ok(GasOutcome::Continue) => Ok(()), + Ok(GasOutcome::Halt) => Err(CosmwasmVMError::OutOfGas), + Err(_) => Err(CosmwasmVMError::InvalidGasCheckpoint), + } + } + + fn gas_checkpoint_pop(&mut self) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "gas_checkpoint_pop"); + self.shared.gas.pop().map_err(|_| CosmwasmVMError::InvalidGasCheckpoint) + } + + fn gas_ensure_available(&mut self) -> Result<(), Self::Error> { + log::debug!(target: "runtime::contracts", "gas_ensure_available"); + if self.shared.gas.remaining() > 0 { + Ok(()) + } else { + Err(CosmwasmVMError::OutOfGas) + } + } + + fn secp256k1_verify( + &mut self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + log::debug!(target: "runtime::contracts", "secp256k1_verify"); + Ok(Pallet::::do_secp256k1_verify(message_hash, signature, public_key)) + } + + fn secp256k1_recover_pubkey( + &mut self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, ()>, Self::Error> { + log::debug!(target: "runtime::contracts", "secp256k1_recover_pubkey"); + Ok(Pallet::::do_secp256k1_recover_pubkey(message_hash, signature, recovery_param)) + } + + fn ed25519_verify( + &mut self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + log::debug!(target: "runtime::contracts", "ed25519_verify"); + Ok(Pallet::::do_ed25519_verify(message, signature, public_key)) + } + + fn ed25519_batch_verify( + &mut self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + log::debug!(target: "runtime::contracts", "ed25519_batch_verify"); + Ok(Pallet::::do_ed25519_batch_verify(messages, signatures, public_keys)) + } + + fn ibc_transfer( + &mut self, + channel_id: String, + to_address: String, + amount: Coin, + timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), Self::Error> { + Pallet::::do_ibc_transfer(self, channel_id, to_address, amount, timeout) + } + + fn ibc_send_packet( + &mut self, + channel_id: String, + data: cosmwasm_std::Binary, + timeout: cosmwasm_std::IbcTimeout, + ) -> Result<(), Self::Error> { + Pallet::::do_ibc_send_packet(self, channel_id, data, timeout) + } + + fn ibc_close_channel(&mut self, channel_id: String) -> Result<(), Self::Error> { + Pallet::::do_ibc_close_channel(self, channel_id) + } + + fn transfer_from( + &mut self, + from: &Self::Address, + to: &Self::Address, + funds: &[Coin], + ) -> Result<(), Self::Error> { + Pallet::::do_transfer(from.as_ref(), to.as_ref(), funds, Preservation::Expendable)?; + Ok(()) + } +} + +impl<'a, T: Config> Transactional for CosmwasmVM<'a, T> { + type Error = CosmwasmVMError; + fn transaction_begin(&mut self) -> Result<(), Self::Error> { + sp_io::storage::start_transaction(); + Ok(()) + } + fn transaction_commit(&mut self) -> Result<(), Self::Error> { + sp_io::storage::commit_transaction(); + Ok(()) + } + fn transaction_rollback(&mut self) -> Result<(), Self::Error> { + sp_io::storage::rollback_transaction(); + Ok(()) + } +} diff --git a/frame/cosmwasm/src/types.rs b/frame/cosmwasm/src/types.rs new file mode 100644 index 00000000..dec0ca41 --- /dev/null +++ b/frame/cosmwasm/src/types.rs @@ -0,0 +1,87 @@ +use crate::{runtimes::vm::CosmwasmVM, Config}; +use codec::{Decode, Encode, MaxEncodedLen}; +use cosmwasm_vm::system::CosmwasmCodeId; +use frame_support::{BoundedBTreeMap, BoundedVec}; +use scale_info::TypeInfo; + +pub type DefaultCosmwasmVM<'a, T> = CosmwasmVM<'a, T>; +pub type KeepAlive = bool; +pub type FundsOf = BoundedBTreeMap, (BalanceOf, KeepAlive), MaxFundsAssetOf>; +pub type ContractSaltOf = BoundedVec>; +pub type ContractMessageOf = BoundedVec>; +pub type ContractCodeOf = BoundedVec>; +pub type ContractInstrumentedCodeOf = BoundedVec>; +pub type ContractTrieIdOf = BoundedVec>; +pub type ContractLabelOf = BoundedVec>; +pub type AccountIdOf = ::AccountIdExtended; +pub type MaxCodeSizeOf = ::MaxCodeSize; +pub type MaxInstrumentedCodeSizeOf = ::MaxInstrumentedCodeSize; +pub type MaxMessageSizeOf = ::MaxMessageSize; +pub type MaxContractLabelSizeOf = ::MaxContractLabelSize; +pub type MaxContractTrieIdSizeOf = ::MaxContractTrieIdSize; +pub type MaxInstantiateSaltSizeOf = ::MaxInstantiateSaltSize; +pub type MaxFundsAssetOf = ::MaxFundsAssets; +pub type AssetIdOf = ::AssetId; +pub type BalanceOf = ::Balance; +pub type ContractInfoOf = ContractInfo, ContractLabelOf, ContractTrieIdOf>; +pub type CodeInfoOf = CodeInfo>; + +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, TypeInfo, Debug)] +pub enum EntryPoint { + Instantiate, + Execute, + Migrate, + Reply, + IbcChannelOpen, + IbcChannelConnect, + IbcChannelClose, + IbcPacketTimeout, + IbcPacketReceive, + IbcPacketAck, +} + +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, TypeInfo, Debug)] +pub enum CodeIdentifier { + CodeId(CosmwasmCodeId), + CodeHash([u8; 32]), +} +/// Pallet contract/code metadata. +#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, Debug)] +pub struct PalletContractCodeInfo { + /// Hardcoded code info representing the precompiled code backing the contract. + pub code: CodeInfo, + /// Hardcoded contract info representing the precompiled contract. + pub contract: ContractInfo, +} + +/// Tracked code metadata. +#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, Debug)] +pub struct CodeInfo { + /// Original owner of the code. + pub creator: AccountId, + /// The hash of the pristine code. + pub pristine_code_hash: [u8; 32], + /// Version of the instrumentation applied to the code. + pub instrumentation_version: u16, + /// Number of contract referencing this code. + pub refcount: u32, + /// Wether the contract export IBC functions and is consequently able to be called back by IBC + /// operations. + pub ibc_capable: bool, +} + +/// Contract metadata. +#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, Debug)] +pub struct ContractInfo { + /// The code this contract is baked by. + pub code_id: CosmwasmCodeId, + /// The contract trie ID (The unique storage prefix). + pub trie_id: TrieId, + /// Account that created this instance. + pub instantiator: AccountId, + /// Current admin of the contract instance. + /// If the value is [`None`], the contract cannot be migrated. + pub admin: Option, + /// Contract label defined by the instantiator. + pub label: Label, +} diff --git a/frame/cosmwasm/src/utils.rs b/frame/cosmwasm/src/utils.rs new file mode 100644 index 00000000..82c1d6ff --- /dev/null +++ b/frame/cosmwasm/src/utils.rs @@ -0,0 +1,129 @@ +use alloc::{string::String, vec::Vec}; +use cosmwasm_std::Coin; +use cosmwasm_vm::system::CosmwasmContractMeta; +use sp_core::storage::ChildInfo; +use sp_runtime::traits::{Convert, Hash}; + +use crate::{ + runtimes::{ + abstraction::{CanonicalCosmwasmAccount, CosmwasmAccount, VMPallet}, + vm::CosmwasmVMError, + }, + types::{AccountIdOf, AssetIdOf, BalanceOf, ContractInfoOf, ContractTrieIdOf}, + Config, ContractToInfo, Error, Pallet, +}; + +impl Pallet { + pub(crate) fn derive_contract_address( + creator: &AccountIdOf, + salt: &[u8], + code_hash: &[u8], + ) -> Result, Error> { + if salt.is_empty() || salt.len() > 64 { + return Err(Error::::InvalidSalt) + } + + let module_hash = sp_io::hashing::sha2_256(b"module"); + let message = b""; + + let mut key = Vec::::from(module_hash); + key.extend_from_slice(b"wasm\0"); + key.extend_from_slice(&(code_hash.len() as u64).to_be_bytes()); + key.extend_from_slice(code_hash); + key.extend_from_slice(&(creator.as_ref().len() as u64).to_be_bytes()); + key.extend_from_slice(creator.as_ref()); + key.extend_from_slice(&(salt.len() as u64).to_be_bytes()); + key.extend_from_slice(salt); + key.extend_from_slice(&(message.len() as u64).to_be_bytes()); + key.extend_from_slice(message); + + let address = sp_io::hashing::sha2_256(&key).into(); + Pallet::::canonical_addr_to_account(address).map_err(|_| Error::::InvalidAccount) + } + + /// Deterministic contract trie id generation. + pub(crate) fn derive_contract_trie_id( + contract: &AccountIdOf, + nonce: u64, + ) -> ContractTrieIdOf { + let data: Vec<_> = contract.as_ref().iter().chain(&nonce.to_le_bytes()).cloned().collect(); + T::Hashing::hash(&data).as_ref().to_vec().try_into().expect( + "hashing len implementation must always be <= defined max contract trie id size; QED;", + ) + } + + /// Handy wrapper to update contract info. + pub(crate) fn set_contract_info(contract: &AccountIdOf, info: ContractInfoOf) { + ContractToInfo::::insert(contract, info) + } + + /// Handy wrapper to return contract info. + pub(crate) fn contract_info(contract: &AccountIdOf) -> Result, Error> { + ContractToInfo::::get(contract).ok_or(Error::::ContractNotFound) + } + + pub(crate) fn canonical_addr_to_account( + canonical: Vec, + ) -> Result, ::VmError> { + T::AccountToAddr::convert(canonical).map_err(|()| CosmwasmVMError::AccountConvert) + } + + /// Try to convert from a CosmWasm address to a native AccountId. + pub(crate) fn cosmwasm_addr_to_account( + cosmwasm_addr: String, + ) -> Result, ::VmError> { + T::AccountToAddr::convert(cosmwasm_addr).map_err(|()| CosmwasmVMError::AccountConvert) + } + + /// Convert from a native ahttps://app.clickup.com/20465559/v/l/6-210281072-1ccount to a CosmWasm address. + pub(crate) fn account_to_cosmwasm_addr(account: AccountIdOf) -> String { + T::AccountToAddr::convert(account) + } + + /// Convert a native asset and amount into a CosmWasm [`Coin`]. + pub(crate) fn native_asset_to_cosmwasm_asset( + asset: AssetIdOf, + amount: BalanceOf, + ) -> Coin { + let denom = T::AssetToDenom::convert(asset); + Coin { denom, amount: amount.into().into() } + } + + /// Try to convert from a CosmWasm denom to a native [`AssetIdOf`]. + pub(crate) fn cosmwasm_asset_to_native_asset(denom: String) -> Result, Error> { + T::AssetToDenom::convert(denom).map_err(|_| Error::::UnknownDenom) + } + + /// Build a [`ChildInfo`] out of a contract trie id. + pub(crate) fn contract_child_trie(trie_id: &[u8]) -> ChildInfo { + ChildInfo::new_default(trie_id) + } + + pub(crate) fn do_contract_meta( + address: AccountIdOf, + ) -> Result>, CosmwasmVMError> { + let info = Pallet::::contract_info(&address)?; + Ok(CosmwasmContractMeta { + code_id: info.code_id, + admin: info.admin.clone().map(CosmwasmAccount::new), + label: String::from_utf8_lossy(&info.label).into(), + }) + } + + /// Validate a string address + pub(crate) fn do_addr_validate(address: String) -> Result, CosmwasmVMError> { + Pallet::::cosmwasm_addr_to_account(address) + } + + /// Canonicalize a human readable address + pub(crate) fn do_addr_canonicalize( + address: String, + ) -> Result, CosmwasmVMError> { + Pallet::::cosmwasm_addr_to_account(address) + } + + /// Humanize a canonical address + pub(crate) fn do_addr_humanize(address: &CanonicalCosmwasmAccount) -> CosmwasmAccount { + address.0.clone() + } +} diff --git a/frame/cosmwasm/src/weights.rs b/frame/cosmwasm/src/weights.rs new file mode 100644 index 00000000..7a1748ac --- /dev/null +++ b/frame/cosmwasm/src/weights.rs @@ -0,0 +1,1371 @@ +//! Autogenerated weights for cosmwasm +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-02-13, STEPS: `50`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `spm`, CPU: `12th Gen Intel(R) Core(TM) i7-1280P` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dali-dev"), DB CACHE: 1024 + +// Executed Command: +// /nix/store/r0plghlp98kfz8h93shlgrqfgzf7cdpm-composable/bin/composable +// benchmark +// pallet +// --chain=dali-dev +// --execution=wasm +// --wasm-execution=compiled +// --pallet=cosmwasm +// --extrinsic=* +// --steps=50 +// --repeat=10 +// --output= +// parachain/frame/cosmwasm/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(trivial_numeric_casts)] +#![allow(clippy::unnecessary_cast)] +#![allow(non_snake_case)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for cosmwasm. +pub trait WeightInfo { + fn upload(n: u32, ) -> Weight; + fn instantiate(n: u32, ) -> Weight; + fn execute(n: u32, ) -> Weight; + fn migrate() -> Weight; + fn update_admin() -> Weight; + fn db_read() -> Weight; + fn db_read_other_contract() -> Weight; + fn db_write() -> Weight; + fn db_scan() -> Weight; + fn db_next() -> Weight; + fn db_remove() -> Weight; + fn balance() -> Weight; + fn transfer(n: u32, ) -> Weight; + fn set_contract_meta() -> Weight; + fn running_contract_meta() -> Weight; + fn contract_meta() -> Weight; + fn addr_validate() -> Weight; + fn addr_canonicalize() -> Weight; + fn addr_humanize() -> Weight; + fn secp256k1_recover_pubkey() -> Weight; + fn secp256k1_verify() -> Weight; + fn ed25519_verify() -> Weight; + fn ed25519_batch_verify() -> Weight; + fn continue_instantiate(n: u32, ) -> Weight; + fn continue_execute(n: u32, ) -> Weight; + fn continue_migrate() -> Weight; + fn continue_query() -> Weight; + fn continue_reply() -> Weight; + fn query_contract_info() -> Weight; + fn query_code_info() -> Weight; + fn query_raw() -> Weight; + fn instruction_I64Const(r: u32, ) -> Weight; + fn instruction_F64Const(r: u32, ) -> Weight; + fn instruction_I64Load(r: u32, ) -> Weight; + fn instruction_F64Load(r: u32, ) -> Weight; + fn instruction_I64Store(r: u32, ) -> Weight; + fn instruction_F64Store(r: u32, ) -> Weight; + fn instruction_I64Eq(r: u32, ) -> Weight; + fn instruction_I64Eqz(r: u32, ) -> Weight; + fn instruction_I64Ne(r: u32, ) -> Weight; + fn instruction_I64LtS(r: u32, ) -> Weight; + fn instruction_I64GtS(r: u32, ) -> Weight; + fn instruction_I64LeS(r: u32, ) -> Weight; + fn instruction_I64GeS(r: u32, ) -> Weight; + fn instruction_I64Clz(r: u32, ) -> Weight; + fn instruction_I64Ctz(r: u32, ) -> Weight; + fn instruction_I64Popcnt(r: u32, ) -> Weight; + fn instruction_I64Add(r: u32, ) -> Weight; + fn instruction_I64Sub(r: u32, ) -> Weight; + fn instruction_I64Mul(r: u32, ) -> Weight; + fn instruction_I64DivS(r: u32, ) -> Weight; + fn instruction_I64DivU(r: u32, ) -> Weight; + fn instruction_I64RemS(r: u32, ) -> Weight; + fn instruction_I64And(r: u32, ) -> Weight; + fn instruction_I64Or(r: u32, ) -> Weight; + fn instruction_I64Xor(r: u32, ) -> Weight; + fn instruction_I64Shl(r: u32, ) -> Weight; + fn instruction_I64ShrS(r: u32, ) -> Weight; + fn instruction_I64Rotl(r: u32, ) -> Weight; + fn instruction_I64Rotr(r: u32, ) -> Weight; + fn instruction_I64ExtendSI32(r: u32, ) -> Weight; + fn instruction_I32WrapI64(r: u32, ) -> Weight; + fn instruction_F64Eq(r: u32, ) -> Weight; + fn instruction_F64Ne(r: u32, ) -> Weight; + fn instruction_F64Lt(r: u32, ) -> Weight; + fn instruction_F64Gt(r: u32, ) -> Weight; + fn instruction_F64Le(r: u32, ) -> Weight; + fn instruction_F64Ge(r: u32, ) -> Weight; + fn instruction_F64Abs(r: u32, ) -> Weight; + fn instruction_F64Neg(r: u32, ) -> Weight; + fn instruction_F64Ceil(r: u32, ) -> Weight; + fn instruction_F64Floor(r: u32, ) -> Weight; + fn instruction_F64Trunc(r: u32, ) -> Weight; + fn instruction_F64Nearest(r: u32, ) -> Weight; + fn instruction_F64Sqrt(r: u32, ) -> Weight; + fn instruction_F64Add(r: u32, ) -> Weight; + fn instruction_F64Sub(r: u32, ) -> Weight; + fn instruction_F64Mul(r: u32, ) -> Weight; + fn instruction_F64Div(r: u32, ) -> Weight; + fn instruction_F64Min(r: u32, ) -> Weight; + fn instruction_F64Max(r: u32, ) -> Weight; + fn instruction_F64Copysign(r: u32, ) -> Weight; + fn instruction_Select(r: u32, ) -> Weight; + fn instruction_If(r: u32, ) -> Weight; + fn instruction_Else(r: u32, ) -> Weight; + fn instruction_GetLocal(r: u32, ) -> Weight; + fn instruction_SetLocal(r: u32, ) -> Weight; + fn instruction_TeeLocal(r: u32, ) -> Weight; + fn instruction_GetGlobal(r: u32, ) -> Weight; + fn instruction_SetGlobal(r: u32, ) -> Weight; + fn instruction_CurrentMemory(r: u32, ) -> Weight; + fn instruction_GrowMemory(r: u32, ) -> Weight; + fn instruction_Br(r: u32, ) -> Weight; + fn instruction_BrIf(r: u32, ) -> Weight; + fn instruction_BrTable(r: u32, ) -> Weight; + fn instruction_BrTable_per_elem(s: u32, ) -> Weight; + fn instruction_Call(r: u32, ) -> Weight; + fn instruction_CallIndirect(r: u32, ) -> Weight; +} + +/// Weights for cosmwasm using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Cosmwasm CodeHashToId (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Cosmwasm CurrentCodeId (r:1 w:1) + // Storage: Cosmwasm PristineCode (r:0 w:1) + // Storage: Cosmwasm InstrumentedCode (r:0 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:0 w:1) + /// The range of component `n` is `[1, 514288]`. + fn upload(n: u32, ) -> Weight { + Weight::from_parts(19_213_176 as u64, 0) + // Standard Error: 134 + .saturating_add(Weight::from_parts(51_744 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(6 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CurrentNonce (r:1 w:1) + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn instantiate(n: u32, ) -> Weight { + Weight::from_parts(271_822_175 as u64, 0) + // Standard Error: 214_443 + .saturating_add(Weight::from_parts(21_368_222 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(6 as u64)) + .saturating_add(T::DbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn execute(n: u32, ) -> Weight { + Weight::from_parts(240_096_021 as u64, 0) + // Standard Error: 217_568 + .saturating_add(Weight::from_parts(21_911_965 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:2 w:2) + // Storage: Cosmwasm InstrumentedCode (r:2 w:1) + // Storage: Cosmwasm PristineCode (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Cosmwasm CodeHashToId (r:0 w:1) + fn migrate() -> Weight { + Weight::from_parts(412_229_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(9 as u64)) + .saturating_add(T::DbWeight::get().writes(7 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + fn update_admin() -> Weight { + Weight::from_parts(198_400_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: unknown [0xe9a804b2e527fd3601d2ffc0bb023cd668656c6c6f20776f726c64] (r:1 w:0) + fn db_read() -> Weight { + Weight::from_parts(13_244_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0xe9a804b2e527fd3601d2ffc0bb023cd668656c6c6f20776f726c64] (r:1 w:0) + fn db_read_other_contract() -> Weight { + Weight::from_parts(12_452_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:1 w:1) + fn db_write() -> Weight { + Weight::from_parts(12_930_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + fn db_scan() -> Weight { + Weight::from_parts(2_732_000 as u64, 0) + } + // Storage: unknown [0x] (r:1 w:0) + fn db_next() -> Weight { + Weight::from_parts(11_524_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:0 w:1) + fn db_remove() -> Weight { + Weight::from_parts(6_755_000 as u64, 0) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Tokens Accounts (r:1 w:0) + fn balance() -> Weight { + Weight::from_parts(8_386_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn transfer(n: u32, ) -> Weight { + Weight::from_parts(27_900_925 as u64, 0) + // Standard Error: 149_091 + .saturating_add(Weight::from_parts(20_291_322 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + fn set_contract_meta() -> Weight { + Weight::from_parts(11_590_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + fn running_contract_meta() -> Weight { + Weight::from_parts(3_475_000 as u64, 0) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn contract_meta() -> Weight { + Weight::from_parts(11_376_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + fn addr_validate() -> Weight { + Weight::from_parts(2_342_000 as u64, 0) + } + fn addr_canonicalize() -> Weight { + Weight::from_parts(2_314_000 as u64, 0) + } + fn addr_humanize() -> Weight { + Weight::from_parts(397_000 as u64, 0) + } + fn secp256k1_recover_pubkey() -> Weight { + Weight::from_parts(86_729_000 as u64, 0) + } + fn secp256k1_verify() -> Weight { + Weight::from_parts(33_438_000 as u64, 0) + } + fn ed25519_verify() -> Weight { + Weight::from_parts(43_648_000 as u64, 0) + } + fn ed25519_batch_verify() -> Weight { + Weight::from_parts(60_481_000 as u64, 0) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CurrentNonce (r:1 w:1) + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn continue_instantiate(n: u32, ) -> Weight { + Weight::from_parts(232_216_094 as u64, 0) + // Standard Error: 194_340 + .saturating_add(Weight::from_parts(23_770_105 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + /// The range of component `n` is `[0, 23]`. + fn continue_execute(n: u32, ) -> Weight { + Weight::from_parts(168_885_098 as u64, 0) + // Standard Error: 693_834 + .saturating_add(Weight::from_parts(8_982_880 as u64, 0).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_migrate() -> Weight { + Weight::from_parts(421_685_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_query() -> Weight { + Weight::from_parts(394_101_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_reply() -> Weight { + Weight::from_parts(417_714_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:0) + fn query_contract_info() -> Weight { + Weight::from_parts(41_799_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:0) + fn query_code_info() -> Weight { + Weight::from_parts(26_298_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:1 w:0) + fn query_raw() -> Weight { + Weight::from_parts(48_135_000 as u64, 0) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Const(r: u32, ) -> Weight { + Weight::from_parts(8_169_317 as u64, 0) + // Standard Error: 55_178 + .saturating_add(Weight::from_parts(1_249_208 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Const(r: u32, ) -> Weight { + Weight::from_parts(5_802_879 as u64, 0) + // Standard Error: 10_779 + .saturating_add(Weight::from_parts(791_357 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Load(r: u32, ) -> Weight { + Weight::from_parts(7_303_250 as u64, 0) + // Standard Error: 15_910 + .saturating_add(Weight::from_parts(1_585_639 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Load(r: u32, ) -> Weight { + Weight::from_parts(6_132_192 as u64, 0) + // Standard Error: 18_934 + .saturating_add(Weight::from_parts(1_382_263 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Store(r: u32, ) -> Weight { + Weight::from_parts(12_845_591 as u64, 0) + // Standard Error: 31_110 + .saturating_add(Weight::from_parts(2_314_991 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Store(r: u32, ) -> Weight { + Weight::from_parts(6_804_160 as u64, 0) + // Standard Error: 12_843 + .saturating_add(Weight::from_parts(2_023_253 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Eq(r: u32, ) -> Weight { + Weight::from_parts(4_453_882 as u64, 0) + // Standard Error: 7_974 + .saturating_add(Weight::from_parts(1_181_230 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Eqz(r: u32, ) -> Weight { + Weight::from_parts(3_446_407 as u64, 0) + // Standard Error: 11_959 + .saturating_add(Weight::from_parts(887_681 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Ne(r: u32, ) -> Weight { + Weight::from_parts(6_063_996 as u64, 0) + // Standard Error: 9_665 + .saturating_add(Weight::from_parts(1_092_921 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64LtS(r: u32, ) -> Weight { + Weight::from_parts(4_745_660 as u64, 0) + // Standard Error: 7_137 + .saturating_add(Weight::from_parts(1_102_659 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64GtS(r: u32, ) -> Weight { + Weight::from_parts(3_071_888 as u64, 0) + // Standard Error: 14_294 + .saturating_add(Weight::from_parts(1_237_101 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64LeS(r: u32, ) -> Weight { + Weight::from_parts(6_954_024 as u64, 0) + // Standard Error: 16_403 + .saturating_add(Weight::from_parts(1_111_512 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64GeS(r: u32, ) -> Weight { + Weight::from_parts(3_202_448 as u64, 0) + // Standard Error: 12_064 + .saturating_add(Weight::from_parts(1_417_996 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Clz(r: u32, ) -> Weight { + Weight::from_parts(5_883_836 as u64, 0) + // Standard Error: 12_773 + .saturating_add(Weight::from_parts(781_624 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Ctz(r: u32, ) -> Weight { + Weight::from_parts(4_205_570 as u64, 0) + // Standard Error: 10_210 + .saturating_add(Weight::from_parts(818_853 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Popcnt(r: u32, ) -> Weight { + Weight::from_parts(2_201_231 as u64, 0) + // Standard Error: 5_237 + .saturating_add(Weight::from_parts(909_349 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Add(r: u32, ) -> Weight { + Weight::from_parts(4_441_796 as u64, 0) + // Standard Error: 8_294 + .saturating_add(Weight::from_parts(1_235_436 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Sub(r: u32, ) -> Weight { + Weight::from_parts(2_387_532 as u64, 0) + // Standard Error: 11_454 + .saturating_add(Weight::from_parts(1_376_809 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Mul(r: u32, ) -> Weight { + Weight::from_parts(3_066_568 as u64, 0) + // Standard Error: 11_765 + .saturating_add(Weight::from_parts(1_363_852 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64DivS(r: u32, ) -> Weight { + Weight::from_parts(5_968_914 as u64, 0) + // Standard Error: 15_273 + .saturating_add(Weight::from_parts(1_368_413 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64DivU(r: u32, ) -> Weight { + Weight::from_parts(3_357_177 as u64, 0) + // Standard Error: 10_548 + .saturating_add(Weight::from_parts(1_537_345 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64RemS(r: u32, ) -> Weight { + Weight::from_parts(5_123_088 as u64, 0) + // Standard Error: 5_779 + .saturating_add(Weight::from_parts(1_361_710 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64And(r: u32, ) -> Weight { + Weight::from_parts(3_136_147 as u64, 0) + // Standard Error: 10_304 + .saturating_add(Weight::from_parts(1_259_503 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Or(r: u32, ) -> Weight { + Weight::from_parts(531_325 as u64, 0) + // Standard Error: 18_361 + .saturating_add(Weight::from_parts(1_503_381 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Xor(r: u32, ) -> Weight { + Weight::from_parts(7_288_542 as u64, 0) + // Standard Error: 10_854 + .saturating_add(Weight::from_parts(1_410_100 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Shl(r: u32, ) -> Weight { + Weight::from_parts(7_482_983 as u64, 0) + // Standard Error: 9_983 + .saturating_add(Weight::from_parts(1_219_115 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64ShrS(r: u32, ) -> Weight { + Weight::from_parts(4_780_812 as u64, 0) + // Standard Error: 8_707 + .saturating_add(Weight::from_parts(1_291_335 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Rotl(r: u32, ) -> Weight { + Weight::from_parts(916_448 as u64, 0) + // Standard Error: 16_265 + .saturating_add(Weight::from_parts(1_651_295 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Rotr(r: u32, ) -> Weight { + Weight::from_parts(10_328_055 as u64, 0) + // Standard Error: 15_254 + .saturating_add(Weight::from_parts(1_055_478 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64ExtendSI32(r: u32, ) -> Weight { + Weight::from_parts(3_725_409 as u64, 0) + // Standard Error: 12_869 + .saturating_add(Weight::from_parts(760_073 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I32WrapI64(r: u32, ) -> Weight { + Weight::from_parts(3_762_402 as u64, 0) + // Standard Error: 13_522 + .saturating_add(Weight::from_parts(769_000 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Eq(r: u32, ) -> Weight { + Weight::from_parts(3_637_578 as u64, 0) + // Standard Error: 13_582 + .saturating_add(Weight::from_parts(1_153_289 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ne(r: u32, ) -> Weight { + Weight::from_parts(4_031_642 as u64, 0) + // Standard Error: 6_567 + .saturating_add(Weight::from_parts(1_052_076 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Lt(r: u32, ) -> Weight { + Weight::from_parts(4_155_441 as u64, 0) + // Standard Error: 9_183 + .saturating_add(Weight::from_parts(1_083_255 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Gt(r: u32, ) -> Weight { + Weight::from_parts(3_528_744 as u64, 0) + // Standard Error: 4_279 + .saturating_add(Weight::from_parts(1_106_018 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Le(r: u32, ) -> Weight { + Weight::from_parts(3_849_075 as u64, 0) + // Standard Error: 3_592 + .saturating_add(Weight::from_parts(1_095_094 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ge(r: u32, ) -> Weight { + Weight::from_parts(3_402_550 as u64, 0) + // Standard Error: 18_575 + .saturating_add(Weight::from_parts(1_109_272 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Abs(r: u32, ) -> Weight { + Weight::from_parts(3_374_684 as u64, 0) + // Standard Error: 16_401 + .saturating_add(Weight::from_parts(1_040_671 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Neg(r: u32, ) -> Weight { + Weight::from_parts(2_924_978 as u64, 0) + // Standard Error: 12_468 + .saturating_add(Weight::from_parts(787_836 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ceil(r: u32, ) -> Weight { + Weight::from_parts(3_487_025 as u64, 0) + // Standard Error: 13_028 + .saturating_add(Weight::from_parts(1_070_649 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Floor(r: u32, ) -> Weight { + Weight::from_parts(3_219_530 as u64, 0) + // Standard Error: 4_195 + .saturating_add(Weight::from_parts(1_096_518 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Trunc(r: u32, ) -> Weight { + Weight::from_parts(3_768_631 as u64, 0) + // Standard Error: 9_045 + .saturating_add(Weight::from_parts(1_087_146 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Nearest(r: u32, ) -> Weight { + Weight::from_parts(4_733_585 as u64, 0) + // Standard Error: 6_538 + .saturating_add(Weight::from_parts(1_415_764 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Sqrt(r: u32, ) -> Weight { + Weight::from_parts(3_963_692 as u64, 0) + // Standard Error: 7_846 + .saturating_add(Weight::from_parts(1_094_319 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Add(r: u32, ) -> Weight { + Weight::from_parts(5_158_468 as u64, 0) + // Standard Error: 10_261 + .saturating_add(Weight::from_parts(1_042_998 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Sub(r: u32, ) -> Weight { + Weight::from_parts(3_306_395 as u64, 0) + // Standard Error: 21_475 + .saturating_add(Weight::from_parts(1_191_614 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Mul(r: u32, ) -> Weight { + Weight::from_parts(939_907 as u64, 0) + // Standard Error: 12_978 + .saturating_add(Weight::from_parts(1_325_608 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Div(r: u32, ) -> Weight { + Weight::from_parts(6_419_089 as u64, 0) + // Standard Error: 6_276 + .saturating_add(Weight::from_parts(1_082_816 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Min(r: u32, ) -> Weight { + Weight::from_parts(3_631_174 as u64, 0) + // Standard Error: 3_841 + .saturating_add(Weight::from_parts(1_326_480 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Max(r: u32, ) -> Weight { + Weight::from_parts(4_000_016 as u64, 0) + // Standard Error: 18_339 + .saturating_add(Weight::from_parts(1_401_801 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Copysign(r: u32, ) -> Weight { + Weight::from_parts(3_512_972 as u64, 0) + // Standard Error: 4_287 + .saturating_add(Weight::from_parts(1_085_900 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Select(r: u32, ) -> Weight { + Weight::from_parts(3_626_360 as u64, 0) + // Standard Error: 4_330 + .saturating_add(Weight::from_parts(1_359_540 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_If(r: u32, ) -> Weight { + Weight::from_parts(3_134_797 as u64, 0) + // Standard Error: 3_704 + .saturating_add(Weight::from_parts(640_563 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Else(r: u32, ) -> Weight { + Weight::from_parts(3_532_948 as u64, 0) + // Standard Error: 8_800 + .saturating_add(Weight::from_parts(1_016_347 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_GetLocal(r: u32, ) -> Weight { + Weight::from_parts(3_523_772 as u64, 0) + // Standard Error: 7_456 + .saturating_add(Weight::from_parts(459_326 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_SetLocal(r: u32, ) -> Weight { + Weight::from_parts(3_197_430 as u64, 0) + // Standard Error: 1_933 + .saturating_add(Weight::from_parts(505_514 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_TeeLocal(r: u32, ) -> Weight { + Weight::from_parts(2_715_808 as u64, 0) + // Standard Error: 1_479 + .saturating_add(Weight::from_parts(1_207 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_GetGlobal(_r: u32, ) -> Weight { + Weight::from_parts(2_988_269 as u64, 0) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_SetGlobal(r: u32, ) -> Weight { + Weight::from_parts(2_798_290 as u64, 0) + // Standard Error: 884 + .saturating_add(Weight::from_parts(2_324 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_CurrentMemory(r: u32, ) -> Weight { + Weight::from_parts(4_488_888 as u64, 0) + // Standard Error: 13_180 + .saturating_add(Weight::from_parts(1_024_150 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 3]`. + fn instruction_GrowMemory(r: u32, ) -> Weight { + Weight::from_parts(2_482_000 as u64, 0) + // Standard Error: 8_606_292 + .saturating_add(Weight::from_parts(1_469_027_660 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Br(r: u32, ) -> Weight { + Weight::from_parts(2_719_253 as u64, 0) + // Standard Error: 4_513 + .saturating_add(Weight::from_parts(587_800 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_BrIf(r: u32, ) -> Weight { + Weight::from_parts(1_877_983 as u64, 0) + // Standard Error: 9_463 + .saturating_add(Weight::from_parts(957_835 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_BrTable(r: u32, ) -> Weight { + Weight::from_parts(1_037_171 as u64, 0) + // Standard Error: 20_753 + .saturating_add(Weight::from_parts(1_476_015 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `s` is `[1, 50]`. + fn instruction_BrTable_per_elem(s: u32, ) -> Weight { + Weight::from_parts(3_959_230 as u64, 0) + // Standard Error: 2_907 + .saturating_add(Weight::from_parts(16_620 as u64, 0).saturating_mul(s as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Call(r: u32, ) -> Weight { + Weight::from_parts(4_017_610 as u64, 0) + // Standard Error: 13_655 + .saturating_add(Weight::from_parts(6_481_999 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_CallIndirect(r: u32, ) -> Weight { + Weight::from_parts(4_945_751 as u64, 0) + // Standard Error: 20_961 + .saturating_add(Weight::from_parts(8_380_434 as u64, 0).saturating_mul(r as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Cosmwasm CodeHashToId (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Cosmwasm CurrentCodeId (r:1 w:1) + // Storage: Cosmwasm PristineCode (r:0 w:1) + // Storage: Cosmwasm InstrumentedCode (r:0 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:0 w:1) + /// The range of component `n` is `[1, 514288]`. + fn upload(n: u32, ) -> Weight { + Weight::from_parts(19_213_176 as u64, 0) + // Standard Error: 134 + .saturating_add(Weight::from_parts(51_744 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(6 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CurrentNonce (r:1 w:1) + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn instantiate(n: u32, ) -> Weight { + Weight::from_parts(271_822_175 as u64, 0) + // Standard Error: 214_443 + .saturating_add(Weight::from_parts(21_368_222 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads(6 as u64)) + .saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn execute(n: u32, ) -> Weight { + Weight::from_parts(240_096_021 as u64, 0) + // Standard Error: 217_568 + .saturating_add(Weight::from_parts(21_911_965 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:2 w:2) + // Storage: Cosmwasm InstrumentedCode (r:2 w:1) + // Storage: Cosmwasm PristineCode (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Cosmwasm CodeHashToId (r:0 w:1) + fn migrate() -> Weight { + Weight::from_parts(412_229_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(9 as u64)) + .saturating_add(RocksDbWeight::get().writes(7 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm InstrumentedCode (r:1 w:0) + fn update_admin() -> Weight { + Weight::from_parts(198_400_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: unknown [0xe9a804b2e527fd3601d2ffc0bb023cd668656c6c6f20776f726c64] (r:1 w:0) + fn db_read() -> Weight { + Weight::from_parts(13_244_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0xe9a804b2e527fd3601d2ffc0bb023cd668656c6c6f20776f726c64] (r:1 w:0) + fn db_read_other_contract() -> Weight { + Weight::from_parts(12_452_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:1 w:1) + fn db_write() -> Weight { + Weight::from_parts(12_930_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + fn db_scan() -> Weight { + Weight::from_parts(2_732_000 as u64, 0) + } + // Storage: unknown [0x] (r:1 w:0) + fn db_next() -> Weight { + Weight::from_parts(11_524_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:0 w:1) + fn db_remove() -> Weight { + Weight::from_parts(6_755_000 as u64, 0) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Tokens Accounts (r:1 w:0) + fn balance() -> Weight { + Weight::from_parts(8_386_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn transfer(n: u32, ) -> Weight { + Weight::from_parts(27_900_925 as u64, 0) + // Standard Error: 149_091 + .saturating_add(Weight::from_parts(20_291_322 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + fn set_contract_meta() -> Weight { + Weight::from_parts(11_590_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + fn running_contract_meta() -> Weight { + Weight::from_parts(3_475_000 as u64, 0) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn contract_meta() -> Weight { + Weight::from_parts(11_376_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + fn addr_validate() -> Weight { + Weight::from_parts(2_342_000 as u64, 0) + } + fn addr_canonicalize() -> Weight { + Weight::from_parts(2_314_000 as u64, 0) + } + fn addr_humanize() -> Weight { + Weight::from_parts(397_000 as u64, 0) + } + fn secp256k1_recover_pubkey() -> Weight { + Weight::from_parts(86_729_000 as u64, 0) + } + fn secp256k1_verify() -> Weight { + Weight::from_parts(33_438_000 as u64, 0) + } + fn ed25519_verify() -> Weight { + Weight::from_parts(43_648_000 as u64, 0) + } + fn ed25519_batch_verify() -> Weight { + Weight::from_parts(60_481_000 as u64, 0) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:1) + // Storage: Cosmwasm ContractToInfo (r:1 w:1) + // Storage: Cosmwasm CurrentNonce (r:1 w:1) + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: Tokens Accounts (r:2 w:2) + /// The range of component `n` is `[0, 23]`. + fn continue_instantiate(n: u32, ) -> Weight { + Weight::from_parts(232_216_094 as u64, 0) + // Standard Error: 194_340 + .saturating_add(Weight::from_parts(23_770_105 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(n as u64))) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(n as u64))) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + /// The range of component `n` is `[0, 23]`. + fn continue_execute(n: u32, ) -> Weight { + Weight::from_parts(168_885_098 as u64, 0) + // Standard Error: 693_834 + .saturating_add(Weight::from_parts(8_982_880 as u64, 0).saturating_mul(n as u64)) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_migrate() -> Weight { + Weight::from_parts(421_685_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_query() -> Weight { + Weight::from_parts(394_101_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + } + // Storage: Timestamp Now (r:1 w:0) + // Storage: unknown [0x3a65787472696e7369635f696e646578] (r:1 w:0) + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + fn continue_reply() -> Weight { + Weight::from_parts(417_714_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:0) + fn query_contract_info() -> Weight { + Weight::from_parts(41_799_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: Cosmwasm CodeIdToInfo (r:1 w:0) + fn query_code_info() -> Weight { + Weight::from_parts(26_298_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: Cosmwasm ContractToInfo (r:1 w:0) + // Storage: unknown [0x46fb7408d4f285228f4af516ea25851b68656c6c6f] (r:1 w:0) + fn query_raw() -> Weight { + Weight::from_parts(48_135_000 as u64, 0) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Const(r: u32, ) -> Weight { + Weight::from_parts(8_169_317 as u64, 0) + // Standard Error: 55_178 + .saturating_add(Weight::from_parts(1_249_208 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Const(r: u32, ) -> Weight { + Weight::from_parts(5_802_879 as u64, 0) + // Standard Error: 10_779 + .saturating_add(Weight::from_parts(791_357 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Load(r: u32, ) -> Weight { + Weight::from_parts(7_303_250 as u64, 0) + // Standard Error: 15_910 + .saturating_add(Weight::from_parts(1_585_639 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Load(r: u32, ) -> Weight { + Weight::from_parts(6_132_192 as u64, 0) + // Standard Error: 18_934 + .saturating_add(Weight::from_parts(1_382_263 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Store(r: u32, ) -> Weight { + Weight::from_parts(12_845_591 as u64, 0) + // Standard Error: 31_110 + .saturating_add(Weight::from_parts(2_314_991 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Store(r: u32, ) -> Weight { + Weight::from_parts(6_804_160 as u64, 0) + // Standard Error: 12_843 + .saturating_add(Weight::from_parts(2_023_253 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Eq(r: u32, ) -> Weight { + Weight::from_parts(4_453_882 as u64, 0) + // Standard Error: 7_974 + .saturating_add(Weight::from_parts(1_181_230 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Eqz(r: u32, ) -> Weight { + Weight::from_parts(3_446_407 as u64, 0) + // Standard Error: 11_959 + .saturating_add(Weight::from_parts(887_681 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Ne(r: u32, ) -> Weight { + Weight::from_parts(6_063_996 as u64, 0) + // Standard Error: 9_665 + .saturating_add(Weight::from_parts(1_092_921 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64LtS(r: u32, ) -> Weight { + Weight::from_parts(4_745_660 as u64, 0) + // Standard Error: 7_137 + .saturating_add(Weight::from_parts(1_102_659 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64GtS(r: u32, ) -> Weight { + Weight::from_parts(3_071_888 as u64, 0) + // Standard Error: 14_294 + .saturating_add(Weight::from_parts(1_237_101 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64LeS(r: u32, ) -> Weight { + Weight::from_parts(6_954_024 as u64, 0) + // Standard Error: 16_403 + .saturating_add(Weight::from_parts(1_111_512 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64GeS(r: u32, ) -> Weight { + Weight::from_parts(3_202_448 as u64, 0) + // Standard Error: 12_064 + .saturating_add(Weight::from_parts(1_417_996 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Clz(r: u32, ) -> Weight { + Weight::from_parts(5_883_836 as u64, 0) + // Standard Error: 12_773 + .saturating_add(Weight::from_parts(781_624 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Ctz(r: u32, ) -> Weight { + Weight::from_parts(4_205_570 as u64, 0) + // Standard Error: 10_210 + .saturating_add(Weight::from_parts(818_853 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Popcnt(r: u32, ) -> Weight { + Weight::from_parts(2_201_231 as u64, 0) + // Standard Error: 5_237 + .saturating_add(Weight::from_parts(909_349 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Add(r: u32, ) -> Weight { + Weight::from_parts(4_441_796 as u64, 0) + // Standard Error: 8_294 + .saturating_add(Weight::from_parts(1_235_436 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Sub(r: u32, ) -> Weight { + Weight::from_parts(2_387_532 as u64, 0) + // Standard Error: 11_454 + .saturating_add(Weight::from_parts(1_376_809 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Mul(r: u32, ) -> Weight { + Weight::from_parts(3_066_568 as u64, 0) + // Standard Error: 11_765 + .saturating_add(Weight::from_parts(1_363_852 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64DivS(r: u32, ) -> Weight { + Weight::from_parts(5_968_914 as u64, 0) + // Standard Error: 15_273 + .saturating_add(Weight::from_parts(1_368_413 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64DivU(r: u32, ) -> Weight { + Weight::from_parts(3_357_177 as u64, 0) + // Standard Error: 10_548 + .saturating_add(Weight::from_parts(1_537_345 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64RemS(r: u32, ) -> Weight { + Weight::from_parts(5_123_088 as u64, 0) + // Standard Error: 5_779 + .saturating_add(Weight::from_parts(1_361_710 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64And(r: u32, ) -> Weight { + Weight::from_parts(3_136_147 as u64, 0) + // Standard Error: 10_304 + .saturating_add(Weight::from_parts(1_259_503 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Or(r: u32, ) -> Weight { + Weight::from_parts(531_325 as u64, 0) + // Standard Error: 18_361 + .saturating_add(Weight::from_parts(1_503_381 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Xor(r: u32, ) -> Weight { + Weight::from_parts(7_288_542 as u64, 0) + // Standard Error: 10_854 + .saturating_add(Weight::from_parts(1_410_100 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Shl(r: u32, ) -> Weight { + Weight::from_parts(7_482_983 as u64, 0) + // Standard Error: 9_983 + .saturating_add(Weight::from_parts(1_219_115 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64ShrS(r: u32, ) -> Weight { + Weight::from_parts(4_780_812 as u64, 0) + // Standard Error: 8_707 + .saturating_add(Weight::from_parts(1_291_335 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Rotl(r: u32, ) -> Weight { + Weight::from_parts(916_448 as u64, 0) + // Standard Error: 16_265 + .saturating_add(Weight::from_parts(1_651_295 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64Rotr(r: u32, ) -> Weight { + Weight::from_parts(10_328_055 as u64, 0) + // Standard Error: 15_254 + .saturating_add(Weight::from_parts(1_055_478 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I64ExtendSI32(r: u32, ) -> Weight { + Weight::from_parts(3_725_409 as u64, 0) + // Standard Error: 12_869 + .saturating_add(Weight::from_parts(760_073 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_I32WrapI64(r: u32, ) -> Weight { + Weight::from_parts(3_762_402 as u64, 0) + // Standard Error: 13_522 + .saturating_add(Weight::from_parts(769_000 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Eq(r: u32, ) -> Weight { + Weight::from_parts(3_637_578 as u64, 0) + // Standard Error: 13_582 + .saturating_add(Weight::from_parts(1_153_289 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ne(r: u32, ) -> Weight { + Weight::from_parts(4_031_642 as u64, 0) + // Standard Error: 6_567 + .saturating_add(Weight::from_parts(1_052_076 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Lt(r: u32, ) -> Weight { + Weight::from_parts(4_155_441 as u64, 0) + // Standard Error: 9_183 + .saturating_add(Weight::from_parts(1_083_255 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Gt(r: u32, ) -> Weight { + Weight::from_parts(3_528_744 as u64, 0) + // Standard Error: 4_279 + .saturating_add(Weight::from_parts(1_106_018 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Le(r: u32, ) -> Weight { + Weight::from_parts(3_849_075 as u64, 0) + // Standard Error: 3_592 + .saturating_add(Weight::from_parts(1_095_094 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ge(r: u32, ) -> Weight { + Weight::from_parts(3_402_550 as u64, 0) + // Standard Error: 18_575 + .saturating_add(Weight::from_parts(1_109_272 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Abs(r: u32, ) -> Weight { + Weight::from_parts(3_374_684 as u64, 0) + // Standard Error: 16_401 + .saturating_add(Weight::from_parts(1_040_671 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Neg(r: u32, ) -> Weight { + Weight::from_parts(2_924_978 as u64, 0) + // Standard Error: 12_468 + .saturating_add(Weight::from_parts(787_836 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Ceil(r: u32, ) -> Weight { + Weight::from_parts(3_487_025 as u64, 0) + // Standard Error: 13_028 + .saturating_add(Weight::from_parts(1_070_649 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Floor(r: u32, ) -> Weight { + Weight::from_parts(3_219_530 as u64, 0) + // Standard Error: 4_195 + .saturating_add(Weight::from_parts(1_096_518 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Trunc(r: u32, ) -> Weight { + Weight::from_parts(3_768_631 as u64, 0) + // Standard Error: 9_045 + .saturating_add(Weight::from_parts(1_087_146 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Nearest(r: u32, ) -> Weight { + Weight::from_parts(4_733_585 as u64, 0) + // Standard Error: 6_538 + .saturating_add(Weight::from_parts(1_415_764 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Sqrt(r: u32, ) -> Weight { + Weight::from_parts(3_963_692 as u64, 0) + // Standard Error: 7_846 + .saturating_add(Weight::from_parts(1_094_319 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Add(r: u32, ) -> Weight { + Weight::from_parts(5_158_468 as u64, 0) + // Standard Error: 10_261 + .saturating_add(Weight::from_parts(1_042_998 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Sub(r: u32, ) -> Weight { + Weight::from_parts(3_306_395 as u64, 0) + // Standard Error: 21_475 + .saturating_add(Weight::from_parts(1_191_614 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Mul(r: u32, ) -> Weight { + Weight::from_parts(939_907 as u64, 0) + // Standard Error: 12_978 + .saturating_add(Weight::from_parts(1_325_608 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Div(r: u32, ) -> Weight { + Weight::from_parts(6_419_089 as u64, 0) + // Standard Error: 6_276 + .saturating_add(Weight::from_parts(1_082_816 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Min(r: u32, ) -> Weight { + Weight::from_parts(3_631_174 as u64, 0) + // Standard Error: 3_841 + .saturating_add(Weight::from_parts(1_326_480 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Max(r: u32, ) -> Weight { + Weight::from_parts(4_000_016 as u64, 0) + // Standard Error: 18_339 + .saturating_add(Weight::from_parts(1_401_801 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_F64Copysign(r: u32, ) -> Weight { + Weight::from_parts(3_512_972 as u64, 0) + // Standard Error: 4_287 + .saturating_add(Weight::from_parts(1_085_900 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Select(r: u32, ) -> Weight { + Weight::from_parts(3_626_360 as u64, 0) + // Standard Error: 4_330 + .saturating_add(Weight::from_parts(1_359_540 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_If(r: u32, ) -> Weight { + Weight::from_parts(3_134_797 as u64, 0) + // Standard Error: 3_704 + .saturating_add(Weight::from_parts(640_563 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Else(r: u32, ) -> Weight { + Weight::from_parts(3_532_948 as u64, 0) + // Standard Error: 8_800 + .saturating_add(Weight::from_parts(1_016_347 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_GetLocal(r: u32, ) -> Weight { + Weight::from_parts(3_523_772 as u64, 0) + // Standard Error: 7_456 + .saturating_add(Weight::from_parts(459_326 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_SetLocal(r: u32, ) -> Weight { + Weight::from_parts(3_197_430 as u64, 0) + // Standard Error: 1_933 + .saturating_add(Weight::from_parts(505_514 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_TeeLocal(r: u32, ) -> Weight { + Weight::from_parts(2_715_808 as u64, 0) + // Standard Error: 1_479 + .saturating_add(Weight::from_parts(1_207 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_GetGlobal(_r: u32, ) -> Weight { + Weight::from_parts(2_988_269 as u64, 0) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_SetGlobal(r: u32, ) -> Weight { + Weight::from_parts(2_798_290 as u64, 0) + // Standard Error: 884 + .saturating_add(Weight::from_parts(2_324 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_CurrentMemory(r: u32, ) -> Weight { + Weight::from_parts(4_488_888 as u64, 0) + // Standard Error: 13_180 + .saturating_add(Weight::from_parts(1_024_150 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 3]`. + fn instruction_GrowMemory(r: u32, ) -> Weight { + Weight::from_parts(2_482_000 as u64, 0) + // Standard Error: 8_606_292 + .saturating_add(Weight::from_parts(1_469_027_660 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Br(r: u32, ) -> Weight { + Weight::from_parts(2_719_253 as u64, 0) + // Standard Error: 4_513 + .saturating_add(Weight::from_parts(587_800 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_BrIf(r: u32, ) -> Weight { + Weight::from_parts(1_877_983 as u64, 0) + // Standard Error: 9_463 + .saturating_add(Weight::from_parts(957_835 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_BrTable(r: u32, ) -> Weight { + Weight::from_parts(1_037_171 as u64, 0) + // Standard Error: 20_753 + .saturating_add(Weight::from_parts(1_476_015 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `s` is `[1, 50]`. + fn instruction_BrTable_per_elem(s: u32, ) -> Weight { + Weight::from_parts(3_959_230 as u64, 0) + // Standard Error: 2_907 + .saturating_add(Weight::from_parts(16_620 as u64, 0).saturating_mul(s as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_Call(r: u32, ) -> Weight { + Weight::from_parts(4_017_610 as u64, 0) + // Standard Error: 13_655 + .saturating_add(Weight::from_parts(6_481_999 as u64, 0).saturating_mul(r as u64)) + } + /// The range of component `r` is `[0, 50]`. + fn instruction_CallIndirect(r: u32, ) -> Weight { + Weight::from_parts(4_945_751 as u64, 0) + // Standard Error: 20_961 + .saturating_add(Weight::from_parts(8_380_434 as u64, 0).saturating_mul(r as u64)) + } +} From 2a85b27912effad541aa60d720c62b4fe445b458 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 21:58:50 +0900 Subject: [PATCH 036/133] feat: Add pallet-cosmos-x-wasm --- Cargo.toml | 2 + frame/cosmos/x/wasm/Cargo.toml | 39 ++++ frame/cosmos/x/wasm/src/lib.rs | 22 ++ frame/cosmos/x/wasm/src/msgs.rs | 380 ++++++++++++++++++++++++++++++++ 4 files changed, 443 insertions(+) create mode 100644 frame/cosmos/x/wasm/Cargo.toml create mode 100644 frame/cosmos/x/wasm/src/lib.rs create mode 100644 frame/cosmos/x/wasm/src/msgs.rs diff --git a/Cargo.toml b/Cargo.toml index 03bd7cc5..1c10db85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "frame/cosmos/x/auth/signing", "frame/cosmos/x/bank", "frame/cosmos/x/bank/types", + "frame/cosmos/x/wasm", "frame/cosmos/x/wasm/types", "frame/cosmwasm", "frame/cosmwasm/rpc", @@ -51,6 +52,7 @@ pallet-cosmos-x-auth-migrations = { path = "frame/cosmos/x/auth/migrations", def pallet-cosmos-x-auth-signing = { path = "frame/cosmos/x/auth/signing", default-features = false } pallet-cosmos-x-bank = { path = "frame/cosmos/x/bank", default-features = false } pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } +pallet-cosmos-x-wasm = { path = "frame/cosmos/x/wasm", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-cosmwasm = { path = "frame/cosmwasm", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/cosmos/x/wasm/Cargo.toml b/frame/cosmos/x/wasm/Cargo.toml new file mode 100644 index 00000000..836bf860 --- /dev/null +++ b/frame/cosmos/x/wasm/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "pallet-cosmos-x-wasm" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +core2 = { version = "0.4.0", default-features = false, features = ["alloc"] } +cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ + "cosmwasm", +] } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +hex = { version = "0.4.3", default-features = false } +libflate = { version = "2.1.0", default-features = false } +pallet-cosmos = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-wasm-types = { workspace = true, default-features = false } +pallet-cosmwasm = { workspace = true, default-features = false } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std"] +std = [ + "core2/std", + "cosmos-sdk-proto/std", + "frame-support/std", + "hex/std", + "libflate/std", + "pallet-cosmos/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-wasm-types/std", + "pallet-cosmwasm/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/frame/cosmos/x/wasm/src/lib.rs b/frame/cosmos/x/wasm/src/lib.rs new file mode 100644 index 00000000..ca2d0f7c --- /dev/null +++ b/frame/cosmos/x/wasm/src/lib.rs @@ -0,0 +1,22 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod msgs; diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs new file mode 100644 index 00000000..bd76ae21 --- /dev/null +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -0,0 +1,380 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::{marker::PhantomData, str::FromStr}; +use core2::io::Read; +use cosmos_sdk_proto::{ + cosmos::base::v1beta1::Coin, + cosmwasm::wasm::v1::{ + MsgExecuteContract, MsgInstantiateContract2, MsgMigrateContract, MsgStoreCode, + MsgUpdateAdmin, + }, + prost::Message, + Any, +}; +use frame_support::ensure; +use libflate::gzip::Decoder; +use pallet_cosmos::AddressMapping; +use pallet_cosmos_types::{ + address::acc_address_from_bech32, + context, + errors::{CosmosError, RootError}, + events::{traits::EventManager, CosmosEvent, EventAttribute}, + gas::traits::GasMeter, + msgservice::MsgHandler, +}; +use pallet_cosmos_x_wasm_types::{ + errors::WasmError, + events::{ + ATTRIBUTE_KEY_CHECKSUM, ATTRIBUTE_KEY_CODE_ID, ATTRIBUTE_KEY_CONTRACT_ADDR, + ATTRIBUTE_KEY_NEW_ADMIN, EVENT_TYPE_EXECUTE, EVENT_TYPE_INSTANTIATE, EVENT_TYPE_MIGRATE, + EVENT_TYPE_STORE_CODE, EVENT_TYPE_UPDATE_CONTRACT_ADMIN, + }, +}; +use pallet_cosmwasm::{ + runtimes::vm::InitialStorageMutability, + types::{ + CodeIdentifier, ContractCodeOf, ContractLabelOf, ContractMessageOf, ContractSaltOf, FundsOf, + }, +}; +use sp_core::H160; +use sp_runtime::{traits::Convert, SaturatedConversion}; + +pub struct MsgStoreCodeHandler(PhantomData); + +impl Default for MsgStoreCodeHandler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl MsgHandler for MsgStoreCodeHandler +where + T: pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + // TODO: Add gas metering + let MsgStoreCode { sender, wasm_byte_code, instantiate_permission: _ } = + MsgStoreCode::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; + + ensure!(!sender.is_empty(), WasmError::Empty); + let (_hrp, address_raw) = + acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); + let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + let mut decoder = Decoder::new(&wasm_byte_code[..]).map_err(|_| WasmError::CreateFailed)?; + let mut decoded_code = Vec::new(); + decoder.read_to_end(&mut decoded_code).map_err(|_| WasmError::CreateFailed)?; + let code: ContractCodeOf = + decoded_code.try_into().map_err(|_| WasmError::CreateFailed)?; + + let (code_hash, code_id) = pallet_cosmwasm::Pallet::::do_upload(&who, code) + .map_err(|_| WasmError::CreateFailed)?; + + // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm + let msg_event = CosmosEvent { + r#type: EVENT_TYPE_STORE_CODE.into(), + attributes: vec![ + EventAttribute { + key: ATTRIBUTE_KEY_CODE_ID.into(), + value: code_id.to_string().into(), + }, + EventAttribute { + key: ATTRIBUTE_KEY_CHECKSUM.into(), + value: hex::encode(code_hash.0).into(), + }, + ], + }; + ctx.event_manager().emit_event(msg_event); + + Ok(()) + } +} + +pub struct MsgInstantiateContract2Handler(PhantomData); + +impl Default for MsgInstantiateContract2Handler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl MsgHandler for MsgInstantiateContract2Handler +where + T: pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + // TODO: Add gas metering + let MsgInstantiateContract2 { sender, admin, code_id, label, msg, funds, salt, fix_msg: _ } = + MsgInstantiateContract2::decode(&mut &*msg.value) + .map_err(|_| RootError::TxDecodeError)?; + + ensure!(!sender.is_empty(), WasmError::Empty); + let (_hrp, address_raw) = + acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); + let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + let gas = ctx.gas_meter().gas_remaining(); + let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( + gas, + InitialStorageMutability::ReadWrite, + ); + let code_identifier = CodeIdentifier::CodeId(code_id); + + let admin_account = if !admin.is_empty() { + let admin_account = + T::AccountToAddr::convert(admin).map_err(|_| RootError::InvalidAddress)?; + Some(admin_account) + } else { + None + }; + + let salt: ContractSaltOf = salt.try_into().map_err(|_| RootError::TxDecodeError)?; + let label: ContractLabelOf = + label.as_bytes().to_vec().try_into().map_err(|_| RootError::TxDecodeError)?; + let funds = convert_funds::(&funds)?; + let message: ContractMessageOf = msg.try_into().map_err(|_| RootError::TxDecodeError)?; + + let contract = pallet_cosmwasm::Pallet::::do_instantiate( + &mut shared, + who, + code_identifier, + salt, + admin_account, + label, + funds, + message, + ) + .map_err(|_| WasmError::InstantiateFailed)?; + let contract = T::AccountToAddr::convert(contract); + + // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm + let msg_event = CosmosEvent { + r#type: EVENT_TYPE_INSTANTIATE.into(), + attributes: vec![ + EventAttribute { key: ATTRIBUTE_KEY_CONTRACT_ADDR.into(), value: contract.into() }, + EventAttribute { + key: ATTRIBUTE_KEY_CODE_ID.into(), + value: code_id.to_string().into(), + }, + ], + }; + ctx.event_manager().emit_event(msg_event); + + Ok(()) + } +} + +pub struct MsgExecuteContractHandler(PhantomData); + +impl Default for MsgExecuteContractHandler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl MsgHandler for MsgExecuteContractHandler +where + T: pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + // TODO: Add gas metering + let MsgExecuteContract { sender, contract, msg, funds } = + MsgExecuteContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; + + ensure!(!sender.is_empty(), WasmError::Empty); + let (_hrp, address_raw) = + acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); + + let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + let gas = ctx.gas_meter().gas_remaining(); + let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( + gas, + InitialStorageMutability::ReadWrite, + ); + + let contract_account = + T::AccountToAddr::convert(contract.clone()).map_err(|_| RootError::TxDecodeError)?; + let funds: FundsOf = convert_funds::(&funds)?; + let message: ContractMessageOf = msg.try_into().map_err(|_| RootError::TxDecodeError)?; + + pallet_cosmwasm::Pallet::::do_execute( + &mut shared, + who, + contract_account, + funds, + message, + ) + .map_err(|_| WasmError::ExecuteFailed)?; + + // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm + let msg_event = CosmosEvent { + r#type: EVENT_TYPE_EXECUTE.into(), + attributes: vec![EventAttribute { + key: ATTRIBUTE_KEY_CONTRACT_ADDR.into(), + value: contract.into(), + }], + }; + ctx.event_manager().emit_event(msg_event); + + Ok(()) + } +} + +fn convert_funds(coins: &[Coin]) -> Result, CosmosError> { + let mut funds = FundsOf::::default(); + for coin in coins.iter() { + let asset_id = + T::AssetToDenom::convert(coin.denom.clone()).map_err(|_| RootError::TxDecodeError)?; + let amount = u128::from_str(&coin.amount).map_err(|_| RootError::TxDecodeError)?; + + funds + .try_insert(asset_id, (amount.saturated_into(), true)) + .map_err(|_| RootError::TxDecodeError)?; + } + + Ok(funds) +} + +pub struct MsgMigrateContractHandler(PhantomData); + +impl Default for MsgMigrateContractHandler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl MsgHandler for MsgMigrateContractHandler +where + T: pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + // TODO: Add gas metering + let MsgMigrateContract { sender, contract, code_id, msg } = + MsgMigrateContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; + + ensure!(!sender.is_empty(), WasmError::Empty); + let (_hrp, address_raw) = + acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); + let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + let gas = ctx.gas_meter().gas_remaining(); + let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( + gas, + InitialStorageMutability::ReadWrite, + ); + + let contract_account = + T::AccountToAddr::convert(contract.clone()).map_err(|_| RootError::TxDecodeError)?; + let new_code_identifier = CodeIdentifier::CodeId(code_id); + let message: ContractMessageOf = msg.try_into().map_err(|_| RootError::TxDecodeError)?; + + pallet_cosmwasm::Pallet::::do_migrate( + &mut shared, + who, + contract_account, + new_code_identifier, + message, + ) + .map_err(|_| WasmError::MigrationFailed)?; + + // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm + let msg_event = CosmosEvent { + r#type: EVENT_TYPE_MIGRATE.into(), + attributes: vec![ + EventAttribute { + key: ATTRIBUTE_KEY_CODE_ID.into(), + value: code_id.to_string().into(), + }, + EventAttribute { key: ATTRIBUTE_KEY_CONTRACT_ADDR.into(), value: contract.into() }, + ], + }; + ctx.event_manager().emit_event(msg_event); + + Ok(()) + } +} + +pub struct MsgUpdateAdminHandler(PhantomData); + +impl Default for MsgUpdateAdminHandler { + fn default() -> Self { + Self(Default::default()) + } +} + +impl MsgHandler for MsgUpdateAdminHandler +where + T: pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + // TODO: Add gas metering + let MsgUpdateAdmin { sender, new_admin, contract } = + MsgUpdateAdmin::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; + + ensure!(!sender.is_empty(), WasmError::Empty); + let (_hrp, address_raw) = + acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); + let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + let gas = ctx.gas_meter().gas_remaining(); + let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( + gas, + InitialStorageMutability::ReadWrite, + ); + + let new_admin_account = if !new_admin.is_empty() { + let new_admin_account = T::AccountToAddr::convert(new_admin.clone()) + .map_err(|_| RootError::InvalidAddress)?; + Some(new_admin_account) + } else { + None + }; + let contract_account = + T::AccountToAddr::convert(contract.clone()).map_err(|_| RootError::TxDecodeError)?; + + pallet_cosmwasm::Pallet::::do_update_admin( + &mut shared, + who, + contract_account, + new_admin_account, + ) + .map_err(|_| WasmError::MigrationFailed)?; + + // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm + let msg_event = CosmosEvent { + r#type: EVENT_TYPE_UPDATE_CONTRACT_ADMIN.into(), + attributes: vec![ + EventAttribute { key: ATTRIBUTE_KEY_CONTRACT_ADDR.into(), value: contract.into() }, + EventAttribute { key: ATTRIBUTE_KEY_NEW_ADMIN.into(), value: new_admin.into() }, + ], + }; + ctx.event_manager().emit_event(msg_event); + + Ok(()) + } +} From 50273f4cdd0a88ead46193ee5dc93850d96fbfd0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 16 Sep 2024 22:00:13 +0900 Subject: [PATCH 037/133] feat: Add default AnteDecorators --- frame/cosmos/x/auth/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frame/cosmos/x/auth/src/lib.rs b/frame/cosmos/x/auth/src/lib.rs index de3e311f..2df5970c 100644 --- a/frame/cosmos/x/auth/src/lib.rs +++ b/frame/cosmos/x/auth/src/lib.rs @@ -23,3 +23,14 @@ pub mod basic; pub mod fee; pub mod msg; pub mod sigverify; + +pub type AnteDecorators = ( + basic::ValidateBasicDecorator, + basic::TxTimeoutHeightDecorator, + basic::ValidateMemoDecorator, + sigverify::ValidateSigCountDecorator, + msg::KnownMsgDecorator, + sigverify::SigVerificationDecorator, + fee::DeductFeeDecorator, + sigverify::IncrementSequenceDecorator, +); From f9beafa3f2689af4d8d95ad1b2cbe196100e3b6a Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 17 Sep 2024 11:41:11 +0900 Subject: [PATCH 038/133] fix: Add client feature to cosmwasm-rpc jsonrpsee dependency --- frame/cosmwasm/rpc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/cosmwasm/rpc/Cargo.toml b/frame/cosmwasm/rpc/Cargo.toml index 0e9aedf2..30be1fa9 100644 --- a/frame/cosmwasm/rpc/Cargo.toml +++ b/frame/cosmwasm/rpc/Cargo.toml @@ -25,4 +25,4 @@ codec = { package = "parity-scale-codec", version = "3.6", default-features = fa ] } # rpc -jsonrpsee = { version = "0.23", features = ["server", "macros"] } +jsonrpsee = { version = "0.22.5", features = ["client", "server", "macros"] } From fd66b64dc18f8351435efffa2ccfae241ed4c6b5 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 17 Sep 2024 18:08:06 +0900 Subject: [PATCH 039/133] chore: Reorganize dependencies in pallet-cosmos-x-auth --- frame/cosmos/x/auth/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/cosmos/x/auth/Cargo.toml b/frame/cosmos/x/auth/Cargo.toml index 3e5c9f91..4c6cf229 100644 --- a/frame/cosmos/x/auth/Cargo.toml +++ b/frame/cosmos/x/auth/Cargo.toml @@ -12,9 +12,9 @@ cosmos-sdk-proto = { version = "0.24.0", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } np-cosmos = { workspace = true, default-features = false } -pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } ripemd = { version = "0.1.3", default-features = false } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -30,9 +30,9 @@ std = [ "frame-support/std", "frame-system/std", "np-cosmos/std", - "pallet-cosmos-x-auth-signing/std", "pallet-cosmos/std", "pallet-cosmos-types/std", + "pallet-cosmos-x-auth-signing/std", "ripemd/std", "sp-core/std", "sp-io/std", From 033742fe2d0e37a28394405b15b5953791bd20d9 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 00:00:52 +0900 Subject: [PATCH 040/133] chore: Add information about msg_responses --- composable/vm/src/system.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composable/vm/src/system.rs b/composable/vm/src/system.rs index 2b4e569d..69cf1cf6 100644 --- a/composable/vm/src/system.rs +++ b/composable/vm/src/system.rs @@ -766,11 +766,11 @@ where (Ok((data, events)), ReplyOn::Always | ReplyOn::Success) => { log::debug!("Commit & Reply"); vm.transaction_commit()?; - // TODO: Handle msg_response + // If your chain is running on CosmWasm 2.0 or higher, msg_responses will be filled. SubCallContinuation::Reply(SubMsgResult::Ok(SubMsgResponse { events, data, - msg_responses: Vec::new(), + msg_responses: vec![], })) }, // If the submessage failed and a reply is required, rollback the From adf5ed02219801656b360e3b57ecec38fae22eae Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 00:02:09 +0900 Subject: [PATCH 041/133] refactor: Remove unused imports from pallet-cosmwasm prelude --- frame/cosmwasm/src/prelude.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frame/cosmwasm/src/prelude.rs b/frame/cosmwasm/src/prelude.rs index 82eb7862..a9768687 100644 --- a/frame/cosmwasm/src/prelude.rs +++ b/frame/cosmwasm/src/prelude.rs @@ -1,6 +1 @@ -pub use alloc::{ - format, - str::FromStr, - string::{String, ToString}, - vec::Vec, -}; +pub use alloc::{format, string::String, vec::Vec}; From 56317fe6f0f00052c975b414d71d00f61a482a7f Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 00:04:40 +0900 Subject: [PATCH 042/133] chore: Add allow deprecated to cosmwasm-vm --- composable/vm/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/composable/vm/src/lib.rs b/composable/vm/src/lib.rs index 6600feac..5e93de90 100644 --- a/composable/vm/src/lib.rs +++ b/composable/vm/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] #![feature(trait_alias)] +#![allow(deprecated)] extern crate alloc; From 1d248ae029ad56b0d9e2bf7959b2680939de683b Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 11:37:48 +0900 Subject: [PATCH 043/133] refactor: Remove reference from src and dest in drain_balance --- frame/babel/src/extensions/unify_account.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index 1b9632a3..50969681 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -62,7 +62,7 @@ impl UnifyAccount { { let address = EthereumAddress::from(public.clone()); let interim = address.clone().into_account_truncating(); - T::DrainBalance::drain_balance(&interim, &who)?; + T::DrainBalance::drain_balance(&interim, who)?; T::AddressMap::try_insert(who.clone(), Address::Ethereum(address)) .map_err(|_| "account unification failed: ethereum")?; } @@ -153,8 +153,8 @@ where type Output = T::Balance; fn drain_balance(src: &AccountId, dest: &AccountId) -> Result { - let amount = T::reducible_balance(&src, Preservation::Expendable, Fortitude::Polite); - T::transfer(&src, &dest, amount, Preservation::Expendable) + let amount = T::reducible_balance(src, Preservation::Expendable, Fortitude::Polite); + T::transfer(src, dest, amount, Preservation::Expendable) .map_err(|_| "account draining failed") .map(|_| amount) } From ed6bd38aef1689881769a813fcff107b48a1b96e Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 12:30:38 +0900 Subject: [PATCH 044/133] refactor: Reorganize vendor package paths --- Cargo.toml | 28 +++++++++--------- .../composable}/composable-support/Cargo.toml | 0 .../src/abstractions/counter/mod.rs | 0 .../src/abstractions/mod.rs | 0 .../src/abstractions/nonce/mod.rs | 0 .../src/abstractions/utils/decrement.rs | 0 .../src/abstractions/utils/increment.rs | 0 .../src/abstractions/utils/mod.rs | 0 .../src/abstractions/utils/start_at.rs | 0 .../composable}/composable-support/src/lib.rs | 0 .../composable-support/src/math/mod.rs | 0 .../composable-support/src/math/safe.rs | 0 .../src/math/wrapping_next.rs | 0 .../composable}/cosmwasm/Cargo.toml | 0 .../composable}/cosmwasm/License.md | 0 .../composable}/cosmwasm/README.md | 0 .../composable}/cosmwasm/rpc/Cargo.toml | 0 .../composable}/cosmwasm/rpc/src/lib.rs | 0 .../cosmwasm/runtime-api/Cargo.toml | 0 .../cosmwasm/runtime-api/src/lib.rs | 0 .../composable}/cosmwasm/src/crypto.rs | 0 .../cosmwasm/src/dispatchable_call.rs | 0 .../composable}/cosmwasm/src/entrypoint.rs | 0 .../composable}/cosmwasm/src/ibc.rs | 0 .../composable}/cosmwasm/src/instrument.rs | 0 .../composable}/cosmwasm/src/lib.rs | 0 .../composable}/cosmwasm/src/pallet_hook.rs | 0 .../composable}/cosmwasm/src/prelude.rs | 0 .../cosmwasm/src/runtimes/abstraction.rs | 0 .../composable}/cosmwasm/src/runtimes/mod.rs | 0 .../composable}/cosmwasm/src/runtimes/vm.rs | 0 .../composable}/cosmwasm/src/types.rs | 0 .../composable}/cosmwasm/src/utils.rs | 0 .../composable}/cosmwasm/src/weights.rs | 0 .../composable}/vm-wasmi/Cargo.toml | 0 .../composable}/vm-wasmi/LICENSE.md | 0 .../vm-wasmi/fixtures/crypto_verify.wasm | Bin .../vm-wasmi/fixtures/cw20_base.wasm | Bin .../vm-wasmi/fixtures/cw20_ics20.wasm | Bin .../vm-wasmi/fixtures/hackatom.wasm | Bin .../vm-wasmi/fixtures/recursion_test.wasm | Bin .../vm-wasmi/fixtures/reflect.wasm | Bin .../vm-wasmi/fixtures/wyndex_factory.wasm | Bin .../vm-wasmi/fixtures/wyndex_pair.wasm | Bin .../composable}/vm-wasmi/src/code_gen.rs | 0 .../composable}/vm-wasmi/src/error.rs | 0 .../vm-wasmi/src/host_functions.rs | 0 .../composable}/vm-wasmi/src/lib.rs | 0 .../composable}/vm-wasmi/src/semantic.rs | 0 .../composable}/vm-wasmi/src/validation.rs | 0 .../composable}/vm-wasmi/src/version.rs | 0 .../composable}/vm-wasmi/src/vm.rs | 0 .../composable}/vm/Cargo.toml | 0 .../composable}/vm/LICENSE.md | 0 .../composable}/vm/src/executor.rs | 0 .../composable}/vm/src/has.rs | 0 .../composable}/vm/src/input.rs | 0 .../composable}/vm/src/lib.rs | 0 .../composable}/vm/src/memory.rs | 0 .../composable}/vm/src/system.rs | 0 .../composable}/vm/src/tagged.rs | 0 .../composable}/vm/src/transaction.rs | 0 .../composable}/vm/src/vm.rs | 0 .../cosmwasm}/std/.cargo/config.toml | 0 {cosmwasm => vendor/cosmwasm}/std/Cargo.toml | 0 {cosmwasm => vendor/cosmwasm}/std/README.md | 0 .../cosmwasm}/std/src/__internal.rs | 0 .../cosmwasm}/std/src/addresses.rs | 0 .../cosmwasm}/std/src/assertions.rs | 0 .../cosmwasm}/std/src/binary.rs | 0 .../cosmwasm}/std/src/checksum.rs | 0 {cosmwasm => vendor/cosmwasm}/std/src/coin.rs | 0 .../cosmwasm}/std/src/coins.rs | 0 .../cosmwasm}/std/src/conversion.rs | 0 {cosmwasm => vendor/cosmwasm}/std/src/deps.rs | 0 .../cosmwasm}/std/src/encoding.rs | 0 .../cosmwasm}/std/src/errors/backtrace.rs | 0 .../cosmwasm}/std/src/errors/mod.rs | 0 .../std/src/errors/recover_pubkey_error.rs | 0 .../cosmwasm}/std/src/errors/std_error.rs | 0 .../cosmwasm}/std/src/errors/system_error.rs | 0 .../std/src/errors/verification_error.rs | 0 .../cosmwasm}/std/src/exports.rs | 0 .../cosmwasm}/std/src/forward_ref.rs | 0 .../cosmwasm}/std/src/hex_binary.rs | 0 {cosmwasm => vendor/cosmwasm}/std/src/ibc.rs | 0 .../cosmwasm}/std/src/ibc/callbacks.rs | 0 .../std/src/ibc/transfer_msg_builder.rs | 0 .../cosmwasm}/std/src/import_helpers.rs | 0 .../cosmwasm}/std/src/imports.rs | 0 .../cosmwasm}/std/src/iterator.rs | 0 {cosmwasm => vendor/cosmwasm}/std/src/lib.rs | 0 .../cosmwasm}/std/src/math/conversion.rs | 0 .../cosmwasm}/std/src/math/decimal.rs | 0 .../cosmwasm}/std/src/math/decimal256.rs | 0 .../cosmwasm}/std/src/math/fraction.rs | 0 .../cosmwasm}/std/src/math/int128.rs | 0 .../cosmwasm}/std/src/math/int256.rs | 0 .../cosmwasm}/std/src/math/int512.rs | 0 .../cosmwasm}/std/src/math/int64.rs | 0 .../cosmwasm}/std/src/math/isqrt.rs | 0 .../cosmwasm}/std/src/math/mod.rs | 0 .../cosmwasm}/std/src/math/num_consts.rs | 0 .../cosmwasm}/std/src/math/signed_decimal.rs | 0 .../std/src/math/signed_decimal_256.rs | 0 .../cosmwasm}/std/src/math/uint128.rs | 0 .../cosmwasm}/std/src/math/uint256.rs | 0 .../cosmwasm}/std/src/math/uint512.rs | 0 .../cosmwasm}/std/src/math/uint64.rs | 0 .../cosmwasm}/std/src/memory.rs | 0 .../cosmwasm}/std/src/metadata.rs | 0 .../cosmwasm}/std/src/never.rs | 0 .../cosmwasm}/std/src/pagination.rs | 0 .../cosmwasm}/std/src/panic.rs | 0 .../cosmwasm}/std/src/prelude.rs | 0 .../cosmwasm}/std/src/query/bank.rs | 0 .../cosmwasm}/std/src/query/distribution.rs | 0 .../cosmwasm}/std/src/query/ibc.rs | 0 .../cosmwasm}/std/src/query/mod.rs | 0 .../cosmwasm}/std/src/query/query_response.rs | 0 .../cosmwasm}/std/src/query/staking.rs | 0 .../cosmwasm}/std/src/query/wasm.rs | 0 .../std/src/results/contract_result.rs | 0 .../cosmwasm}/std/src/results/cosmos_msg.rs | 0 .../cosmwasm}/std/src/results/empty.rs | 0 .../cosmwasm}/std/src/results/events.rs | 0 .../cosmwasm}/std/src/results/mod.rs | 0 .../cosmwasm}/std/src/results/query.rs | 0 .../cosmwasm}/std/src/results/response.rs | 0 .../cosmwasm}/std/src/results/submessages.rs | 0 .../std/src/results/system_result.rs | 0 .../cosmwasm}/std/src/sections.rs | 0 .../cosmwasm}/std/src/serde.rs | 0 .../cosmwasm}/std/src/stdack.rs | 0 .../cosmwasm}/std/src/storage.rs | 0 .../std/src/storage_keys/length_prefixed.rs | 0 .../cosmwasm}/std/src/storage_keys/mod.rs | 0 .../cosmwasm}/std/src/testing/assertions.rs | 0 .../cosmwasm}/std/src/testing/message_info.rs | 0 .../cosmwasm}/std/src/testing/mock.rs | 0 .../cosmwasm}/std/src/testing/mod.rs | 0 .../cosmwasm}/std/src/timestamp.rs | 0 .../cosmwasm}/std/src/traits.rs | 0 .../cosmwasm}/std/src/types.rs | 0 .../std/testdata/instantiate2_addresses.json | 0 ziggurat/node/src/eth/mod.rs | 4 +-- 146 files changed, 16 insertions(+), 16 deletions(-) rename {composable => vendor/composable}/composable-support/Cargo.toml (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/counter/mod.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/mod.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/nonce/mod.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/utils/decrement.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/utils/increment.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/utils/mod.rs (100%) rename {composable => vendor/composable}/composable-support/src/abstractions/utils/start_at.rs (100%) rename {composable => vendor/composable}/composable-support/src/lib.rs (100%) rename {composable => vendor/composable}/composable-support/src/math/mod.rs (100%) rename {composable => vendor/composable}/composable-support/src/math/safe.rs (100%) rename {composable => vendor/composable}/composable-support/src/math/wrapping_next.rs (100%) rename {frame => vendor/composable}/cosmwasm/Cargo.toml (100%) rename {frame => vendor/composable}/cosmwasm/License.md (100%) rename {frame => vendor/composable}/cosmwasm/README.md (100%) rename {frame => vendor/composable}/cosmwasm/rpc/Cargo.toml (100%) rename {frame => vendor/composable}/cosmwasm/rpc/src/lib.rs (100%) rename {frame => vendor/composable}/cosmwasm/runtime-api/Cargo.toml (100%) rename {frame => vendor/composable}/cosmwasm/runtime-api/src/lib.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/crypto.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/dispatchable_call.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/entrypoint.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/ibc.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/instrument.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/lib.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/pallet_hook.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/prelude.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/runtimes/abstraction.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/runtimes/mod.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/runtimes/vm.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/types.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/utils.rs (100%) rename {frame => vendor/composable}/cosmwasm/src/weights.rs (100%) rename {composable => vendor/composable}/vm-wasmi/Cargo.toml (100%) rename {composable => vendor/composable}/vm-wasmi/LICENSE.md (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/crypto_verify.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/cw20_base.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/cw20_ics20.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/hackatom.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/recursion_test.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/reflect.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/wyndex_factory.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/fixtures/wyndex_pair.wasm (100%) rename {composable => vendor/composable}/vm-wasmi/src/code_gen.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/error.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/host_functions.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/lib.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/semantic.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/validation.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/version.rs (100%) rename {composable => vendor/composable}/vm-wasmi/src/vm.rs (100%) rename {composable => vendor/composable}/vm/Cargo.toml (100%) rename {composable => vendor/composable}/vm/LICENSE.md (100%) rename {composable => vendor/composable}/vm/src/executor.rs (100%) rename {composable => vendor/composable}/vm/src/has.rs (100%) rename {composable => vendor/composable}/vm/src/input.rs (100%) rename {composable => vendor/composable}/vm/src/lib.rs (100%) rename {composable => vendor/composable}/vm/src/memory.rs (100%) rename {composable => vendor/composable}/vm/src/system.rs (100%) rename {composable => vendor/composable}/vm/src/tagged.rs (100%) rename {composable => vendor/composable}/vm/src/transaction.rs (100%) rename {composable => vendor/composable}/vm/src/vm.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/.cargo/config.toml (100%) rename {cosmwasm => vendor/cosmwasm}/std/Cargo.toml (100%) rename {cosmwasm => vendor/cosmwasm}/std/README.md (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/__internal.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/addresses.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/assertions.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/binary.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/checksum.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/coin.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/coins.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/conversion.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/deps.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/encoding.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/backtrace.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/recover_pubkey_error.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/std_error.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/system_error.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/errors/verification_error.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/exports.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/forward_ref.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/hex_binary.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/ibc.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/ibc/callbacks.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/ibc/transfer_msg_builder.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/import_helpers.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/imports.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/iterator.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/lib.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/conversion.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/decimal.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/decimal256.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/fraction.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/int128.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/int256.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/int512.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/int64.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/isqrt.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/num_consts.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/signed_decimal.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/signed_decimal_256.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/uint128.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/uint256.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/uint512.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/math/uint64.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/memory.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/metadata.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/never.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/pagination.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/panic.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/prelude.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/bank.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/distribution.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/ibc.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/query_response.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/staking.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/query/wasm.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/contract_result.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/cosmos_msg.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/empty.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/events.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/query.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/response.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/submessages.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/results/system_result.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/sections.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/serde.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/stdack.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/storage.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/storage_keys/length_prefixed.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/storage_keys/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/testing/assertions.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/testing/message_info.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/testing/mock.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/testing/mod.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/timestamp.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/traits.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/src/types.rs (100%) rename {cosmwasm => vendor/cosmwasm}/std/testdata/instantiate2_addresses.json (100%) diff --git a/Cargo.toml b/Cargo.toml index 1c10db85..a01c4daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,6 @@ resolver = "2" members = [ "core-primitives", - "composable/composable-support", - "composable/vm", - "composable/vm-wasmi", - "cosmwasm/std", "frame/babel", "frame/cosmos", "frame/cosmos/types", @@ -18,27 +14,31 @@ members = [ "frame/cosmos/x/bank/types", "frame/cosmos/x/wasm", "frame/cosmos/x/wasm/types", - "frame/cosmwasm", - "frame/cosmwasm/rpc", - "frame/cosmwasm/runtime-api", "frame/multimap", "primitives/cosmos", "primitives/ethereum", "primitives/runtime", "runtime/common", + "vendor/composable/composable-support", + "vendor/composable/vm", + "vendor/composable/vm-wasmi", + "vendor/cosmwasm/std", + "vendor/composable/cosmwasm", + "vendor/composable/cosmwasm/rpc", + "vendor/composable/cosmwasm/runtime-api", "ziggurat/node", "ziggurat/runtime", ] [workspace.dependencies] -composable-support = { path = "composable/composable-support", default-features = false } +composable-support = { path = "vendor/composable/composable-support", default-features = false } cosmos-rpc = { path = "frame/cosmos/rpc", default-features = false } cosmos-runtime-api = { path = "frame/cosmos/runtime-api", default-features = false } -cosmwasm-rpc = { path = "frame/cosmwasm/rpc" } -cosmwasm-runtime-api = { path = "frame/cosmwasm/runtime-api", default-features = false } -cosmwasm-std = { path = "cosmwasm/std", default-features = false } -cosmwasm-vm = { path = "composable/vm", default-features = false } -cosmwasm-vm-wasmi = { path = "composable/vm-wasmi", default-features = false } +cosmwasm-rpc = { path = "vendor/composable/cosmwasm/rpc" } +cosmwasm-runtime-api = { path = "vendor/composable/cosmwasm/runtime-api", default-features = false } +cosmwasm-std = { path = "vendor/cosmwasm/std", default-features = false } +cosmwasm-vm = { path = "vendor/composable/vm", default-features = false } +cosmwasm-vm-wasmi = { path = "vendor/composable/vm-wasmi", default-features = false } frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } @@ -54,7 +54,7 @@ pallet-cosmos-x-bank = { path = "frame/cosmos/x/bank", default-features = false pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-features = false } pallet-cosmos-x-wasm = { path = "frame/cosmos/x/wasm", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } -pallet-cosmwasm = { path = "frame/cosmwasm", default-features = false } +pallet-cosmwasm = { path = "vendor/composable/cosmwasm", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } [profile.release] diff --git a/composable/composable-support/Cargo.toml b/vendor/composable/composable-support/Cargo.toml similarity index 100% rename from composable/composable-support/Cargo.toml rename to vendor/composable/composable-support/Cargo.toml diff --git a/composable/composable-support/src/abstractions/counter/mod.rs b/vendor/composable/composable-support/src/abstractions/counter/mod.rs similarity index 100% rename from composable/composable-support/src/abstractions/counter/mod.rs rename to vendor/composable/composable-support/src/abstractions/counter/mod.rs diff --git a/composable/composable-support/src/abstractions/mod.rs b/vendor/composable/composable-support/src/abstractions/mod.rs similarity index 100% rename from composable/composable-support/src/abstractions/mod.rs rename to vendor/composable/composable-support/src/abstractions/mod.rs diff --git a/composable/composable-support/src/abstractions/nonce/mod.rs b/vendor/composable/composable-support/src/abstractions/nonce/mod.rs similarity index 100% rename from composable/composable-support/src/abstractions/nonce/mod.rs rename to vendor/composable/composable-support/src/abstractions/nonce/mod.rs diff --git a/composable/composable-support/src/abstractions/utils/decrement.rs b/vendor/composable/composable-support/src/abstractions/utils/decrement.rs similarity index 100% rename from composable/composable-support/src/abstractions/utils/decrement.rs rename to vendor/composable/composable-support/src/abstractions/utils/decrement.rs diff --git a/composable/composable-support/src/abstractions/utils/increment.rs b/vendor/composable/composable-support/src/abstractions/utils/increment.rs similarity index 100% rename from composable/composable-support/src/abstractions/utils/increment.rs rename to vendor/composable/composable-support/src/abstractions/utils/increment.rs diff --git a/composable/composable-support/src/abstractions/utils/mod.rs b/vendor/composable/composable-support/src/abstractions/utils/mod.rs similarity index 100% rename from composable/composable-support/src/abstractions/utils/mod.rs rename to vendor/composable/composable-support/src/abstractions/utils/mod.rs diff --git a/composable/composable-support/src/abstractions/utils/start_at.rs b/vendor/composable/composable-support/src/abstractions/utils/start_at.rs similarity index 100% rename from composable/composable-support/src/abstractions/utils/start_at.rs rename to vendor/composable/composable-support/src/abstractions/utils/start_at.rs diff --git a/composable/composable-support/src/lib.rs b/vendor/composable/composable-support/src/lib.rs similarity index 100% rename from composable/composable-support/src/lib.rs rename to vendor/composable/composable-support/src/lib.rs diff --git a/composable/composable-support/src/math/mod.rs b/vendor/composable/composable-support/src/math/mod.rs similarity index 100% rename from composable/composable-support/src/math/mod.rs rename to vendor/composable/composable-support/src/math/mod.rs diff --git a/composable/composable-support/src/math/safe.rs b/vendor/composable/composable-support/src/math/safe.rs similarity index 100% rename from composable/composable-support/src/math/safe.rs rename to vendor/composable/composable-support/src/math/safe.rs diff --git a/composable/composable-support/src/math/wrapping_next.rs b/vendor/composable/composable-support/src/math/wrapping_next.rs similarity index 100% rename from composable/composable-support/src/math/wrapping_next.rs rename to vendor/composable/composable-support/src/math/wrapping_next.rs diff --git a/frame/cosmwasm/Cargo.toml b/vendor/composable/cosmwasm/Cargo.toml similarity index 100% rename from frame/cosmwasm/Cargo.toml rename to vendor/composable/cosmwasm/Cargo.toml diff --git a/frame/cosmwasm/License.md b/vendor/composable/cosmwasm/License.md similarity index 100% rename from frame/cosmwasm/License.md rename to vendor/composable/cosmwasm/License.md diff --git a/frame/cosmwasm/README.md b/vendor/composable/cosmwasm/README.md similarity index 100% rename from frame/cosmwasm/README.md rename to vendor/composable/cosmwasm/README.md diff --git a/frame/cosmwasm/rpc/Cargo.toml b/vendor/composable/cosmwasm/rpc/Cargo.toml similarity index 100% rename from frame/cosmwasm/rpc/Cargo.toml rename to vendor/composable/cosmwasm/rpc/Cargo.toml diff --git a/frame/cosmwasm/rpc/src/lib.rs b/vendor/composable/cosmwasm/rpc/src/lib.rs similarity index 100% rename from frame/cosmwasm/rpc/src/lib.rs rename to vendor/composable/cosmwasm/rpc/src/lib.rs diff --git a/frame/cosmwasm/runtime-api/Cargo.toml b/vendor/composable/cosmwasm/runtime-api/Cargo.toml similarity index 100% rename from frame/cosmwasm/runtime-api/Cargo.toml rename to vendor/composable/cosmwasm/runtime-api/Cargo.toml diff --git a/frame/cosmwasm/runtime-api/src/lib.rs b/vendor/composable/cosmwasm/runtime-api/src/lib.rs similarity index 100% rename from frame/cosmwasm/runtime-api/src/lib.rs rename to vendor/composable/cosmwasm/runtime-api/src/lib.rs diff --git a/frame/cosmwasm/src/crypto.rs b/vendor/composable/cosmwasm/src/crypto.rs similarity index 100% rename from frame/cosmwasm/src/crypto.rs rename to vendor/composable/cosmwasm/src/crypto.rs diff --git a/frame/cosmwasm/src/dispatchable_call.rs b/vendor/composable/cosmwasm/src/dispatchable_call.rs similarity index 100% rename from frame/cosmwasm/src/dispatchable_call.rs rename to vendor/composable/cosmwasm/src/dispatchable_call.rs diff --git a/frame/cosmwasm/src/entrypoint.rs b/vendor/composable/cosmwasm/src/entrypoint.rs similarity index 100% rename from frame/cosmwasm/src/entrypoint.rs rename to vendor/composable/cosmwasm/src/entrypoint.rs diff --git a/frame/cosmwasm/src/ibc.rs b/vendor/composable/cosmwasm/src/ibc.rs similarity index 100% rename from frame/cosmwasm/src/ibc.rs rename to vendor/composable/cosmwasm/src/ibc.rs diff --git a/frame/cosmwasm/src/instrument.rs b/vendor/composable/cosmwasm/src/instrument.rs similarity index 100% rename from frame/cosmwasm/src/instrument.rs rename to vendor/composable/cosmwasm/src/instrument.rs diff --git a/frame/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs similarity index 100% rename from frame/cosmwasm/src/lib.rs rename to vendor/composable/cosmwasm/src/lib.rs diff --git a/frame/cosmwasm/src/pallet_hook.rs b/vendor/composable/cosmwasm/src/pallet_hook.rs similarity index 100% rename from frame/cosmwasm/src/pallet_hook.rs rename to vendor/composable/cosmwasm/src/pallet_hook.rs diff --git a/frame/cosmwasm/src/prelude.rs b/vendor/composable/cosmwasm/src/prelude.rs similarity index 100% rename from frame/cosmwasm/src/prelude.rs rename to vendor/composable/cosmwasm/src/prelude.rs diff --git a/frame/cosmwasm/src/runtimes/abstraction.rs b/vendor/composable/cosmwasm/src/runtimes/abstraction.rs similarity index 100% rename from frame/cosmwasm/src/runtimes/abstraction.rs rename to vendor/composable/cosmwasm/src/runtimes/abstraction.rs diff --git a/frame/cosmwasm/src/runtimes/mod.rs b/vendor/composable/cosmwasm/src/runtimes/mod.rs similarity index 100% rename from frame/cosmwasm/src/runtimes/mod.rs rename to vendor/composable/cosmwasm/src/runtimes/mod.rs diff --git a/frame/cosmwasm/src/runtimes/vm.rs b/vendor/composable/cosmwasm/src/runtimes/vm.rs similarity index 100% rename from frame/cosmwasm/src/runtimes/vm.rs rename to vendor/composable/cosmwasm/src/runtimes/vm.rs diff --git a/frame/cosmwasm/src/types.rs b/vendor/composable/cosmwasm/src/types.rs similarity index 100% rename from frame/cosmwasm/src/types.rs rename to vendor/composable/cosmwasm/src/types.rs diff --git a/frame/cosmwasm/src/utils.rs b/vendor/composable/cosmwasm/src/utils.rs similarity index 100% rename from frame/cosmwasm/src/utils.rs rename to vendor/composable/cosmwasm/src/utils.rs diff --git a/frame/cosmwasm/src/weights.rs b/vendor/composable/cosmwasm/src/weights.rs similarity index 100% rename from frame/cosmwasm/src/weights.rs rename to vendor/composable/cosmwasm/src/weights.rs diff --git a/composable/vm-wasmi/Cargo.toml b/vendor/composable/vm-wasmi/Cargo.toml similarity index 100% rename from composable/vm-wasmi/Cargo.toml rename to vendor/composable/vm-wasmi/Cargo.toml diff --git a/composable/vm-wasmi/LICENSE.md b/vendor/composable/vm-wasmi/LICENSE.md similarity index 100% rename from composable/vm-wasmi/LICENSE.md rename to vendor/composable/vm-wasmi/LICENSE.md diff --git a/composable/vm-wasmi/fixtures/crypto_verify.wasm b/vendor/composable/vm-wasmi/fixtures/crypto_verify.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/crypto_verify.wasm rename to vendor/composable/vm-wasmi/fixtures/crypto_verify.wasm diff --git a/composable/vm-wasmi/fixtures/cw20_base.wasm b/vendor/composable/vm-wasmi/fixtures/cw20_base.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/cw20_base.wasm rename to vendor/composable/vm-wasmi/fixtures/cw20_base.wasm diff --git a/composable/vm-wasmi/fixtures/cw20_ics20.wasm b/vendor/composable/vm-wasmi/fixtures/cw20_ics20.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/cw20_ics20.wasm rename to vendor/composable/vm-wasmi/fixtures/cw20_ics20.wasm diff --git a/composable/vm-wasmi/fixtures/hackatom.wasm b/vendor/composable/vm-wasmi/fixtures/hackatom.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/hackatom.wasm rename to vendor/composable/vm-wasmi/fixtures/hackatom.wasm diff --git a/composable/vm-wasmi/fixtures/recursion_test.wasm b/vendor/composable/vm-wasmi/fixtures/recursion_test.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/recursion_test.wasm rename to vendor/composable/vm-wasmi/fixtures/recursion_test.wasm diff --git a/composable/vm-wasmi/fixtures/reflect.wasm b/vendor/composable/vm-wasmi/fixtures/reflect.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/reflect.wasm rename to vendor/composable/vm-wasmi/fixtures/reflect.wasm diff --git a/composable/vm-wasmi/fixtures/wyndex_factory.wasm b/vendor/composable/vm-wasmi/fixtures/wyndex_factory.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/wyndex_factory.wasm rename to vendor/composable/vm-wasmi/fixtures/wyndex_factory.wasm diff --git a/composable/vm-wasmi/fixtures/wyndex_pair.wasm b/vendor/composable/vm-wasmi/fixtures/wyndex_pair.wasm similarity index 100% rename from composable/vm-wasmi/fixtures/wyndex_pair.wasm rename to vendor/composable/vm-wasmi/fixtures/wyndex_pair.wasm diff --git a/composable/vm-wasmi/src/code_gen.rs b/vendor/composable/vm-wasmi/src/code_gen.rs similarity index 100% rename from composable/vm-wasmi/src/code_gen.rs rename to vendor/composable/vm-wasmi/src/code_gen.rs diff --git a/composable/vm-wasmi/src/error.rs b/vendor/composable/vm-wasmi/src/error.rs similarity index 100% rename from composable/vm-wasmi/src/error.rs rename to vendor/composable/vm-wasmi/src/error.rs diff --git a/composable/vm-wasmi/src/host_functions.rs b/vendor/composable/vm-wasmi/src/host_functions.rs similarity index 100% rename from composable/vm-wasmi/src/host_functions.rs rename to vendor/composable/vm-wasmi/src/host_functions.rs diff --git a/composable/vm-wasmi/src/lib.rs b/vendor/composable/vm-wasmi/src/lib.rs similarity index 100% rename from composable/vm-wasmi/src/lib.rs rename to vendor/composable/vm-wasmi/src/lib.rs diff --git a/composable/vm-wasmi/src/semantic.rs b/vendor/composable/vm-wasmi/src/semantic.rs similarity index 100% rename from composable/vm-wasmi/src/semantic.rs rename to vendor/composable/vm-wasmi/src/semantic.rs diff --git a/composable/vm-wasmi/src/validation.rs b/vendor/composable/vm-wasmi/src/validation.rs similarity index 100% rename from composable/vm-wasmi/src/validation.rs rename to vendor/composable/vm-wasmi/src/validation.rs diff --git a/composable/vm-wasmi/src/version.rs b/vendor/composable/vm-wasmi/src/version.rs similarity index 100% rename from composable/vm-wasmi/src/version.rs rename to vendor/composable/vm-wasmi/src/version.rs diff --git a/composable/vm-wasmi/src/vm.rs b/vendor/composable/vm-wasmi/src/vm.rs similarity index 100% rename from composable/vm-wasmi/src/vm.rs rename to vendor/composable/vm-wasmi/src/vm.rs diff --git a/composable/vm/Cargo.toml b/vendor/composable/vm/Cargo.toml similarity index 100% rename from composable/vm/Cargo.toml rename to vendor/composable/vm/Cargo.toml diff --git a/composable/vm/LICENSE.md b/vendor/composable/vm/LICENSE.md similarity index 100% rename from composable/vm/LICENSE.md rename to vendor/composable/vm/LICENSE.md diff --git a/composable/vm/src/executor.rs b/vendor/composable/vm/src/executor.rs similarity index 100% rename from composable/vm/src/executor.rs rename to vendor/composable/vm/src/executor.rs diff --git a/composable/vm/src/has.rs b/vendor/composable/vm/src/has.rs similarity index 100% rename from composable/vm/src/has.rs rename to vendor/composable/vm/src/has.rs diff --git a/composable/vm/src/input.rs b/vendor/composable/vm/src/input.rs similarity index 100% rename from composable/vm/src/input.rs rename to vendor/composable/vm/src/input.rs diff --git a/composable/vm/src/lib.rs b/vendor/composable/vm/src/lib.rs similarity index 100% rename from composable/vm/src/lib.rs rename to vendor/composable/vm/src/lib.rs diff --git a/composable/vm/src/memory.rs b/vendor/composable/vm/src/memory.rs similarity index 100% rename from composable/vm/src/memory.rs rename to vendor/composable/vm/src/memory.rs diff --git a/composable/vm/src/system.rs b/vendor/composable/vm/src/system.rs similarity index 100% rename from composable/vm/src/system.rs rename to vendor/composable/vm/src/system.rs diff --git a/composable/vm/src/tagged.rs b/vendor/composable/vm/src/tagged.rs similarity index 100% rename from composable/vm/src/tagged.rs rename to vendor/composable/vm/src/tagged.rs diff --git a/composable/vm/src/transaction.rs b/vendor/composable/vm/src/transaction.rs similarity index 100% rename from composable/vm/src/transaction.rs rename to vendor/composable/vm/src/transaction.rs diff --git a/composable/vm/src/vm.rs b/vendor/composable/vm/src/vm.rs similarity index 100% rename from composable/vm/src/vm.rs rename to vendor/composable/vm/src/vm.rs diff --git a/cosmwasm/std/.cargo/config.toml b/vendor/cosmwasm/std/.cargo/config.toml similarity index 100% rename from cosmwasm/std/.cargo/config.toml rename to vendor/cosmwasm/std/.cargo/config.toml diff --git a/cosmwasm/std/Cargo.toml b/vendor/cosmwasm/std/Cargo.toml similarity index 100% rename from cosmwasm/std/Cargo.toml rename to vendor/cosmwasm/std/Cargo.toml diff --git a/cosmwasm/std/README.md b/vendor/cosmwasm/std/README.md similarity index 100% rename from cosmwasm/std/README.md rename to vendor/cosmwasm/std/README.md diff --git a/cosmwasm/std/src/__internal.rs b/vendor/cosmwasm/std/src/__internal.rs similarity index 100% rename from cosmwasm/std/src/__internal.rs rename to vendor/cosmwasm/std/src/__internal.rs diff --git a/cosmwasm/std/src/addresses.rs b/vendor/cosmwasm/std/src/addresses.rs similarity index 100% rename from cosmwasm/std/src/addresses.rs rename to vendor/cosmwasm/std/src/addresses.rs diff --git a/cosmwasm/std/src/assertions.rs b/vendor/cosmwasm/std/src/assertions.rs similarity index 100% rename from cosmwasm/std/src/assertions.rs rename to vendor/cosmwasm/std/src/assertions.rs diff --git a/cosmwasm/std/src/binary.rs b/vendor/cosmwasm/std/src/binary.rs similarity index 100% rename from cosmwasm/std/src/binary.rs rename to vendor/cosmwasm/std/src/binary.rs diff --git a/cosmwasm/std/src/checksum.rs b/vendor/cosmwasm/std/src/checksum.rs similarity index 100% rename from cosmwasm/std/src/checksum.rs rename to vendor/cosmwasm/std/src/checksum.rs diff --git a/cosmwasm/std/src/coin.rs b/vendor/cosmwasm/std/src/coin.rs similarity index 100% rename from cosmwasm/std/src/coin.rs rename to vendor/cosmwasm/std/src/coin.rs diff --git a/cosmwasm/std/src/coins.rs b/vendor/cosmwasm/std/src/coins.rs similarity index 100% rename from cosmwasm/std/src/coins.rs rename to vendor/cosmwasm/std/src/coins.rs diff --git a/cosmwasm/std/src/conversion.rs b/vendor/cosmwasm/std/src/conversion.rs similarity index 100% rename from cosmwasm/std/src/conversion.rs rename to vendor/cosmwasm/std/src/conversion.rs diff --git a/cosmwasm/std/src/deps.rs b/vendor/cosmwasm/std/src/deps.rs similarity index 100% rename from cosmwasm/std/src/deps.rs rename to vendor/cosmwasm/std/src/deps.rs diff --git a/cosmwasm/std/src/encoding.rs b/vendor/cosmwasm/std/src/encoding.rs similarity index 100% rename from cosmwasm/std/src/encoding.rs rename to vendor/cosmwasm/std/src/encoding.rs diff --git a/cosmwasm/std/src/errors/backtrace.rs b/vendor/cosmwasm/std/src/errors/backtrace.rs similarity index 100% rename from cosmwasm/std/src/errors/backtrace.rs rename to vendor/cosmwasm/std/src/errors/backtrace.rs diff --git a/cosmwasm/std/src/errors/mod.rs b/vendor/cosmwasm/std/src/errors/mod.rs similarity index 100% rename from cosmwasm/std/src/errors/mod.rs rename to vendor/cosmwasm/std/src/errors/mod.rs diff --git a/cosmwasm/std/src/errors/recover_pubkey_error.rs b/vendor/cosmwasm/std/src/errors/recover_pubkey_error.rs similarity index 100% rename from cosmwasm/std/src/errors/recover_pubkey_error.rs rename to vendor/cosmwasm/std/src/errors/recover_pubkey_error.rs diff --git a/cosmwasm/std/src/errors/std_error.rs b/vendor/cosmwasm/std/src/errors/std_error.rs similarity index 100% rename from cosmwasm/std/src/errors/std_error.rs rename to vendor/cosmwasm/std/src/errors/std_error.rs diff --git a/cosmwasm/std/src/errors/system_error.rs b/vendor/cosmwasm/std/src/errors/system_error.rs similarity index 100% rename from cosmwasm/std/src/errors/system_error.rs rename to vendor/cosmwasm/std/src/errors/system_error.rs diff --git a/cosmwasm/std/src/errors/verification_error.rs b/vendor/cosmwasm/std/src/errors/verification_error.rs similarity index 100% rename from cosmwasm/std/src/errors/verification_error.rs rename to vendor/cosmwasm/std/src/errors/verification_error.rs diff --git a/cosmwasm/std/src/exports.rs b/vendor/cosmwasm/std/src/exports.rs similarity index 100% rename from cosmwasm/std/src/exports.rs rename to vendor/cosmwasm/std/src/exports.rs diff --git a/cosmwasm/std/src/forward_ref.rs b/vendor/cosmwasm/std/src/forward_ref.rs similarity index 100% rename from cosmwasm/std/src/forward_ref.rs rename to vendor/cosmwasm/std/src/forward_ref.rs diff --git a/cosmwasm/std/src/hex_binary.rs b/vendor/cosmwasm/std/src/hex_binary.rs similarity index 100% rename from cosmwasm/std/src/hex_binary.rs rename to vendor/cosmwasm/std/src/hex_binary.rs diff --git a/cosmwasm/std/src/ibc.rs b/vendor/cosmwasm/std/src/ibc.rs similarity index 100% rename from cosmwasm/std/src/ibc.rs rename to vendor/cosmwasm/std/src/ibc.rs diff --git a/cosmwasm/std/src/ibc/callbacks.rs b/vendor/cosmwasm/std/src/ibc/callbacks.rs similarity index 100% rename from cosmwasm/std/src/ibc/callbacks.rs rename to vendor/cosmwasm/std/src/ibc/callbacks.rs diff --git a/cosmwasm/std/src/ibc/transfer_msg_builder.rs b/vendor/cosmwasm/std/src/ibc/transfer_msg_builder.rs similarity index 100% rename from cosmwasm/std/src/ibc/transfer_msg_builder.rs rename to vendor/cosmwasm/std/src/ibc/transfer_msg_builder.rs diff --git a/cosmwasm/std/src/import_helpers.rs b/vendor/cosmwasm/std/src/import_helpers.rs similarity index 100% rename from cosmwasm/std/src/import_helpers.rs rename to vendor/cosmwasm/std/src/import_helpers.rs diff --git a/cosmwasm/std/src/imports.rs b/vendor/cosmwasm/std/src/imports.rs similarity index 100% rename from cosmwasm/std/src/imports.rs rename to vendor/cosmwasm/std/src/imports.rs diff --git a/cosmwasm/std/src/iterator.rs b/vendor/cosmwasm/std/src/iterator.rs similarity index 100% rename from cosmwasm/std/src/iterator.rs rename to vendor/cosmwasm/std/src/iterator.rs diff --git a/cosmwasm/std/src/lib.rs b/vendor/cosmwasm/std/src/lib.rs similarity index 100% rename from cosmwasm/std/src/lib.rs rename to vendor/cosmwasm/std/src/lib.rs diff --git a/cosmwasm/std/src/math/conversion.rs b/vendor/cosmwasm/std/src/math/conversion.rs similarity index 100% rename from cosmwasm/std/src/math/conversion.rs rename to vendor/cosmwasm/std/src/math/conversion.rs diff --git a/cosmwasm/std/src/math/decimal.rs b/vendor/cosmwasm/std/src/math/decimal.rs similarity index 100% rename from cosmwasm/std/src/math/decimal.rs rename to vendor/cosmwasm/std/src/math/decimal.rs diff --git a/cosmwasm/std/src/math/decimal256.rs b/vendor/cosmwasm/std/src/math/decimal256.rs similarity index 100% rename from cosmwasm/std/src/math/decimal256.rs rename to vendor/cosmwasm/std/src/math/decimal256.rs diff --git a/cosmwasm/std/src/math/fraction.rs b/vendor/cosmwasm/std/src/math/fraction.rs similarity index 100% rename from cosmwasm/std/src/math/fraction.rs rename to vendor/cosmwasm/std/src/math/fraction.rs diff --git a/cosmwasm/std/src/math/int128.rs b/vendor/cosmwasm/std/src/math/int128.rs similarity index 100% rename from cosmwasm/std/src/math/int128.rs rename to vendor/cosmwasm/std/src/math/int128.rs diff --git a/cosmwasm/std/src/math/int256.rs b/vendor/cosmwasm/std/src/math/int256.rs similarity index 100% rename from cosmwasm/std/src/math/int256.rs rename to vendor/cosmwasm/std/src/math/int256.rs diff --git a/cosmwasm/std/src/math/int512.rs b/vendor/cosmwasm/std/src/math/int512.rs similarity index 100% rename from cosmwasm/std/src/math/int512.rs rename to vendor/cosmwasm/std/src/math/int512.rs diff --git a/cosmwasm/std/src/math/int64.rs b/vendor/cosmwasm/std/src/math/int64.rs similarity index 100% rename from cosmwasm/std/src/math/int64.rs rename to vendor/cosmwasm/std/src/math/int64.rs diff --git a/cosmwasm/std/src/math/isqrt.rs b/vendor/cosmwasm/std/src/math/isqrt.rs similarity index 100% rename from cosmwasm/std/src/math/isqrt.rs rename to vendor/cosmwasm/std/src/math/isqrt.rs diff --git a/cosmwasm/std/src/math/mod.rs b/vendor/cosmwasm/std/src/math/mod.rs similarity index 100% rename from cosmwasm/std/src/math/mod.rs rename to vendor/cosmwasm/std/src/math/mod.rs diff --git a/cosmwasm/std/src/math/num_consts.rs b/vendor/cosmwasm/std/src/math/num_consts.rs similarity index 100% rename from cosmwasm/std/src/math/num_consts.rs rename to vendor/cosmwasm/std/src/math/num_consts.rs diff --git a/cosmwasm/std/src/math/signed_decimal.rs b/vendor/cosmwasm/std/src/math/signed_decimal.rs similarity index 100% rename from cosmwasm/std/src/math/signed_decimal.rs rename to vendor/cosmwasm/std/src/math/signed_decimal.rs diff --git a/cosmwasm/std/src/math/signed_decimal_256.rs b/vendor/cosmwasm/std/src/math/signed_decimal_256.rs similarity index 100% rename from cosmwasm/std/src/math/signed_decimal_256.rs rename to vendor/cosmwasm/std/src/math/signed_decimal_256.rs diff --git a/cosmwasm/std/src/math/uint128.rs b/vendor/cosmwasm/std/src/math/uint128.rs similarity index 100% rename from cosmwasm/std/src/math/uint128.rs rename to vendor/cosmwasm/std/src/math/uint128.rs diff --git a/cosmwasm/std/src/math/uint256.rs b/vendor/cosmwasm/std/src/math/uint256.rs similarity index 100% rename from cosmwasm/std/src/math/uint256.rs rename to vendor/cosmwasm/std/src/math/uint256.rs diff --git a/cosmwasm/std/src/math/uint512.rs b/vendor/cosmwasm/std/src/math/uint512.rs similarity index 100% rename from cosmwasm/std/src/math/uint512.rs rename to vendor/cosmwasm/std/src/math/uint512.rs diff --git a/cosmwasm/std/src/math/uint64.rs b/vendor/cosmwasm/std/src/math/uint64.rs similarity index 100% rename from cosmwasm/std/src/math/uint64.rs rename to vendor/cosmwasm/std/src/math/uint64.rs diff --git a/cosmwasm/std/src/memory.rs b/vendor/cosmwasm/std/src/memory.rs similarity index 100% rename from cosmwasm/std/src/memory.rs rename to vendor/cosmwasm/std/src/memory.rs diff --git a/cosmwasm/std/src/metadata.rs b/vendor/cosmwasm/std/src/metadata.rs similarity index 100% rename from cosmwasm/std/src/metadata.rs rename to vendor/cosmwasm/std/src/metadata.rs diff --git a/cosmwasm/std/src/never.rs b/vendor/cosmwasm/std/src/never.rs similarity index 100% rename from cosmwasm/std/src/never.rs rename to vendor/cosmwasm/std/src/never.rs diff --git a/cosmwasm/std/src/pagination.rs b/vendor/cosmwasm/std/src/pagination.rs similarity index 100% rename from cosmwasm/std/src/pagination.rs rename to vendor/cosmwasm/std/src/pagination.rs diff --git a/cosmwasm/std/src/panic.rs b/vendor/cosmwasm/std/src/panic.rs similarity index 100% rename from cosmwasm/std/src/panic.rs rename to vendor/cosmwasm/std/src/panic.rs diff --git a/cosmwasm/std/src/prelude.rs b/vendor/cosmwasm/std/src/prelude.rs similarity index 100% rename from cosmwasm/std/src/prelude.rs rename to vendor/cosmwasm/std/src/prelude.rs diff --git a/cosmwasm/std/src/query/bank.rs b/vendor/cosmwasm/std/src/query/bank.rs similarity index 100% rename from cosmwasm/std/src/query/bank.rs rename to vendor/cosmwasm/std/src/query/bank.rs diff --git a/cosmwasm/std/src/query/distribution.rs b/vendor/cosmwasm/std/src/query/distribution.rs similarity index 100% rename from cosmwasm/std/src/query/distribution.rs rename to vendor/cosmwasm/std/src/query/distribution.rs diff --git a/cosmwasm/std/src/query/ibc.rs b/vendor/cosmwasm/std/src/query/ibc.rs similarity index 100% rename from cosmwasm/std/src/query/ibc.rs rename to vendor/cosmwasm/std/src/query/ibc.rs diff --git a/cosmwasm/std/src/query/mod.rs b/vendor/cosmwasm/std/src/query/mod.rs similarity index 100% rename from cosmwasm/std/src/query/mod.rs rename to vendor/cosmwasm/std/src/query/mod.rs diff --git a/cosmwasm/std/src/query/query_response.rs b/vendor/cosmwasm/std/src/query/query_response.rs similarity index 100% rename from cosmwasm/std/src/query/query_response.rs rename to vendor/cosmwasm/std/src/query/query_response.rs diff --git a/cosmwasm/std/src/query/staking.rs b/vendor/cosmwasm/std/src/query/staking.rs similarity index 100% rename from cosmwasm/std/src/query/staking.rs rename to vendor/cosmwasm/std/src/query/staking.rs diff --git a/cosmwasm/std/src/query/wasm.rs b/vendor/cosmwasm/std/src/query/wasm.rs similarity index 100% rename from cosmwasm/std/src/query/wasm.rs rename to vendor/cosmwasm/std/src/query/wasm.rs diff --git a/cosmwasm/std/src/results/contract_result.rs b/vendor/cosmwasm/std/src/results/contract_result.rs similarity index 100% rename from cosmwasm/std/src/results/contract_result.rs rename to vendor/cosmwasm/std/src/results/contract_result.rs diff --git a/cosmwasm/std/src/results/cosmos_msg.rs b/vendor/cosmwasm/std/src/results/cosmos_msg.rs similarity index 100% rename from cosmwasm/std/src/results/cosmos_msg.rs rename to vendor/cosmwasm/std/src/results/cosmos_msg.rs diff --git a/cosmwasm/std/src/results/empty.rs b/vendor/cosmwasm/std/src/results/empty.rs similarity index 100% rename from cosmwasm/std/src/results/empty.rs rename to vendor/cosmwasm/std/src/results/empty.rs diff --git a/cosmwasm/std/src/results/events.rs b/vendor/cosmwasm/std/src/results/events.rs similarity index 100% rename from cosmwasm/std/src/results/events.rs rename to vendor/cosmwasm/std/src/results/events.rs diff --git a/cosmwasm/std/src/results/mod.rs b/vendor/cosmwasm/std/src/results/mod.rs similarity index 100% rename from cosmwasm/std/src/results/mod.rs rename to vendor/cosmwasm/std/src/results/mod.rs diff --git a/cosmwasm/std/src/results/query.rs b/vendor/cosmwasm/std/src/results/query.rs similarity index 100% rename from cosmwasm/std/src/results/query.rs rename to vendor/cosmwasm/std/src/results/query.rs diff --git a/cosmwasm/std/src/results/response.rs b/vendor/cosmwasm/std/src/results/response.rs similarity index 100% rename from cosmwasm/std/src/results/response.rs rename to vendor/cosmwasm/std/src/results/response.rs diff --git a/cosmwasm/std/src/results/submessages.rs b/vendor/cosmwasm/std/src/results/submessages.rs similarity index 100% rename from cosmwasm/std/src/results/submessages.rs rename to vendor/cosmwasm/std/src/results/submessages.rs diff --git a/cosmwasm/std/src/results/system_result.rs b/vendor/cosmwasm/std/src/results/system_result.rs similarity index 100% rename from cosmwasm/std/src/results/system_result.rs rename to vendor/cosmwasm/std/src/results/system_result.rs diff --git a/cosmwasm/std/src/sections.rs b/vendor/cosmwasm/std/src/sections.rs similarity index 100% rename from cosmwasm/std/src/sections.rs rename to vendor/cosmwasm/std/src/sections.rs diff --git a/cosmwasm/std/src/serde.rs b/vendor/cosmwasm/std/src/serde.rs similarity index 100% rename from cosmwasm/std/src/serde.rs rename to vendor/cosmwasm/std/src/serde.rs diff --git a/cosmwasm/std/src/stdack.rs b/vendor/cosmwasm/std/src/stdack.rs similarity index 100% rename from cosmwasm/std/src/stdack.rs rename to vendor/cosmwasm/std/src/stdack.rs diff --git a/cosmwasm/std/src/storage.rs b/vendor/cosmwasm/std/src/storage.rs similarity index 100% rename from cosmwasm/std/src/storage.rs rename to vendor/cosmwasm/std/src/storage.rs diff --git a/cosmwasm/std/src/storage_keys/length_prefixed.rs b/vendor/cosmwasm/std/src/storage_keys/length_prefixed.rs similarity index 100% rename from cosmwasm/std/src/storage_keys/length_prefixed.rs rename to vendor/cosmwasm/std/src/storage_keys/length_prefixed.rs diff --git a/cosmwasm/std/src/storage_keys/mod.rs b/vendor/cosmwasm/std/src/storage_keys/mod.rs similarity index 100% rename from cosmwasm/std/src/storage_keys/mod.rs rename to vendor/cosmwasm/std/src/storage_keys/mod.rs diff --git a/cosmwasm/std/src/testing/assertions.rs b/vendor/cosmwasm/std/src/testing/assertions.rs similarity index 100% rename from cosmwasm/std/src/testing/assertions.rs rename to vendor/cosmwasm/std/src/testing/assertions.rs diff --git a/cosmwasm/std/src/testing/message_info.rs b/vendor/cosmwasm/std/src/testing/message_info.rs similarity index 100% rename from cosmwasm/std/src/testing/message_info.rs rename to vendor/cosmwasm/std/src/testing/message_info.rs diff --git a/cosmwasm/std/src/testing/mock.rs b/vendor/cosmwasm/std/src/testing/mock.rs similarity index 100% rename from cosmwasm/std/src/testing/mock.rs rename to vendor/cosmwasm/std/src/testing/mock.rs diff --git a/cosmwasm/std/src/testing/mod.rs b/vendor/cosmwasm/std/src/testing/mod.rs similarity index 100% rename from cosmwasm/std/src/testing/mod.rs rename to vendor/cosmwasm/std/src/testing/mod.rs diff --git a/cosmwasm/std/src/timestamp.rs b/vendor/cosmwasm/std/src/timestamp.rs similarity index 100% rename from cosmwasm/std/src/timestamp.rs rename to vendor/cosmwasm/std/src/timestamp.rs diff --git a/cosmwasm/std/src/traits.rs b/vendor/cosmwasm/std/src/traits.rs similarity index 100% rename from cosmwasm/std/src/traits.rs rename to vendor/cosmwasm/std/src/traits.rs diff --git a/cosmwasm/std/src/types.rs b/vendor/cosmwasm/std/src/types.rs similarity index 100% rename from cosmwasm/std/src/types.rs rename to vendor/cosmwasm/std/src/types.rs diff --git a/cosmwasm/std/testdata/instantiate2_addresses.json b/vendor/cosmwasm/std/testdata/instantiate2_addresses.json similarity index 100% rename from cosmwasm/std/testdata/instantiate2_addresses.json rename to vendor/cosmwasm/std/testdata/instantiate2_addresses.json diff --git a/ziggurat/node/src/eth/mod.rs b/ziggurat/node/src/eth/mod.rs index 76686016..bfac4967 100644 --- a/ziggurat/node/src/eth/mod.rs +++ b/ziggurat/node/src/eth/mod.rs @@ -101,7 +101,7 @@ where let prometheus_config = config.prometheus_config.take(); // TODO: Make the Ethereum RPC port configurable. - config.rpc_addr.as_mut().map(|addr| addr.set_port(8545)); + let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(8545)); let rpc = sc_service::start_rpc_servers( &config, @@ -181,7 +181,7 @@ where ), ); - config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); + let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); config.prometheus_config = prometheus_config; Ok(config) From b16ad4be1089ed827135bd714e0d85e058850bdb Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 13:01:32 +0900 Subject: [PATCH 045/133] feat: Implement AddressMapping for pallet-cosmos --- frame/babel/Cargo.toml | 3 +++ frame/babel/src/cosmos.rs | 38 ++++++++++++++++++++++++++++++++++++ frame/babel/src/lib.rs | 2 ++ frame/cosmos/src/lib.rs | 1 + primitives/cosmos/Cargo.toml | 2 ++ primitives/cosmos/src/lib.rs | 27 +++++++++++++++++++++++-- ziggurat/runtime/Cargo.toml | 3 +++ 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 frame/babel/src/cosmos.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index bdffbe68..d974121d 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -14,6 +14,7 @@ frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "s np-cosmos = { workspace = true, optional = true } np-ethereum = { workspace = true, optional = true } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-cosmos = { workspace = true, default-features = false, optional = true } pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-blake2 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } @@ -41,6 +42,7 @@ std = [ "np-cosmos?/std", "np-ethereum?/std", "pallet-balances/std", + "pallet-cosmos?/std", "pallet-ethereum?/std", "pallet-evm?/std", "pallet-evm-precompile-blake2?/std", @@ -56,6 +58,7 @@ std = [ ] cosmos = [ "np-cosmos", + "pallet-cosmos", ] ethereum = [ "dep:ethereum", diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs new file mode 100644 index 00000000..427b2dc2 --- /dev/null +++ b/frame/babel/src/cosmos.rs @@ -0,0 +1,38 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{extensions::unify_account, Address}; +use core::marker::PhantomData; +#[cfg(feature = "cosmos")] +pub use np_cosmos::Address as CosmosAddress; +use pallet_multimap::traits::UniqueMultimap; +use sp_core::H160; +use sp_runtime::traits::AccountIdConversion; + +pub struct AddressMapping(PhantomData); + +impl pallet_evm::AddressMapping for AddressMapping +where + T: unify_account::Config, +{ + fn into_account_id(who: H160) -> T::AccountId { + let address = CosmosAddress::from(who); + T::AddressMap::find_key(Address::Cosmos(address.clone())) + .unwrap_or_else(|| address.into_account_truncating()) + } +} diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 4d93f2fd..ad744e91 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -20,6 +20,8 @@ extern crate alloc; +#[cfg(feature = "cosmos")] +pub mod cosmos; #[cfg(feature = "ethereum")] pub mod ethereum; pub mod extensions; diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index db094486..d5cb8574 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -132,6 +132,7 @@ pub trait AddressMapping { #[frame_support::pallet] pub mod pallet { use super::*; + use alloc::{string::String, vec::Vec}; use cosmos_sdk_proto::Any; use frame_support::{ dispatch::WithPostDispatchInfo, diff --git a/primitives/cosmos/Cargo.toml b/primitives/cosmos/Cargo.toml index 224d8fdf..f3d46f43 100644 --- a/primitives/cosmos/Cargo.toml +++ b/primitives/cosmos/Cargo.toml @@ -15,6 +15,7 @@ ripemd = { version = "0.1", default-features = false } scale-info = { version = "2.11", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [dev-dependencies] const-hex = "1.12.0" @@ -29,4 +30,5 @@ std = [ "scale-info/std", "sp-core/std", "sp-io/std", + "sp-runtime/std", ] diff --git a/primitives/cosmos/src/lib.rs b/primitives/cosmos/src/lib.rs index 5acfd0fc..79750a65 100644 --- a/primitives/cosmos/src/lib.rs +++ b/primitives/cosmos/src/lib.rs @@ -30,9 +30,11 @@ use crate::traits::CosmosHub; use bech32::{Bech32, Hrp}; use buidl::FixedBytes; use core::marker::PhantomData; +use parity_scale_codec::{Decode, Encode}; use ripemd::{Digest, Ripemd160}; -use sp_core::{ecdsa, H160}; -use sp_io::hashing::sha2_256; +use sp_core::{ecdsa, H160, H256}; +use sp_io::hashing::{blake2_256, sha2_256}; +use sp_runtime::traits::AccountIdConversion; /// Cosmos address. #[derive(FixedBytes)] @@ -73,6 +75,27 @@ impl core::fmt::Debug for Address { } } +impl> AccountIdConversion for Address { + fn into_account_truncating(&self) -> AccountId { + let mut data = [0u8; 25]; + data[0..5].copy_from_slice(b"cosm:"); + data[5..25].copy_from_slice(&self.0); + H256(blake2_256(&data)).into() + } + + fn into_sub_account_truncating(&self, _: S) -> AccountId { + unimplemented!() + } + + fn try_into_sub_account(&self, _: S) -> Option { + unimplemented!() + } + + fn try_from_sub_account(_: &AccountId) -> Option<(Self, S)> { + unimplemented!() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index a68663d7..5d9bfdd7 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -75,6 +75,8 @@ pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk" parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-cosmos = { workspace = true, default-features = false } + [build-dependencies] substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", optional = true } @@ -111,6 +113,7 @@ std = [ "pallet-balances/std", "pallet-base-fee/std", "pallet-collator-selection/std", + "pallet-cosmos/std", "pallet-ethereum/std", "pallet-evm/std", "pallet-multimap/std", From 4697d085dadc9801531d201f5c4fe7c25ae57ee4 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 14:08:42 +0900 Subject: [PATCH 046/133] feat: Add pallet origin to pallet-cosmos --- frame/cosmos/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index d5cb8574..57ef0a93 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -149,6 +149,9 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(PhantomData); + #[pallet::origin] + pub type Origin = RawOrigin; + #[pallet::event] #[pallet::generate_deposit(pub fn deposit_event)] pub enum Event { From 769be6104fa19a5d584c54d0cf5a66dcd1c57e21 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 14:20:31 +0900 Subject: [PATCH 047/133] feat: Add pallet-cosmos config_preludes --- frame/cosmos/src/lib.rs | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 57ef0a93..299b676b 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -240,6 +240,67 @@ pub mod pallet { type MaxDenomLimit: Get; } + pub mod config_preludes { + use super::*; + use cosmos_sdk_proto::{ + cosmos::bank::v1beta1::MsgSend, + cosmwasm::wasm::v1::{ + MsgExecuteContract, MsgInstantiateContract2, MsgMigrateContract, MsgStoreCode, + MsgUpdateAdmin, + }, + }; + use frame_support::parameter_types; + use pallet_cosmos_types::any_match; + + pub struct MsgFilter; + impl Contains for MsgFilter { + fn contains(msg: &Any) -> bool { + any_match!( + msg, { + MsgSend => true, + MsgStoreCode => true, + MsgInstantiateContract2 => true, + MsgExecuteContract => true, + MsgMigrateContract => true, + MsgUpdateAdmin => true, + }, + false + ) + } + } + + pub struct WeightToGas; + impl Convert for WeightToGas { + fn convert(weight: Weight) -> Gas { + weight.ref_time() + } + } + + impl Convert for WeightToGas { + fn convert(gas: Gas) -> Weight { + Weight::from_parts(gas, 0) + } + } + + parameter_types! { + pub const MaxMemoCharacters: u64 = 256; + pub NativeDenom: &'static str = "azig"; + pub const TxSigLimit: u64 = 7; + pub const MaxDenomLimit: u32 = 128; + pub const NativeAssetId: u32 = 0; + } + } + + #[pallet::storage] + #[pallet::getter(fn denom_to_asset)] + pub type DenomAssetRouter = + StorageMap<_, Twox64Concat, BoundedVec, T::AssetId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn asset_to_denom)] + pub type AssetDenomRouter = + StorageMap<_, Twox64Concat, T::AssetId, BoundedVec, OptionQuery>; + #[pallet::call] impl Pallet where From 9e7054f2a1ca23e461e87e72967948396374abf0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 18:22:06 +0900 Subject: [PATCH 048/133] feat: Implement pallet-cosmos and pallet-cosmwasm --- frame/babel/Cargo.toml | 32 +++- frame/babel/src/cosmos.rs | 125 +++++++++++++++- frame/cosmos/src/lib.rs | 8 +- frame/cosmos/src/types.rs | 21 +++ frame/cosmos/types/src/msgservice/mod.rs | 18 +++ .../{msgservice.rs => msgservice/traits.rs} | 0 frame/cosmos/x/auth/src/fee.rs | 1 + frame/cosmos/x/auth/src/sigverify.rs | 5 +- frame/cosmos/x/bank/src/lib.rs | 5 +- frame/cosmos/x/wasm/src/msgs.rs | 5 +- vendor/composable/cosmwasm/Cargo.toml | 3 + vendor/composable/cosmwasm/src/lib.rs | 10 +- ziggurat/runtime/Cargo.toml | 14 +- ziggurat/runtime/src/configs/mod.rs | 138 ++++++++++++++++-- ziggurat/runtime/src/lib.rs | 8 + 15 files changed, 353 insertions(+), 40 deletions(-) create mode 100644 frame/cosmos/src/types.rs create mode 100644 frame/cosmos/types/src/msgservice/mod.rs rename frame/cosmos/types/src/{msgservice.rs => msgservice/traits.rs} (100%) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index d974121d..b92bd117 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -8,6 +8,8 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] +bech32 = { version = "0.11", default-features = false, features = ["alloc"], optional = true } +cosmos-sdk-proto = { version = "0.24.0", default-features = false, optional = true } ethereum = { version = "0.15.0", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -15,6 +17,10 @@ np-cosmos = { workspace = true, optional = true } np-ethereum = { workspace = true, optional = true } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } +pallet-cosmos-types = { workspace = true, default-features = false, optional = true } +pallet-cosmos-x-bank = { workspace = true, default-features = false, optional = true } +pallet-cosmos-x-wasm = { workspace = true, default-features = false, optional = true } +pallet-cosmwasm = { workspace = true, default-features = false, optional = true } pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-blake2 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } @@ -23,19 +29,21 @@ pallet-evm-precompile-modexp = { git = "https://github.com/polkadot-evm/frontier pallet-evm-precompile-sha3fips = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-simple = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-multimap = { workspace = true } -parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } -scale-info = { version = "2.11", default-features = false, features = ["derive"] } +parity-scale-codec = { version = "3.6", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.11", default-features = false, features = [ + "derive", +] } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [features] -default = [ - "std", - "cosmos", - "ethereum", -] +default = ["std", "cosmos", "ethereum"] std = [ + "bech32?/std", + "cosmos-sdk-proto?/std", "ethereum?/std", "frame-support/std", "frame-system/std", @@ -43,6 +51,10 @@ std = [ "np-ethereum?/std", "pallet-balances/std", "pallet-cosmos?/std", + "pallet-cosmos-types?/std", + "pallet-cosmos-x-bank?/std", + "pallet-cosmos-x-wasm?/std", + "pallet-cosmwasm?/std", "pallet-ethereum?/std", "pallet-evm?/std", "pallet-evm-precompile-blake2?/std", @@ -57,8 +69,14 @@ std = [ "sp-runtime/std", ] cosmos = [ + "bech32", + "cosmos-sdk-proto", "np-cosmos", "pallet-cosmos", + "pallet-cosmos-types", + "pallet-cosmos-x-bank", + "pallet-cosmos-x-wasm", + "pallet-cosmwasm", ] ethereum = [ "dep:ethereum", diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs index 427b2dc2..86a7560d 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos.rs @@ -17,16 +17,36 @@ // along with this program. If not, see . use crate::{extensions::unify_account, Address}; +use alloc::{ + boxed::Box, + string::{String, ToString}, + vec::Vec, +}; +use bech32::{Bech32, Hrp}; use core::marker::PhantomData; -#[cfg(feature = "cosmos")] +use cosmos_sdk_proto::{ + cosmos::bank::v1beta1::MsgSend, + cosmwasm::wasm::v1::{ + MsgExecuteContract, MsgInstantiateContract2, MsgMigrateContract, MsgStoreCode, + MsgUpdateAdmin, + }, + Any, +}; +use np_cosmos::traits::ChainInfo; pub use np_cosmos::Address as CosmosAddress; +use pallet_cosmos::types::DenomOf; +use pallet_cosmos_types::{address::acc_address_from_bech32, any_match, context, msgservice}; +use pallet_cosmos_x_bank::MsgSendHandler; +use pallet_cosmos_x_wasm::msgs::{ + MsgExecuteContractHandler, MsgInstantiateContract2Handler, MsgMigrateContractHandler, + MsgStoreCodeHandler, MsgUpdateAdminHandler, +}; use pallet_multimap::traits::UniqueMultimap; -use sp_core::H160; -use sp_runtime::traits::AccountIdConversion; +use sp_core::{Get, H160, H256}; +use sp_runtime::traits::{AccountIdConversion, Convert}; pub struct AddressMapping(PhantomData); - -impl pallet_evm::AddressMapping for AddressMapping +impl pallet_cosmos::AddressMapping for AddressMapping where T: unify_account::Config, { @@ -36,3 +56,98 @@ where .unwrap_or_else(|| address.into_account_truncating()) } } + +pub struct AssetToDenom(PhantomData); +impl Convert> for AssetToDenom +where + T: pallet_cosmos::Config, +{ + fn convert(denom: String) -> Result { + if denom == T::NativeDenom::get() { + Ok(T::NativeAssetId::get()) + } else { + let denom: DenomOf = denom.as_bytes().to_vec().try_into().map_err(|_| ())?; + pallet_cosmos::DenomAssetRouter::::get(denom).ok_or(()) + } + } +} +impl Convert for AssetToDenom +where + T: pallet_cosmos::Config, +{ + fn convert(asset_id: T::AssetId) -> String { + if asset_id == T::NativeAssetId::get() { + T::NativeDenom::get().to_string() + } else { + // TODO: Handle option + let denom = pallet_cosmos::AssetDenomRouter::::get(asset_id).unwrap().to_vec(); + String::from_utf8(denom).unwrap() + } + } +} + +pub struct MsgServiceRouter(PhantomData); +impl msgservice::traits::MsgServiceRouter for MsgServiceRouter +where + T: frame_system::Config + pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn route(msg: &Any) -> Option>> { + any_match!( + msg, { + MsgSend => Some(Box::>::default()), + MsgStoreCode => Some(Box::>::default()), + MsgInstantiateContract2 => Some(Box::>::default()), + MsgExecuteContract => Some(Box::>::default()), + MsgMigrateContract => Some(Box::>::default()), + MsgUpdateAdmin => Some(Box::>::default()), + }, + None + ) + } +} + +pub struct AccountToAddr(PhantomData); +impl Convert for AccountToAddr +where + T: pallet_cosmwasm::Config + unify_account::Config, +{ + fn convert(account: T::AccountIdExtended) -> String { + let addresses = T::AddressMap::get(account.clone()); + let address: Option<&CosmosAddress> = addresses.iter().find_map(|address| match address { + Address::Cosmos(address) => Some(address), + _ => None, + }); + let address_raw = match address { + Some(address) => address.as_ref(), + None => account.as_ref(), + }; + let hrp = Hrp::parse(T::ChainInfo::bech32_prefix()).unwrap(); + bech32::encode::(hrp, address_raw).unwrap() + } +} +impl Convert> for AccountToAddr +where + T: pallet_cosmwasm::Config + unify_account::Config, +{ + fn convert(address: String) -> Result { + let (_hrp, address_raw) = acc_address_from_bech32(&address).map_err(|_| ())?; + Self::convert(address_raw) + } +} +impl Convert, Result> for AccountToAddr +where + T: pallet_cosmwasm::Config + unify_account::Config, + T::AccountIdExtended: From, +{ + fn convert(address: Vec) -> Result { + match address.len() { + 20 => { + let address = CosmosAddress::from(H160::from_slice(&address)); + T::AddressMap::find_key(Address::Cosmos(address)).ok_or(()) + }, + 32 => Ok(H256::from_slice(&address).into()), + _ => return Err(()), + } + } +} diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 299b676b..cf322e24 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -19,6 +19,7 @@ extern crate alloc; +pub mod types; pub mod weights; pub use self::pallet::*; @@ -32,7 +33,7 @@ use frame_system::{pallet_prelude::*, CheckWeight}; use pallet_cosmos_types::{ address::acc_address_from_bech32, context::traits::Context, errors::RootError, events::traits::EventManager, gas::traits::GasMeter, handler::AnteDecorator, - msgservice::MsgServiceRouter, + msgservice::traits::MsgServiceRouter, }; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -132,6 +133,7 @@ pub trait AddressMapping { #[frame_support::pallet] pub mod pallet { use super::*; + use crate::types::DenomOf; use alloc::{string::String, vec::Vec}; use cosmos_sdk_proto::Any; use frame_support::{ @@ -294,12 +296,12 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn denom_to_asset)] pub type DenomAssetRouter = - StorageMap<_, Twox64Concat, BoundedVec, T::AssetId, OptionQuery>; + StorageMap<_, Twox64Concat, DenomOf, T::AssetId, OptionQuery>; #[pallet::storage] #[pallet::getter(fn asset_to_denom)] pub type AssetDenomRouter = - StorageMap<_, Twox64Concat, T::AssetId, BoundedVec, OptionQuery>; + StorageMap<_, Twox64Concat, T::AssetId, DenomOf, OptionQuery>; #[pallet::call] impl Pallet diff --git a/frame/cosmos/src/types.rs b/frame/cosmos/src/types.rs new file mode 100644 index 00000000..c78e6481 --- /dev/null +++ b/frame/cosmos/src/types.rs @@ -0,0 +1,21 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Config; +use sp_runtime::BoundedVec; + +pub type DenomOf = BoundedVec::MaxDenomLimit>; diff --git a/frame/cosmos/types/src/msgservice/mod.rs b/frame/cosmos/types/src/msgservice/mod.rs new file mode 100644 index 00000000..d40c752a --- /dev/null +++ b/frame/cosmos/types/src/msgservice/mod.rs @@ -0,0 +1,18 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod traits; diff --git a/frame/cosmos/types/src/msgservice.rs b/frame/cosmos/types/src/msgservice/traits.rs similarity index 100% rename from frame/cosmos/types/src/msgservice.rs rename to frame/cosmos/types/src/msgservice/traits.rs diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs index a224b74d..f9ec70f2 100644 --- a/frame/cosmos/x/auth/src/fee.rs +++ b/frame/cosmos/x/auth/src/fee.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::vec; use core::marker::PhantomData; use cosmos_sdk_proto::cosmos::tx::v1beta1::{Fee, Tx}; use frame_support::{ diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index 5a154a48..af6b181e 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -60,8 +60,8 @@ where let (_hrp, signer_addr_raw) = acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); - let who = T::AddressMapping::into_account_id(H160::from_slice(&signer_addr_raw)); + let sequence = frame_system::Pallet::::account_nonce(&who).saturated_into(); match signer_info.sequence.cmp(&sequence) { @@ -71,7 +71,7 @@ where }?; let public_key = signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; - let chain_id = T::ChainInfo::chain_id().to_string(); + let chain_id = T::ChainInfo::chain_id().into(); let signer_data = SignerData { address: signer.clone(), chain_id, @@ -116,7 +116,6 @@ where InvalidTransaction::BadSigner })?; ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); - ensure!(H160::from_slice(&signer_addr_raw) == address, InvalidTransaction::BadSigner); let sign_bytes = T::SignModeHandler::get_sign_bytes(sign_mode, signer_data, tx) diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index 9f7d027b..58a471d5 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -19,6 +19,7 @@ extern crate alloc; +use alloc::vec; use core::marker::PhantomData; use cosmos_sdk_proto::{cosmos::bank::v1beta1::MsgSend, prost::Message, Any}; use frame_support::{ @@ -39,7 +40,7 @@ use pallet_cosmos_types::{ traits::EventManager, CosmosEvent, EventAttribute, ATTRIBUTE_KEY_AMOUNT, ATTRIBUTE_KEY_SENDER, }, - msgservice::MsgHandler, + msgservice::traits::MsgHandler, }; use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; use sp_core::{Get, H160}; @@ -65,13 +66,11 @@ where let (_hrp, from_address_raw) = acc_address_from_bech32(&from_address).map_err(|_| RootError::InvalidAddress)?; - ensure!(from_address_raw.len() == 20, RootError::InvalidAddress); let from_account = T::AddressMapping::into_account_id(H160::from_slice(&from_address_raw)); let (_hrp, to_address_raw) = acc_address_from_bech32(&to_address).map_err(|_| RootError::InvalidAddress)?; - ensure!(to_address_raw.len() == 20, RootError::InvalidAddress); let to_account = T::AddressMapping::into_account_id(H160::from_slice(&to_address_raw)); diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index bd76ae21..e3db307d 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::{string::ToString, vec, vec::Vec}; use core::{marker::PhantomData, str::FromStr}; use core2::io::Read; use cosmos_sdk_proto::{ @@ -35,7 +36,7 @@ use pallet_cosmos_types::{ errors::{CosmosError, RootError}, events::{traits::EventManager, CosmosEvent, EventAttribute}, gas::traits::GasMeter, - msgservice::MsgHandler, + msgservice::traits::MsgHandler, }; use pallet_cosmos_x_wasm_types::{ errors::WasmError, @@ -205,8 +206,8 @@ where let (_hrp, address_raw) = acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; ensure!(address_raw.len() == 20, RootError::InvalidAddress); - let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + let gas = ctx.gas_meter().gas_remaining(); let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( gas, diff --git a/vendor/composable/cosmwasm/Cargo.toml b/vendor/composable/cosmwasm/Cargo.toml index 7d3aa82a..165b3a5a 100644 --- a/vendor/composable/cosmwasm/Cargo.toml +++ b/vendor/composable/cosmwasm/Cargo.toml @@ -55,6 +55,8 @@ cosmwasm-vm-wasmi = { workspace = true, default-features = false, features = [ "stargate", ] } +np-cosmos = { workspace = true, default-features = false } + [features] default = ["std"] std = [ @@ -85,5 +87,6 @@ std = [ "cosmwasm-std/std", "cosmwasm-vm/std", "cosmwasm-vm-wasmi/std", + "np-cosmos/std", ] try-runtime = [] diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index 5644b3bb..d2c3fb73 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -101,6 +101,7 @@ use frame_support::{ }, ReversibleStorageHasher, StorageHasher, }; +use np_cosmos::traits::ChainInfo; use sp_runtime::traits::SaturatedConversion; use wasmi::AsContext; use wasmi_validation::PlainValidator; @@ -119,6 +120,7 @@ pub mod pallet { }; use core::fmt::Debug; use cosmwasm_vm::system::CosmwasmCodeId; + use np_cosmos::traits::ChainInfo; use frame_support::{ dispatch::DispatchResultWithPostInfo, @@ -131,7 +133,6 @@ pub mod pallet { transactional, PalletId, Twox64Concat, }; use frame_system::{ensure_signed, pallet_prelude::OriginFor}; - use sp_core::crypto::UncheckedFrom; use sp_runtime::traits::{Convert, MaybeDisplay}; #[pallet::event] @@ -214,16 +215,13 @@ pub mod pallet { + MaybeDisplay + Ord + MaxEncodedLen - + UncheckedFrom + AsRef<[u8]>; /// Pallet unique ID. #[pallet::constant] type PalletId: Get; - /// Current chain ID. Provided to the contract via the [`Env`]. - #[pallet::constant] - type ChainId: Get<&'static str>; + type ChainInfo: ChainInfo; /// Max accepted code size in bytes. #[pallet::constant] @@ -1044,7 +1042,7 @@ impl Pallet { BlockInfo { height: frame_system::Pallet::::block_number().saturated_into(), time: Timestamp::from_seconds(T::UnixTime::now().as_secs()), - chain_id: T::ChainId::get().into(), + chain_id: T::ChainInfo::chain_id().into(), } } diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 5d9bfdd7..117513fd 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -16,7 +16,7 @@ log = { version = "0.4", default-features = false } scale-info = { version = "2.11", default-features = false, features = ["derive"] } smallvec = "1.13" -frame-babel = { workspace = true, features = ["ethereum"] } +frame-babel = { workspace = true, features = ["cosmos", "ethereum"] } np-ethereum = { workspace = true } np-runtime = { workspace = true, features = ["serde"] } pallet-multimap = { workspace = true } @@ -36,6 +36,7 @@ frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "s frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-try-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } +pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -75,7 +76,12 @@ pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk" parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +np-cosmos = { workspace = true, default-features = false } pallet-cosmos = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } +pallet-cosmos-x-auth = { workspace = true, default-features = false } +pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } +pallet-cosmwasm = { workspace = true, default-features = false } [build-dependencies] substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", optional = true } @@ -106,14 +112,20 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "log/std", + "np-cosmos/std", "np-ethereum/std", "np-runtime/std", + "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", "pallet-balances/std", "pallet-base-fee/std", "pallet-collator-selection/std", "pallet-cosmos/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-auth/std", + "pallet-cosmos-x-auth-signing/std", + "pallet-cosmwasm/std", "pallet-ethereum/std", "pallet-evm/std", "pallet-multimap/std", diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 559eed0d..660d8bf6 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -30,7 +30,8 @@ use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ - ethereum::{AddressMapping, EnsureAddress, FrontierPrecompiles}, + cosmos::{self, AccountToAddr, AssetToDenom, MsgServiceRouter}, + ethereum::{self, EnsureAddress, FrontierPrecompiles}, extensions::unify_account, }; use frame_support::{ @@ -38,7 +39,8 @@ use frame_support::{ dispatch::DispatchClass, parameter_types, traits::{ - ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, TransformOrigin, VariantCountOf, + AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, + TransformOrigin, VariantCountOf, }, weights::{ConstantMultiplier, Weight}, PalletId, @@ -47,6 +49,15 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_cosmos::config_preludes::{ + MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, TxSigLimit, + WeightToGas, +}; +use pallet_cosmos_types::context::Context; +use pallet_cosmos_x_auth_signing::{ + sign_mode_handler::SignModeHandler, sign_verifiable_tx::SigVerifiableTx, +}; +use pallet_cosmwasm::instrument::CostRules; use pallet_ethereum::PostLogContent; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; @@ -54,7 +65,7 @@ use polkadot_runtime_common::{ xcm_sender::NoPriceForMessageDelivery, BlockHashCount, SlowAdjustingFeeUpdate, }; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::U256; +use sp_core::{ConstU128, U256}; use sp_runtime::{Perbill, Permill}; use sp_version::RuntimeVersion; use xcm::latest::prelude::BodyId; @@ -62,12 +73,12 @@ use xcm::latest::prelude::BodyId; // Local module imports use super::{ weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, - AccountId, AddressMap, Aura, Balance, Balances, BaseFee, Block, BlockNumber, CollatorSelection, - ConsensusHook, Hash, Instance1, MessageQueue, Nonce, PalletInfo, ParachainSystem, Runtime, - RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, - Session, SessionKeys, System, Timestamp, WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, - EXISTENTIAL_DEPOSIT, HOURS, MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, - SLOT_DURATION, VERSION, + AccountId, AddressMap, AssetId, Assets, Aura, Balance, Balances, BaseFee, Block, BlockNumber, + CollatorSelection, ConsensusHook, Hash, Instance1, MessageQueue, Nonce, PalletInfo, + ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, + RuntimeOrigin, RuntimeTask, Session, SessionKeys, System, Timestamp, WeightToFee, XcmpQueue, + AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, MAXIMUM_BLOCK_WEIGHT, MICROUNIT, + NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; use xcm_config::{RelayLocation, XcmOriginToTransactDispatchOrigin}; @@ -345,7 +356,7 @@ impl pallet_evm::Config for Runtime { type BlockHashMapping = pallet_ethereum::EthereumBlockHashMapping; type CallOrigin = EnsureAddress; type WithdrawOrigin = EnsureAddress; - type AddressMapping = AddressMapping; + type AddressMapping = ethereum::AddressMapping; type Currency = Balances; type RuntimeEvent = RuntimeEvent; type PrecompilesType = FrontierPrecompiles; @@ -397,3 +408,110 @@ impl pallet_base_fee::Config for Runtime { type DefaultBaseFeePerGas = DefaultBaseFeePerGas; type DefaultElasticity = DefaultElasticity; } + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type AssetIdParameter = codec::Compact; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = ConstU128<500>; + type AssetAccountDeposit = ConstU128<500>; + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type ApprovalDeposit = ConstU128<0>; + type StringLimit = ConstU32<20>; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; +} + +impl pallet_cosmos::Config for Runtime { + type AddressMapping = cosmos::AddressMapping; + type Balance = Balance; + type AssetId = AssetId; + type NativeAsset = Balances; + type Assets = Assets; + type NativeDenom = NativeDenom; + type NativeAssetId = NativeAssetId; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_cosmos::weights::CosmosWeight; + type WeightToGas = WeightToGas; + type AssetToDenom = AssetToDenom; + type Context = Context; + type ChainInfo = np_cosmos::traits::CosmosHub; + type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; + type MsgFilter = MsgFilter; + type MsgServiceRouter = MsgServiceRouter; + type SigVerifiableTx = SigVerifiableTx; + type SignModeHandler = SignModeHandler; + type MaxMemoCharacters = MaxMemoCharacters; + type TxSigLimit = TxSigLimit; + type MaxDenomLimit = MaxDenomLimit; +} + +parameter_types! { + pub const CosmwasmPalletId: PalletId = PalletId(*b"cosmwasm"); + pub const MaxContractLabelSize: u32 = 64; + pub const MaxContractTrieIdSize: u32 = Hash::len_bytes() as u32; + pub const MaxInstantiateSaltSize: u32 = 128; + pub const MaxFundsAssets: u32 = 32; + pub const CodeTableSizeLimit: u32 = 4096; + pub const CodeGlobalVariableLimit: u32 = 256; + pub const CodeParameterLimit: u32 = 128; + pub const CodeBranchTableSizeLimit: u32 = 256; + pub const CodeStorageByteDeposit: u32 = 1_000_000; + pub const ContractStorageByteReadPrice: u32 = 1; + pub const ContractStorageByteWritePrice: u32 = 1; + pub WasmCostRules: CostRules = Default::default(); +} + +impl pallet_cosmwasm::Config for Runtime { + const MAX_FRAMES: u8 = 64; + type RuntimeEvent = RuntimeEvent; + type AccountIdExtended = AccountId; + type PalletId = CosmwasmPalletId; + type MaxCodeSize = ConstU32<{ 1024 * 1024 }>; + type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; + type MaxMessageSize = ConstU32<{ 64 * 1024 }>; + type AccountToAddr = AccountToAddr; + type AssetToDenom = AssetToDenom; + type Balance = Balance; + type AssetId = AssetId; + type Assets = Assets; + type NativeAsset = Balances; + type ChainInfo = np_cosmos::traits::CosmosHub; + type MaxContractLabelSize = MaxContractLabelSize; + type MaxContractTrieIdSize = MaxContractTrieIdSize; + type MaxInstantiateSaltSize = MaxInstantiateSaltSize; + type MaxFundsAssets = MaxFundsAssets; + + type CodeTableSizeLimit = CodeTableSizeLimit; + type CodeGlobalVariableLimit = CodeGlobalVariableLimit; + type CodeStackLimit = ConstU32<{ u32::MAX }>; + + type CodeParameterLimit = CodeParameterLimit; + type CodeBranchTableSizeLimit = CodeBranchTableSizeLimit; + type CodeStorageByteDeposit = CodeStorageByteDeposit; + type ContractStorageByteReadPrice = ContractStorageByteReadPrice; + type ContractStorageByteWritePrice = ContractStorageByteWritePrice; + + type WasmCostRules = WasmCostRules; + type UnixTime = Timestamp; + type WeightInfo = pallet_cosmwasm::weights::SubstrateWeight; + + // TODO: Precompile to use execute or query pallet + type PalletHook = (); + + type UploadWasmOrigin = frame_system::EnsureSigned; + + type ExecuteWasmOrigin = frame_system::EnsureSigned; + + type NativeDenom = NativeDenom; + + type NativeAssetId = NativeAssetId; +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs index 79cff117..f6c782bf 100644 --- a/ziggurat/runtime/src/lib.rs +++ b/ziggurat/runtime/src/lib.rs @@ -75,6 +75,8 @@ pub type SignedBlock = generic::SignedBlock; /// BlockId type as expected by this runtime. pub type BlockId = generic::BlockId; +pub type AssetId = u32; + /// The SignedExtension to the basic transaction logic. #[docify::export(template_signed_extra)] pub type SignedExtra = ( @@ -300,6 +302,8 @@ mod runtime { pub type CumulusXcm = cumulus_pallet_xcm; #[runtime::pallet_index(33)] pub type MessageQueue = pallet_message_queue; + #[runtime::pallet_index(39)] + pub type Assets = pallet_assets; // Ethereum compatibility. #[runtime::pallet_index(60)] @@ -311,6 +315,10 @@ mod runtime { #[runtime::pallet_index(128)] pub type AddressMap = pallet_multimap; + #[runtime::pallet_index(179)] + pub type Cosmos = pallet_cosmos; + #[runtime::pallet_index(180)] + pub type Cosmwasm = pallet_cosmwasm; } #[docify::export(register_validate_block)] From 14edd1e79ac8a261c13333675ebe4978ce3803cf Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 19 Sep 2024 18:30:19 +0900 Subject: [PATCH 049/133] refactor: Remove needless return --- frame/babel/src/cosmos.rs | 2 +- frame/cosmos/x/auth/src/sigverify.rs | 2 +- frame/cosmos/x/wasm/src/msgs.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs index 86a7560d..4eb00f35 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos.rs @@ -147,7 +147,7 @@ where T::AddressMap::find_key(Address::Cosmos(address)).ok_or(()) }, 32 => Ok(H256::from_slice(&address).into()), - _ => return Err(()), + _ => Err(()), } } } diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index af6b181e..3270c0b7 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -61,7 +61,7 @@ where acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); let who = T::AddressMapping::into_account_id(H160::from_slice(&signer_addr_raw)); - + let sequence = frame_system::Pallet::::account_nonce(&who).saturated_into(); match signer_info.sequence.cmp(&sequence) { diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index e3db307d..8ca16ac1 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -207,7 +207,7 @@ where acc_address_from_bech32(&sender).map_err(|_| RootError::InvalidAddress)?; ensure!(address_raw.len() == 20, RootError::InvalidAddress); let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); - + let gas = ctx.gas_meter().gas_remaining(); let mut shared = pallet_cosmwasm::Pallet::::do_create_vm_shared( gas, From 7a92f44eca63394d1fe2b380310a2cb63c253402 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 09:56:44 +0900 Subject: [PATCH 050/133] feat: Implement SelfContainedCall for cosmos transaction --- frame/babel/src/extensions/unify_account.rs | 8 ++ frame/cosmos/src/lib.rs | 55 ++++++------ frame/cosmos/types/src/msgservice/traits.rs | 2 +- frame/cosmos/x/bank/src/lib.rs | 2 +- frame/cosmos/x/wasm/src/msgs.rs | 10 +-- ziggurat/runtime/Cargo.toml | 14 +++- ziggurat/runtime/src/call.rs | 93 +++++++++++++++++++++ 7 files changed, 146 insertions(+), 38 deletions(-) diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index 50969681..d3a4ef77 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -66,6 +66,14 @@ impl UnifyAccount { T::AddressMap::try_insert(who.clone(), Address::Ethereum(address)) .map_err(|_| "account unification failed: ethereum")?; } + #[cfg(feature = "cosmos")] + { + let address = CosmosAddress::from(public); + let interim = address.clone().into_account_truncating(); + T::DrainBalance::drain_balance(&interim, who)?; + T::AddressMap::try_insert(who.clone(), Address::Cosmos(address)) + .map_err(|_| "account unification failed: cosmos")?; + } } Ok(()) } diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index cf322e24..c977dacb 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -31,8 +31,12 @@ use frame_support::{ }; use frame_system::{pallet_prelude::*, CheckWeight}; use pallet_cosmos_types::{ - address::acc_address_from_bech32, context::traits::Context, errors::RootError, - events::traits::EventManager, gas::traits::GasMeter, handler::AnteDecorator, + address::acc_address_from_bech32, + context::traits::Context, + errors::{CosmosError, RootError}, + events::traits::EventManager, + gas::traits::GasMeter, + handler::AnteDecorator, msgservice::traits::MsgServiceRouter, }; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; @@ -74,14 +78,12 @@ where if let Call::transact { tx_bytes } = self { let check = || { let tx = Tx::decode(&mut &tx_bytes[..]).map_err(|_| InvalidTransaction::Call)?; + let fee_payer = T::SigVerifiableTx::fee_payer(&tx).map_err(|_| InvalidTransaction::Call)?; let (_hrp, address_raw) = acc_address_from_bech32(&fee_payer) .map_err(|_| InvalidTransaction::BadSigner)?; - - if address_raw.len() != 20 { - return Err(InvalidTransaction::BadSigner.into()); - } + ensure!(address_raw.len() == 20, InvalidTransaction::BadSigner); Ok(H160::from_slice(&address_raw)) }; @@ -277,7 +279,6 @@ pub mod pallet { weight.ref_time() } } - impl Convert for WeightToGas { fn convert(gas: Gas) -> Weight { Weight::from_parts(gas, 0) @@ -378,28 +379,11 @@ impl Pallet { .gas_limit; let mut ctx = T::Context::new(gas_limit); - ctx.gas_meter() - .consume_gas(T::WeightInfo::default_weight().ref_time(), "") - .map_err(|_| { - Error::::CosmosError(RootError::OutOfGas.into()) - .with_weight(T::WeightInfo::default_weight()) - })?; - let body = tx.body.ok_or( - Error::::CosmosError(RootError::TxDecodeError.into()) - .with_weight(T::WeightInfo::default_weight()), - )?; - for msg in body.messages.iter() { - let handler = T::MsgServiceRouter::route(msg).ok_or( - Error::::CosmosError(RootError::UnknownRequest.into()) - .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())), - )?; - - handler.handle(msg, &mut ctx).map_err(|e| { - Error::::CosmosError(e) - .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())) - })?; - } + Self::run_tx(&mut ctx, &tx).map_err(|e| { + Error::::CosmosError(e) + .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())) + })?; Self::deposit_event(Event::Executed { gas_wanted: gas_limit, @@ -412,4 +396,19 @@ impl Pallet { pays_fee: Pays::Yes, }) } + + fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { + ctx.gas_meter() + .consume_gas(T::WeightToGas::convert(T::WeightInfo::default_weight()), "") + .map_err(|_| RootError::OutOfGas)?; + + let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; + + for msg in body.messages.iter() { + let handler = T::MsgServiceRouter::route(msg).ok_or(RootError::UnknownRequest)?; + handler.handle(ctx, msg)?; + } + + Ok(()) + } } diff --git a/frame/cosmos/types/src/msgservice/traits.rs b/frame/cosmos/types/src/msgservice/traits.rs index 167c487d..9ea59377 100644 --- a/frame/cosmos/types/src/msgservice/traits.rs +++ b/frame/cosmos/types/src/msgservice/traits.rs @@ -20,7 +20,7 @@ use alloc::boxed::Box; use cosmos_sdk_proto::Any; pub trait MsgHandler { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError>; + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError>; } pub trait MsgServiceRouter { diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index 58a471d5..28170d56 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -59,7 +59,7 @@ where T: pallet_cosmos::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgSend { from_address, to_address, amount } = MsgSend::decode(&mut &*msg.value).map_err(|_| RootError::UnpackAnyError)?; diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index 8ca16ac1..42580fe0 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -68,7 +68,7 @@ where T: pallet_cosmos::Config + pallet_cosmwasm::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgStoreCode { sender, wasm_byte_code, instantiate_permission: _ } = MsgStoreCode::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -121,7 +121,7 @@ where T: pallet_cosmos::Config + pallet_cosmwasm::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgInstantiateContract2 { sender, admin, code_id, label, msg, funds, salt, fix_msg: _ } = MsgInstantiateContract2::decode(&mut &*msg.value) @@ -197,7 +197,7 @@ where T: pallet_cosmos::Config + pallet_cosmwasm::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgExecuteContract { sender, contract, msg, funds } = MsgExecuteContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -270,7 +270,7 @@ where T: pallet_cosmos::Config + pallet_cosmwasm::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgMigrateContract { sender, contract, code_id, msg } = MsgMigrateContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -331,7 +331,7 @@ where T: pallet_cosmos::Config + pallet_cosmwasm::Config, Context: context::traits::Context, { - fn handle(&self, msg: &Any, ctx: &mut Context) -> Result<(), CosmosError> { + fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { // TODO: Add gas metering let MsgUpdateAdmin { sender, new_admin, contract } = MsgUpdateAdmin::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 117513fd..eaf26456 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -9,11 +9,15 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = [ + "derive", +] } docify = "0.2" hex-literal = { version = "0.4", optional = true } log = { version = "0.4", default-features = false } -scale-info = { version = "2.11", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = [ + "derive", +] } smallvec = "1.13" frame-babel = { workspace = true, features = ["cosmos", "ethereum"] } @@ -31,7 +35,9 @@ pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = ["experimental"] } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = [ + "experimental", +] } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -76,6 +82,7 @@ pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk" parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cosmos-sdk-proto = { version = "0.24.0", default-features = false } np-cosmos = { workspace = true, default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } @@ -90,6 +97,7 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", b default = ["std"] std = [ "codec/std", + "cosmos-sdk-proto/std", "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", "cumulus-pallet-session-benchmarking/std", diff --git a/ziggurat/runtime/src/call.rs b/ziggurat/runtime/src/call.rs index 4c8b1290..b4824dad 100644 --- a/ziggurat/runtime/src/call.rs +++ b/ziggurat/runtime/src/call.rs @@ -16,10 +16,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use cosmos_sdk_proto::{ + cosmos::{crypto::secp256k1, tx::v1beta1::Tx}, + prost::Message, +}; use fp_evm::TransactionValidationError; use fp_self_contained::SelfContainedCall; use frame_babel::{ethereum::TransactionExt, UnifyAccount}; +use np_cosmos::Address as CosmosAddress; use np_ethereum::Address as EthereumAddress; +use pallet_cosmos_types::any_match; use sp_core::ecdsa; use sp_runtime::{ traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf}, @@ -37,6 +43,7 @@ impl SelfContainedCall for RuntimeCall { fn is_self_contained(&self) -> bool { match self { RuntimeCall::Ethereum(call) => call.is_self_contained(), + RuntimeCall::Cosmos(call) => call.is_self_contained(), _ => false, } } @@ -58,6 +65,38 @@ impl SelfContainedCall for RuntimeCall { None } }, + RuntimeCall::Cosmos(call) => + if let pallet_cosmos::Call::transact { tx_bytes } = call { + let check = || { + let tx = + Tx::decode(&mut &tx_bytes[..]).map_err(|_| InvalidTransaction::Call)?; + let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + let fee = auth_info.fee.as_ref().ok_or(InvalidTransaction::Call)?; + + let public_key = if fee.payer.is_empty() { + auth_info.signer_infos.first() + } else { + auth_info.signer_infos.last() + } + .and_then(|signer_info| signer_info.public_key.as_ref()) + .ok_or(InvalidTransaction::Call)?; + + let origin = any_match!( + public_key, { + secp256k1::PubKey => { + let pubkey = secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| InvalidTransaction::BadSigner)?; + ecdsa::Public::try_from(pubkey.key.as_ref()).map(Self::SignedInfo::from).map_err(|_| InvalidTransaction::BadSigner) + } + }, + Err(InvalidTransaction::BadSigner) + )?; + + Ok(origin) + }; + Some(check()) + } else { + None + }, _ => None, } } @@ -85,6 +124,17 @@ impl SelfContainedCall for RuntimeCall { let address: EthereumAddress = public.into(); call.validate_self_contained(&address.into(), dispatch_info, len) }, + RuntimeCall::Cosmos(call) => { + if let pallet_cosmos::Call::transact { tx_bytes } = call { + match Self::unify_cosmos_account(tx_bytes) { + Ok(_) => (), + Err(e) => return Some(Err(e)), + }; + }; + let public: ecdsa::Public = info.clone().try_into().unwrap(); + let address: CosmosAddress = public.into(); + call.validate_self_contained(&address.into(), dispatch_info, len) + }, _ => None, } } @@ -112,6 +162,15 @@ impl SelfContainedCall for RuntimeCall { let address: EthereumAddress = public.into(); call.pre_dispatch_self_contained(&address.into(), dispatch_info, len) }, + RuntimeCall::Cosmos(call) => { + if let pallet_cosmos::Call::transact { tx_bytes } = call { + match Self::unify_cosmos_account(tx_bytes) { + Ok(_) => (), + Err(e) => return Some(Err(e)), + }; + }; + call.pre_dispatch_self_contained(dispatch_info, len) + }, _ => None, } } @@ -128,7 +187,41 @@ impl SelfContainedCall for RuntimeCall { pallet_ethereum::RawOrigin::EthereumTransaction(address.into()), ))) }, + call @ RuntimeCall::Cosmos(pallet_cosmos::Call::transact { .. }) => { + let public: ecdsa::Public = info.clone().try_into().unwrap(); + let address: CosmosAddress = public.into(); + Some(call.dispatch(RuntimeOrigin::from( + pallet_cosmos::RawOrigin::CosmosTransaction(address.into()), + ))) + }, _ => None, } } } + +impl RuntimeCall { + fn unify_cosmos_account(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; + let signer_infos = &tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?.signer_infos; + + for signer_info in signer_infos.iter() { + if signer_info.sequence == 0 { + let public_key = signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; + let signer = any_match!( + public_key, { + secp256k1::PubKey => { + let pubkey = secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| InvalidTransaction::BadSigner)?; + ecdsa::Public::try_from(pubkey.key.as_ref()).map(AccountId::from).map_err(|_| InvalidTransaction::BadSigner) + } + }, + Err(InvalidTransaction::BadSigner) + )?; + + UnifyAccount::::unify_ecdsa(&signer) + .map_err(|_| InvalidTransaction::Call)?; + } + } + + Ok(()) + } +} From 78e8dab76ca628a6ef1186cf5171bac6c4f3ca97 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 11:41:48 +0900 Subject: [PATCH 051/133] feat: Implement cosmos and cosmwasm runtime apis --- frame/babel/src/cosmos.rs | 1 + frame/cosmos/src/lib.rs | 8 +++-- vendor/composable/cosmwasm/src/lib.rs | 6 +++- ziggurat/runtime/Cargo.toml | 2 ++ ziggurat/runtime/src/apis.rs | 50 +++++++++++++++++++++++++-- 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs index 4eb00f35..1f6f417c 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos.rs @@ -42,6 +42,7 @@ use pallet_cosmos_x_wasm::msgs::{ MsgStoreCodeHandler, MsgUpdateAdminHandler, }; use pallet_multimap::traits::UniqueMultimap; +use scale_info::TypeInfo; use sp_core::{Get, H160, H256}; use sp_runtime::traits::{AccountIdConversion, Convert}; diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index c977dacb..d9f9d507 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -339,7 +339,7 @@ impl Pallet { fn validate_transaction_in_pool(origin: H160, tx_bytes: &[u8]) -> TransactionValidity { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; - T::AnteHandler::ante_handle(&tx, true)?; + Self::ante_handle(&tx, true)?; let transaction_nonce = T::SigVerifiableTx::sequence(&tx).map_err(|_| InvalidTransaction::Call)?; @@ -362,7 +362,7 @@ impl Pallet { pub fn validate_transaction_in_block(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; - T::AnteHandler::ante_handle(&tx, false)?; + Self::ante_handle(&tx, false)?; Ok(()) } @@ -397,6 +397,10 @@ impl Pallet { }) } + pub fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { + T::AnteHandler::ante_handle(tx, simulate) + } + fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { ctx.gas_meter() .consume_gas(T::WeightToGas::convert(T::WeightInfo::default_weight()), "") diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index d2c3fb73..aaddab9e 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -102,7 +102,7 @@ use frame_support::{ ReversibleStorageHasher, StorageHasher, }; use np_cosmos::traits::ChainInfo; -use sp_runtime::traits::SaturatedConversion; +use sp_runtime::traits::{Convert, SaturatedConversion}; use wasmi::AsContext; use wasmi_validation::PlainValidator; @@ -596,6 +596,10 @@ pub fn query( ) } +pub fn contract_account_of(contract: String) -> Option> { + T::AccountToAddr::convert(contract).ok() +} + #[allow(clippy::too_many_arguments)] pub fn instantiate( instantiator: AccountIdOf, diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index eaf26456..5a167ff2 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -82,7 +82,9 @@ pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk" parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +cosmos-runtime-api = { workspace = true, default-features = false } cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmwasm-runtime-api = { workspace = true, default-features = false } np-cosmos = { workspace = true, default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 3fe31ced..19d123bb 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -24,8 +24,10 @@ // For more information, please refer to // External crates imports -use alloc::vec::Vec; +use alloc::{format, string::String, vec::Vec}; use codec::Encode; +use cosmos_runtime_api::{GasInfo, SimulateError, SimulateResponse, SimulateResult}; +use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; use frame_support::{ genesis_builder_helper::{build_state, get_preset}, traits::OnFinalize, @@ -47,7 +49,7 @@ use sp_version::RuntimeVersion; // Local module imports use super::{ AccountId, Balance, Block, ConsensusHook, Ethereum, Executive, InherentDataExt, Nonce, - ParachainSystem, Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, + ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, UncheckedExtrinsic, SLOT_DURATION, VERSION, }; @@ -560,4 +562,48 @@ impl_runtime_apis! { Default::default() } } + + impl cosmos_runtime_api::CosmosRuntimeApi for Runtime { + fn convert_tx(tx_bytes: Vec) -> ::Extrinsic { + UncheckedExtrinsic::new_unsigned( + pallet_cosmos::Call::::transact { tx_bytes }.into(), + ) + } + + fn simulate(tx_bytes: Vec) -> SimulateResult { + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| SimulateError::InvalidTx)?; + + pallet_cosmos::Pallet::::ante_handle(&tx, true) + .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; + pallet_cosmos::Pallet::::apply_validated_transaction(tx) + .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; + + System::read_events_no_consensus() + .find_map(|record| { + if let RuntimeEvent::Cosmos(pallet_cosmos::Event::Executed { gas_wanted, gas_used, events }) = record.event { + Some(SimulateResponse{gas_info: GasInfo { gas_wanted, gas_used }, events}) + } else { + None + } + }).ok_or(SimulateError::InternalError("Cosmos events does not exist".into())) + } + } + + impl cosmwasm_runtime_api::CosmwasmRuntimeApi> for Runtime { + fn query( + contract: String, + gas: u64, + query_request: Vec, + ) -> Result, Vec>{ + let contract = pallet_cosmwasm::contract_account_of::(contract.clone()).ok_or("Not exist contract".as_bytes().to_vec())?; + match pallet_cosmwasm::query::( + contract, + gas, + query_request, + ) { + Ok(response) => Ok(response.into()), + Err(err) => Err(format!("{:?}", err).into_bytes()) + } + } + } } From 08788ca0791b218c8021f298d6c4efd7e56fa13e Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 13:22:08 +0900 Subject: [PATCH 052/133] feat: Implement cosmos rpc --- frame/babel/Cargo.toml | 2 +- frame/cosmos/Cargo.toml | 2 +- frame/cosmos/rpc/Cargo.toml | 2 +- frame/cosmos/rpc/src/cosmos.rs | 2 +- frame/cosmos/types/Cargo.toml | 2 +- frame/cosmos/x/auth/Cargo.toml | 2 +- frame/cosmos/x/auth/migrations/Cargo.toml | 2 +- frame/cosmos/x/auth/signing/Cargo.toml | 2 +- frame/cosmos/x/bank/Cargo.toml | 2 +- frame/cosmos/x/bank/types/Cargo.toml | 2 +- frame/cosmos/x/wasm/Cargo.toml | 2 +- frame/cosmos/x/wasm/types/Cargo.toml | 2 +- vendor/composable/cosmwasm/rpc/Cargo.toml | 2 +- ziggurat/node/Cargo.toml | 5 +++ ziggurat/node/src/cosmos/mod.rs | 19 ++++++++ ziggurat/node/src/cosmos/rpc.rs | 55 +++++++++++++++++++++++ ziggurat/node/src/main.rs | 1 + ziggurat/runtime/Cargo.toml | 2 +- 18 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 ziggurat/node/src/cosmos/mod.rs create mode 100644 ziggurat/node/src/cosmos/rpc.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index b92bd117..08325042 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] bech32 = { version = "0.11", default-features = false, features = ["alloc"], optional = true } -cosmos-sdk-proto = { version = "0.24.0", default-features = false, optional = true } +cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true } ethereum = { version = "0.15.0", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } diff --git a/frame/cosmos/Cargo.toml b/frame/cosmos/Cargo.toml index 11bc0130..738f8b97 100644 --- a/frame/cosmos/Cargo.toml +++ b/frame/cosmos/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } diff --git a/frame/cosmos/rpc/Cargo.toml b/frame/cosmos/rpc/Cargo.toml index 1d4e460b..1c78f621 100644 --- a/frame/cosmos/rpc/Cargo.toml +++ b/frame/cosmos/rpc/Cargo.toml @@ -11,7 +11,7 @@ publish = false cosmos-runtime-api = { workspace = true, features = ["std"] } futures = { version = "0.3" } hex = { version = "0.4.3" } -jsonrpsee = { version = "0.23", features = ["server", "macros"] } +jsonrpsee = { version = "0.23", features = ["client", "server", "macros"] } sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } diff --git a/frame/cosmos/rpc/src/cosmos.rs b/frame/cosmos/rpc/src/cosmos.rs index c045367a..e176cb93 100644 --- a/frame/cosmos/rpc/src/cosmos.rs +++ b/frame/cosmos/rpc/src/cosmos.rs @@ -29,7 +29,7 @@ use sp_core::{sha2_256, Bytes, H256}; use sp_runtime::{traits::Block as BlockT, transaction_validity::TransactionSource}; use std::sync::Arc; -#[rpc(server)] +#[rpc(client, server)] #[async_trait] pub trait CosmosApi { #[method(name = "cosmos_broadcastTx")] diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml index 6b5fd125..dcb4c7ff 100644 --- a/frame/cosmos/types/Cargo.toml +++ b/frame/cosmos/types/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] bech32 = { version = "0.11", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = ["cosmwasm"] } +cosmos-sdk-proto = { version = "0.24", default-features = false, features = ["cosmwasm"] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } impl-trait-for-tuples = { version = "0.2.2" } parity-scale-codec = { version = "3.6", default-features = false, features = [ diff --git a/frame/cosmos/x/auth/Cargo.toml b/frame/cosmos/x/auth/Cargo.toml index 4c6cf229..84642fbf 100644 --- a/frame/cosmos/x/auth/Cargo.toml +++ b/frame/cosmos/x/auth/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } np-cosmos = { workspace = true, default-features = false } diff --git a/frame/cosmos/x/auth/migrations/Cargo.toml b/frame/cosmos/x/auth/migrations/Cargo.toml index bebd7c4f..48dd04d4 100644 --- a/frame/cosmos/x/auth/migrations/Cargo.toml +++ b/frame/cosmos/x/auth/migrations/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } serde = { version = "1.0.210", default-features = false, features = ["derive"] } serde_json = { version = "1.0.127", default-features = false } diff --git a/frame/cosmos/x/auth/signing/Cargo.toml b/frame/cosmos/x/auth/signing/Cargo.toml index 3e878df9..3d994727 100644 --- a/frame/cosmos/x/auth/signing/Cargo.toml +++ b/frame/cosmos/x/auth/signing/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ +cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ "cosmwasm", ] } pallet-cosmos-types = { workspace = true, default-features = false } diff --git a/frame/cosmos/x/bank/Cargo.toml b/frame/cosmos/x/bank/Cargo.toml index e18a7dfa..ffdb5661 100644 --- a/frame/cosmos/x/bank/Cargo.toml +++ b/frame/cosmos/x/bank/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } diff --git a/frame/cosmos/x/bank/types/Cargo.toml b/frame/cosmos/x/bank/types/Cargo.toml index 63267148..f7b25760 100644 --- a/frame/cosmos/x/bank/types/Cargo.toml +++ b/frame/cosmos/x/bank/types/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } pallet-cosmos-x-auth-migrations = { workspace = true, default-features = false } serde = { version = "1.0.210", default-features = false, features = ["derive"] } diff --git a/frame/cosmos/x/wasm/Cargo.toml b/frame/cosmos/x/wasm/Cargo.toml index 836bf860..33851d8f 100644 --- a/frame/cosmos/x/wasm/Cargo.toml +++ b/frame/cosmos/x/wasm/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] core2 = { version = "0.4.0", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ +cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ "cosmwasm", ] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } diff --git a/frame/cosmos/x/wasm/types/Cargo.toml b/frame/cosmos/x/wasm/types/Cargo.toml index 11d7c676..92de61a4 100644 --- a/frame/cosmos/x/wasm/types/Cargo.toml +++ b/frame/cosmos/x/wasm/types/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -cosmos-sdk-proto = { version = "0.24.0", default-features = false, features = [ +cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ "cosmwasm", ] } pallet-cosmos-types = { workspace = true, default-features = false } diff --git a/vendor/composable/cosmwasm/rpc/Cargo.toml b/vendor/composable/cosmwasm/rpc/Cargo.toml index 30be1fa9..5b1cf942 100644 --- a/vendor/composable/cosmwasm/rpc/Cargo.toml +++ b/vendor/composable/cosmwasm/rpc/Cargo.toml @@ -25,4 +25,4 @@ codec = { package = "parity-scale-codec", version = "3.6", default-features = fa ] } # rpc -jsonrpsee = { version = "0.22.5", features = ["client", "server", "macros"] } +jsonrpsee = { version = "0.23", features = ["client", "server", "macros"] } diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml index 8062cac9..902271e7 100644 --- a/ziggurat/node/Cargo.toml +++ b/ziggurat/node/Cargo.toml @@ -78,6 +78,11 @@ cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/p cumulus-relay-chain-interface = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } cumulus-test-relay-sproof-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } +cosmos-rpc = { workspace = true } +cosmos-runtime-api = { workspace = true } +cosmwasm-rpc = { workspace = true } +cosmwasm-runtime-api = { workspace = true } + [build-dependencies] docify = "0.2" substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } diff --git a/ziggurat/node/src/cosmos/mod.rs b/ziggurat/node/src/cosmos/mod.rs new file mode 100644 index 00000000..daefbad8 --- /dev/null +++ b/ziggurat/node/src/cosmos/mod.rs @@ -0,0 +1,19 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub mod rpc; diff --git a/ziggurat/node/src/cosmos/rpc.rs b/ziggurat/node/src/cosmos/rpc.rs new file mode 100644 index 00000000..09225716 --- /dev/null +++ b/ziggurat/node/src/cosmos/rpc.rs @@ -0,0 +1,55 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ziggurat_runtime::opaque::Block; + +use jsonrpsee::RpcModule; +use sc_transaction_pool_api::TransactionPool; +use sp_api::ProvideRuntimeApi; +use sp_block_builder::BlockBuilder; +use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use std::{error::Error, sync::Arc}; + +pub struct FullDeps { + pub client: Arc, + pub pool: Arc

, +} + +pub fn create_full( + deps: FullDeps, +) -> Result, Box> +where + C: ProvideRuntimeApi, + C: HeaderBackend + HeaderMetadata + 'static, + C: Send + Sync + 'static, + C::Api: BlockBuilder, + P: TransactionPool + 'static, + C::Api: cosmos_runtime_api::CosmosRuntimeApi, + C::Api: cosmwasm_runtime_api::CosmwasmRuntimeApi>, +{ + use cosmos_rpc::cosmos::{Cosmos, CosmosApiServer}; + use cosmwasm_rpc::{Cosmwasm, CosmwasmApiServer}; + + let mut module = RpcModule::new(()); + let FullDeps { client, pool } = deps; + + module.merge(Cosmos::new(client.clone(), pool).into_rpc())?; + module.merge(Cosmwasm::new(client).into_rpc())?; + + Ok(module) +} diff --git a/ziggurat/node/src/main.rs b/ziggurat/node/src/main.rs index 9b660805..28a2556e 100644 --- a/ziggurat/node/src/main.rs +++ b/ziggurat/node/src/main.rs @@ -5,6 +5,7 @@ mod chain_spec; mod cli; mod command; +mod cosmos; mod eth; mod rpc; mod service; diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 5a167ff2..cef8ec2f 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -83,7 +83,7 @@ parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } cosmos-runtime-api = { workspace = true, default-features = false } -cosmos-sdk-proto = { version = "0.24.0", default-features = false } +cosmos-sdk-proto = { version = "0.24", default-features = false } cosmwasm-runtime-api = { workspace = true, default-features = false } np-cosmos = { workspace = true, default-features = false } pallet-cosmos = { workspace = true, default-features = false } From 22094ef6b8681b386f4878dd1a73c6d5c26af14e Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 13:56:30 +0900 Subject: [PATCH 053/133] feat: Add cosmos rpc to node --- frame/babel/src/cosmos.rs | 1 - frame/cosmos/rpc/Cargo.toml | 4 ++-- ziggurat/node/src/cosmos/mod.rs | 39 +++++++++++++++++++++++++++++++++ ziggurat/node/src/service.rs | 22 +++++++++++++++++-- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs index 1f6f417c..4eb00f35 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos.rs @@ -42,7 +42,6 @@ use pallet_cosmos_x_wasm::msgs::{ MsgStoreCodeHandler, MsgUpdateAdminHandler, }; use pallet_multimap::traits::UniqueMultimap; -use scale_info::TypeInfo; use sp_core::{Get, H160, H256}; use sp_runtime::traits::{AccountIdConversion, Convert}; diff --git a/frame/cosmos/rpc/Cargo.toml b/frame/cosmos/rpc/Cargo.toml index 1c78f621..69d3e798 100644 --- a/frame/cosmos/rpc/Cargo.toml +++ b/frame/cosmos/rpc/Cargo.toml @@ -9,8 +9,8 @@ publish = false [dependencies] cosmos-runtime-api = { workspace = true, features = ["std"] } -futures = { version = "0.3" } -hex = { version = "0.4.3" } +futures = "0.3" +hex = "0.4.3" jsonrpsee = { version = "0.23", features = ["client", "server", "macros"] } sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } diff --git a/ziggurat/node/src/cosmos/mod.rs b/ziggurat/node/src/cosmos/mod.rs index daefbad8..607767fb 100644 --- a/ziggurat/node/src/cosmos/mod.rs +++ b/ziggurat/node/src/cosmos/mod.rs @@ -17,3 +17,42 @@ // along with this program. If not, see . pub mod rpc; + +use jsonrpsee::{core::id_providers::RandomIntegerIdProvider, RpcModule}; +use sc_rpc::DenyUnsafe; +use sc_service::{Error as ServiceError, TaskManager}; + +pub struct SpawnTasksParams<'a> { + pub config: sc_service::Configuration, + pub rpc_builder: Box Result, ServiceError>>, + pub task_manager: &'a mut TaskManager, +} + +pub fn spawn_tasks( + params: SpawnTasksParams, +) -> Result { + let SpawnTasksParams { mut config, rpc_builder, task_manager } = params; + + let rpc_port = config.rpc_addr.map(|addr| addr.port()).unwrap_or(config.rpc_port); + let prometheus_config = config.prometheus_config.take(); + + // TODO: Make the Cosmos RPC port configurable. + let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(26657)); + + let rpc = sc_service::start_rpc_servers( + &config, + rpc_builder, + Some(Box::new(RandomIntegerIdProvider)), + ); + if rpc.is_ok() { + log::info!("Cosmos RPC started: {}", config.rpc_port); + } else { + log::warn!("Cosmos RPC not started"); + } + task_manager.keep_alive(rpc); + + let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); + config.prometheus_config = prometheus_config; + + Ok(config) +} diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs index 3f8654cf..4aeca43b 100644 --- a/ziggurat/node/src/service.rs +++ b/ziggurat/node/src/service.rs @@ -10,8 +10,7 @@ use ziggurat_runtime::{ opaque::{Block, Hash}, }; -// Frontier Imports -use crate::eth; +use crate::{cosmos, eth}; use fc_rpc::StorageOverrideHandler; // Cumulus Imports @@ -377,6 +376,25 @@ pub async fn start_parachain_node( pubsub_notification_sinks, })?; + let cosmos_rpc_builder = { + let client = client.clone(); + let pool = transaction_pool.clone(); + + Box::new(move |_deny_unsafe| { + cosmos::rpc::create_full(cosmos::rpc::FullDeps { + client: client.clone(), + pool: pool.clone(), + }) + .map_err(Into::into) + }) + }; + + let parachain_config = cosmos::spawn_tasks(cosmos::SpawnTasksParams { + config: parachain_config, + rpc_builder: cosmos_rpc_builder, + task_manager: &mut task_manager, + })?; + let rpc_builder = { let client = client.clone(); let transaction_pool = transaction_pool.clone(); From ebf2a4896d39a50dd1a8fc48918a9342dd253d9d Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 15:13:43 +0900 Subject: [PATCH 054/133] refactor: Refactor get sequence from tx --- frame/babel/src/cosmos.rs | 4 ++-- .../signing/src/sign_verifiable_tx/mod.rs | 19 ++++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos.rs index 4eb00f35..dc355971 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos.rs @@ -80,8 +80,8 @@ where T::NativeDenom::get().to_string() } else { // TODO: Handle option - let denom = pallet_cosmos::AssetDenomRouter::::get(asset_id).unwrap().to_vec(); - String::from_utf8(denom).unwrap() + let denom = pallet_cosmos::AssetDenomRouter::::get(asset_id).unwrap(); + String::from_utf8(denom.into()).unwrap() } } } diff --git a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs index c77c7469..9aad89d2 100644 --- a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs +++ b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs @@ -102,20 +102,13 @@ impl traits::SigVerifiableTx for SigVerifiableTx { let auth_info = tx.auth_info.as_ref().ok_or(SigVerifiableTxError::EmptyAuthInfo)?; let fee = auth_info.fee.as_ref().ok_or(SigVerifiableTxError::EmptyFee)?; - let sequence = if !fee.payer.is_empty() { - // TODO: Verify that the last signer is the fee payer. - auth_info - .signer_infos - .last() - .ok_or(SigVerifiableTxError::EmptySigners)? - .sequence + let sequence = if fee.payer.is_empty() { + auth_info.signer_infos.first() } else { - auth_info - .signer_infos - .first() - .ok_or(SigVerifiableTxError::EmptySigners)? - .sequence - }; + auth_info.signer_infos.last() + } + .ok_or(SigVerifiableTxError::EmptySigners)? + .sequence; Ok(sequence) } From 42b25ad77f434346232143e48770e7301faf9353 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 15:26:19 +0900 Subject: [PATCH 055/133] feat: Handle descriptor in consume_gas --- frame/cosmos/src/lib.rs | 2 +- frame/cosmos/types/src/gas/mod.rs | 11 +++++------ frame/cosmos/types/src/gas/traits.rs | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index d9f9d507..2cdc6f8c 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -403,7 +403,7 @@ impl Pallet { fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { ctx.gas_meter() - .consume_gas(T::WeightToGas::convert(T::WeightInfo::default_weight()), "") + .consume_gas(T::WeightToGas::convert(T::WeightInfo::default_weight()), "decode tx") .map_err(|_| RootError::OutOfGas)?; let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; diff --git a/frame/cosmos/types/src/gas/mod.rs b/frame/cosmos/types/src/gas/mod.rs index e85ee1a4..de566c6b 100644 --- a/frame/cosmos/types/src/gas/mod.rs +++ b/frame/cosmos/types/src/gas/mod.rs @@ -21,8 +21,8 @@ pub type Gas = u64; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { - GasOverflow, - OutOfGas, + GasOverflow(&'static str), + OutOfGas(&'static str), } #[derive(Clone, Debug)] @@ -48,11 +48,10 @@ impl traits::GasMeter for BasicGasMeter { self.limit } - // TODO: Handle or remove descriptor - fn consume_gas(&mut self, amount: Gas, _descriptor: &str) -> Result { - let consumed = self.consumed.checked_add(amount).ok_or(Error::GasOverflow)?; + fn consume_gas(&mut self, amount: Gas, descriptor: &'static str) -> Result { + let consumed = self.consumed.checked_add(amount).ok_or(Error::GasOverflow(descriptor))?; if consumed > self.limit { - return Err(Error::OutOfGas); + return Err(Error::OutOfGas(descriptor)); } self.consumed = consumed; diff --git a/frame/cosmos/types/src/gas/traits.rs b/frame/cosmos/types/src/gas/traits.rs index 01c70e6f..dfc21b59 100644 --- a/frame/cosmos/types/src/gas/traits.rs +++ b/frame/cosmos/types/src/gas/traits.rs @@ -22,5 +22,5 @@ pub trait GasMeter { fn consumed_gas(&self) -> Gas; fn gas_remaining(&self) -> Gas; fn limit(&self) -> Gas; - fn consume_gas(&mut self, amount: Gas, descriptor: &str) -> Result; + fn consume_gas(&mut self, amount: Gas, descriptor: &'static str) -> Result; } From 0a6e972b8f0bd5eef36232a7b23e1702b6aa0025 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 15:47:46 +0900 Subject: [PATCH 056/133] refactor: Extract base_gas from consume_gas --- frame/cosmos/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 2cdc6f8c..9f2dc243 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -402,8 +402,9 @@ impl Pallet { } fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { + let base_gas = T::WeightToGas::convert(T::WeightInfo::default_weight()); ctx.gas_meter() - .consume_gas(T::WeightToGas::convert(T::WeightInfo::default_weight()), "decode tx") + .consume_gas(base_gas, "base gas") .map_err(|_| RootError::OutOfGas)?; let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; From b0b40c75d3816ba8528125268c74c706df50e7a6 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 16:06:45 +0900 Subject: [PATCH 057/133] fix: Correct incorrect port print when starting rpc --- build.log | 44 --------------------------------- ziggurat/node/src/cosmos/mod.rs | 6 ++++- ziggurat/node/src/eth/mod.rs | 5 +++- 3 files changed, 9 insertions(+), 46 deletions(-) delete mode 100644 build.log diff --git a/build.log b/build.log deleted file mode 100644 index 4fcdac75..00000000 --- a/build.log +++ /dev/null @@ -1,44 +0,0 @@ -⚡ Found 3 strongly connected components which includes at least one cycle each -cycle(001) ∈ α: DisputeCoordinator ~~{"DisputeDistributionMessage"}~~> DisputeDistribution ~~{"DisputeCoordinatorMessage"}~~> * -cycle(002) ∈ β: CandidateBacking ~~{"CollatorProtocolMessage"}~~> CollatorProtocol ~~{"CandidateBackingMessage"}~~> * -cycle(003) ∈ γ: NetworkBridgeRx ~~{"GossipSupportMessage"}~~> GossipSupport ~~{"NetworkBridgeRxMessage"}~~> * - Checking ziggurat-node v0.1.0 (/Users/conr2d/Projects/noir/ziggurat/node) -warning: unused import: `backend::*` - --> ziggurat/node/src/eth/mod.rs:23:9 - | -23 | pub use backend::*; - | ^^^^^^^^^^ - | - = note: `#[warn(unused_imports)]` on by default - -warning: unused variable: `eth_rpc_builder` - --> ziggurat/node/src/service.rs:303:6 - | -303 | let eth_rpc_builder = {}; - | ^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_eth_rpc_builder` - | - = note: `#[warn(unused_variables)]` on by default - -error[E0277]: the trait bound `C: ProvideRuntimeApi<_>` is not satisfied - --> ziggurat/node/src/eth/backend.rs:67:5 - | -67 | overrides, - | ^^^^^^^^^ the trait `ProvideRuntimeApi<_>` is not implemented for `C`, which is required by `StorageOverrideHandler<_, C, _>: StorageOverride<_>` - | - = help: the trait `ProvideRuntimeApi` is implemented for `sc_service::client::client::Client` - = note: required for `StorageOverrideHandler<_, C, _>` to implement `StorageOverride<_>` - = note: required for the cast from `Arc>` to `Arc<(dyn StorageOverride<_> + 'static)>` - -error[E0277]: the trait bound `C: StorageProvider<_, _>` is not satisfied - --> ziggurat/node/src/eth/backend.rs:67:5 - | -67 | overrides, - | ^^^^^^^^^ the trait `StorageProvider<_, _>` is not implemented for `C`, which is required by `StorageOverrideHandler<_, C, _>: StorageOverride<_>` - | - = help: the trait `StorageProvider` is implemented for `sc_service::client::client::Client` - = note: required for `StorageOverrideHandler<_, C, _>` to implement `StorageOverride<_>` - = note: required for the cast from `Arc>` to `Arc<(dyn StorageOverride<_> + 'static)>` - -For more information about this error, try `rustc --explain E0277`. -warning: `ziggurat-node` (bin "ziggurat") generated 2 warnings -error: could not compile `ziggurat-node` (bin "ziggurat") due to 2 previous errors; 2 warnings emitted diff --git a/ziggurat/node/src/cosmos/mod.rs b/ziggurat/node/src/cosmos/mod.rs index 607767fb..97da4d92 100644 --- a/ziggurat/node/src/cosmos/mod.rs +++ b/ziggurat/node/src/cosmos/mod.rs @@ -44,8 +44,12 @@ pub fn spawn_tasks( rpc_builder, Some(Box::new(RandomIntegerIdProvider)), ); + if rpc.is_ok() { - log::info!("Cosmos RPC started: {}", config.rpc_port); + log::info!( + "Cosmos RPC started: {}", + config.rpc_addr.as_ref().map(|addr| addr.port()).unwrap_or(0) + ); } else { log::warn!("Cosmos RPC not started"); } diff --git a/ziggurat/node/src/eth/mod.rs b/ziggurat/node/src/eth/mod.rs index bfac4967..0a5a7ec1 100644 --- a/ziggurat/node/src/eth/mod.rs +++ b/ziggurat/node/src/eth/mod.rs @@ -109,7 +109,10 @@ where Some(Box::new(fc_rpc::EthereumSubIdProvider)), ); if rpc.is_ok() { - log::info!("Ethereum RPC started: {}", config.rpc_port); + log::info!( + "Ethereum RPC started: {}", + config.rpc_addr.as_ref().map(|addr| addr.port()).unwrap_or(0) + ); } else { log::warn!("Ethereum RPC not started"); } From a597f0f661b5a2be40d1a49772219afe23875b36 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 19:09:20 +0900 Subject: [PATCH 058/133] fix: Merge cosmos rpc to system rpc --- ziggurat/node/src/cosmos/mod.rs | 62 --------------------------------- ziggurat/node/src/cosmos/rpc.rs | 55 ----------------------------- ziggurat/node/src/main.rs | 1 - ziggurat/node/src/rpc.rs | 14 ++++++-- ziggurat/node/src/service.rs | 21 +---------- 5 files changed, 12 insertions(+), 141 deletions(-) delete mode 100644 ziggurat/node/src/cosmos/mod.rs delete mode 100644 ziggurat/node/src/cosmos/rpc.rs diff --git a/ziggurat/node/src/cosmos/mod.rs b/ziggurat/node/src/cosmos/mod.rs deleted file mode 100644 index 97da4d92..00000000 --- a/ziggurat/node/src/cosmos/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pub mod rpc; - -use jsonrpsee::{core::id_providers::RandomIntegerIdProvider, RpcModule}; -use sc_rpc::DenyUnsafe; -use sc_service::{Error as ServiceError, TaskManager}; - -pub struct SpawnTasksParams<'a> { - pub config: sc_service::Configuration, - pub rpc_builder: Box Result, ServiceError>>, - pub task_manager: &'a mut TaskManager, -} - -pub fn spawn_tasks( - params: SpawnTasksParams, -) -> Result { - let SpawnTasksParams { mut config, rpc_builder, task_manager } = params; - - let rpc_port = config.rpc_addr.map(|addr| addr.port()).unwrap_or(config.rpc_port); - let prometheus_config = config.prometheus_config.take(); - - // TODO: Make the Cosmos RPC port configurable. - let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(26657)); - - let rpc = sc_service::start_rpc_servers( - &config, - rpc_builder, - Some(Box::new(RandomIntegerIdProvider)), - ); - - if rpc.is_ok() { - log::info!( - "Cosmos RPC started: {}", - config.rpc_addr.as_ref().map(|addr| addr.port()).unwrap_or(0) - ); - } else { - log::warn!("Cosmos RPC not started"); - } - task_manager.keep_alive(rpc); - - let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); - config.prometheus_config = prometheus_config; - - Ok(config) -} diff --git a/ziggurat/node/src/cosmos/rpc.rs b/ziggurat/node/src/cosmos/rpc.rs deleted file mode 100644 index 09225716..00000000 --- a/ziggurat/node/src/cosmos/rpc.rs +++ /dev/null @@ -1,55 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ziggurat_runtime::opaque::Block; - -use jsonrpsee::RpcModule; -use sc_transaction_pool_api::TransactionPool; -use sp_api::ProvideRuntimeApi; -use sp_block_builder::BlockBuilder; -use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; -use std::{error::Error, sync::Arc}; - -pub struct FullDeps { - pub client: Arc, - pub pool: Arc

, -} - -pub fn create_full( - deps: FullDeps, -) -> Result, Box> -where - C: ProvideRuntimeApi, - C: HeaderBackend + HeaderMetadata + 'static, - C: Send + Sync + 'static, - C::Api: BlockBuilder, - P: TransactionPool + 'static, - C::Api: cosmos_runtime_api::CosmosRuntimeApi, - C::Api: cosmwasm_runtime_api::CosmwasmRuntimeApi>, -{ - use cosmos_rpc::cosmos::{Cosmos, CosmosApiServer}; - use cosmwasm_rpc::{Cosmwasm, CosmwasmApiServer}; - - let mut module = RpcModule::new(()); - let FullDeps { client, pool } = deps; - - module.merge(Cosmos::new(client.clone(), pool).into_rpc())?; - module.merge(Cosmwasm::new(client).into_rpc())?; - - Ok(module) -} diff --git a/ziggurat/node/src/main.rs b/ziggurat/node/src/main.rs index 28a2556e..9b660805 100644 --- a/ziggurat/node/src/main.rs +++ b/ziggurat/node/src/main.rs @@ -5,7 +5,6 @@ mod chain_spec; mod cli; mod command; -mod cosmos; mod eth; mod rpc; mod service; diff --git a/ziggurat/node/src/rpc.rs b/ziggurat/node/src/rpc.rs index e1f6a148..874895b7 100644 --- a/ziggurat/node/src/rpc.rs +++ b/ziggurat/node/src/rpc.rs @@ -42,15 +42,23 @@ where C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, C::Api: substrate_frame_rpc_system::AccountNonceApi, C::Api: BlockBuilder, - P: TransactionPool + Sync + Send + 'static, + P: TransactionPool + Sync + Send + 'static, + C::Api: cosmos_runtime_api::CosmosRuntimeApi, + C::Api: cosmwasm_runtime_api::CosmwasmRuntimeApi>, { + use cosmos_rpc::cosmos::{Cosmos, CosmosApiServer}; + use cosmwasm_rpc::{Cosmwasm, CosmwasmApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use substrate_frame_rpc_system::{System, SystemApiServer}; let mut module = RpcExtension::new(()); let FullDeps { client, pool, deny_unsafe } = deps; - module.merge(System::new(client.clone(), pool, deny_unsafe).into_rpc())?; - module.merge(TransactionPayment::new(client).into_rpc())?; + module.merge(System::new(client.clone(), pool.clone(), deny_unsafe).into_rpc())?; + module.merge(TransactionPayment::new(client.clone()).into_rpc())?; + + module.merge(Cosmos::new(client.clone(), pool).into_rpc())?; + module.merge(Cosmwasm::new(client).into_rpc())?; + Ok(module) } diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs index 4aeca43b..9752836b 100644 --- a/ziggurat/node/src/service.rs +++ b/ziggurat/node/src/service.rs @@ -10,7 +10,7 @@ use ziggurat_runtime::{ opaque::{Block, Hash}, }; -use crate::{cosmos, eth}; +use crate::eth; use fc_rpc::StorageOverrideHandler; // Cumulus Imports @@ -376,25 +376,6 @@ pub async fn start_parachain_node( pubsub_notification_sinks, })?; - let cosmos_rpc_builder = { - let client = client.clone(); - let pool = transaction_pool.clone(); - - Box::new(move |_deny_unsafe| { - cosmos::rpc::create_full(cosmos::rpc::FullDeps { - client: client.clone(), - pool: pool.clone(), - }) - .map_err(Into::into) - }) - }; - - let parachain_config = cosmos::spawn_tasks(cosmos::SpawnTasksParams { - config: parachain_config, - rpc_builder: cosmos_rpc_builder, - task_manager: &mut task_manager, - })?; - let rpc_builder = { let client = client.clone(); let transaction_pool = transaction_pool.clone(); From 317383ae18fd805aa978b8503ad044dc6815b965 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 20 Sep 2024 19:20:21 +0900 Subject: [PATCH 059/133] feat: Add sidecar for cosmos apis --- sidecar/.eslintrc | 15 ++ sidecar/.gitignore | 138 ++++++++++ sidecar/LICENSE | 201 ++++++++++++++ sidecar/README.md | 2 + sidecar/config/default.json | 18 ++ sidecar/eslint.config.mjs | 7 + sidecar/package.json | 61 +++++ sidecar/public/config.js | 17 ++ sidecar/public/cosmostation_logo.png | Bin 0 -> 10459 bytes sidecar/public/index.html | 126 +++++++++ sidecar/public/keplr_logo.png | Bin 0 -> 21579 bytes sidecar/src/app.ts | 381 +++++++++++++++++++++++++++ sidecar/src/constants/dummy.ts | 8 + sidecar/src/constants/rpc.ts | 50 ++++ sidecar/src/main.ts | 10 + sidecar/src/services/abci.ts | 117 ++++++++ sidecar/src/services/account.ts | 66 +++++ sidecar/src/services/balance.ts | 69 +++++ sidecar/src/services/distribution.ts | 13 + sidecar/src/services/index.ts | 9 + sidecar/src/services/nodeinfo.ts | 49 ++++ sidecar/src/services/service.ts | 17 ++ sidecar/src/services/staking.ts | 30 +++ sidecar/src/services/status.ts | 66 +++++ sidecar/src/services/tx.ts | 214 +++++++++++++++ sidecar/src/types/account.ts | 20 ++ sidecar/src/types/cometbft.ts | 34 +++ sidecar/src/types/index.ts | 4 + sidecar/src/types/status.ts | 46 ++++ sidecar/src/types/tx.ts | 7 + sidecar/src/utils/index.ts | 22 ++ sidecar/tsconfig.json | 25 ++ 32 files changed, 1842 insertions(+) create mode 100644 sidecar/.eslintrc create mode 100644 sidecar/.gitignore create mode 100644 sidecar/LICENSE create mode 100644 sidecar/README.md create mode 100644 sidecar/config/default.json create mode 100644 sidecar/eslint.config.mjs create mode 100644 sidecar/package.json create mode 100644 sidecar/public/config.js create mode 100644 sidecar/public/cosmostation_logo.png create mode 100644 sidecar/public/index.html create mode 100644 sidecar/public/keplr_logo.png create mode 100644 sidecar/src/app.ts create mode 100644 sidecar/src/constants/dummy.ts create mode 100644 sidecar/src/constants/rpc.ts create mode 100644 sidecar/src/main.ts create mode 100644 sidecar/src/services/abci.ts create mode 100644 sidecar/src/services/account.ts create mode 100644 sidecar/src/services/balance.ts create mode 100644 sidecar/src/services/distribution.ts create mode 100644 sidecar/src/services/index.ts create mode 100644 sidecar/src/services/nodeinfo.ts create mode 100644 sidecar/src/services/service.ts create mode 100644 sidecar/src/services/staking.ts create mode 100644 sidecar/src/services/status.ts create mode 100644 sidecar/src/services/tx.ts create mode 100644 sidecar/src/types/account.ts create mode 100644 sidecar/src/types/cometbft.ts create mode 100644 sidecar/src/types/index.ts create mode 100644 sidecar/src/types/status.ts create mode 100644 sidecar/src/types/tx.ts create mode 100644 sidecar/src/utils/index.ts create mode 100644 sidecar/tsconfig.json diff --git a/sidecar/.eslintrc b/sidecar/.eslintrc new file mode 100644 index 00000000..60f52edf --- /dev/null +++ b/sidecar/.eslintrc @@ -0,0 +1,15 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } +} \ No newline at end of file diff --git a/sidecar/.gitignore b/sidecar/.gitignore new file mode 100644 index 00000000..dc5f233f --- /dev/null +++ b/sidecar/.gitignore @@ -0,0 +1,138 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# lmdb +db + +# build +dist + +package-lock.json diff --git a/sidecar/LICENSE b/sidecar/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/sidecar/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sidecar/README.md b/sidecar/README.md new file mode 100644 index 00000000..e3b51941 --- /dev/null +++ b/sidecar/README.md @@ -0,0 +1,2 @@ +# cosmos-api-sidecar +Cosmos API Sidecar for Noir diff --git a/sidecar/config/default.json b/sidecar/config/default.json new file mode 100644 index 00000000..68fae05e --- /dev/null +++ b/sidecar/config/default.json @@ -0,0 +1,18 @@ +{ + "db": { + "path": "db" + }, + "server": { + "endpoint": "http://localhost:1317", + "port": 1317, + "logger": true + }, + "chain": { + "endpoint": "ws://127.0.0.1:59954", + "network": "dev", + "moniker": "cosmos", + "name": "noir", + "version": "1", + "denom": "azig" + } +} \ No newline at end of file diff --git a/sidecar/eslint.config.mjs b/sidecar/eslint.config.mjs new file mode 100644 index 00000000..e7de8c63 --- /dev/null +++ b/sidecar/eslint.config.mjs @@ -0,0 +1,7 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, +); \ No newline at end of file diff --git a/sidecar/package.json b/sidecar/package.json new file mode 100644 index 00000000..ce58bcdc --- /dev/null +++ b/sidecar/package.json @@ -0,0 +1,61 @@ +{ + "name": "@noir/sidecar", + "version": "0.1.0", + "description": "Cosmos API Sidecar for Noir", + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "lint": "npx eslint ./src", + "test": "jest", + "dev": "nodemon --exec ts-node ./src/main.ts", + "start": "node --es-module-specifier-resolution=node ./dist/main.js", + "fmt": "eslint ./src --ext .ts --fix" + }, + "keywords": [], + "author": "code0xff ", + "license": "Apache-2.0", + "jest": { + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "testRegex": "\\.spec\\.ts$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ] + }, + "devDependencies": { + "@eslint/js": "^9.10.0", + "@types/config": "^3.3.0", + "@types/eslint__js": "^8.42.3", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.11", + "@types/ws": "^8.5.5", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.57.1", + "jest": "^29.3.1", + "nodemon": "^3.1.6", + "patch-package": "^8.0.0", + "ts-jest": "^29.0.4", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "typescript-eslint": "^8.6.0" + }, + "dependencies": { + "@cosmjs/encoding": "^0.30.1", + "@fastify/static": "^6.10.2", + "@fastify/websocket": "^8.1.0", + "@polkadot/api": "^13.1.1", + "config": "^3.3.9", + "cosmjs-types": "^0.8.0", + "ethereum-cryptography": "^2.0.0", + "fastify": "4.17.0", + "json-rpc-2.0": "^1.6.0", + "lmdb": "^2.8.3", + "protobufjs": "^7.2.4" + } +} diff --git a/sidecar/public/config.js b/sidecar/public/config.js new file mode 100644 index 00000000..f9e40247 --- /dev/null +++ b/sidecar/public/config.js @@ -0,0 +1,17 @@ +const config = { + chainId: "dev", + chainName: "Noir", + addressPrefix: "cosmos", + baseDenom: "azig", // atto zig + displayDenom: "ZIG", + restURL: "http://127.0.0.1:1317", + coinType: "118", + decimals: 18, + gasRate: { + high: "1.3", + average: "1", + low: "0.8", + tiny: "0.6", + }, + sendGas: "400000000" +}; diff --git a/sidecar/public/cosmostation_logo.png b/sidecar/public/cosmostation_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5d66b9db61aff2a026e3e343c90316d258c839d5 GIT binary patch literal 10459 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 HnTPxj4DuVh literal 0 HcmV?d00001 diff --git a/sidecar/public/index.html b/sidecar/public/index.html new file mode 100644 index 00000000..804f9842 --- /dev/null +++ b/sidecar/public/index.html @@ -0,0 +1,126 @@ + + + + + + + + Cosmos API Sidecar + + + + +

+

Welcome to Noir!

+
+ +
+
+ +
+ + + + + diff --git a/sidecar/public/keplr_logo.png b/sidecar/public/keplr_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34a73c9da03c58b8d8250f704875a5e65aa46361 GIT binary patch literal 21579 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
) { + ( + account.clone().into(), + vec![Address::Cosmos(account.clone().into()), Address::Ethereum(account.clone().into())], + ) +} From 38db23f5a93d44b89eac4b3a369987f37204e87b Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 26 Sep 2024 23:39:23 +0900 Subject: [PATCH 091/133] feat: Add charge gas to dispatch call and code store --- frame/babel/src/cosmos/precompile.rs | 6 +++++- frame/cosmos/x/wasm/src/msgs.rs | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frame/babel/src/cosmos/precompile.rs b/frame/babel/src/cosmos/precompile.rs index 1c0c60a5..4d12ba51 100644 --- a/frame/babel/src/cosmos/precompile.rs +++ b/frame/babel/src/cosmos/precompile.rs @@ -38,7 +38,7 @@ use pallet_cosmwasm::{ use parity_scale_codec::{Decode, DecodeLimit}; use serde::{Deserialize, Serialize}; use sp_core::H160; -use sp_runtime::traits::{AccountIdConversion, Dispatchable}; +use sp_runtime::traits::{AccountIdConversion, Convert, Dispatchable}; const ID: PalletId = PalletId(*b"dispatch"); const DECODE_LIMIT: u32 = 8; @@ -86,6 +86,10 @@ where if let Ok(ExecuteMsg::Dispatch { input }) = serde_json_wasm::from_slice(message) { let call = T::RuntimeCall::decode_with_depth_limit(DECODE_LIMIT, &mut &*input) .unwrap(); + let weight = call.get_dispatch_info().weight; + vm.0.data_mut() + .charge_raw(T::WeightToGas::convert(weight)) + .map_err(|_| CosmwasmVMError::OutOfGas)?; let sender = vm.0.data().cosmwasm_message_info.sender.clone().into_string(); let (_hrp, address_raw) = acc_address_from_bech32(&sender) diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index 6d58d46b..a02bd652 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -79,6 +79,10 @@ where ensure!(address_raw.len() == 20, RootError::InvalidAddress); let who = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + ctx.gas_meter() + .consume_gas(wasm_byte_code.len() as u64, "") + .map_err(|_| RootError::OutOfGas)?; + let mut decoder = Decoder::new(&wasm_byte_code[..]).map_err(|_| WasmError::CreateFailed)?; let mut decoded_code = Vec::new(); decoder.read_to_end(&mut decoded_code).map_err(|_| WasmError::CreateFailed)?; From 68accdafd81b219d3445b54e0f74e55d9ead381c Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 10:33:46 +0900 Subject: [PATCH 092/133] fix: Correct addressMap cosmos code in sidecar --- sidecar/src/services/account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index b4119dfe..6829782d 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -44,7 +44,7 @@ export class AccountService implements ApiService { public async origin(address: string): Promise { const { data } = fromBech32(address); return this.chainApi.query['addressMap']['index']( - Buffer.concat([Buffer.from([0x00]), data]) + Buffer.concat([Buffer.from([0x01]), data]) ); } From c8f0cfa4959c00aeb1d9e0fb6651d1ec6f8dbecc Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 11:48:09 +0900 Subject: [PATCH 093/133] fix: Add deserialize call error handling --- frame/babel/src/cosmos/precompile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/babel/src/cosmos/precompile.rs b/frame/babel/src/cosmos/precompile.rs index 4d12ba51..4665b596 100644 --- a/frame/babel/src/cosmos/precompile.rs +++ b/frame/babel/src/cosmos/precompile.rs @@ -85,7 +85,7 @@ where address if address == dispatch => { if let Ok(ExecuteMsg::Dispatch { input }) = serde_json_wasm::from_slice(message) { let call = T::RuntimeCall::decode_with_depth_limit(DECODE_LIMIT, &mut &*input) - .unwrap(); + .map_err(|_| CosmwasmVMError::ExecuteDeserialize)?; let weight = call.get_dispatch_info().weight; vm.0.data_mut() .charge_raw(T::WeightToGas::convert(weight)) From e600df351c017498092e9794e8a83f69e61adde3 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Fri, 27 Sep 2024 12:08:41 +0900 Subject: [PATCH 094/133] feat: Require root permission to create asset --- ziggurat/runtime/src/configs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index fdb9b711..6883d53e 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -434,7 +434,7 @@ impl pallet_assets::Config for Runtime { type AssetId = AssetId; type AssetIdParameter = codec::Compact; type Currency = Balances; - type CreateOrigin = AsEnsureOriginWithArg>; + type CreateOrigin = EnsureRoot; type ForceOrigin = EnsureRoot; type AssetDeposit = ConstU128<500>; type AssetAccountDeposit = ConstU128<500>; From e85b97d2cf82602aabc6e1689c0d6bed9804ac43 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 13:37:32 +0900 Subject: [PATCH 095/133] fix: Add serde rename snake_case to dispatch message --- frame/babel/Cargo.toml | 3 +++ frame/babel/src/cosmos/precompile.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 18f1dfcb..38a6a718 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -51,6 +51,9 @@ sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +[dev-dependencies] +hex = "0.4.3" + [features] default = ["std", "pallet"] std = [ diff --git a/frame/babel/src/cosmos/precompile.rs b/frame/babel/src/cosmos/precompile.rs index 4665b596..9771c9d3 100644 --- a/frame/babel/src/cosmos/precompile.rs +++ b/frame/babel/src/cosmos/precompile.rs @@ -44,6 +44,7 @@ const ID: PalletId = PalletId(*b"dispatch"); const DECODE_LIMIT: u32 = 8; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] pub enum ExecuteMsg { Dispatch { input: Binary }, } @@ -129,3 +130,17 @@ where Err(CosmwasmVMError::ContractNotFound) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_msg_test() { + let message = r#"{ "dispatch": { "input" : "CgMAkLWrIFxpdMnqhBvmiIZGM9ycqKNXhD7qzyMUZJll/iIPAADBb/KGIw==" } }"#; + let ExecuteMsg::Dispatch { input } = + serde_json_wasm::from_slice(message.as_bytes()).unwrap(); + + assert_eq!(input, hex::decode("0a030090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe220f0000c16ff28623").unwrap()); + } +} From 81f4fd547b1937ed231e57cf9263e996f5419946 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 14:08:42 +0900 Subject: [PATCH 096/133] fix: Replace pallet-assets CreateOrigin to NeverEnsureOrigin --- ziggurat/runtime/src/configs/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 6883d53e..82980aa7 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -40,7 +40,7 @@ use frame_support::{ dispatch::DispatchClass, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, + ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, NeverEnsureOrigin, TransformOrigin, VariantCountOf, }, weights::{ConstantMultiplier, Weight}, @@ -434,7 +434,7 @@ impl pallet_assets::Config for Runtime { type AssetId = AssetId; type AssetIdParameter = codec::Compact; type Currency = Balances; - type CreateOrigin = EnsureRoot; + type CreateOrigin = NeverEnsureOrigin; type ForceOrigin = EnsureRoot; type AssetDeposit = ConstU128<500>; type AssetAccountDeposit = ConstU128<500>; From 619d083c8f52917f232bbf2ec52a0c31f7e6d7d3 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Fri, 27 Sep 2024 13:54:54 +0900 Subject: [PATCH 097/133] feat: Add balances erc20 precomile --- Cargo.toml | 2 + frame/babel/Cargo.toml | 3 + frame/babel/src/ethereum/mod.rs | 12 +- frame/babel/src/ethereum/precompile.rs | 3 +- .../precompiles/balances-erc20/Cargo.toml | 59 + .../precompiles/balances-erc20/ERC20.sol | 123 ++ .../precompiles/balances-erc20/Permit.sol | 39 + .../precompiles/balances-erc20/src/eip2612.rs | 171 +++ .../precompiles/balances-erc20/src/lib.rs | 490 +++++++ .../precompiles/balances-erc20/src/mock.rs | 217 +++ .../precompiles/balances-erc20/src/tests.rs | 1223 +++++++++++++++++ ziggurat/runtime/src/configs/mod.rs | 18 + 12 files changed, 2358 insertions(+), 2 deletions(-) create mode 100644 vendor/moonbeam/precompiles/balances-erc20/Cargo.toml create mode 100644 vendor/moonbeam/precompiles/balances-erc20/ERC20.sol create mode 100644 vendor/moonbeam/precompiles/balances-erc20/Permit.sol create mode 100644 vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs create mode 100644 vendor/moonbeam/precompiles/balances-erc20/src/lib.rs create mode 100644 vendor/moonbeam/precompiles/balances-erc20/src/mock.rs create mode 100644 vendor/moonbeam/precompiles/balances-erc20/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 0f94c1a7..96a3c52d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "vendor/composable/cosmwasm", "vendor/composable/cosmwasm/rpc", "vendor/composable/cosmwasm/runtime-api", + "vendor/moonbeam/precompiles/balances-erc20", "ziggurat/node", "ziggurat/runtime", ] @@ -57,6 +58,7 @@ pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-featu pallet-cosmos-x-wasm = { path = "frame/cosmos/x/wasm", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-cosmwasm = { path = "vendor/composable/cosmwasm", default-features = false } +pallet-evm-precompile-balances-erc20 = { path = "vendor/moonbeam/precompiles/balances-erc20", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } [profile.release] diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 38a6a718..650c08b5 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -31,6 +31,7 @@ pallet-cosmos-x-wasm = { workspace = true, default-features = false, optional = pallet-cosmwasm = { workspace = true, default-features = false, optional = true } pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } +pallet-evm-precompile-balances-erc20 = { workspace = true, optional = true } pallet-evm-precompile-blake2 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-bn128 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-modexp = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } @@ -78,6 +79,7 @@ std = [ "pallet-cosmwasm?/std", "pallet-ethereum?/std", "pallet-evm?/std", + "pallet-evm-precompile-balances-erc20?/std", "pallet-evm-precompile-blake2?/std", "pallet-evm-precompile-bn128?/std", "pallet-evm-precompile-modexp?/std", @@ -114,6 +116,7 @@ ethereum = [ "np-babel/ethereum", "pallet-ethereum", "pallet-evm", + "pallet-evm-precompile-balances-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-modexp", diff --git a/frame/babel/src/ethereum/mod.rs b/frame/babel/src/ethereum/mod.rs index 493835d2..0f7be75e 100644 --- a/frame/babel/src/ethereum/mod.rs +++ b/frame/babel/src/ethereum/mod.rs @@ -29,6 +29,7 @@ use pallet_evm::{ EnsureAddressOrigin, IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet, }; +use pallet_evm_precompile_balances_erc20::Erc20BalancesPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_modexp::Modexp; @@ -39,6 +40,8 @@ use precompile::Babel; use sp_core::{ecdsa, H160}; use sp_runtime::traits::{AccountIdConversion, Dispatchable}; +pub use pallet_evm_precompile_balances_erc20::Erc20Metadata; + pub struct EnsureAddress(PhantomData); impl EnsureAddressOrigin for EnsureAddress @@ -87,6 +90,9 @@ impl Default for BabelPrecompiles { } } +use pallet_evm_precompile_balances_erc20::BalanceOf; +use sp_core::U256; + impl BabelPrecompiles where T: precompile::Config, @@ -95,7 +101,7 @@ where Self::default() } - pub fn used_addresses() -> [H160; 10] { + pub fn used_addresses() -> [H160; 11] { [ hash(1), hash(2), @@ -107,6 +113,7 @@ where hash(8), hash(9), hash(0x400 /* 1024 */), + hash(0x401 /* 1025 */), ] } } @@ -116,6 +123,8 @@ where T: precompile::Config, T::RuntimeCall: Dispatchable + GetDispatchInfo + Decode, ::RuntimeOrigin: From>, + T::RuntimeCall: From>, + BalanceOf: TryFrom + Into, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { match handle.code_address() { @@ -130,6 +139,7 @@ where a if a == hash(8) => Some(Bn128Pairing::execute(handle)), a if a == hash(9) => Some(Blake2F::execute(handle)), a if a == hash(0x400) => Some(Babel::::execute(handle)), + a if a == hash(0x401) => Some(Erc20BalancesPrecompile::::execute(handle)), _ => None, } } diff --git a/frame/babel/src/ethereum/precompile.rs b/frame/babel/src/ethereum/precompile.rs index 708a96ca..a2a692b9 100644 --- a/frame/babel/src/ethereum/precompile.rs +++ b/frame/babel/src/ethereum/precompile.rs @@ -24,11 +24,12 @@ use frame_support::{ StorageHasher, Twox128, }; use pallet_evm::{AddressMapping, GasWeightMapping}; +use pallet_evm_precompile_balances_erc20::Erc20Metadata; use parity_scale_codec::{Decode, DecodeLimit, Encode}; use precompile_utils::{prelude::*, EvmResult}; use sp_runtime::traits::{Dispatchable, Get}; -pub trait Config: pallet_evm::Config { +pub trait Config: pallet_balances::Config + pallet_evm::Config + Erc20Metadata { type DispatchValidator: DispatchValidate; type DecodeLimit: Get; type StorageFilter: StorageFilter; diff --git a/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml new file mode 100644 index 00000000..45860e65 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-evm-precompile-balances-erc20" +authors = ["PureStake"] +description = "A Precompile to expose a Balances pallet through an ERC20-compliant interface." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { version = "0.4", default-features = false } +num_enum = { version = "0.7", default-features = false } +paste = "1.0" +slices = "0.2" + +# Substrate +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +parity-scale-codec = { version = "3.6", default-features = false, features = ["max-encoded-len"] } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +# Frontier +fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, features = ["forbid-evm-reentrancy"] } +precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } + +[dev-dependencies] +derive_more = "1.0" +hex-literal = "0.4" +libsecp256k1 = "0.7" +serde = "1.0" +sha3 = "0.10" + +# Moonbeam +precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["testing"] } + +scale-info = { version = "2.11", features = ["derive"] } + +[features] +default = ["std"] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "log/std", + "num_enum/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/vendor/moonbeam/precompiles/balances-erc20/ERC20.sol b/vendor/moonbeam/precompiles/balances-erc20/ERC20.sol new file mode 100644 index 00000000..d23ff1a6 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/ERC20.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The IERC20 contract's address. +address constant IERC20_ADDRESS = 0x0000000000000000000000000000000000000802; + +/// @dev The IERC20 contract's instance. +IERC20 constant IERC20_CONTRACT = IERC20(IERC20_ADDRESS); + +/// @title ERC20 interface +/// @dev see https://github.com/ethereum/EIPs/issues/20 +/// @dev copied from https://github.com/OpenZeppelin/openzeppelin-contracts +/// @custom:address 0x0000000000000000000000000000000000000802 +interface IERC20 { + /// @dev Returns the name of the token. + /// @custom:selector 06fdde03 + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + /// @custom:selector 95d89b41 + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + /// @custom:selector 313ce567 + function decimals() external view returns (uint8); + + /// @dev Total number of tokens in existence + /// @custom:selector 18160ddd + function totalSupply() external view returns (uint256); + + /// @dev Gets the balance of the specified address. + /// @custom:selector 70a08231 + /// @param owner The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + function balanceOf(address owner) external view returns (uint256); + + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @custom:selector dd62ed3e + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + function allowance(address owner, address spender) + external + view + returns (uint256); + + /// @dev Transfer token for a specified address + /// @custom:selector a9059cbb + /// @param to The address to transfer to. + /// @param value The amount to be transferred. + /// @return true if the transfer was succesful, revert otherwise. + function transfer(address to, uint256 value) external returns (bool); + + /// @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + /// Beware that changing an allowance with this method brings the risk that someone may use both the old + /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + /// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @custom:selector 095ea7b3 + /// @param spender The address which will spend the funds. + /// @param value The amount of tokens to be spent. + /// @return true, this cannot fail + function approve(address spender, uint256 value) external returns (bool); + + /// @dev Transfer tokens from one address to another + /// @custom:selector 23b872dd + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param value uint256 the amount of tokens to be transferred + /// @return true if the transfer was succesful, revert otherwise. + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /// @dev Event emited when a transfer has been performed. + /// @custom:selector ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + /// @param from address The address sending the tokens + /// @param to address The address receiving the tokens. + /// @param value uint256 The amount of tokens transfered. + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev Event emited when an approval has been registered. + /// @custom:selector 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + /// @param owner address Owner of the tokens. + /// @param spender address Allowed spender. + /// @param value uint256 Amount of tokens approved. + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/// @title Native currency wrapper interface. +/// @dev Allow compatibility with dApps expecting this precompile to be +/// a WETH-like contract. +/// Moonbase address : 0x0000000000000000000000000000000000000802 +interface WrappedNativeCurrency { + /// @dev Provide compatibility for contracts that expect wETH design. + /// Returns funds to sender as this precompile tokens and the native tokens are the same. + /// @custom:selector d0e30db0 + function deposit() external payable; + + /// @dev Provide compatibility for contracts that expect wETH design. + /// Does nothing. + /// @custom:selector 2e1a7d4d + /// @param value uint256 The amount to withdraw/unwrap. + function withdraw(uint256 value) external; + + /// @dev Event emited when deposit() has been called. + /// @custom:selector e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c + /// @param owner address Owner of the tokens + /// @param value uint256 The amount of tokens "wrapped". + event Deposit(address indexed owner, uint256 value); + + /// @dev Event emited when withdraw(uint256) has been called. + /// @custom:selector 7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65 + /// @param owner address Owner of the tokens + /// @param value uint256 The amount of tokens "unwrapped". + event Withdrawal(address indexed owner, uint256 value); +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/Permit.sol b/vendor/moonbeam/precompiles/balances-erc20/Permit.sol new file mode 100644 index 00000000..fe781547 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/Permit.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Extension of the ERC20 interface that allows users to +/// @dev Sign permit messages to interact with contracts without needing to +/// make a first approve transaction. +interface Permit { + /// @dev Consumes an approval permit. + /// Anyone can call this function for a permit. + /// @custom:selector d505accf + /// @param owner Owner of the tokens issuing the permit + /// @param spender Address whose allowance will be increased. + /// @param value Allowed value. + /// @param deadline Timestamp after which the permit will no longer be valid. + /// @param v V component of the signature. + /// @param r R component of the signature. + /// @param s S component of the signature. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @dev Returns the current nonce for given owner. + /// A permit must have this nonce to be consumed, which will + /// increase the nonce by one. + /// @custom:selector 7ecebe00 + function nonces(address owner) external view returns (uint256); + + /// @dev Returns the EIP712 domain separator. It is used to avoid replay + /// attacks accross assets or other similar EIP712 message structures. + /// @custom:selector 3644e515 + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs b/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs new file mode 100644 index 00000000..50d6d9a2 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs @@ -0,0 +1,171 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use super::*; +use frame_support::{ + ensure, + traits::{Get, Time}, +}; +use sp_core::H256; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::UniqueSaturatedInto; +use sp_std::vec::Vec; + +/// EIP2612 permit typehash. +pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +); + +/// EIP2612 permit domain used to compute an individualized domain separator. +const PERMIT_DOMAIN: [u8; 32] = keccak256!( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +pub struct Eip2612(PhantomData<(Runtime, Instance)>); + +impl Eip2612 +where + Runtime: pallet_balances::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + Runtime: Erc20Metadata, + Instance: InstanceToPrefix + 'static, +{ + pub fn compute_domain_separator(address: H160) -> [u8; 32] { + let name: H256 = keccak_256(Runtime::name().as_bytes()).into(); + let version: H256 = keccak256!("1").into(); + let chain_id: U256 = Runtime::ChainId::get().into(); + + let domain_separator_inner = solidity::encode_arguments(( + H256::from(PERMIT_DOMAIN), + name, + version, + chain_id, + Address(address), + )); + + keccak_256(&domain_separator_inner).into() + } + + pub fn generate_permit( + address: H160, + owner: H160, + spender: H160, + value: U256, + nonce: U256, + deadline: U256, + ) -> [u8; 32] { + let domain_separator = Self::compute_domain_separator(address); + + let permit_content = solidity::encode_arguments(( + H256::from(PERMIT_TYPEHASH), + Address(owner), + Address(spender), + value, + nonce, + deadline, + )); + let permit_content = keccak_256(&permit_content); + + let mut pre_digest = Vec::with_capacity(2 + 32 + 32); + pre_digest.extend_from_slice(b"\x19\x01"); + pre_digest.extend_from_slice(&domain_separator); + pre_digest.extend_from_slice(&permit_content); + keccak_256(&pre_digest) + } + + // Translated from + // https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81 + pub(crate) fn permit( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + // Blockchain time is in ms while Ethereum use second timestamps. + let timestamp: u128 = + ::Timestamp::now().unique_saturated_into(); + let timestamp: U256 = U256::from(timestamp / 1000); + + ensure!(deadline >= timestamp, revert("Permit expired")); + + let nonce = NoncesStorage::::get(owner); + + let permit = + Self::generate_permit(handle.context().address, owner, spender, value, nonce, deadline); + + let mut sig = [0u8; 65]; + sig[0..32].copy_from_slice(&r.as_bytes()); + sig[32..64].copy_from_slice(&s.as_bytes()); + sig[64] = v; + + let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) + .map_err(|_| revert("Invalid permit"))?; + let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); + + ensure!(signer != H160::zero() && signer == owner, revert("Invalid permit")); + + NoncesStorage::::insert(owner, nonce + U256::one()); + + { + let amount = Erc20BalancesPrecompile::::u256_to_amount(value) + .unwrap_or_else(|_| Bounded::max_value()); + + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + ApprovesStorage::::insert(owner, spender, amount); + } + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + owner, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(()) + } + + pub(crate) fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + + Ok(NoncesStorage::::get(owner)) + } + + pub(crate) fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult { + // ChainId + handle.record_db_read::(8)?; + + Ok(Self::compute_domain_separator(handle.context().address).into()) + } +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs b/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs new file mode 100644 index 00000000..f406b815 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs @@ -0,0 +1,490 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! Precompile to interact with pallet_balances instances using the ERC20 interface standard. + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::PrecompileHandle; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + sp_runtime::traits::{Bounded, CheckedSub, Dispatchable, StaticLookup}, + storage::types::{StorageDoubleMap, StorageMap, ValueQuery}, + traits::StorageInstance, + Blake2_128Concat, +}; +use pallet_balances::pallet::{ + Instance1, Instance10, Instance11, Instance12, Instance13, Instance14, Instance15, Instance16, + Instance2, Instance3, Instance4, Instance5, Instance6, Instance7, Instance8, Instance9, +}; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_core::{H160, H256, U256}; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, +}; + +mod eip2612; +use eip2612::Eip2612; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); + +/// Solidity selector of the Approval log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)"); + +/// Solidity selector of the Deposit log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_DEPOSIT: [u8; 32] = keccak256!("Deposit(address,uint256)"); + +/// Solidity selector of the Withdraw log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_WITHDRAWAL: [u8; 32] = keccak256!("Withdrawal(address,uint256)"); + +/// Associates pallet Instance to a prefix used for the Approves storage. +/// This trait is implemented for () and the 16 substrate Instance. +pub trait InstanceToPrefix { + /// Prefix used for the Approves storage. + type ApprovesPrefix: StorageInstance; + + /// Prefix used for the Approves storage. + type NoncesPrefix: StorageInstance; +} + +// We use a macro to implement the trait for () and the 16 substrate Instance. +macro_rules! impl_prefix { + ($instance:ident, $name:literal) => { + // Using `paste!` we generate a dedicated module to avoid collisions + // between each instance `Approves` struct. + paste::paste! { + mod [<_impl_prefix_ $instance:snake>] { + use super::*; + + pub struct Approves; + + impl StorageInstance for Approves { + const STORAGE_PREFIX: &'static str = "Approves"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + pub struct Nonces; + + impl StorageInstance for Nonces { + const STORAGE_PREFIX: &'static str = "Nonces"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + impl InstanceToPrefix for $instance { + type ApprovesPrefix = Approves; + type NoncesPrefix = Nonces; + } + } + } + }; +} + +// Since the macro expect a `ident` to be used with `paste!` we cannot provide `()` directly. +type Instance0 = (); + +impl_prefix!(Instance0, "Erc20Instance0Balances"); +impl_prefix!(Instance1, "Erc20Instance1Balances"); +impl_prefix!(Instance2, "Erc20Instance2Balances"); +impl_prefix!(Instance3, "Erc20Instance3Balances"); +impl_prefix!(Instance4, "Erc20Instance4Balances"); +impl_prefix!(Instance5, "Erc20Instance5Balances"); +impl_prefix!(Instance6, "Erc20Instance6Balances"); +impl_prefix!(Instance7, "Erc20Instance7Balances"); +impl_prefix!(Instance8, "Erc20Instance8Balances"); +impl_prefix!(Instance9, "Erc20Instance9Balances"); +impl_prefix!(Instance10, "Erc20Instance10Balances"); +impl_prefix!(Instance11, "Erc20Instance11Balances"); +impl_prefix!(Instance12, "Erc20Instance12Balances"); +impl_prefix!(Instance13, "Erc20Instance13Balances"); +impl_prefix!(Instance14, "Erc20Instance14Balances"); +impl_prefix!(Instance15, "Erc20Instance15Balances"); +impl_prefix!(Instance16, "Erc20Instance16Balances"); + +/// Alias for the Balance type for the provided Runtime and Instance. +pub type BalanceOf = + >::Balance; + +/// Storage type used to store approvals, since `pallet_balances` doesn't +/// handle this behavior. +/// (Owner => Allowed => Amount) +pub type ApprovesStorage = StorageDoubleMap< + ::ApprovesPrefix, + Blake2_128Concat, + ::AccountId, + Blake2_128Concat, + ::AccountId, + BalanceOf, +>; + +/// Storage type used to store EIP2612 nonces. +pub type NoncesStorage = StorageMap< + ::NoncesPrefix, + // Owner + Blake2_128Concat, + H160, + // Nonce + U256, + ValueQuery, +>; + +/// Metadata of an ERC20 token. +pub trait Erc20Metadata { + /// Returns the name of the token. + fn name() -> &'static str; + + /// Returns the symbol of the token. + fn symbol() -> &'static str; + + /// Returns the decimals places of the token. + fn decimals() -> u8; + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool; +} + +/// Precompile exposing a pallet_balance as an ERC20. +/// Multiple precompiles can support instances of pallet_balance. +/// The precompile uses an additional storage to store approvals. +pub struct Erc20BalancesPrecompile( + PhantomData<(Runtime, Instance)>, +); + +#[precompile_utils::precompile] +impl Erc20BalancesPrecompile +where + Runtime: pallet_balances::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + Instance: InstanceToPrefix + 'static, + Runtime: Erc20Metadata, +{ + #[precompile::public("totalSupply()")] + #[precompile::view] + fn total_supply(handle: &mut impl PrecompileHandle) -> EvmResult { + // TotalIssuance: Balance(16) + handle.record_db_read::(16)?; + + Ok(pallet_balances::Pallet::::total_issuance().into()) + } + + #[precompile::public("balanceOf(address)")] + #[precompile::view] + fn balance_of(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + // frame_system::Account: + // Blake2_128(16) + AccountId(32) + AccountInfo ((4 * 4) + AccountData(16 * 4)) + handle.record_db_read::(128)?; + + let owner: H160 = owner.into(); + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + + Ok(pallet_balances::Pallet::::usable_balance(&owner).into()) + } + + #[precompile::public("allowance(address,address)")] + #[precompile::view] + fn allowance( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + ) -> EvmResult { + // frame_system::ApprovesStorage: + // (2 * (Blake2_128(16) + AccountId(32)) + Balanceof(16) + handle.record_db_read::(100)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + + Ok(ApprovesStorage::::get(owner, spender) + .unwrap_or_default() + .into()) + } + + #[precompile::public("approve(address,uint256)")] + fn approve( + handle: &mut impl PrecompileHandle, + spender: Address, + value: U256, + ) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_log_costs_manual(3, 32)?; + + let spender: H160 = spender.into(); + + // Write into storage. + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + // Amount saturate if too high. + let value = Self::u256_to_amount(value).unwrap_or_else(|_| Bounded::max_value()); + + ApprovesStorage::::insert(caller, spender, value); + } + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + handle.context().caller, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + #[precompile::public("transfer(address,uint256)")] + fn transfer(handle: &mut impl PrecompileHandle, to: Address, value: U256) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let to: H160 = to.into(); + + // Build call with origin. + { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let to = Runtime::AddressMapping::into_account_id(to); + let value = Self::u256_to_amount(value).in_field("value")?; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(to), + value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + handle.context().caller, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("transferFrom(address,address,uint256)")] + fn transfer_from( + handle: &mut impl PrecompileHandle, + from: Address, + to: Address, + value: U256, + ) -> EvmResult { + // frame_system::ApprovesStorage: + // (2 * (Blake2_128(16) + AccountId(32)) + Balanceof(16) + handle.record_db_read::(100)?; + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_log_costs_manual(3, 32)?; + + let from: H160 = from.into(); + let to: H160 = to.into(); + + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from); + let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to); + let value = Self::u256_to_amount(value).in_field("value")?; + + // If caller is "from", it can spend as much as it wants. + if caller != from { + ApprovesStorage::::mutate(from.clone(), caller, |entry| { + // Get current allowed value, exit if None. + let allowed = entry.ok_or(revert("spender not allowed"))?; + + // Remove "value" from allowed, exit if underflow. + let allowed = allowed + .checked_sub(&value) + .ok_or_else(|| revert("trying to spend more than allowed"))?; + + // Update allowed value. + *entry = Some(allowed); + + EvmResult::Ok(()) + })?; + } + + // Build call with origin. Here origin is the "from"/owner field. + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(from).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(to), + value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + from, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("name()")] + #[precompile::view] + fn name(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Runtime::name().into()) + } + + #[precompile::public("symbol()")] + #[precompile::view] + fn symbol(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Runtime::symbol().into()) + } + + #[precompile::public("decimals()")] + #[precompile::view] + fn decimals(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Runtime::decimals()) + } + + #[precompile::public("deposit()")] + #[precompile::fallback] + #[precompile::payable] + fn deposit(handle: &mut impl PrecompileHandle) -> EvmResult { + // Deposit only makes sense for the native currency. + if !Runtime::is_native_currency() { + return Err(RevertReason::UnknownSelector.into()); + } + + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let precompile = Runtime::AddressMapping::into_account_id(handle.context().address); + let amount = Self::u256_to_amount(handle.context().apparent_value)?; + + if amount.into() == U256::from(0u32) { + return Err(revert("deposited amount must be non-zero")); + } + + handle.record_log_costs_manual(2, 32)?; + + // Send back funds received by the precompile. + RuntimeHelper::::try_dispatch( + handle, + Some(precompile).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(caller), + value: amount, + }, + )?; + + log2( + handle.context().address, + SELECTOR_LOG_DEPOSIT, + handle.context().caller, + solidity::encode_event_data(handle.context().apparent_value), + ) + .record(handle)?; + + Ok(()) + } + + #[precompile::public("withdraw(uint256)")] + fn withdraw(handle: &mut impl PrecompileHandle, value: U256) -> EvmResult { + // Withdraw only makes sense for the native currency. + if !Runtime::is_native_currency() { + return Err(RevertReason::UnknownSelector.into()); + } + + handle.record_log_costs_manual(2, 32)?; + + let account_amount: U256 = { + let owner: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + pallet_balances::Pallet::::usable_balance(&owner).into() + }; + + if value > account_amount { + return Err(revert("Trying to withdraw more than owned")); + } + + log2( + handle.context().address, + SELECTOR_LOG_WITHDRAWAL, + handle.context().caller, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(()) + } + + #[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")] + fn eip2612_permit( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + >::permit(handle, owner, spender, value, deadline, v, r, s) + } + + #[precompile::public("nonces(address)")] + #[precompile::view] + fn eip2612_nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + >::nonces(handle, owner) + } + + #[precompile::public("DOMAIN_SEPARATOR()")] + #[precompile::view] + fn eip2612_domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult { + >::domain_separator(handle) + } + + fn u256_to_amount(value: U256) -> MayRevert> { + value + .try_into() + .map_err(|_| RevertReason::value_is_too_large("balance type").into()) + } +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs b/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs new file mode 100644 index 00000000..c3b09f25 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs @@ -0,0 +1,217 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! Testing utilities. + +use super::*; + +use frame_support::{construct_runtime, parameter_types, traits::Everything, weights::Weight}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{precompile_set::*, testing::MockAccount}; +use sp_core::{ConstU32, H256, U256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; +pub type Block = frame_system::mocking::MockBlockU32; + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = + PrecompileSetBuilder, Erc20BalancesPrecompile>,)>; + +pub type PCall = Erc20BalancesPrecompileCall; + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +/// Block storage limit in bytes. Set to 40 KB. +const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; + pub GasLimitStorageGrowthRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) + }; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +impl Erc20Metadata for Runtime { + /// Returns the name of the token. + fn name() -> &'static str { + "Mock token" + } + + /// Returns the symbol of the token. + fn symbol() -> &'static str { + "MOCK" + } + + /// Returns the decimals places of the token. + fn decimals() -> u8 { + 18 + } + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool { + true + } +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub(crate) fn events() -> Vec { + System::events().into_iter().map(|r| r.event).collect::>() +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs b/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs new file mode 100644 index 00000000..e68fd525 --- /dev/null +++ b/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs @@ -0,0 +1,1223 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use std::str::from_utf8; + +use crate::{eip2612::Eip2612, mock::*, *}; + +use libsecp256k1::{sign, Message, SecretKey}; +use precompile_utils::testing::*; +use sha3::{Digest, Keccak256}; +use sp_core::{H256, U256}; + +// No test of invalid selectors since we have a fallback behavior (deposit). +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn selectors() { + assert!(PCall::balance_of_selectors().contains(&0x70a08231)); + assert!(PCall::total_supply_selectors().contains(&0x18160ddd)); + assert!(PCall::approve_selectors().contains(&0x095ea7b3)); + assert!(PCall::allowance_selectors().contains(&0xdd62ed3e)); + assert!(PCall::transfer_selectors().contains(&0xa9059cbb)); + assert!(PCall::transfer_from_selectors().contains(&0x23b872dd)); + assert!(PCall::name_selectors().contains(&0x06fdde03)); + assert!(PCall::symbol_selectors().contains(&0x95d89b41)); + assert!(PCall::deposit_selectors().contains(&0xd0e30db0)); + assert!(PCall::withdraw_selectors().contains(&0x2e1a7d4d)); + assert!(PCall::eip2612_nonces_selectors().contains(&0x7ecebe00)); + assert!(PCall::eip2612_permit_selectors().contains(&0xd505accf)); + assert!(PCall::eip2612_domain_separator_selectors().contains(&0x3644e515)); + + assert_eq!( + crate::SELECTOR_LOG_TRANSFER, + &Keccak256::digest(b"Transfer(address,address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_APPROVAL, + &Keccak256::digest(b"Approval(address,address,uint256)")[..] + ); + + assert_eq!(crate::SELECTOR_LOG_DEPOSIT, &Keccak256::digest(b"Deposit(address,uint256)")[..]); + + assert_eq!( + crate::SELECTOR_LOG_WITHDRAWAL, + &Keccak256::digest(b"Withdrawal(address,uint256)")[..] + ); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let mut tester = + PrecompilesModifierTester::new(precompiles(), CryptoAlith, Precompile1); + + tester.test_view_modifier(PCall::balance_of_selectors()); + tester.test_view_modifier(PCall::total_supply_selectors()); + tester.test_default_modifier(PCall::approve_selectors()); + tester.test_view_modifier(PCall::allowance_selectors()); + tester.test_default_modifier(PCall::transfer_selectors()); + tester.test_default_modifier(PCall::transfer_from_selectors()); + tester.test_view_modifier(PCall::name_selectors()); + tester.test_view_modifier(PCall::symbol_selectors()); + tester.test_view_modifier(PCall::decimals_selectors()); + tester.test_payable_modifier(PCall::deposit_selectors()); + tester.test_default_modifier(PCall::withdraw_selectors()); + tester.test_view_modifier(PCall::eip2612_nonces_selectors()); + tester.test_default_modifier(PCall::eip2612_permit_selectors()); + tester.test_view_modifier(PCall::eip2612_domain_separator_selectors()); + }); +} + +#[test] +fn get_total_supply() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::total_supply {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(3500u64)); + }); +} + +#[test] +fn get_balances_known_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_unknown_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn approve() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .expect_cost(1756) + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + }); +} + +#[test] +fn approve_saturating() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { spender: Address(Bob.into()), value: U256::MAX }, + ) + .expect_cost(1756u64) + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::MAX), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) + .expect_no_logs() + .execute_returns(U256::from(u128::MAX)); + }); +} + +#[test] +fn check_allowance_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u64)); + }); +} + +#[test] +fn check_allowance_not_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn transfer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::transfer { to: Address(Bob.into()), value: 400.into() }, + ) + .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn transfer_not_enough_funds() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (CryptoBaltathar.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::transfer { to: Address(Bob.into()), value: 1400.into() }, + ) + .execute_reverts(|output| { + from_utf8(&output).unwrap().contains("Dispatched call failed with error: ") && + from_utf8(&output).unwrap().contains("FundsUnavailable") + }); + }); +} + +#[test] +fn transfer_from() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(100u64)); + }); +} + +#[test] +fn transfer_from_above_allowance() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { spender: Address(Bob.into()), value: 300.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .execute_reverts(|output| output == b"trying to spend more than allowed"); + }); +} + +#[test] +fn transfer_from_self() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, /* CryptoAlith sending transferFrom herself, no need for + * allowance. */ + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn get_metadata_name() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::name {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("Mock token")); + }); +} + +#[test] +fn get_metadata_symbol() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::symbol {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("MOCK")); + }); +} + +#[test] +fn get_metadata_decimals() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::decimals {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(18u8); + }); +} + +fn deposit(data: Vec) { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Deposit + // We need to call using EVM pallet so we can check the EVM correctly sends the amount + // to the precompile. + Evm::call( + RuntimeOrigin::root(), + CryptoAlith.into(), + Precompile1.into(), + data, + From::from(500), // amount sent + u64::MAX, // gas limit + 0u32.into(), // gas price + None, // max priority + None, // nonce + vec![], // access list + ) + .expect("it works"); + + assert_eq!( + events(), + vec![ + RuntimeEvent::System(frame_system::Event::NewAccount { + account: Precompile1.into() + }), + RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: Precompile1.into(), + free_balance: 500 + }), + // EVM make a transfer because some value is provided. + RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: CryptoAlith.into(), + to: Precompile1.into(), + amount: 500 + }), + // Precompile1 send it back since deposit should be a no-op. + RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: Precompile1.into(), + to: CryptoAlith.into(), + amount: 500 + }), + // Log is correctly emited. + RuntimeEvent::Evm(pallet_evm::Event::Log { + log: log2( + Precompile1, + SELECTOR_LOG_DEPOSIT, + CryptoAlith, + solidity::encode_event_data(U256::from(500)), + ) + }), + RuntimeEvent::Evm(pallet_evm::Event::Executed { address: Precompile1.into() }), + ] + ); + + // Check precompile balance is still 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn deposit_function() { + deposit(PCall::deposit {}.into()) +} + +#[test] +fn deposit_fallback() { + deposit(solidity::encode_with_selector(0x01234567u32, ())) +} + +#[test] +fn deposit_receive() { + deposit(vec![]) +} + +#[test] +fn deposit_zero() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Deposit + // We need to call using EVM pallet so we can check the EVM correctly sends the amount + // to the precompile. + Evm::call( + RuntimeOrigin::root(), + CryptoAlith.into(), + Precompile1.into(), + PCall::deposit {}.into(), + From::from(0), // amount sent + u64::MAX, // gas limit + 0u32.into(), // gas price + None, // max priority + None, // nonce + vec![], // access list + ) + .expect("it works"); + + assert_eq!( + events(), + vec![RuntimeEvent::Evm(pallet_evm::Event::ExecutedFailed { + address: Precompile1.into() + }),] + ); + + // Check precompile balance is still 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn withdraw() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Withdraw + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::withdraw { value: 500.into() }) + .expect_cost(1381) + .expect_log(log2( + Precompile1, + SELECTOR_LOG_WITHDRAWAL, + CryptoAlith, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn withdraw_more_than_owned() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(Precompile1.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Withdraw + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::withdraw { value: 1001.into() }) + .execute_reverts(|output| output == b"Trying to withdraw more than owned"); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn permit_valid() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(value)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_invalid_nonce() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 1u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_signature() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: 0, + r: H256::repeat_byte(0x11), + s: H256::repeat_byte(0x11), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_deadline() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + pallet_timestamp::Pallet::::set_timestamp(10_000); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 5u8.into(); // deadline < timestamp => expired + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .execute_reverts(|output| output == b"Permit expired"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +// This test checks the validity of a metamask signed message against the permit precompile +// The code used to generate the signature is the following. +// You will need to import ALICE_PRIV_KEY in metamask. +// If you put this code in the developer tools console, it will log the signature +/* +await window.ethereum.enable(); +const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + +const value = 1000; + +const fromAddress = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"; +const deadline = 1; +const nonce = 0; +const spender = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const from = accounts[0]; + +const createPermitMessageData = function () { + const message = { + owner: from, + spender: spender, + value: value, + nonce: nonce, + deadline: deadline, + }; + + const typedData = JSON.stringify({ + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name: "Mock token", + version: "1", + chainId: 0, + verifyingContract: "0x0000000000000000000000000000000000000001", + }, + message: message, + }); + + return { + typedData, + message, + }; +}; + +const method = "eth_signTypedData_v4" +const messageData = createPermitMessageData(); +const params = [from, messageData.typedData]; + +web3.currentProvider.sendAsync( + { + method, + params, + from, + }, + function (err, result) { + if (err) return console.dir(err); + if (result.error) { + alert(result.error.message); + } + if (result.error) return console.error('ERROR', result); + console.log('TYPED SIGNED:' + JSON.stringify(result.result)); + + const recovered = sigUtil.recoverTypedSignature_v4({ + data: JSON.parse(msgParams), + sig: result.result, + }); + + if ( + ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from) + ) { + alert('Successfully recovered signer as ' + from); + } else { + alert( + 'Failed to verify signer when comparing ' + result + ' to ' + from + ); + } + } +); +*/ + +#[test] +fn permit_valid_with_metamask_signed_data() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 1000u16.into(); + let deadline: U256 = 1u16.into(); // todo: proper timestamp + + let rsv = hex_literal::hex!( + "612960858951e133d05483804be5456a030be4ce6c000a855d865c0be75a8fc11d89ca96d5a153e8c + 7155ab1147f0f6d3326388b8d866c2406ce34567b7501a01b" + ) + .as_slice(); + let (r, sv) = rsv.split_at(32); + let (s, v) = sv.split_at(32); + let v_real = v[0]; + let r_real: [u8; 32] = r.try_into().unwrap(); + let s_real: [u8; 32] = s.try_into().unwrap(); + + precompiles() + .prepare_test( + Charlie, // can be anyone, + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v_real, + r: r_real.into(), + s: s_real.into(), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(1000)), + )) + .execute_returns(()); + }); +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces( + &["ERC20.sol", "Permit.sol"], + PCall::supports_selector, + ) +} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 82980aa7..31f96a29 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -360,6 +360,24 @@ impl frame_babel::ethereum::precompile::Config for Runtime { type StorageFilter = (); } +impl frame_babel::ethereum::Erc20Metadata for Runtime { + fn name() -> &'static str { + "Ziggurat" + } + + fn symbol() -> &'static str { + "ZIG" + } + + fn decimals() -> u8 { + 18 + } + + fn is_native_currency() -> bool { + true + } +} + impl pallet_evm::Config for Runtime { type FeeCalculator = BaseFee; type GasWeightMapping = pallet_evm::FixedGasWeightMapping; From 05ea0ef3530b910f2e55487fd4ed1409a936b7f4 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 17:06:26 +0900 Subject: [PATCH 098/133] refactor: Refactor cosmos check_self_contained --- frame/cosmos/x/auth/src/sigverify.rs | 1 - ziggurat/runtime/src/call.rs | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index b40215df..407fa57c 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -189,7 +189,6 @@ impl ValidateSigCountDecorator { } pub struct IncrementSequenceDecorator(PhantomData); - impl AnteDecorator for IncrementSequenceDecorator where T: frame_system::Config + pallet_cosmos::Config, diff --git a/ziggurat/runtime/src/call.rs b/ziggurat/runtime/src/call.rs index b4824dad..cfe02df0 100644 --- a/ziggurat/runtime/src/call.rs +++ b/ziggurat/runtime/src/call.rs @@ -25,7 +25,7 @@ use fp_self_contained::SelfContainedCall; use frame_babel::{ethereum::TransactionExt, UnifyAccount}; use np_cosmos::Address as CosmosAddress; use np_ethereum::Address as EthereumAddress; -use pallet_cosmos_types::any_match; +use pallet_cosmos_types::{any_match, tx_msgs::FeeTx}; use sp_core::ecdsa; use sp_runtime::{ traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf}, @@ -70,13 +70,14 @@ impl SelfContainedCall for RuntimeCall { let check = || { let tx = Tx::decode(&mut &tx_bytes[..]).map_err(|_| InvalidTransaction::Call)?; - let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; - let fee = auth_info.fee.as_ref().ok_or(InvalidTransaction::Call)?; + let fee_payer = tx.fee_payer().ok_or(InvalidTransaction::Call)?; + let signer_infos = + tx.auth_info.ok_or(InvalidTransaction::Call)?.signer_infos; - let public_key = if fee.payer.is_empty() { - auth_info.signer_infos.first() + let public_key = if fee_payer.is_empty() { + signer_infos.first() } else { - auth_info.signer_infos.last() + signer_infos.last() } .and_then(|signer_info| signer_info.public_key.as_ref()) .ok_or(InvalidTransaction::Call)?; From 138e1e2cc701c4077a19d8967b263da6659f5e21 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 17:36:46 +0900 Subject: [PATCH 099/133] fix: Remove asset callback handler --- frame/babel/src/cosmos/asset.rs | 23 ----------------------- frame/babel/src/lib.rs | 2 +- ziggurat/runtime/src/configs/mod.rs | 2 +- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/frame/babel/src/cosmos/asset.rs b/frame/babel/src/cosmos/asset.rs index 02397c41..1b840540 100644 --- a/frame/babel/src/cosmos/asset.rs +++ b/frame/babel/src/cosmos/asset.rs @@ -18,7 +18,6 @@ use alloc::string::{String, ToString}; use core::marker::PhantomData; -use frame_support::{ensure, traits::fungibles::metadata::Inspect}; use pallet_cosmos::types::{AssetIdOf, DenomOf}; use pallet_multimap::traits::UniqueMap; use sp_core::Get; @@ -57,25 +56,3 @@ where } } } - -pub struct AssetsCallback(PhantomData<(T, I)>); -impl pallet_assets::AssetsCallback, T::AccountId> - for AssetsCallback -where - T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, -{ - fn created(id: &AssetIdOf, _owner: &T::AccountId) -> Result<(), ()> { - let symbol = T::Assets::symbol(id.clone()); - ensure!(!symbol.is_empty(), ()); - - let denom: DenomOf = symbol.try_into().map_err(|_| ())?; - pallet_multimap::Pallet::::try_insert(id.clone(), denom).map_err(|_| ())?; - - Ok(()) - } - - fn destroyed(id: &AssetIdOf) -> Result<(), ()> { - pallet_multimap::Pallet::::remove(id.clone()); - Ok(()) - } -} diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index d736ab46..5e0ca6de 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -69,7 +69,7 @@ pub mod pallet { use pallet_ethereum::{Transaction, TransactionData}; use pallet_evm::GasWeightMapping; let without_base_extrinsic_weight = true; - match ::decode(&transaction) { + match ::decode(transaction) { Ok(transaction) => { ::GasWeightMapping::gas_to_weight({ let transaction_data = TransactionData::from(&transaction); diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 31f96a29..b4b8b159 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -462,7 +462,7 @@ impl pallet_assets::Config for Runtime { type StringLimit = ConstU32<20>; type Freezer = (); type Extra = (); - type CallbackHandle = cosmos::asset::AssetsCallback; + type CallbackHandle = (); type WeightInfo = (); type RemoveItemsLimit = ConstU32<1000>; } From 9c6cece5951872325eedca567140ae07d85c77b5 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Fri, 27 Sep 2024 19:21:08 +0900 Subject: [PATCH 100/133] feat: Add assets erc20 precompile set --- Cargo.toml | 2 + frame/babel/Cargo.toml | 7 + frame/babel/src/ethereum/mod.rs | 94 +- frame/babel/src/ethereum/precompile.rs | 9 +- frame/babel/src/ethereum/precompiles.rs | 68 + primitives/runtime/src/accountid32.rs | 14 +- .../precompiles/assets-erc20/Cargo.toml | 63 + .../precompiles/assets-erc20/ERC20.sol | 88 + .../precompiles/assets-erc20/Permit.sol | 39 + .../precompiles/assets-erc20/Roles.sol | 43 + .../precompiles/assets-erc20/src/eip2612.rs | 275 +++ .../precompiles/assets-erc20/src/lib.rs | 545 ++++++ .../precompiles/assets-erc20/src/mock.rs | 292 +++ .../precompiles/assets-erc20/src/tests.rs | 1665 +++++++++++++++++ .../precompiles/balances-erc20/Cargo.toml | 2 - .../precompiles/balances-erc20/src/eip2612.rs | 2 +- .../precompiles/balances-erc20/src/lib.rs | 7 +- ziggurat/runtime/src/configs/mod.rs | 24 +- 18 files changed, 3141 insertions(+), 98 deletions(-) create mode 100644 frame/babel/src/ethereum/precompiles.rs create mode 100644 vendor/moonbeam/precompiles/assets-erc20/Cargo.toml create mode 100644 vendor/moonbeam/precompiles/assets-erc20/ERC20.sol create mode 100644 vendor/moonbeam/precompiles/assets-erc20/Permit.sol create mode 100644 vendor/moonbeam/precompiles/assets-erc20/Roles.sol create mode 100644 vendor/moonbeam/precompiles/assets-erc20/src/eip2612.rs create mode 100644 vendor/moonbeam/precompiles/assets-erc20/src/lib.rs create mode 100644 vendor/moonbeam/precompiles/assets-erc20/src/mock.rs create mode 100644 vendor/moonbeam/precompiles/assets-erc20/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 96a3c52d..fbad4123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "vendor/composable/cosmwasm", "vendor/composable/cosmwasm/rpc", "vendor/composable/cosmwasm/runtime-api", + "vendor/moonbeam/precompiles/assets-erc20", "vendor/moonbeam/precompiles/balances-erc20", "ziggurat/node", "ziggurat/runtime", @@ -58,6 +59,7 @@ pallet-cosmos-x-bank-types = { path = "frame/cosmos/x/bank/types", default-featu pallet-cosmos-x-wasm = { path = "frame/cosmos/x/wasm", default-features = false } pallet-cosmos-x-wasm-types = { path = "frame/cosmos/x/wasm/types", default-features = false } pallet-cosmwasm = { path = "vendor/composable/cosmwasm", default-features = false } +pallet-evm-precompileset-assets-erc20 = { path = "vendor/moonbeam/precompiles/assets-erc20", default-features = false } pallet-evm-precompile-balances-erc20 = { path = "vendor/moonbeam/precompiles/balances-erc20", default-features = false } pallet-multimap = { path = "frame/multimap", default-features = false } diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 650c08b5..4da169e7 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -19,9 +19,11 @@ ethereum = { version = "0.15.0", default-features = false, optional = true } fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +hex-literal = "0.4" np-babel = { workspace = true, default-features = false } np-cosmos = { workspace = true, optional = true } np-ethereum = { workspace = true, optional = true } +num_enum = { version = "0.7", default-features = false, optional = true } pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } @@ -31,6 +33,7 @@ pallet-cosmos-x-wasm = { workspace = true, default-features = false, optional = pallet-cosmwasm = { workspace = true, default-features = false, optional = true } pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } +pallet-evm-precompileset-assets-erc20 = { workspace = true, optional = true } pallet-evm-precompile-balances-erc20 = { workspace = true, optional = true } pallet-evm-precompile-blake2 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-bn128 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } @@ -70,6 +73,7 @@ std = [ "np-babel/std", "np-cosmos?/std", "np-ethereum?/std", + "num_enum?/std", "pallet-assets?/std", "pallet-balances/std", "pallet-cosmos?/std", @@ -79,6 +83,7 @@ std = [ "pallet-cosmwasm?/std", "pallet-ethereum?/std", "pallet-evm?/std", + "pallet-evm-precompileset-assets-erc20?/std", "pallet-evm-precompile-balances-erc20?/std", "pallet-evm-precompile-blake2?/std", "pallet-evm-precompile-bn128?/std", @@ -114,8 +119,10 @@ ethereum = [ "dep:ethereum", "fp-evm", "np-babel/ethereum", + "num_enum", "pallet-ethereum", "pallet-evm", + "pallet-evm-precompileset-assets-erc20", "pallet-evm-precompile-balances-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", diff --git a/frame/babel/src/ethereum/mod.rs b/frame/babel/src/ethereum/mod.rs index 0f7be75e..4b8f96c0 100644 --- a/frame/babel/src/ethereum/mod.rs +++ b/frame/babel/src/ethereum/mod.rs @@ -17,30 +17,24 @@ // along with this program. If not, see . pub mod precompile; +mod precompiles; + +pub use precompiles::*; use crate::*; use crate::extensions::unify_account; use core::marker::PhantomData; -use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo, RawOrigin}; +use frame_support::dispatch::RawOrigin; use np_babel::EthereumAddress; use pallet_ethereum::Transaction; -use pallet_evm::{ - EnsureAddressOrigin, IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, - PrecompileSet, -}; -use pallet_evm_precompile_balances_erc20::Erc20BalancesPrecompile; -use pallet_evm_precompile_blake2::Blake2F; -use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; -use pallet_evm_precompile_modexp::Modexp; -use pallet_evm_precompile_simple::{ECRecover, Identity, Ripemd160, Sha256}; +use pallet_evm::EnsureAddressOrigin; use pallet_multimap::traits::UniqueMultimap; -use parity_scale_codec::Decode; -use precompile::Babel; use sp_core::{ecdsa, H160}; -use sp_runtime::traits::{AccountIdConversion, Dispatchable}; +use sp_runtime::traits::AccountIdConversion; pub use pallet_evm_precompile_balances_erc20::Erc20Metadata; +pub use pallet_evm_precompileset_assets_erc20::AddressToAssetId; pub struct EnsureAddress(PhantomData); @@ -82,80 +76,6 @@ where } } -pub struct BabelPrecompiles(PhantomData); - -impl Default for BabelPrecompiles { - fn default() -> Self { - Self(Default::default()) - } -} - -use pallet_evm_precompile_balances_erc20::BalanceOf; -use sp_core::U256; - -impl BabelPrecompiles -where - T: precompile::Config, -{ - pub fn new() -> Self { - Self::default() - } - - pub fn used_addresses() -> [H160; 11] { - [ - hash(1), - hash(2), - hash(3), - hash(4), - hash(5), - hash(6), - hash(7), - hash(8), - hash(9), - hash(0x400 /* 1024 */), - hash(0x401 /* 1025 */), - ] - } -} - -impl PrecompileSet for BabelPrecompiles -where - T: precompile::Config, - T::RuntimeCall: Dispatchable + GetDispatchInfo + Decode, - ::RuntimeOrigin: From>, - T::RuntimeCall: From>, - BalanceOf: TryFrom + Into, -{ - fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { - match handle.code_address() { - // Ethereum precompiles : - a if a == hash(1) => Some(ECRecover::execute(handle)), - a if a == hash(2) => Some(Sha256::execute(handle)), - a if a == hash(3) => Some(Ripemd160::execute(handle)), - a if a == hash(4) => Some(Identity::execute(handle)), - a if a == hash(5) => Some(Modexp::execute(handle)), - a if a == hash(6) => Some(Bn128Add::execute(handle)), - a if a == hash(7) => Some(Bn128Mul::execute(handle)), - a if a == hash(8) => Some(Bn128Pairing::execute(handle)), - a if a == hash(9) => Some(Blake2F::execute(handle)), - a if a == hash(0x400) => Some(Babel::::execute(handle)), - a if a == hash(0x401) => Some(Erc20BalancesPrecompile::::execute(handle)), - _ => None, - } - } - - fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult { - IsPrecompileResult::Answer { - is_precompile: Self::used_addresses().contains(&address), - extra_cost: 0, - } - } -} - -fn hash(a: u64) -> H160 { - H160::from_low_u64_be(a) -} - pub trait TransactionExt { fn recover_key(&self) -> Option; fn nonce(&self) -> u64; diff --git a/frame/babel/src/ethereum/precompile.rs b/frame/babel/src/ethereum/precompile.rs index a2a692b9..600abb34 100644 --- a/frame/babel/src/ethereum/precompile.rs +++ b/frame/babel/src/ethereum/precompile.rs @@ -25,11 +25,18 @@ use frame_support::{ }; use pallet_evm::{AddressMapping, GasWeightMapping}; use pallet_evm_precompile_balances_erc20::Erc20Metadata; +use pallet_evm_precompileset_assets_erc20::AddressToAssetId; use parity_scale_codec::{Decode, DecodeLimit, Encode}; use precompile_utils::{prelude::*, EvmResult}; use sp_runtime::traits::{Dispatchable, Get}; -pub trait Config: pallet_balances::Config + pallet_evm::Config + Erc20Metadata { +pub trait Config: + pallet_assets::Config + + pallet_balances::Config + + pallet_evm::Config + + Erc20Metadata + + AddressToAssetId +{ type DispatchValidator: DispatchValidate; type DecodeLimit: Get; type StorageFilter: StorageFilter; diff --git a/frame/babel/src/ethereum/precompiles.rs b/frame/babel/src/ethereum/precompiles.rs new file mode 100644 index 00000000..8f811cbf --- /dev/null +++ b/frame/babel/src/ethereum/precompiles.rs @@ -0,0 +1,68 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use super::precompile; +use frame_support::parameter_types; +use pallet_evm_precompile_balances_erc20::Erc20BalancesPrecompile; +use pallet_evm_precompile_blake2::Blake2F; +use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; +use pallet_evm_precompile_modexp::Modexp; +use pallet_evm_precompile_simple::{ECRecover, Identity, Ripemd160, Sha256}; +use pallet_evm_precompileset_assets_erc20::Erc20AssetsPrecompileSet; +use precompile::Babel; +use precompile_utils::precompile_set::*; + +pub const ASSET_PRECOMPILE_ADDRESS_PREFIX: &[u8] = + &hex_literal::hex!("ffffffff000000000000000000000000"); + +parameter_types! { + pub AssetPrefix: &'static [u8] = ASSET_PRECOMPILE_ADDRESS_PREFIX; +} + +type EthereumPrecompilesChecks = (AcceptDelegateCall, CallableByContract, CallableByPrecompile); + +#[precompile_utils::precompile_name_from_address] +type BabelPrecompilesAt = ( + PrecompileAt, ECRecover, EthereumPrecompilesChecks>, + PrecompileAt, Sha256, EthereumPrecompilesChecks>, + PrecompileAt, Ripemd160, EthereumPrecompilesChecks>, + PrecompileAt, Identity, EthereumPrecompilesChecks>, + PrecompileAt, Modexp, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Add, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Mul, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Pairing, EthereumPrecompilesChecks>, + PrecompileAt, Blake2F, EthereumPrecompilesChecks>, + PrecompileAt, Babel, (CallableByContract, CallableByPrecompile)>, + PrecompileAt< + AddressU64<0x401>, + Erc20BalancesPrecompile, + (CallableByContract, CallableByPrecompile), + >, +); + +pub type BabelPrecompiles = PrecompileSetBuilder< + T, + ( + PrecompilesInRangeInclusive<(AddressU64<1>, AddressU64<2048>), BabelPrecompilesAt>, + PrecompileSetStartingWith< + AssetPrefix, + Erc20AssetsPrecompileSet, + (CallableByContract, CallableByPrecompile), + >, + ), +>; diff --git a/primitives/runtime/src/accountid32.rs b/primitives/runtime/src/accountid32.rs index 3799a0dc..2ba73f69 100644 --- a/primitives/runtime/src/accountid32.rs +++ b/primitives/runtime/src/accountid32.rs @@ -22,7 +22,7 @@ use buidl::FixedBytes; use scale_info::{Type, TypeInfo}; #[cfg(feature = "serde")] use sp_core::crypto::Ss58Codec; -use sp_core::{crypto, ecdsa, ed25519, sr25519, H256}; +use sp_core::{crypto, ecdsa, ed25519, sr25519, H160, H256}; use sp_io::hashing::blake2_256; /// An opaque 32-byte cryptographic identifier. @@ -53,6 +53,18 @@ impl From for AccountId32 { } } +impl From> for H256 { + fn from(acc: AccountId32) -> Self { + Self(acc.0) + } +} + +impl From> for H160 { + fn from(acc: AccountId32) -> Self { + H256::from(acc).into() + } +} + impl From for AccountId32 { fn from(acc: crypto::AccountId32) -> Self { Self(Into::<[u8; 32]>::into(acc), None) diff --git a/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml new file mode 100644 index 00000000..61568122 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-evm-precompileset-assets-erc20" +authors = ["PureStake"] +description = "A Precompile to expose Assets pallet through an ERC20-compliant interface." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { version = "0.4", default-features = false } +num_enum = { version = "0.7", default-features = false } +paste = "1.0" + +# Substrate +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +parity-scale-codec = { version = "3.6", default-features = false, features = ["max-encoded-len"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +# Frontier +fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } +pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, features = ["forbid-evm-reentrancy"] } +precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } + +# Moonkit +#moonkit-xcm-primitives = { workspace = true } + +[dev-dependencies] +derive_more = "1.0" +hex-literal = "0.4" +libsecp256k1 = "0.7" +serde = "1.0" +sha3 = "0.10" + +# Moonbeam +precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["testing"] } + +[features] +default = ["std"] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "log/std", + #"moonkit-xcm-primitives/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "precompile-utils/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/vendor/moonbeam/precompiles/assets-erc20/ERC20.sol b/vendor/moonbeam/precompiles/assets-erc20/ERC20.sol new file mode 100644 index 00000000..cb88acd2 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/ERC20.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title ERC20 interface +/// @dev see https://github.com/ethereum/EIPs/issues/20 +/// @dev copied from https://github.com/OpenZeppelin/openzeppelin-contracts +interface IERC20 { + /// @dev Returns the name of the token. + /// @custom:selector 06fdde03 + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + /// @custom:selector 95d89b41 + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + /// @custom:selector 313ce567 + function decimals() external view returns (uint8); + + /// @dev Total number of tokens in existence + /// @custom:selector 18160ddd + function totalSupply() external view returns (uint256); + + /// @dev Gets the balance of the specified address. + /// @custom:selector 70a08231 + /// @param who The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + function balanceOf(address who) external view returns (uint256); + + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @custom:selector dd62ed3e + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + function allowance(address owner, address spender) + external + view + returns (uint256); + + /// @dev Transfer token for a specified address + /// @custom:selector a9059cbb + /// @param to The address to transfer to. + /// @param value The amount to be transferred. + function transfer(address to, uint256 value) external returns (bool); + + /// @dev Approve the passed address to spend the specified amount of tokens on behalf + /// of msg.sender. + /// Beware that changing an allowance with this method brings the risk that someone may + /// use both the old + /// and the new allowance by unfortunate transaction ordering. One possible solution to + /// mitigate this race condition is to first reduce the spender's allowance to 0 and set + /// the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @custom:selector 095ea7b3 + /// @param spender The address which will spend the funds. + /// @param value The amount of tokens to be spent. + function approve(address spender, uint256 value) external returns (bool); + + /// @dev Transfer tokens from one address to another + /// @custom:selector 23b872dd + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param value uint256 the amount of tokens to be transferred + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /// @dev Event emited when a transfer has been performed. + /// @custom:selector ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + /// @param from address The address sending the tokens + /// @param to address The address receiving the tokens. + /// @param value uint256 The amount of tokens transfered. + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev Event emited when an approval has been registered. + /// @custom:selector 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + /// @param owner address Owner of the tokens. + /// @param spender address Allowed spender. + /// @param value uint256 Amount of tokens approved. + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/Permit.sol b/vendor/moonbeam/precompiles/assets-erc20/Permit.sol new file mode 100644 index 00000000..3d132113 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/Permit.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Extension of the ERC20 interface that allows users to +/// sign permit messages to interact with contracts without needing to +/// make a first approve transaction. +interface Permit { + /// @dev Consumes an approval permit. + /// Anyone can call this function for a permit. + /// @custom:selector d505accf + /// @param owner Owner of the tokens issuing the permit + /// @param spender Address whose allowance will be increased. + /// @param value Allowed value. + /// @param deadline Timestamp after which the permit will no longer be valid. + /// @param v V component of the signature. + /// @param r R component of the signature. + /// @param s S component of the signature. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @dev Returns the current nonce for given owner. + /// A permit must have this nonce to be consumed, which will + /// increase the nonce by one. + /// @custom:selector 7ecebe00 + function nonces(address owner) external view returns (uint256); + + /// @dev Returns the EIP712 domain separator. It is used to avoid replay + /// attacks accross assets or other similar EIP712 message structures. + /// @custom:selector 3644e515 + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/Roles.sol b/vendor/moonbeam/precompiles/assets-erc20/Roles.sol new file mode 100644 index 00000000..9b3bef05 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/Roles.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title ERC20 interface Asset Roles +/// @dev Extension of the ERC20 interface that allows users to get the account capable of fulfilling different asset roles +/// @custom:address 0xFFFFFFFE + hex(assetId) +interface Roles { + /// @dev Function to check the owner of the asset + /// @custom:selector 8da5cb5b + /// @return the address of the owner. + function owner() + external + view + returns (address); + + /// @dev Function to check the freezer of the asset + /// @dev Freezer: the account that can freeze an asset + /// @custom:selector 92716054 + /// @return the address of the freezer. + function freezer() + external + view + returns (address); + + /// @dev Function to check the issuer of the asset + /// @dev Issuer: the account that can issue tokens for an asset + /// @custom:selector 1d143848 + /// @return the address of the issuer. + function issuer() + external + view + returns (address); + + /// @dev Function to check the admin of the asset + /// @dev Admin: the account that can unfreeze and force transfer + /// @custom:selector f851a440 + /// @return the address of the admin. + function admin() + external + view + returns (address); +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/eip2612.rs b/vendor/moonbeam/precompiles/assets-erc20/src/eip2612.rs new file mode 100644 index 00000000..efecfe91 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/src/eip2612.rs @@ -0,0 +1,275 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use super::*; +use frame_support::{ + ensure, + storage::types::{StorageDoubleMap, ValueQuery}, + traits::{StorageInstance, Time}, + Blake2_128Concat, +}; +use pallet_assets::pallet::{ + Instance1, Instance10, Instance11, Instance12, Instance13, Instance14, Instance15, Instance16, + Instance2, Instance3, Instance4, Instance5, Instance6, Instance7, Instance8, Instance9, +}; +use scale_info::prelude::string::ToString; +use sp_core::H256; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::UniqueSaturatedInto; + +/// EIP2612 permit typehash. +pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +); + +/// EIP2612 permit domain used to compute an individualized domain separator. +const PERMIT_DOMAIN: [u8; 32] = keccak256!( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +/// Associates pallet Instance to a prefix used for the Nonces storage. +/// This trait is implemented for () and the 16 substrate Instance. +pub trait InstanceToPrefix { + /// Prefix used for the Approves storage. + type NoncesPrefix: StorageInstance; +} + +// We use a macro to implement the trait for () and the 16 substrate Instance. +macro_rules! impl_prefix { + ($instance:ident, $name:literal) => { + // Using `paste!` we generate a dedicated module to avoid collisions + // between each instance `Nonces` struct. + paste::paste! { + mod [<_impl_prefix_ $instance:snake>] { + use super::*; + + pub struct Nonces; + + impl StorageInstance for Nonces { + const STORAGE_PREFIX: &'static str = "Nonces"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + impl InstanceToPrefix for $instance { + type NoncesPrefix = Nonces; + } + } + } + }; +} + +// Since the macro expect a `ident` to be used with `paste!` we cannot provide `()` directly. +type Instance0 = (); + +impl_prefix!(Instance0, "Erc20Instance0Assets"); +impl_prefix!(Instance1, "Erc20Instance1Assets"); +impl_prefix!(Instance2, "Erc20Instance2Assets"); +impl_prefix!(Instance3, "Erc20Instance3Assets"); +impl_prefix!(Instance4, "Erc20Instance4Assets"); +impl_prefix!(Instance5, "Erc20Instance5Assets"); +impl_prefix!(Instance6, "Erc20Instance6Assets"); +impl_prefix!(Instance7, "Erc20Instance7Assets"); +impl_prefix!(Instance8, "Erc20Instance8Assets"); +impl_prefix!(Instance9, "Erc20Instance9Assets"); +impl_prefix!(Instance10, "Erc20Instance10Assets"); +impl_prefix!(Instance11, "Erc20Instance11Assets"); +impl_prefix!(Instance12, "Erc20Instance12Assets"); +impl_prefix!(Instance13, "Erc20Instance13Assets"); +impl_prefix!(Instance14, "Erc20Instance14Assets"); +impl_prefix!(Instance15, "Erc20Instance15Assets"); +impl_prefix!(Instance16, "Erc20Instance16Assets"); + +/// Storage type used to store EIP2612 nonces. +pub type NoncesStorage = StorageDoubleMap< + ::NoncesPrefix, + // Asset contract address + Blake2_128Concat, + H160, + // Owner + Blake2_128Concat, + H160, + // Nonce + U256, + ValueQuery, +>; + +pub struct Eip2612(PhantomData<(Runtime, Instance)>); + +impl Eip2612 +where + Instance: InstanceToPrefix + 'static, + Runtime: pallet_assets::Config + pallet_evm::Config + frame_system::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime: AddressToAssetId>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait, + AssetIdOf: Display, + Runtime::AccountId: Into, +{ + fn compute_domain_separator(address: H160, asset_id: AssetIdOf) -> [u8; 32] { + let asset_name = pallet_assets::Pallet::::name(asset_id.clone()); + + let name = if asset_name.is_empty() { + let mut name = b"Unnamed XC20 #".to_vec(); + name.extend_from_slice(asset_id.to_string().as_bytes()); + name + } else { + asset_name + }; + + let name: H256 = keccak_256(&name).into(); + let version: H256 = keccak256!("1").into(); + let chain_id: U256 = Runtime::ChainId::get().into(); + + let domain_separator_inner = solidity::encode_arguments(( + H256::from(PERMIT_DOMAIN), + name, + version, + chain_id, + Address(address), + )); + + keccak_256(&domain_separator_inner).into() + } + + pub fn generate_permit( + address: H160, + asset_id: AssetIdOf, + owner: H160, + spender: H160, + value: U256, + nonce: U256, + deadline: U256, + ) -> [u8; 32] { + let domain_separator = Self::compute_domain_separator(address, asset_id); + + let permit_content = solidity::encode_arguments(( + H256::from(PERMIT_TYPEHASH), + Address(owner), + Address(spender), + value, + nonce, + deadline, + )); + let permit_content = keccak_256(&permit_content); + + let mut pre_digest = Vec::with_capacity(2 + 32 + 32); + pre_digest.extend_from_slice(b"\x19\x01"); + pre_digest.extend_from_slice(&domain_separator); + pre_digest.extend_from_slice(&permit_content); + keccak_256(&pre_digest) + } + + // Translated from + // https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81 + pub(crate) fn permit( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + let address = handle.code_address(); + + // Blockchain time is in ms while Ethereum use second timestamps. + let timestamp: u128 = + ::Timestamp::now().unique_saturated_into(); + let timestamp: U256 = U256::from(timestamp / 1000); + + ensure!(deadline >= timestamp, revert("Permit expired")); + + let nonce = NoncesStorage::::get(address, owner); + + let permit = Self::generate_permit( + address, + asset_id.clone(), + owner, + spender, + value, + nonce, + deadline, + ); + + let mut sig = [0u8; 65]; + sig[0..32].copy_from_slice(&r.as_bytes()); + sig[32..64].copy_from_slice(&s.as_bytes()); + sig[64] = v; + + let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) + .map_err(|_| revert("Invalid permit"))?; + let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); + + ensure!(signer != H160::zero() && signer == owner, revert("Invalid permit")); + + NoncesStorage::::insert(address, owner, nonce + U256::one()); + + Erc20AssetsPrecompileSet::::approve_inner( + asset_id, handle, owner, spender, value, + )?; + + log3(address, SELECTOR_LOG_APPROVAL, owner, spender, solidity::encode_event_data(value)) + .record(handle)?; + + Ok(()) + } + + pub(crate) fn nonces( + _asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + + let nonce = NoncesStorage::::get(handle.code_address(), owner); + + Ok(nonce) + } + + pub(crate) fn domain_separator( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: AssetMetadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let domain_separator: H256 = + Self::compute_domain_separator(handle.code_address(), asset_id).into(); + + Ok(domain_separator) + } +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs new file mode 100644 index 00000000..8f5b1669 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs @@ -0,0 +1,545 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; +use core::{fmt::Display, marker::PhantomData}; +use fp_evm::{ExitError, PrecompileHandle}; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + sp_runtime::traits::StaticLookup, + traits::{ + fungibles::{ + approvals::Inspect as ApprovalInspect, metadata::Inspect as MetadataInspect, + roles::Inspect as RolesInspect, Inspect, + }, + Get, OriginTrait, + }, +}; +//use moonkit_xcm_primitives::AccountIdAssetIdConversion; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_runtime::traits::{Bounded, Dispatchable}; + +use sp_core::{MaxEncodedLen, H160, H256, U256}; + +mod eip2612; +use eip2612::Eip2612; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); + +/// Solidity selector of the Approval log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)"); + +/// Alias for the Balance type for the provided Runtime and Instance. +pub type BalanceOf = >::Balance; + +/// Alias for the Asset Id type for the provided Runtime and Instance. +pub type AssetIdOf = >::AssetId; + +/// This trait ensure we can convert EVM address to AssetIds +/// We will require Runtime to have this trait implemented +pub trait AddressToAssetId { + // Get assetId from address + fn address_to_asset_id(address: H160) -> Option; + + // Get address from AssetId + fn asset_id_to_address(asset_id: AssetId) -> H160; +} + +/// The following distribution has been decided for the precompiles +/// 0-1023: Ethereum Mainnet Precompiles +/// 1024-2047 Precompiles that are not in Ethereum Mainnet but are neither Moonbeam specific +/// 2048-4095 Moonbeam specific precompiles +/// Asset precompiles can only fall between +/// 0xFFFFFFFF00000000000000000000000000000000 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +/// The precompile for AssetId X, where X is a u128 (i.e.16 bytes), if 0XFFFFFFFF + Bytes(AssetId) +/// In order to route the address to Erc20AssetsPrecompile, we first check whether the AssetId +/// exists in pallet-assets +/// We cannot do this right now, so instead we check whether the total supply is zero. If so, we +/// do not route to the precompiles + +/// This means that every address that starts with 0xFFFFFFFF will go through an additional db read, +/// but the probability for this to happen is 2^-32 for random addresses +pub struct Erc20AssetsPrecompileSet( + PhantomData<(Runtime, Instance)>, +); + +impl Clone for Erc20AssetsPrecompileSet { + fn clone(&self) -> Self { + Self(PhantomData) + } +} + +impl Default for Erc20AssetsPrecompileSet { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Erc20AssetsPrecompileSet { + pub fn new() -> Self { + Self(PhantomData) + } +} + +#[precompile_utils::precompile] +#[precompile::precompile_set] +#[precompile::test_concrete_types(mock::Runtime, pallet_assets::Instance1)] +impl Erc20AssetsPrecompileSet +where + Instance: eip2612::InstanceToPrefix + 'static, + Runtime: pallet_assets::Config + pallet_evm::Config + frame_system::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime: AddressToAssetId>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait, + AssetIdOf: Display, + Runtime::AccountId: Into, +{ + /// PrecompileSet discriminant. Allows to knows if the address maps to an asset id, + /// and if this is the case which one. + #[precompile::discriminant] + fn discriminant(address: H160, gas: u64) -> DiscriminantResult> { + let extra_cost = RuntimeHelper::::db_read_gas_cost(); + if gas < extra_cost { + return DiscriminantResult::OutOfGas; + } + + let asset_id = match Runtime::address_to_asset_id(address) { + Some(asset_id) => asset_id, + None => return DiscriminantResult::None(extra_cost), + }; + + if pallet_assets::Pallet::::maybe_total_supply(asset_id.clone()) + .is_some() + { + DiscriminantResult::Some(asset_id, extra_cost) + } else { + DiscriminantResult::None(extra_cost) + } + } + + #[precompile::public("totalSupply()")] + #[precompile::view] + fn total_supply( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15) + handle.record_db_read::(187)?; + + Ok(pallet_assets::Pallet::::total_issuance(asset_id).into()) + } + + #[precompile::public("balanceOf(address)")] + #[precompile::view] + fn balance_of( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + who: Address, + ) -> EvmResult { + // Storage item: Account: + // Blake2_128(16) + AssetId(16) + Blake2_128(16) + AccountId(32) + AssetAccount(19 + Extra) + handle.record_db_read::( + 99 + >::Extra::max_encoded_len(), + )?; + + let who: H160 = who.into(); + + // Fetch info. + let amount: U256 = { + let who: Runtime::AccountId = Runtime::AddressMapping::into_account_id(who); + pallet_assets::Pallet::::balance(asset_id, &who).into() + }; + + // Build output. + Ok(amount) + } + + #[precompile::public("allowance(address,address)")] + #[precompile::view] + fn allowance( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + ) -> EvmResult { + // Storage item: Approvals: + // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(32)) + Approval(32) + handle.record_db_read::(148)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + // Fetch info. + let amount: U256 = { + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + + // Fetch info. + pallet_assets::Pallet::::allowance(asset_id, &owner, &spender).into() + }; + + // Build output. + Ok(amount) + } + + #[precompile::public("approve(address,uint256)")] + fn approve( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + spender: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let spender: H160 = spender.into(); + + Self::approve_inner(asset_id, handle, handle.context().caller, spender, value)?; + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + handle.context().caller, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + fn approve_inner( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: H160, + spender: H160, + value: U256, + ) -> EvmResult { + let owner = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + // Amount saturate if too high. + let amount: BalanceOf = + value.try_into().unwrap_or_else(|_| Bounded::max_value()); + + // Storage item: Approvals: + // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(32)) + Approval(32) + handle.record_db_read::(148)?; + + // If previous approval exists, we need to clean it + if pallet_assets::Pallet::::allowance(asset_id.clone(), &owner, &spender) != + 0u32.into() + { + RuntimeHelper::::try_dispatch( + handle, + Some(owner.clone()).into(), + pallet_assets::Call::::cancel_approval { + id: asset_id.clone().into(), + delegate: Runtime::Lookup::unlookup(spender.clone()), + }, + )?; + } + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(owner).into(), + pallet_assets::Call::::approve_transfer { + id: asset_id.into(), + delegate: Runtime::Lookup::unlookup(spender), + amount, + }, + )?; + + Ok(()) + } + + #[precompile::public("transfer(address,uint256)")] + fn transfer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + to: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let to: H160 = to.into(); + let value = Self::u256_to_amount(value).in_field("value")?; + + // Build call with origin. + { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let to = Runtime::AddressMapping::into_account_id(to); + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + pallet_assets::Call::::transfer { + id: asset_id.into(), + target: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + handle.context().caller, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("transferFrom(address,address,uint256)")] + fn transfer_from( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + from: Address, + to: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let from: H160 = from.into(); + let to: H160 = to.into(); + let value = Self::u256_to_amount(value).in_field("value")?; + + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from.clone()); + let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to); + + // If caller is "from", it can spend as much as it wants from its own balance. + if caller != from { + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(caller).into(), + pallet_assets::Call::::transfer_approved { + id: asset_id.into(), + owner: Runtime::Lookup::unlookup(from), + destination: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } else { + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(from).into(), + pallet_assets::Call::::transfer { + id: asset_id.into(), + target: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + from, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + #[precompile::public("name()")] + #[precompile::view] + fn name( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let name = pallet_assets::Pallet::::name(asset_id).as_slice().into(); + + Ok(name) + } + + #[precompile::public("symbol()")] + #[precompile::view] + fn symbol( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let symbol = pallet_assets::Pallet::::symbol(asset_id).as_slice().into(); + + Ok(symbol) + } + + #[precompile::public("decimals()")] + #[precompile::view] + fn decimals( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + Ok(pallet_assets::Pallet::::decimals(asset_id)) + } + + #[precompile::public("owner()")] + #[precompile::view] + fn owner( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15) + handle.record_db_read::(187)?; + + let owner: H160 = pallet_assets::Pallet::::owner(asset_id) + .ok_or(revert("No owner set"))? + .into(); + + Ok(Address(owner)) + } + + #[precompile::public("issuer()")] + #[precompile::view] + fn issuer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15) + handle.record_db_read::(187)?; + + let issuer: H160 = pallet_assets::Pallet::::issuer(asset_id) + .ok_or(revert("No issuer set"))? + .into(); + + Ok(Address(issuer)) + } + + #[precompile::public("admin()")] + #[precompile::view] + fn admin( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15) + handle.record_db_read::(187)?; + + let admin: H160 = pallet_assets::Pallet::::admin(asset_id) + .ok_or(revert("No admin set"))? + .into(); + + Ok(Address(admin)) + } + + #[precompile::public("freezer()")] + #[precompile::view] + fn freezer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15) + handle.record_db_read::(187)?; + + let freezer: H160 = pallet_assets::Pallet::::freezer(asset_id) + .ok_or(revert("No freezer set"))? + .into(); + + Ok(Address(freezer)) + } + + #[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")] + fn eip2612_permit( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + >::permit( + asset_id, handle, owner, spender, value, deadline, v, r, s, + ) + } + + #[precompile::public("nonces(address)")] + #[precompile::view] + fn eip2612_nonces( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + ) -> EvmResult { + >::nonces(asset_id, handle, owner) + } + + #[precompile::public("DOMAIN_SEPARATOR()")] + #[precompile::view] + fn eip2612_domain_separator( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + >::domain_separator(asset_id, handle) + } + + fn u256_to_amount(value: U256) -> MayRevert> { + value + .try_into() + .map_err(|_| RevertReason::value_is_too_large("balance type").into()) + } +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs b/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs new file mode 100644 index 00000000..f815c5bd --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs @@ -0,0 +1,292 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! Testing utilities. + +use super::*; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{AsEnsureOriginWithArg, Everything}, + weights::Weight, +}; + +use frame_system::{EnsureNever, EnsureRoot}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{ + mock_account, + precompile_set::*, + testing::{AddressInPrefixedSet, MockAccount}, +}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, ConstU32, IdentityLookup}, + BuildStorage, +}; + +pub type AccountId = MockAccount; +pub type AssetId = u128; +pub type Balance = u128; +pub type Block = frame_system::mocking::MockBlockU32; + +/// The foreign asset precompile address prefix. Addresses that match against this prefix will +/// be routed to Erc20AssetsPrecompileSet being marked as foreign +pub const FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX: u32 = 0xffffffff; + +parameter_types! { + pub ForeignAssetPrefix: &'static [u8] = &[0xff, 0xff, 0xff, 0xff]; +} + +mock_account!(ForeignAssetId(AssetId), |value: ForeignAssetId| { + AddressInPrefixedSet(FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX, value.0).into() +}); + +// Implement the trait, where we convert AccountId to AssetID +impl AccountIdAssetIdConversion for Runtime { + /// The way to convert an account to assetId is by ensuring that the prefix is 0XFFFFFFFF + /// and by taking the lowest 128 bits as the assetId + fn account_to_asset_id(account: AccountId) -> Option<(Vec, AssetId)> { + if account.has_prefix_u32(FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX) { + return Some(( + FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX.to_be_bytes().to_vec(), + account.without_prefix(), + )); + } + None + } + + // Not used for now + fn asset_id_to_account(_prefix: &[u8], asset_id: AssetId) -> AccountId { + ForeignAssetId(asset_id).into() + } +} + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileSetStartingWith< + ForeignAssetPrefix, + Erc20AssetsPrecompileSet, + >, + ), +>; + +pub type ForeignPCall = Erc20AssetsPrecompileSetCall; + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +/// Block Storage Limit in bytes. Set to 40KB. +const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; + pub GasLimitStorageGrowthRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) + }; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +type ForeignAssetInstance = pallet_assets::Instance1; + +// Required for runtime benchmarks +pallet_assets::runtime_benchmarks_enabled! { + pub struct BenchmarkHelper; + impl pallet_assets::BenchmarkHelper for BenchmarkHelper + where + AssetIdParameter: From, + { + fn create_asset_id_parameter(id: u32) -> AssetIdParameter { + (id as u128).into() + } + } +} + +// These parameters dont matter much as this will only be called by root with the forced arguments +// No deposit is substracted with those methods +parameter_types! { + pub const AssetDeposit: Balance = 0; + pub const ApprovalDeposit: Balance = 0; + pub const AssetsStringLimit: u32 = 50; + pub const MetadataDepositBase: Balance = 0; + pub const MetadataDepositPerByte: Balance = 0; + pub const AssetAccountDeposit: Balance = 0; +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type Currency = Balances; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = AssetsStringLimit; + type Freezer = (); + type Extra = (); + type AssetAccountDeposit = AssetAccountDeposit; + type WeightInfo = pallet_assets::weights::SubstrateWeight; + type RemoveItemsLimit = ConstU32<656>; + type AssetIdParameter = AssetId; + type CreateOrigin = AsEnsureOriginWithArg>; + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = BenchmarkHelper; + } +} + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + ForeignAssets: pallet_assets::, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/tests.rs b/vendor/moonbeam/precompiles/assets-erc20/src/tests.rs new file mode 100644 index 00000000..955605d5 --- /dev/null +++ b/vendor/moonbeam/precompiles/assets-erc20/src/tests.rs @@ -0,0 +1,1665 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use crate::{eip2612::Eip2612, mock::*, *}; +use frame_support::assert_ok; +use hex_literal::hex; +use libsecp256k1::{sign, Message, SecretKey}; +use precompile_utils::testing::*; +use sha3::{Digest, Keccak256}; +use sp_core::H256; +use std::str::from_utf8; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn selector_less_than_four_bytes() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + // This selector is only three bytes long when four are required. + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), vec![1u8, 2u8, 3u8]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn no_selector_exists_but_length_is_right() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), vec![1u8, 2u8, 3u8, 4u8]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +#[test] +fn selectors() { + assert!(ForeignPCall::balance_of_selectors().contains(&0x70a08231)); + assert!(ForeignPCall::total_supply_selectors().contains(&0x18160ddd)); + assert!(ForeignPCall::approve_selectors().contains(&0x095ea7b3)); + assert!(ForeignPCall::allowance_selectors().contains(&0xdd62ed3e)); + assert!(ForeignPCall::freezer_selectors().contains(&0x92716054)); + assert!(ForeignPCall::owner_selectors().contains(&0x8da5cb5b)); + assert!(ForeignPCall::issuer_selectors().contains(&0x1d143848)); + assert!(ForeignPCall::admin_selectors().contains(&0xf851a440)); + assert!(ForeignPCall::transfer_selectors().contains(&0xa9059cbb)); + assert!(ForeignPCall::transfer_from_selectors().contains(&0x23b872dd)); + assert!(ForeignPCall::name_selectors().contains(&0x06fdde03)); + assert!(ForeignPCall::symbol_selectors().contains(&0x95d89b41)); + assert!(ForeignPCall::decimals_selectors().contains(&0x313ce567)); + assert!(ForeignPCall::eip2612_nonces_selectors().contains(&0x7ecebe00)); + assert!(ForeignPCall::eip2612_permit_selectors().contains(&0xd505accf)); + assert!(ForeignPCall::eip2612_domain_separator_selectors().contains(&0x3644e515)); + + assert_eq!( + crate::SELECTOR_LOG_TRANSFER, + &Keccak256::digest(b"Transfer(address,address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_APPROVAL, + &Keccak256::digest(b"Approval(address,address,uint256)")[..] + ); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + let mut tester = + PrecompilesModifierTester::new(precompiles(), CryptoAlith, ForeignAssetId(0u128)); + + tester.test_view_modifier(ForeignPCall::balance_of_selectors()); + tester.test_view_modifier(ForeignPCall::total_supply_selectors()); + tester.test_default_modifier(ForeignPCall::approve_selectors()); + tester.test_view_modifier(ForeignPCall::allowance_selectors()); + tester.test_default_modifier(ForeignPCall::transfer_selectors()); + tester.test_default_modifier(ForeignPCall::transfer_from_selectors()); + tester.test_view_modifier(ForeignPCall::name_selectors()); + tester.test_view_modifier(ForeignPCall::symbol_selectors()); + tester.test_view_modifier(ForeignPCall::decimals_selectors()); + tester.test_view_modifier(ForeignPCall::eip2612_nonces_selectors()); + tester.test_default_modifier(ForeignPCall::eip2612_permit_selectors()); + tester.test_view_modifier(ForeignPCall::eip2612_domain_separator_selectors()); + }); +} + +#[test] +fn get_total_supply() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::total_supply {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_known_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_unknown_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn approve() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .expect_cost(31625756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + }); +} + +#[test] +fn approve_saturating() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: U256::MAX }, + ) + .expect_cost(31625756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::MAX), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0u64) + .expect_no_logs() + .execute_returns(U256::from(u128::MAX)); + }); +} + +#[test] +fn check_allowance_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u64)); + }); +} + +#[test] +fn check_allowance_not_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn transfer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { to: Address(Bob.into()), value: 400.into() }, + ) + .expect_cost(44795756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + }); +} + +#[test] +fn transfer_not_enough_founds() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { to: Address(Charlie.into()), value: 50.into() }, + ) + .execute_reverts(|output| { + from_utf8(&output).unwrap().contains("Dispatched call failed with error: ") && + from_utf8(&output).unwrap().contains("BalanceLow") + }); + }); +} + +#[test] +fn transfer_from() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + // TODO: Duplicate approve (noop)? + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Charlie.into()), + value: 400.into(), + }, + ) + .expect_cost(66146756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Charlie, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Charlie.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn transfer_from_non_incremental_approval() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + // We first approve 500 + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .expect_cost(31625756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + + // We then approve 300. Non-incremental, so this is + // the approved new value + // Additionally, the gas used in this approval is higher because we + // need to clear the previous one + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 300.into() }, + ) + .expect_cost(65259756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(300)), + )) + .execute_returns(true); + + // This should fail, as now the new approved quantity is 300 + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_reverts(|output| { + output + == b"Dispatched call failed with error: Module(ModuleError { index: 2, error: [10, 0, 0, 0], \ + message: Some(\"Unapproved\") })" + }); + }); +} + +#[test] +fn transfer_from_above_allowance() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 300.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .execute_reverts(|output| { + output + == b"Dispatched call failed with error: Module(ModuleError { index: 2, error: [10, 0, 0, 0], \ + message: Some(\"Unapproved\") })" + }); + }); +} + +#[test] +fn transfer_from_self() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, /* CryptoAlith sending transferFrom herself, no need for + * allowance. */ + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(44795756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn get_metadata() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::force_set_metadata( + RuntimeOrigin::root(), + 0u128, + b"TestToken".to_vec(), + b"Test".to_vec(), + 12, + false + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::name {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("TestToken")); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::symbol {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("Test")); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::decimals {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(12u8); + }); +} + +#[test] +fn permit_valid() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .expect_cost(31624000) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_valid_named_asset() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + assert_ok!(ForeignAssets::set_metadata( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + b"Test token".to_vec(), + b"TEST".to_vec(), + 18 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .expect_cost(31624000) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_invalid_nonce() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 1u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_signature() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: 0, + r: H256::random(), + s: H256::random(), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_deadline() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + pallet_timestamp::Pallet::::set_timestamp(10_000); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 5u8.into(); // deadline < timestamp => expired + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .execute_reverts(|output| output == b"Permit expired"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +// This test checks the validity of a metamask signed message against the permit precompile +// The code used to generate the signature is the following. +// You will need to import CryptoAlith_PRIV_KEY in metamask. +// If you put this code in the developer tools console, it will log the signature +/* +await window.ethereum.enable(); +const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + +const value = 1000; + +const fromAddress = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"; +const deadline = 1; +const nonce = 0; +const spender = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const from = accounts[0]; + +const createPermitMessageData = function () { + const message = { + owner: from, + spender: spender, + value: value, + nonce: nonce, + deadline: deadline, + }; + + const typedData = JSON.stringify({ + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name: "Unnamed XC20 #1", + version: "1", + chainId: 0, + verifyingContract: "0xffffffff00000000000000000000000000000001", + }, + message: message, + }); + + return { + typedData, + message, + }; +}; + +const method = "eth_signTypedData_v4" +const messageData = createPermitMessageData(); +const params = [from, messageData.typedData]; + +web3.currentProvider.sendAsync( + { + method, + params, + from, + }, + function (err, result) { + if (err) return console.dir(err); + if (result.error) { + alert(result.error.message); + } + if (result.error) return console.error('ERROR', result); + console.log('TYPED SIGNED:' + JSON.stringify(result.result)); + + const recovered = sigUtil.recoverTypedSignature_v4({ + data: JSON.parse(msgParams), + sig: result.result, + }); + + if ( + ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from) + ) { + alert('Successfully recovered signer as ' + from); + } else { + alert( + 'Failed to verify signer when comparing ' + result + ' to ' + from + ); + } + } +); +*/ + +#[test] +fn permit_valid_with_metamask_signed_data() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // assetId 1 + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 1u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 1u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 1000u16.into(); + let deadline: U256 = 1u16.into(); // todo: proper timestamp + + let rsv = hex!( + "3aac886f06729d76067b6b0dbae23978fe48224b10b5648265b8f0e8c4cf25ff7625965d64bf9a6069d + b00ef5771b65fd24dd118531fc6e86b61a238ca76b9a11c" + ) + .as_slice(); + let (r, sv) = rsv.split_at(32); + let (s, v) = sv.split_at(32); + let v_real = v[0]; + let r_real: [u8; 32] = r.try_into().unwrap(); + let s_real: [u8; 32] = s.try_into().unwrap(); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(1u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v_real, + r: H256::from(r_real), + s: H256::from(s_real), + }, + ) + .expect_cost(31624000) + .expect_log(log3( + ForeignAssetId(1u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(1000)), + )) + .execute_returns(()); + }); +} + +#[test] +fn transfer_amount_overflow() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { + to: Address(Bob.into()), + value: U256::from(u128::MAX) + 1, + }, + ) + .expect_cost(1756u64) // 1 weight => 1 gas in mock + .expect_no_logs() + .execute_reverts(|e| e == b"value: Value is too large for balance type"); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(Bob.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { who: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn transfer_from_overflow() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + // TODO: Duplicate approve of same value (noop?) + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { spender: Address(Bob.into()), value: 500.into() }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Charlie.into()), + value: U256::from(u128::MAX) + 1, + }, + ) + .expect_cost(1756u64) // 1 weight => 1 gas in mock + .expect_no_logs() + .execute_reverts(|e| e == b"value: Value is too large for balance type"); + }); +} + +#[test] +fn get_owner() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::transfer_ownership( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // owner + Bob.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::owner {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_issuer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + Bob.into(), + // admin + CryptoAlith.into(), + // freezer + CryptoAlith.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::issuer {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_admin() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + CryptoAlith.into(), + // admin + Bob.into(), + // freezer + CryptoAlith.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::admin {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_freezer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + CryptoAlith.into(), + // admin + CryptoAlith.into(), + // freezer + Bob.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::freezer {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces( + &["ERC20.sol", "Permit.sol"], + ForeignPCall::supports_selector, + ) +} diff --git a/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml index 45860e65..38c2db4f 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml +++ b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml @@ -20,7 +20,6 @@ parity-scale-codec = { version = "3.6", default-features = false, features = ["m sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } # Frontier fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } @@ -55,5 +54,4 @@ std = [ "sp-core/std", "sp-io/std", "sp-runtime/std", - "sp-std/std", ] diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs b/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs index 50d6d9a2..4cc64b07 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs +++ b/vendor/moonbeam/precompiles/balances-erc20/src/eip2612.rs @@ -15,6 +15,7 @@ // along with Moonbeam. If not, see . use super::*; +use alloc::vec::Vec; use frame_support::{ ensure, traits::{Get, Time}, @@ -22,7 +23,6 @@ use frame_support::{ use sp_core::H256; use sp_io::hashing::keccak_256; use sp_runtime::traits::UniqueSaturatedInto; -use sp_std::vec::Vec; /// EIP2612 permit typehash. pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs b/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs index f406b815..b0c09638 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs +++ b/vendor/moonbeam/precompiles/balances-erc20/src/lib.rs @@ -18,6 +18,9 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + +use core::marker::PhantomData; use fp_evm::PrecompileHandle; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo}, @@ -33,10 +36,6 @@ use pallet_balances::pallet::{ use pallet_evm::AddressMapping; use precompile_utils::prelude::*; use sp_core::{H160, H256, U256}; -use sp_std::{ - convert::{TryFrom, TryInto}, - marker::PhantomData, -}; mod eip2612; use eip2612::Eip2612; diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index b4b8b159..3feb5a96 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -32,7 +32,7 @@ use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ cosmos::{self, precompile::Precompiles}, - ethereum::{self, BabelPrecompiles, EnsureAddress}, + ethereum::{self, BabelPrecompiles, EnsureAddress, ASSET_PRECOMPILE_ADDRESS_PREFIX}, extensions::unify_account, }; use frame_support::{ @@ -70,7 +70,7 @@ use polkadot_runtime_common::{ xcm_sender::NoPriceForMessageDelivery, BlockHashCount, SlowAdjustingFeeUpdate, }; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{ConstU128, U256}; +use sp_core::{ConstU128, H160, U256}; use sp_runtime::{Perbill, Permill}; use sp_version::RuntimeVersion; use xcm::latest::prelude::BodyId; @@ -378,6 +378,26 @@ impl frame_babel::ethereum::Erc20Metadata for Runtime { } } +impl frame_babel::ethereum::AddressToAssetId for Runtime { + fn address_to_asset_id(address: H160) -> Option { + let mut data = [0u8; 4]; + let address_bytes: [u8; 20] = address.into(); + if ASSET_PRECOMPILE_ADDRESS_PREFIX.eq(&address_bytes[0..16]) { + data.copy_from_slice(&address_bytes[16..20]); + Some(AssetId::from_be_bytes(data)) + } else { + None + } + } + + fn asset_id_to_address(asset_id: AssetId) -> H160 { + let mut data = [0u8; 20]; + data[0..16].copy_from_slice(ASSET_PRECOMPILE_ADDRESS_PREFIX); + data[16..20].copy_from_slice(&asset_id.to_be_bytes()); + H160::from(data) + } +} + impl pallet_evm::Config for Runtime { type FeeCalculator = BaseFee; type GasWeightMapping = pallet_evm::FixedGasWeightMapping; From 8b45418f7470710887fc49a7eed10520d6e7f4ec Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 27 Sep 2024 23:48:26 +0900 Subject: [PATCH 101/133] feat: Add create asset with metadata and denom --- frame/babel/src/lib.rs | 61 +++++++++++++++++++++++++++-- ziggurat/runtime/src/configs/mod.rs | 28 +++++++------ ziggurat/runtime/src/lib.rs | 3 ++ 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 5e0ca6de..98aff400 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . #![cfg_attr(not(feature = "std"), no_std)] - +#![allow(clippy::too_many_arguments)] extern crate alloc; #[cfg(feature = "cosmos")] @@ -37,14 +37,22 @@ pub use pallet::*; pub mod pallet { use alloc::vec::Vec; use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; + use frame_system::{ensure_root, pallet_prelude::*}; + use pallet_cosmos::types::{AssetIdOf, DenomOf}; + use pallet_multimap::traits::UniqueMap; use sp_core::ecdsa; - use sp_runtime::traits::UniqueSaturatedInto; + use sp_runtime::traits::{StaticLookup, UniqueSaturatedInto}; + + type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; #[pallet::config] pub trait Config: - frame_system::Config + pallet_cosmos::Config + pallet_ethereum::Config + frame_system::Config + + pallet_assets::Config + + pallet_cosmos::Config::AssetId> + + pallet_ethereum::Config { + type AssetMap: UniqueMap, DenomOf>; } #[pallet::pallet] @@ -95,5 +103,50 @@ pub mod pallet { pallet_ethereum::Pallet::::transact(origin, transaction) } + + #[pallet::call_index(1)] + #[pallet::weight({ + use pallet_assets::weights::WeightInfo; + + ::WeightInfo::force_create() + .saturating_add(::WeightInfo::force_set_metadata(name.len() as u32, symbol.len() as u32)) + })] + pub fn force_create_asset( + origin: OriginFor, + id: T::AssetIdParameter, + name: Vec, + symbol: Vec, + denom: Vec, + decimals: u8, + is_frozon: bool, + is_sufficient: bool, + owner: AccountIdLookupOf, + #[pallet::compact] min_balance: ::Balance, + ) -> DispatchResult { + ensure_root(origin.clone())?; + + pallet_assets::Pallet::::force_create( + origin.clone(), + id.clone(), + owner, + is_sufficient, + min_balance, + )?; + pallet_assets::Pallet::::force_set_metadata( + origin, + id.clone(), + symbol, + name, + decimals, + is_frozon, + )?; + let id: ::AssetId = id.into(); + let denom: DenomOf = + denom.try_into().map_err(|_| DispatchError::Other("Too long denom"))?; + T::AssetMap::try_insert(id, denom) + .map_err(|_| DispatchError::Other("Failed to insert to asset map"))?; + + Ok(()) + } } } diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 3feb5a96..8d3a1c2f 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -78,12 +78,12 @@ use xcm::latest::prelude::BodyId; // Local module imports use super::{ weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, - AccountId, AddressMap, AssetId, Assets, Aura, Balance, Balances, BaseFee, Block, BlockNumber, - CollatorSelection, ConsensusHook, Hash, Instance1, MessageQueue, Nonce, PalletInfo, - ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, - RuntimeOrigin, RuntimeTask, Session, SessionKeys, System, Timestamp, WeightToFee, XcmpQueue, - AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, MAXIMUM_BLOCK_WEIGHT, MICROUNIT, - NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, + AccountId, AddressMap, AssetId, AssetMap, Assets, Aura, Balance, Balances, BaseFee, Block, + BlockNumber, CollatorSelection, ConsensusHook, Hash, Instance1, MessageQueue, Nonce, + PalletInfo, ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, System, Timestamp, + WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, + MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; use xcm_config::{RelayLocation, XcmOriginToTransactDispatchOrigin}; @@ -543,8 +543,8 @@ impl pallet_cosmwasm::Config for Runtime { type MaxCodeSize = ConstU32<{ 1024 * 1024 }>; type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; type MaxMessageSize = ConstU32<{ 64 * 1024 }>; - type AccountToAddr = cosmos::address::AccountToAddr; - type AssetToDenom = cosmos::asset::AssetToDenom; + type AccountToAddr = cosmos::address::AccountToAddr; + type AssetToDenom = cosmos::asset::AssetToDenom; type Balance = Balance; type AssetId = AssetId; type Assets = Assets; @@ -567,15 +567,19 @@ impl pallet_cosmwasm::Config for Runtime { type WasmCostRules = WasmCostRules; type UnixTime = Timestamp; - type WeightInfo = pallet_cosmwasm::weights::SubstrateWeight; + type WeightInfo = pallet_cosmwasm::weights::SubstrateWeight; - type PalletHook = Precompiles; + type PalletHook = Precompiles; - type UploadWasmOrigin = frame_system::EnsureSigned; + type UploadWasmOrigin = EnsureSigned; - type ExecuteWasmOrigin = frame_system::EnsureSigned; + type ExecuteWasmOrigin = EnsureSigned; type NativeDenom = NativeDenom; type NativeAssetId = NativeAssetId; } + +impl frame_babel::Config for Runtime { + type AssetMap = AssetMap; +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs index 02b5a7a6..ee824c8e 100644 --- a/ziggurat/runtime/src/lib.rs +++ b/ziggurat/runtime/src/lib.rs @@ -321,6 +321,9 @@ mod runtime { pub type Cosmos = pallet_cosmos; #[runtime::pallet_index(180)] pub type Cosmwasm = pallet_cosmwasm; + + #[runtime::pallet_index(255)] + pub type Babel = frame_babel; } #[docify::export(register_validate_block)] From d1a62a28e1c18c06afec63e0eebe47a2f794a450 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 28 Sep 2024 00:07:50 +0900 Subject: [PATCH 102/133] fix: Add EnsureSigned dependency --- ziggurat/runtime/src/configs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 8d3a1c2f..4ea49bb2 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -48,7 +48,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, + EnsureRoot, EnsureSigned, }; use pallet_assets::pallet::Instance2; use pallet_cosmos::{ From af9ea9d419d0f5275a9109d342523115fe8850e8 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 29 Sep 2024 00:07:02 +0900 Subject: [PATCH 103/133] feat: Add handle vm error --- frame/cosmos/x/wasm/src/error.rs | 42 +++++++++++++++++++++++++ frame/cosmos/x/wasm/src/lib.rs | 1 + frame/cosmos/x/wasm/src/msgs.rs | 22 +++---------- frame/cosmos/x/wasm/types/src/errors.rs | 3 ++ 4 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 frame/cosmos/x/wasm/src/error.rs diff --git a/frame/cosmos/x/wasm/src/error.rs b/frame/cosmos/x/wasm/src/error.rs new file mode 100644 index 00000000..0526dcff --- /dev/null +++ b/frame/cosmos/x/wasm/src/error.rs @@ -0,0 +1,42 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use pallet_cosmos_types::errors::{CosmosError, RootError}; +use pallet_cosmos_x_wasm_types::errors::WasmError; +use pallet_cosmwasm::runtimes::vm::CosmwasmVMError; + +pub fn handle_vm_error(e: CosmwasmVMError, default: E) -> CosmosError +where + T: pallet_cosmwasm::Config, + E: Into, +{ + log::debug!(target: "runtime::cosmos", "{:?}", e); + + match e { + CosmwasmVMError::OutOfGas => WasmError::GasLimit.into(), + CosmwasmVMError::ContractNotFound => WasmError::NoSuchContract.into(), + CosmwasmVMError::AccountConvert => RootError::InvalidAddress.into(), + CosmwasmVMError::NotImplemented | CosmwasmVMError::Unsupported => + RootError::NotSupported.into(), + CosmwasmVMError::AssetConversion => RootError::InvalidCoins.into(), + CosmwasmVMError::ExecuteDeserialize | + CosmwasmVMError::ExecuteSerialize | + CosmwasmVMError::QueryDeserialize | + CosmwasmVMError::QuerySerialize => WasmError::Invalid.into(), + _ => default.into(), + } +} diff --git a/frame/cosmos/x/wasm/src/lib.rs b/frame/cosmos/x/wasm/src/lib.rs index ca2d0f7c..6d12e987 100644 --- a/frame/cosmos/x/wasm/src/lib.rs +++ b/frame/cosmos/x/wasm/src/lib.rs @@ -19,4 +19,5 @@ extern crate alloc; +pub mod error; pub mod msgs; diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index a02bd652..b98ef89c 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::error::handle_vm_error; use alloc::{string::ToString, vec, vec::Vec}; use core::{marker::PhantomData, str::FromStr}; use core2::io::Read; @@ -69,7 +70,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgStoreCode { sender, wasm_byte_code, instantiate_permission: _ } = MsgStoreCode::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -167,10 +167,7 @@ where funds, message, ) - .map_err(|e| { - log::debug!("{:?}", e); - WasmError::InstantiateFailed - })?; + .map_err(|e| handle_vm_error(e, WasmError::InstantiateFailed))?; ctx.gas_meter() .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") .map_err(|_| RootError::OutOfGas)?; @@ -235,10 +232,7 @@ where funds, message, ) - .map_err(|e| { - log::debug!("{:?}", e); - WasmError::ExecuteFailed - })?; + .map_err(|e| handle_vm_error(e, WasmError::ExecuteFailed))?; ctx.gas_meter() .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") .map_err(|_| RootError::OutOfGas)?; @@ -313,10 +307,7 @@ where new_code_identifier, message, ) - .map_err(|e| { - log::debug!("{:?}", e); - WasmError::MigrationFailed - })?; + .map_err(|e| handle_vm_error(e, WasmError::MigrationFailed))?; ctx.gas_meter() .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") .map_err(|_| RootError::OutOfGas)?; @@ -383,10 +374,7 @@ where contract_account, new_admin_account, ) - .map_err(|e| { - log::debug!("{:?}", e); - RootError::Unauthorized - })?; + .map_err(|e| handle_vm_error(e, RootError::Unauthorized))?; ctx.gas_meter() .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") .map_err(|_| RootError::OutOfGas)?; diff --git a/frame/cosmos/x/wasm/types/src/errors.rs b/frame/cosmos/x/wasm/types/src/errors.rs index 0aa9a1cd..9803d34e 100644 --- a/frame/cosmos/x/wasm/types/src/errors.rs +++ b/frame/cosmos/x/wasm/types/src/errors.rs @@ -24,8 +24,11 @@ pub enum WasmError { CreateFailed = 2, InstantiateFailed = 4, ExecuteFailed = 5, + GasLimit = 6, MigrationFailed = 11, Empty = 12, + Invalid = 14, + NoSuchContract = 22, } impl From for CosmosError { From a808fb7be3c5b7fe496b35ab0e1cddd061363c32 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 29 Sep 2024 11:56:03 +0900 Subject: [PATCH 104/133] feat: Add consume gas to msg_send --- frame/babel/src/lib.rs | 2 +- frame/cosmos/src/lib.rs | 10 ++++----- frame/cosmos/src/weights.rs | 4 ++-- frame/cosmos/x/bank/src/gas.rs | 39 ++++++++++++++++++++++++++++++++++ frame/cosmos/x/bank/src/lib.rs | 13 +++++++++++- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 frame/cosmos/x/bank/src/gas.rs diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 98aff400..c3635c81 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -144,7 +144,7 @@ pub mod pallet { let denom: DenomOf = denom.try_into().map_err(|_| DispatchError::Other("Too long denom"))?; T::AssetMap::try_insert(id, denom) - .map_err(|_| DispatchError::Other("Failed to insert to asset map"))?; + .map_err(|_| DispatchError::Other("Failed to insert into asset map"))?; Ok(()) } diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 4b743ba1..424e5e6d 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -313,7 +313,7 @@ pub mod pallet { .ok() .and_then(|tx| tx.auth_info) .and_then(|auth_info| auth_info.fee) - .map_or(T::WeightInfo::default_weight(), |fee| { + .map_or(T::WeightInfo::base_weight(), |fee| { T::WeightToGas::convert(fee.gas_limit) }) })] @@ -322,7 +322,7 @@ pub mod pallet { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| { Error::::CosmosError(RootError::TxDecodeError.into()) - .with_weight(T::WeightInfo::default_weight()) + .with_weight(T::WeightInfo::base_weight()) })?; Self::apply_validated_transaction(tx) @@ -365,7 +365,7 @@ impl Pallet { pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { let gas_limit = tx.gas().ok_or( Error::::CosmosError(RootError::TxDecodeError.into()) - .with_weight(T::WeightInfo::default_weight()), + .with_weight(T::WeightInfo::base_weight()), )?; let mut ctx = T::Context::new(gas_limit); @@ -389,9 +389,9 @@ impl Pallet { } pub fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { - let base_gas = T::WeightToGas::convert(T::WeightInfo::default_weight()); + let base_gas = T::WeightToGas::convert(T::WeightInfo::base_weight()); ctx.gas_meter() - .consume_gas(base_gas, "base gas") + .consume_gas(base_gas, "base_gas") .map_err(|_| RootError::OutOfGas)?; let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; diff --git a/frame/cosmos/src/weights.rs b/frame/cosmos/src/weights.rs index a8b97b8e..43fde031 100644 --- a/frame/cosmos/src/weights.rs +++ b/frame/cosmos/src/weights.rs @@ -20,7 +20,7 @@ use frame_support::{dispatch::DispatchClass, weights::Weight}; use sp_core::Get; pub trait WeightInfo { - fn default_weight() -> Weight; + fn base_weight() -> Weight; } pub struct CosmosWeight(PhantomData); @@ -28,7 +28,7 @@ impl WeightInfo for CosmosWeight where T: frame_system::Config, { - fn default_weight() -> Weight { + fn base_weight() -> Weight { T::BlockWeights::get().get(DispatchClass::Normal).base_extrinsic } } diff --git a/frame/cosmos/x/bank/src/gas.rs b/frame/cosmos/x/bank/src/gas.rs new file mode 100644 index 00000000..00b6a24a --- /dev/null +++ b/frame/cosmos/x/bank/src/gas.rs @@ -0,0 +1,39 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; +use frame_support::weights::Weight; +use pallet_cosmos_types::gas::Gas; +use sp_core::Get; +use sp_runtime::traits::Convert; + +pub struct GasInfo(PhantomData); +impl GasInfo { + pub fn msg_send_native() -> Gas { + let weight = Weight::from_parts(61_290_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)); + T::WeightToGas::convert(weight) + } + + pub fn msg_send_asset() -> Gas { + let weight = Weight::from_parts(40_059_000, 6208) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)); + T::WeightToGas::convert(weight) + } +} diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index f9220233..b24c1f0d 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -19,6 +19,8 @@ extern crate alloc; +pub mod gas; + use alloc::vec; use core::marker::PhantomData; use cosmos_sdk_proto::{cosmos::bank::v1beta1::MsgSend, prost::Message, Any}; @@ -30,6 +32,7 @@ use frame_support::{ ExistenceRequirement, }, }; +use gas::GasInfo; use pallet_cosmos::AddressMapping; use pallet_cosmos_types::{ address::acc_address_from_bech32, @@ -40,6 +43,7 @@ use pallet_cosmos_types::{ traits::EventManager, CosmosEvent, EventAttribute, ATTRIBUTE_KEY_AMOUNT, ATTRIBUTE_KEY_SENDER, }, + gas::traits::GasMeter, msgservice::traits::MsgHandler, }; use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; @@ -60,7 +64,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgSend { from_address, to_address, amount } = MsgSend::decode(&mut &*msg.value).map_err(|_| RootError::UnpackAnyError)?; @@ -85,6 +88,10 @@ where ExistenceRequirement::KeepAlive, ) .map_err(|_| RootError::InsufficientFunds)?; + + ctx.gas_meter() + .consume_gas(GasInfo::::msg_send_native(), "msg_send_native") + .map_err(|_| RootError::OutOfGas)?; } else { let asset_id = T::AssetToDenom::convert(amt.denom.clone()) .map_err(|_| RootError::InvalidCoins)?; @@ -96,6 +103,10 @@ where Preservation::Preserve, ) .map_err(|_| RootError::InsufficientFunds)?; + + ctx.gas_meter() + .consume_gas(GasInfo::::msg_send_asset(), "msg_send_asset") + .map_err(|_| RootError::OutOfGas)?; } } From 4f60af1bb66c87bcc5384e527382ebe55b0eade0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 29 Sep 2024 14:46:51 +0900 Subject: [PATCH 105/133] feat: Add handling error to AssetToDenom --- frame/babel/src/cosmos/asset.rs | 25 ++++++++++++------------- frame/cosmos/src/lib.rs | 4 ++-- frame/cosmos/x/auth/src/fee.rs | 4 ++-- frame/cosmos/x/bank/src/lib.rs | 4 ++-- frame/cosmos/x/wasm/src/msgs.rs | 9 ++++++--- vendor/composable/cosmwasm/src/lib.rs | 14 +++++++------- vendor/composable/cosmwasm/src/utils.rs | 10 +++++----- 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/frame/babel/src/cosmos/asset.rs b/frame/babel/src/cosmos/asset.rs index 1b840540..3965b4e6 100644 --- a/frame/babel/src/cosmos/asset.rs +++ b/frame/babel/src/cosmos/asset.rs @@ -21,38 +21,37 @@ use core::marker::PhantomData; use pallet_cosmos::types::{AssetIdOf, DenomOf}; use pallet_multimap::traits::UniqueMap; use sp_core::Get; -use sp_runtime::traits::Convert; +use sp_runtime::traits::TryConvert; pub struct AssetToDenom(PhantomData<(T, I)>); -impl Convert, ()>> for AssetToDenom +impl TryConvert> for AssetToDenom where T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, { - fn convert(denom: String) -> Result, ()> { + fn try_convert(denom: String) -> Result, String> { if denom == T::NativeDenom::get() { Ok(T::NativeAssetId::get()) } else { - let denom: DenomOf = denom.as_bytes().to_vec().try_into().map_err(|_| ())?; - pallet_multimap::Pallet::::find_key(denom).ok_or(()) + let denom_raw: DenomOf = + denom.as_bytes().to_vec().try_into().map_err(|_| denom.clone())?; + pallet_multimap::Pallet::::find_key(denom_raw).ok_or(denom.clone()) } } } - -impl Convert, String> for AssetToDenom +impl TryConvert, String> for AssetToDenom where T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, { - fn convert(asset_id: AssetIdOf) -> String { + fn try_convert(asset_id: AssetIdOf) -> Result> { if asset_id == T::NativeAssetId::get() { - T::NativeDenom::get().to_string() + Ok(T::NativeDenom::get().to_string()) } else { - // TODO: Handle option let denom = as UniqueMap, DenomOf>>::get( - asset_id, + asset_id.clone(), ) - .unwrap(); - String::from_utf8(denom.into()).unwrap() + .ok_or(asset_id.clone())?; + String::from_utf8(denom.into()).map_err(|_| asset_id.clone()) } } } diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 424e5e6d..098c6bae 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -152,6 +152,7 @@ pub mod pallet { context::traits::MinGasPrices, errors::CosmosError, events::CosmosEvent, gas::Gas, }; use pallet_cosmos_x_auth_signing::sign_mode_handler::traits::SignModeHandler; + use sp_runtime::traits::{Convert, TryConvert}; #[pallet::pallet] pub struct Pallet(PhantomData); @@ -211,8 +212,7 @@ pub mod pallet { /// Converter between AssetId and Denom. #[pallet::no_default] - type AssetToDenom: Convert> - + Convert; + type AssetToDenom: TryConvert + TryConvert; /// Context for executing transactions. type Context: Context; diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs index cfcefa6e..80473c5a 100644 --- a/frame/cosmos/x/auth/src/fee.rs +++ b/frame/cosmos/x/auth/src/fee.rs @@ -41,7 +41,7 @@ use pallet_cosmos_types::{ use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use sp_core::{Get, H160}; use sp_runtime::{ - traits::{Convert, Zero}, + traits::{TryConvert, Zero}, SaturatedConversion, }; @@ -126,7 +126,7 @@ where // TODO: Resolve imbalance } else { - let asset_id = T::AssetToDenom::convert(amt.denom.clone()) + let asset_id = T::AssetToDenom::try_convert(amt.denom.clone()) .map_err(|_| RootError::InsufficientFunds)?; let _imbalance = T::Assets::withdraw( asset_id, diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index b24c1f0d..15da5e19 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -48,7 +48,7 @@ use pallet_cosmos_types::{ }; use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; use sp_core::{Get, H160}; -use sp_runtime::{traits::Convert, SaturatedConversion}; +use sp_runtime::{traits::TryConvert, SaturatedConversion}; pub struct MsgSendHandler(PhantomData); @@ -93,7 +93,7 @@ where .consume_gas(GasInfo::::msg_send_native(), "msg_send_native") .map_err(|_| RootError::OutOfGas)?; } else { - let asset_id = T::AssetToDenom::convert(amt.denom.clone()) + let asset_id = T::AssetToDenom::try_convert(amt.denom.clone()) .map_err(|_| RootError::InvalidCoins)?; T::Assets::transfer( asset_id, diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index b98ef89c..eb46ed66 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -54,7 +54,10 @@ use pallet_cosmwasm::{ }, }; use sp_core::H160; -use sp_runtime::{traits::Convert, SaturatedConversion}; +use sp_runtime::{ + traits::{Convert, TryConvert}, + SaturatedConversion, +}; pub struct MsgStoreCodeHandler(PhantomData); @@ -254,8 +257,8 @@ where fn convert_funds(coins: &[Coin]) -> Result, CosmosError> { let mut funds = FundsOf::::default(); for coin in coins.iter() { - let asset_id = - T::AssetToDenom::convert(coin.denom.clone()).map_err(|_| RootError::TxDecodeError)?; + let asset_id = T::AssetToDenom::try_convert(coin.denom.clone()) + .map_err(|_| RootError::TxDecodeError)?; let amount = u128::from_str(&coin.amount).map_err(|_| RootError::TxDecodeError)?; funds diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index b26cc153..d819f217 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -133,7 +133,7 @@ pub mod pallet { transactional, PalletId, Twox64Concat, }; use frame_system::{ensure_signed, pallet_prelude::OriginFor}; - use sp_runtime::traits::{Convert, MaybeDisplay}; + use sp_runtime::traits::{Convert, MaybeDisplay, TryConvert}; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -302,8 +302,7 @@ pub mod pallet { type AssetId: AssetId + Ord; /// A way to convert from our native currency to cosmwasm `Denom`. - type AssetToDenom: Convert, String> - + Convert, ()>>; + type AssetToDenom: TryConvert + TryConvert>; /// Interface used to pay when uploading code. type NativeAsset: ReservableCurrency, Balance = BalanceOf> @@ -682,10 +681,11 @@ impl Pallet { CosmwasmVMError, >, { - let cosmwasm_funds = funds - .into_iter() - .map(|(asset, (amount, _))| Self::native_asset_to_cosmwasm_asset(asset, amount)) - .collect::>(); + let mut cosmwasm_funds = Vec::::new(); + for (asset, (amount, _)) in funds.into_iter() { + let cosmwasm_fund = Self::native_asset_to_cosmwasm_asset(asset, amount)?; + cosmwasm_funds.push(cosmwasm_fund); + } Self::sub_level_dispatch(shared, sender, contract.clone(), cosmwasm_funds, call).map( |(data, events)| { diff --git a/vendor/composable/cosmwasm/src/utils.rs b/vendor/composable/cosmwasm/src/utils.rs index 82c1d6ff..fd8665f7 100644 --- a/vendor/composable/cosmwasm/src/utils.rs +++ b/vendor/composable/cosmwasm/src/utils.rs @@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec}; use cosmwasm_std::Coin; use cosmwasm_vm::system::CosmwasmContractMeta; use sp_core::storage::ChildInfo; -use sp_runtime::traits::{Convert, Hash}; +use sp_runtime::traits::{Convert, Hash, TryConvert}; use crate::{ runtimes::{ @@ -84,14 +84,14 @@ impl Pallet { pub(crate) fn native_asset_to_cosmwasm_asset( asset: AssetIdOf, amount: BalanceOf, - ) -> Coin { - let denom = T::AssetToDenom::convert(asset); - Coin { denom, amount: amount.into().into() } + ) -> Result> { + let denom = T::AssetToDenom::try_convert(asset).map_err(|_| Error::::AssetConversion)?; + Ok(Coin { denom, amount: amount.into().into() }) } /// Try to convert from a CosmWasm denom to a native [`AssetIdOf`]. pub(crate) fn cosmwasm_asset_to_native_asset(denom: String) -> Result, Error> { - T::AssetToDenom::convert(denom).map_err(|_| Error::::UnknownDenom) + T::AssetToDenom::try_convert(denom).map_err(|_| Error::::UnknownDenom) } /// Build a [`ChildInfo`] out of a contract trie id. From ac9c0bfa71d65b41abc36a5e9778bdc0f9387198 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 29 Sep 2024 16:01:58 +0900 Subject: [PATCH 106/133] refactor: Move AssetToDenom to runtime --- frame/babel/src/cosmos/asset.rs | 57 ----------------------------- frame/babel/src/cosmos/mod.rs | 1 - ziggurat/runtime/src/configs/mod.rs | 38 +++++++++++++++++-- 3 files changed, 34 insertions(+), 62 deletions(-) delete mode 100644 frame/babel/src/cosmos/asset.rs diff --git a/frame/babel/src/cosmos/asset.rs b/frame/babel/src/cosmos/asset.rs deleted file mode 100644 index 3965b4e6..00000000 --- a/frame/babel/src/cosmos/asset.rs +++ /dev/null @@ -1,57 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use alloc::string::{String, ToString}; -use core::marker::PhantomData; -use pallet_cosmos::types::{AssetIdOf, DenomOf}; -use pallet_multimap::traits::UniqueMap; -use sp_core::Get; -use sp_runtime::traits::TryConvert; - -pub struct AssetToDenom(PhantomData<(T, I)>); -impl TryConvert> for AssetToDenom -where - T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, -{ - fn try_convert(denom: String) -> Result, String> { - if denom == T::NativeDenom::get() { - Ok(T::NativeAssetId::get()) - } else { - let denom_raw: DenomOf = - denom.as_bytes().to_vec().try_into().map_err(|_| denom.clone())?; - pallet_multimap::Pallet::::find_key(denom_raw).ok_or(denom.clone()) - } - } -} -impl TryConvert, String> for AssetToDenom -where - T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, -{ - fn try_convert(asset_id: AssetIdOf) -> Result> { - if asset_id == T::NativeAssetId::get() { - Ok(T::NativeDenom::get().to_string()) - } else { - let denom = - as UniqueMap, DenomOf>>::get( - asset_id.clone(), - ) - .ok_or(asset_id.clone())?; - String::from_utf8(denom.into()).map_err(|_| asset_id.clone()) - } - } -} diff --git a/frame/babel/src/cosmos/mod.rs b/frame/babel/src/cosmos/mod.rs index 9143a891..dd9b90fe 100644 --- a/frame/babel/src/cosmos/mod.rs +++ b/frame/babel/src/cosmos/mod.rs @@ -17,6 +17,5 @@ // along with this program. If not, see . pub mod address; -pub mod asset; pub mod msg; pub mod precompile; diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 4ea49bb2..6d862dd0 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -26,7 +26,11 @@ mod xcm_config; // Substrate and Polkadot dependencies -use alloc::{string::ToString, vec, vec::Vec}; +use alloc::{ + string::{String, ToString}, + vec, + vec::Vec, +}; use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; @@ -64,6 +68,7 @@ use pallet_cosmos_x_auth_signing::{ }; use pallet_cosmwasm::instrument::CostRules; use pallet_ethereum::PostLogContent; +use pallet_multimap::traits::UniqueMap; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use polkadot_runtime_common::{ @@ -71,7 +76,7 @@ use polkadot_runtime_common::{ }; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{ConstU128, H160, U256}; -use sp_runtime::{Perbill, Permill}; +use sp_runtime::{traits::TryConvert, BoundedVec, Perbill, Permill}; use sp_version::RuntimeVersion; use xcm::latest::prelude::BodyId; @@ -494,6 +499,31 @@ impl context::traits::MinGasPrices for MinGasPrices { } } +pub struct AssetToDenom; +impl TryConvert for AssetToDenom { + fn try_convert(denom: String) -> Result { + if denom == NativeDenom::get() { + Ok(NativeAssetId::get()) + } else { + let denom_raw: BoundedVec = + denom.as_bytes().to_vec().try_into().map_err(|_| denom.clone())?; + AssetMap::find_key(denom_raw).ok_or(denom.clone()) + } + } +} +impl TryConvert for AssetToDenom { + fn try_convert(asset_id: AssetId) -> Result { + if asset_id == NativeAssetId::get() { + Ok(NativeDenom::get().to_string()) + } else { + let denom = + >>::get(asset_id) + .ok_or(asset_id)?; + String::from_utf8(denom.into()).map_err(|_| asset_id) + } + } +} + impl pallet_cosmos::Config for Runtime { type AddressMapping = cosmos::address::AddressMapping; type Balance = Balance; @@ -506,7 +536,7 @@ impl pallet_cosmos::Config for Runtime { type WeightInfo = pallet_cosmos::weights::CosmosWeight; type WeightToGas = WeightToGas; type MinGasPrices = MinGasPrices; - type AssetToDenom = cosmos::asset::AssetToDenom; + type AssetToDenom = AssetToDenom; type Context = context::Context; type ChainInfo = np_cosmos::traits::CosmosHub; type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; @@ -544,7 +574,7 @@ impl pallet_cosmwasm::Config for Runtime { type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; type MaxMessageSize = ConstU32<{ 64 * 1024 }>; type AccountToAddr = cosmos::address::AccountToAddr; - type AssetToDenom = cosmos::asset::AssetToDenom; + type AssetToDenom = AssetToDenom; type Balance = Balance; type AssetId = AssetId; type Assets = Assets; From 436483298704d464b0f613bca67100592300c8ee Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 29 Sep 2024 23:30:33 +0900 Subject: [PATCH 107/133] fix: Fix query cosmos balances --- sidecar/src/services/balance.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index d556e933..790f25b8 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -5,6 +5,7 @@ import { IConfig } from "config"; import { QueryAllBalancesResponse } from "cosmjs-types/cosmos/bank/v1beta1/query.js"; import Long from "long"; import { AccountInfo } from "@polkadot/types/interfaces"; +import { encodeTo } from "../utils"; export class BalanceService implements ApiService { config: IConfig; @@ -41,26 +42,33 @@ export class BalanceService implements ApiService { const assets = []; const metadata = await this.chainApi.query.assets.metadata.entries(); for (const [{ args: [assetId] }, value] of metadata) { - const asset = await this.chainApi.query.assets.account(assetId.toString(), origin) + const asset = await this.chainApi.query.assets.account(assetId.toString(), origin); - if (asset) { - const denom = value.toHuman()['symbol']; - const amount = asset.toJSON() ? BigInt(asset.toJSON()['balance']).toString() : '0'; - assets.push({ denom, amount }); + if (!asset.isEmpty) { + const assetDenom = await this.chainApi.query.assetMap.map(assetId); + + if (!assetDenom.isEmpty) { + const denomSet = assetDenom.toJSON(); + const denom = encodeTo(denomSet[0].toString(), 'hex', 'utf8'); + const amount = BigInt(asset.toJSON()['balance']).toString(); + + console.debug(`denom: ${denom}, amount: ${amount}`); + + assets.push({ denom, amount }); + } } } - console.debug([ + const balances = [ nativeBalance, ...assets, - ]); + ]; + + console.debug(`balances: ${JSON.stringify(balances)}`); return { - balances: [ - nativeBalance, - ...assets, - ], + balances, pagination: { nextKey: new Uint8Array(), total: Long.ZERO, From e42b32fe75b6d283525f448a07c760fc8b30d515 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 30 Sep 2024 00:38:13 +0900 Subject: [PATCH 108/133] fix: Preconsume gas before transfer asset --- frame/cosmos/x/bank/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index 15da5e19..9640d74d 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -81,6 +81,10 @@ where let amount = amt.amount.parse::().map_err(|_| RootError::InvalidCoins)?; if T::NativeDenom::get() == amt.denom { + ctx.gas_meter() + .consume_gas(GasInfo::::msg_send_native(), "msg_send_native") + .map_err(|_| RootError::OutOfGas)?; + T::NativeAsset::transfer( &from_account, &to_account, @@ -88,11 +92,11 @@ where ExistenceRequirement::KeepAlive, ) .map_err(|_| RootError::InsufficientFunds)?; - + } else { ctx.gas_meter() - .consume_gas(GasInfo::::msg_send_native(), "msg_send_native") + .consume_gas(GasInfo::::msg_send_asset(), "msg_send_asset") .map_err(|_| RootError::OutOfGas)?; - } else { + let asset_id = T::AssetToDenom::try_convert(amt.denom.clone()) .map_err(|_| RootError::InvalidCoins)?; T::Assets::transfer( @@ -103,10 +107,6 @@ where Preservation::Preserve, ) .map_err(|_| RootError::InsufficientFunds)?; - - ctx.gas_meter() - .consume_gas(GasInfo::::msg_send_asset(), "msg_send_asset") - .map_err(|_| RootError::OutOfGas)?; } } From 8997c7d7e857d84f3e2c58a7024ca9d4a9180830 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 30 Sep 2024 02:19:41 +0900 Subject: [PATCH 109/133] feat: Add abci query for balances --- frame/cosmos/src/lib.rs | 5 +++ sidecar/src/app.ts | 2 +- sidecar/src/services/abci.ts | 53 ++++++++++++++++++++++++++-- sidecar/src/services/balance.ts | 54 +++++++++++++++++++++++++---- ziggurat/runtime/src/apis.rs | 25 ++++++------- ziggurat/runtime/src/configs/mod.rs | 5 +-- 6 files changed, 119 insertions(+), 25 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 098c6bae..9a2749cf 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -247,6 +247,10 @@ pub mod pallet { /// The maximum length of a denomination for an asset. #[pallet::constant] type MaxDenomLimit: Get; + + /// The gas limit for simulation. + #[pallet::constant] + type SimulationGasLimit: Get; } pub mod config_preludes { @@ -296,6 +300,7 @@ pub mod pallet { pub const TxSigLimit: u64 = 7; pub const MaxDenomLimit: u32 = 128; pub const NativeAssetId: u32 = 0; + pub const SimulationGasLimit: u64 = 1000000000; } } diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index cfc216f8..2b7b6e58 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -129,12 +129,12 @@ export class App { const accountService = new AccountService(this.chainApi); const txService = new TxService(this.db, this.chainApi); - const abciService = new AbciService(this.chainApi, accountService, txService); const balanceService = new BalanceService( this.config, this.chainApi, accountService ); + const abciService = new AbciService(this.chainApi, accountService, balanceService, txService); const distributionService = new DistributionService(); const nodeInfoService = new NodeInfoService(this.config); const stakingService = new StakingService(); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index 3f5de677..9dd2c07c 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -14,20 +14,25 @@ import { TxService } from "./tx"; import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'cosmjs-types/cosmwasm/wasm/v1/query.js' import { convertToCodespace } from "../constants/codespace"; import { encodeTo } from "../utils"; +import { BalanceService } from "./balance"; +import { QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; export class AbciService implements ApiService { chainApi: ApiPromise; accountService: AccountService; + balanceService: BalanceService; txService: TxService; - constructor(chainApi: ApiPromise, accountService: AccountService, txService: TxService) { + constructor(chainApi: ApiPromise, accountService: AccountService, balanceService: BalanceService, txService: TxService) { this.chainApi = chainApi; this.accountService = accountService; + this.balanceService = balanceService; this.txService = txService; } async query(path: string, data: string): Promise { console.debug('query'); + console.debug(`path: ${path}`); if (path === '/cosmos.auth.v1beta1.Query/Account') { const address = QueryAccountRequest.decode( @@ -51,13 +56,55 @@ export class AbciService implements ApiService { sequence: Long.fromNumber(parseInt(account.sequence)), }; - const queryAccountResponse: QueryAccountResponse = { + const response: QueryAccountResponse = { account: { typeUrl: '/cosmos.auth.v1beta1.BaseAccount', value: BaseAccount.encode(baseAccount).finish(), }, }; - const value = QueryAccountResponse.encode(queryAccountResponse).finish(); + const value = QueryAccountResponse.encode(response).finish(); + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value, + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: '', + }; + } else if (path === '/cosmos.bank.v1beta1.Query/AllBalances') { + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + + const { address } = QueryAllBalancesRequest.decode( + Buffer.from(data, 'hex') + ); + + const response = await this.balanceService.balances(address, blockHash.toString()); + const value = QueryAllBalancesResponse.encode(response).finish(); + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value, + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: '', + }; + } else if (path === '/cosmos.bank.v1beta1.Query/Balance') { + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + + const { address, denom } = QueryBalanceRequest.decode( + Buffer.from(data, 'hex') + ); + + const response = await this.balanceService.balance(address, denom, blockHash.toString()); + const value = QueryBalanceResponse.encode(response).finish(); return { code: 0, log: '', diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 790f25b8..fb01c5dd 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -2,7 +2,7 @@ import { ApiPromise } from "@polkadot/api"; import { AccountService } from "./account"; import { ApiService } from "./service"; import { IConfig } from "config"; -import { QueryAllBalancesResponse } from "cosmjs-types/cosmos/bank/v1beta1/query.js"; +import { QueryAllBalancesResponse, QueryBalanceResponse } from "cosmjs-types/cosmos/bank/v1beta1/query.js"; import Long from "long"; import { AccountInfo } from "@polkadot/types/interfaces"; import { encodeTo } from "../utils"; @@ -22,7 +22,48 @@ export class BalanceService implements ApiService { this.accountService = accountService; } - public async balances(address: string): Promise { + public async balance(address: string, denom: string, blockHash?: string): Promise { + console.debug('balance'); + + const originRaw = await this.accountService.origin(address); + let amount = '0'; + let origin = originRaw.toString(); + if (!origin) { + origin = this.accountService.interim(address); + } + + const nativeDenom = this.config.get('chain.denom'); + if (nativeDenom === denom) { + const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); + if (account) { + const { data } = account.toJSON() as unknown as AccountInfo; + amount = BigInt(data.free.toString()).toString(); + return { + balance: { denom, amount } + } + } else { + return { + balance: { denom, amount: '0' } + } + } + } else { + const assetId = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assetMap.index(denom); + const asset = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.account(assetId.toString(), origin); + if (!asset.isEmpty) { + const amount = BigInt(asset.toJSON()['balance']).toString(); + console.debug(`denom: ${denom}, amount: ${amount}`); + return { + balance: { denom, amount } + } + } else { + return { + balance: { denom, amount: '0' } + } + } + } + } + + public async balances(address: string, blockHash?: string): Promise { console.debug('balances'); const originRaw = await this.accountService.origin(address); @@ -31,7 +72,7 @@ export class BalanceService implements ApiService { if (!origin) { origin = this.accountService.interim(address); } - const account = await this.chainApi.query.system.account(origin); + const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); if (account) { const { data } = account.toJSON() as unknown as AccountInfo; amount = BigInt(data.free.toString()).toString(); @@ -40,13 +81,12 @@ export class BalanceService implements ApiService { const nativeBalance = { denom, amount }; const assets = []; - const metadata = await this.chainApi.query.assets.metadata.entries(); + const metadata = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.metadata.entries(); for (const [{ args: [assetId] }, value] of metadata) { - const asset = await this.chainApi.query.assets.account(assetId.toString(), origin); - + const asset = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.account(assetId.toString(), origin); if (!asset.isEmpty) { - const assetDenom = await this.chainApi.query.assetMap.map(assetId); + const assetDenom = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assetMap.map(assetId); if (!assetDenom.isEmpty) { const denomSet = assetDenom.toJSON(); diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index c4658515..d9d14a65 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -34,7 +34,10 @@ use frame_support::{ weights::Weight, }; use pallet_aura::Authorities; -use pallet_cosmos_types::{context::traits::Context, handler::AnteDecorator, tx_msgs::FeeTx}; +use pallet_cosmos_types::{ + context::traits::Context, events::traits::EventManager, gas::traits::GasMeter, + handler::AnteDecorator, +}; use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use sp_api::impl_runtime_apis; @@ -50,7 +53,7 @@ use sp_version::RuntimeVersion; // Local module imports use super::{ AccountId, Balance, Block, ConsensusHook, Ethereum, Executive, InherentDataExt, Nonce, - ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeGenesisConfig, SessionKeys, System, + ParachainSystem, Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, UncheckedExtrinsic, SLOT_DURATION, VERSION, }; @@ -577,19 +580,17 @@ impl_runtime_apis! { ::AnteHandler::ante_handle(&tx, true) .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; - let gas_limit = tx.gas().ok_or(SimulateError::InternalError("Empty gas limit".into()))?; - let mut context = ::Context::new(gas_limit); + let mut context = ::Context::new(::SimulationGasLimit::get()); pallet_cosmos::Pallet::::run_tx(&mut context, &tx) .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; - System::read_events_no_consensus() - .find_map(|record| { - if let RuntimeEvent::Cosmos(pallet_cosmos::Event::Executed { gas_wanted, gas_used, events }) = record.event { - Some(SimulateResponse{gas_info: GasInfo { gas_wanted, gas_used }, events}) - } else { - None - } - }).ok_or(SimulateError::InternalError("Cosmos events does not exist".into())) + Ok(SimulateResponse { + gas_info: GasInfo { + gas_wanted: 0, + gas_used: context.gas_meter().consumed_gas() + }, + events: context.event_manager().events() + }) } } diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 6d862dd0..98e5f23f 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -57,8 +57,8 @@ use frame_system::{ use pallet_assets::pallet::Instance2; use pallet_cosmos::{ config_preludes::{ - MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, TxSigLimit, - WeightToGas, + MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, + SimulationGasLimit, TxSigLimit, WeightToGas, }, types::{AssetIdOf, DenomOf}, }; @@ -547,6 +547,7 @@ impl pallet_cosmos::Config for Runtime { type MaxMemoCharacters = MaxMemoCharacters; type TxSigLimit = TxSigLimit; type MaxDenomLimit = MaxDenomLimit; + type SimulationGasLimit = SimulationGasLimit; } parameter_types! { From 121579b093e35e055faca6ee3ee2cb786a799476 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 30 Sep 2024 11:02:14 +0900 Subject: [PATCH 110/133] feat: Add cosmos_transact to frame_babel --- frame/babel/Cargo.toml | 3 +++ frame/babel/src/lib.rs | 50 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 4da169e7..b13c6ea2 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -28,6 +28,7 @@ pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = " pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } pallet-cosmos-types = { workspace = true, default-features = false, optional = true } +pallet-cosmos-x-auth-signing = { workspace = true, default-features = false, optional = true } pallet-cosmos-x-bank = { workspace = true, default-features = false, optional = true } pallet-cosmos-x-wasm = { workspace = true, default-features = false, optional = true } pallet-cosmwasm = { workspace = true, default-features = false, optional = true } @@ -78,6 +79,7 @@ std = [ "pallet-balances/std", "pallet-cosmos?/std", "pallet-cosmos-types?/std", + "pallet-cosmos-x-auth-signing?/std", "pallet-cosmos-x-bank?/std", "pallet-cosmos-x-wasm?/std", "pallet-cosmwasm?/std", @@ -109,6 +111,7 @@ cosmos = [ "pallet-assets", "pallet-cosmos", "pallet-cosmos-types", + "pallet-cosmos-x-auth-signing", "pallet-cosmos-x-bank", "pallet-cosmos-x-wasm", "pallet-cosmwasm", diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index c3635c81..c3b62bfd 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -36,9 +36,12 @@ pub use pallet::*; #[frame_support::pallet] pub mod pallet { use alloc::vec::Vec; + use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; use frame_support::pallet_prelude::*; use frame_system::{ensure_root, pallet_prelude::*}; use pallet_cosmos::types::{AssetIdOf, DenomOf}; + use pallet_cosmos_types::address::acc_address_from_bech32; + use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use pallet_multimap::traits::UniqueMap; use sp_core::ecdsa; use sp_runtime::traits::{StaticLookup, UniqueSaturatedInto}; @@ -67,9 +70,10 @@ pub mod pallet { #[pallet::call] impl Pallet where - OriginFor: Into>>, + OriginFor: Into>> + + Into>>, T::AccountId: TryInto, - T::RuntimeOrigin: From, + T::RuntimeOrigin: From + From, { #[pallet::call_index(0)] #[pallet::weight({ @@ -105,6 +109,48 @@ pub mod pallet { } #[pallet::call_index(1)] + #[pallet::weight({ + use cosmos_sdk_proto::traits::Message; + use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; + use pallet_cosmos::weights::WeightInfo; + use sp_runtime::traits::Convert; + + Tx::decode(&mut &tx_bytes[..]) + .ok() + .and_then(|tx| tx.auth_info) + .and_then(|auth_info| auth_info.fee) + .map_or(::WeightInfo::base_weight(), |fee| { + ::WeightToGas::convert(fee.gas_limit) + }) + })] + pub fn cosmos_transact( + origin: OriginFor, + tx_bytes: Vec, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let public: ecdsa::Public = who.try_into().map_err(|_| Error::::InvalidOrigin)?; + let address: np_babel::CosmosAddress = public.into(); + + let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| Error::::InvalidTransaction)?; + let signers = + T::SigVerifiableTx::get_signers(&tx).map_err(|_| Error::::InvalidTransaction)?; + ensure!(signers.len() == 1, Error::::InvalidTransaction); + + let signer = signers.first().ok_or(Error::::InvalidTransaction)?; + let (_hrp, address_raw) = + acc_address_from_bech32(signer).map_err(|_| Error::::InvalidTransaction)?; + ensure!( + address_raw.len() == 20 && address.to_vec() == address_raw, + Error::::InvalidTransaction + ); + + let origin = + T::RuntimeOrigin::from(pallet_cosmos::RawOrigin::CosmosTransaction(address.into())); + + pallet_cosmos::Pallet::::transact(origin, tx_bytes) + } + + #[pallet::call_index(2)] #[pallet::weight({ use pallet_assets::weights::WeightInfo; From 0acd6da28bf9622701977fa46a26bf3edcab9a2e Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 1 Oct 2024 21:19:18 +0900 Subject: [PATCH 111/133] feat: Add transfer balances call for multi-type addresses --- frame/babel/src/lib.rs | 48 ++++++++++++++++++++++++++--- primitives/babel/src/lib.rs | 2 +- ziggurat/runtime/src/configs/mod.rs | 1 + 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index c3b62bfd..1493633b 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -35,16 +35,27 @@ pub use pallet::*; #[cfg(feature = "pallet")] #[frame_support::pallet] pub mod pallet { + use super::Address; use alloc::vec::Vec; use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; - use frame_support::pallet_prelude::*; + use frame_support::{ + pallet_prelude::*, + traits::{fungible::Mutate, tokens::Preservation::Preserve}, + }; use frame_system::{ensure_root, pallet_prelude::*}; - use pallet_cosmos::types::{AssetIdOf, DenomOf}; + use pallet_cosmos::{ + types::{AssetIdOf, DenomOf}, + AddressMapping as _, + }; use pallet_cosmos_types::address::acc_address_from_bech32; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; - use pallet_multimap::traits::UniqueMap; + use pallet_evm::AddressMapping as _; + use pallet_multimap::traits::{UniqueMap, UniqueMultimap}; use sp_core::ecdsa; - use sp_runtime::traits::{StaticLookup, UniqueSaturatedInto}; + use sp_runtime::{ + traits::{StaticLookup, UniqueSaturatedInto}, + AccountId32, + }; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; @@ -52,9 +63,12 @@ pub mod pallet { pub trait Config: frame_system::Config + pallet_assets::Config + + pallet_balances::Config + pallet_cosmos::Config::AssetId> + pallet_ethereum::Config + + pallet_evm::Config { + type AddressMap: UniqueMultimap; type AssetMap: UniqueMap, DenomOf>; } @@ -72,7 +86,7 @@ pub mod pallet { where OriginFor: Into>> + Into>>, - T::AccountId: TryInto, + T::AccountId: TryInto + From, T::RuntimeOrigin: From + From, { #[pallet::call_index(0)] @@ -194,5 +208,29 @@ pub mod pallet { Ok(()) } + + #[pallet::call_index(3)] + #[pallet::weight({ + use pallet_balances::weights::WeightInfo; + + ::WeightInfo::transfer_keep_alive() + })] + pub fn transfer( + origin: OriginFor, + dest: Address, + #[pallet::compact] value: ::Balance, + ) -> DispatchResult { + let source = ensure_signed(origin.clone())?; + + let dest: T::AccountId = match dest { + Address::Cosmos(address) => + ::AddressMapping::into_account_id(address.into()), + Address::Ethereum(address) => + ::AddressMapping::into_account_id(address.into()), + Address::Polkadot(address) => address.into(), + }; + + pallet_balances::Pallet::::transfer(&source, &dest, value, Preserve).map(|_| ()) + } } } diff --git a/primitives/babel/src/lib.rs b/primitives/babel/src/lib.rs index f1c2d528..04e35333 100644 --- a/primitives/babel/src/lib.rs +++ b/primitives/babel/src/lib.rs @@ -35,7 +35,7 @@ pub use np_ethereum as ethereum; pub use np_ethereum::Address as EthereumAddress; pub use sp_core::crypto::AccountId32; -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Address { Polkadot(AccountId32), diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 98e5f23f..31535abc 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -612,5 +612,6 @@ impl pallet_cosmwasm::Config for Runtime { } impl frame_babel::Config for Runtime { + type AddressMap = AddressMap; type AssetMap = AssetMap; } From b8d8a5e7de99a87fadb591005df0a7e0e3ce77ec Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 2 Oct 2024 13:41:44 +0900 Subject: [PATCH 112/133] feat: Block asset transfer to non-unified accounts --- .editorconfig | 1 + frame/cosmos/types/src/errors.rs | 1 + frame/cosmos/x/bank/Cargo.toml | 3 +++ frame/cosmos/x/bank/src/lib.rs | 9 ++++++++- primitives/babel/Cargo.toml | 4 ++++ vendor/moonbeam/precompiles/assets-erc20/src/lib.rs | 10 +++++++++- 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index b29741a6..6d62b52f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,7 @@ max_line_length=100 insert_final_newline=true [*.toml] +indent_style=space max_line_length=off [*.md] diff --git a/frame/cosmos/types/src/errors.rs b/frame/cosmos/types/src/errors.rs index 07b35d0c..0bf55881 100644 --- a/frame/cosmos/types/src/errors.rs +++ b/frame/cosmos/types/src/errors.rs @@ -42,6 +42,7 @@ pub enum RootError { UnknownRequest = 6, InvalidAddress = 7, InvalidPubKey = 8, + UnknownAddress = 9, InvalidCoins = 10, OutOfGas = 11, MemoTooLarge = 12, diff --git a/frame/cosmos/x/bank/Cargo.toml b/frame/cosmos/x/bank/Cargo.toml index ffdb5661..3d8f098a 100644 --- a/frame/cosmos/x/bank/Cargo.toml +++ b/frame/cosmos/x/bank/Cargo.toml @@ -10,6 +10,7 @@ publish = false [dependencies] cosmos-sdk-proto = { version = "0.24", default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } pallet-cosmos-x-bank-types = { workspace = true, default-features = false } @@ -21,8 +22,10 @@ default = ["std"] std = [ "cosmos-sdk-proto/std", "frame-support/std", + "frame-system/std", "pallet-cosmos/std", "pallet-cosmos-types/std", + "pallet-cosmos-x-bank-types/std", "sp-core/std", "sp-runtime/std", ] diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index 9640d74d..14e71c14 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -48,7 +48,10 @@ use pallet_cosmos_types::{ }; use pallet_cosmos_x_bank_types::events::{ATTRIBUTE_KEY_RECIPIENT, EVENT_TYPE_TRANSFER}; use sp_core::{Get, H160}; -use sp_runtime::{traits::TryConvert, SaturatedConversion}; +use sp_runtime::{ + traits::{TryConvert, Zero}, + SaturatedConversion, +}; pub struct MsgSendHandler(PhantomData); @@ -97,6 +100,10 @@ where .consume_gas(GasInfo::::msg_send_asset(), "msg_send_asset") .map_err(|_| RootError::OutOfGas)?; + // XXX: Need a general way to handle non-unified account + if frame_system::Account::::get(&to_account).nonce.is_zero() { + return Err(RootError::UnknownAddress.into()); + } let asset_id = T::AssetToDenom::try_convert(amt.denom.clone()) .map_err(|_| RootError::InvalidCoins)?; T::Assets::transfer( diff --git a/primitives/babel/Cargo.toml b/primitives/babel/Cargo.toml index 716bf5a2..e28d7780 100644 --- a/primitives/babel/Cargo.toml +++ b/primitives/babel/Cargo.toml @@ -20,12 +20,16 @@ default = ["std", "cosmos", "ethereum"] std = [ "np-cosmos?/std", "np-ethereum?/std", + "parity-scale-codec/std", + "scale-info/std", "serde/std", + "sp-core/std", ] serde = [ "dep:serde", "np-cosmos?/serde", "np-ethereum?/serde", + "sp-core/serde", ] cosmos = [ "np-cosmos", diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs index 8f5b1669..7522cabf 100644 --- a/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs +++ b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs @@ -35,7 +35,7 @@ use frame_support::{ //use moonkit_xcm_primitives::AccountIdAssetIdConversion; use pallet_evm::AddressMapping; use precompile_utils::prelude::*; -use sp_runtime::traits::{Bounded, Dispatchable}; +use sp_runtime::traits::{Bounded, Dispatchable, Zero}; use sp_core::{MaxEncodedLen, H160, H256, U256}; @@ -245,6 +245,10 @@ where ) -> EvmResult { let owner = Runtime::AddressMapping::into_account_id(owner); let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + // XXX: Need a general way to handle non-unified account + if frame_system::Account::::get(&spender).nonce.is_zero() { + return Err(revert("spender not unified")); + } // Amount saturate if too high. let amount: BalanceOf = value.try_into().unwrap_or_else(|_| Bounded::max_value()); @@ -296,6 +300,10 @@ where { let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let to = Runtime::AddressMapping::into_account_id(to); + // XXX: Need a general way to handle non-unified account + if frame_system::Account::::get(&to).nonce.is_zero() { + return Err(revert("to not unified")); + } // Dispatch call (if enough gas). RuntimeHelper::::try_dispatch( From 1b662546d748898c0c839139bef7cdf7a43cd6d6 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 2 Oct 2024 13:53:20 +0900 Subject: [PATCH 113/133] chore: Remove unused dependency --- vendor/moonbeam/precompiles/assets-erc20/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml index 61568122..639fb9f6 100644 --- a/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml +++ b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml @@ -21,7 +21,6 @@ scale-info = { version = "2.11", default-features = false, features = ["derive"] sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } # Frontier fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } @@ -59,5 +58,4 @@ std = [ "sp-core/std", "sp-io/std", "sp-runtime/std", - "sp-std/std", ] From c0d14fcea469f82435d95d320b309963a648bd70 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 2 Oct 2024 22:27:41 +0900 Subject: [PATCH 114/133] feat: Block asset transfer to non-unified accounts in cosmwasm --- vendor/composable/cosmwasm/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index d819f217..ec38d32f 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -102,7 +102,7 @@ use frame_support::{ ReversibleStorageHasher, StorageHasher, }; use np_cosmos::traits::ChainInfo; -use sp_runtime::traits::SaturatedConversion; +use sp_runtime::traits::{SaturatedConversion, Zero}; use wasmi::AsContext; use wasmi_validation::PlainValidator; @@ -1308,6 +1308,12 @@ impl Pallet { .map_err(|_| Error::::TransferFailed)?; } else { let asset = Self::cosmwasm_asset_to_native_asset(denom.clone())?; + // XXX: Need a general way to handle non-unified account + if frame_system::Account::::get(&to).nonce.is_zero() && + Self::contract_exists(&to).is_err() + { + return Err(Error::::InvalidAccount); + } T::Assets::transfer(asset, from, to, amount, preservation) .map_err(|_| Error::::TransferFailed)?; } From 42bcf0307cf508ee05132bacb897ad231eeeaa2e Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 2 Oct 2024 23:09:58 +0900 Subject: [PATCH 115/133] fix: Fix type name conflict with Address --- frame/babel/src/cosmos/address.rs | 8 ++++---- frame/babel/src/ethereum/mod.rs | 2 +- frame/babel/src/extensions/unify_account.rs | 8 ++++---- frame/babel/src/lib.rs | 14 +++++++------- primitives/babel/src/lib.rs | 4 ++-- ziggurat/node/src/chain_spec.rs | 9 ++++++--- ziggurat/runtime/src/configs/mod.rs | 7 ++++--- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/frame/babel/src/cosmos/address.rs b/frame/babel/src/cosmos/address.rs index 15ea43d1..615f0a6a 100644 --- a/frame/babel/src/cosmos/address.rs +++ b/frame/babel/src/cosmos/address.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{extensions::unify_account, Address}; +use crate::{extensions::unify_account, VarAddress}; use alloc::{string::String, vec::Vec}; use bech32::{Bech32, Hrp}; use core::marker::PhantomData; @@ -34,7 +34,7 @@ where { fn into_account_id(who: H160) -> T::AccountId { let address = CosmosAddress::from(who); - T::AddressMap::find_key(Address::Cosmos(address.clone())) + T::AddressMap::find_key(VarAddress::Cosmos(address.clone())) .unwrap_or_else(|| address.into_account_truncating()) } } @@ -47,7 +47,7 @@ where fn convert(account: AccountIdOf) -> String { let addresses = T::AddressMap::get(account.clone()); let address: Option<&CosmosAddress> = addresses.iter().find_map(|address| match address { - Address::Cosmos(address) => Some(address), + VarAddress::Cosmos(address) => Some(address), _ => None, }); let address_raw = match address { @@ -77,7 +77,7 @@ where match address.len() { 20 => { let address = CosmosAddress::from(H160::from_slice(&address)); - T::AddressMap::find_key(Address::Cosmos(address)).ok_or(()) + T::AddressMap::find_key(VarAddress::Cosmos(address)).ok_or(()) }, 32 => Ok(H256::from_slice(&address).into()), _ => Err(()), diff --git a/frame/babel/src/ethereum/mod.rs b/frame/babel/src/ethereum/mod.rs index 4b8f96c0..050d320d 100644 --- a/frame/babel/src/ethereum/mod.rs +++ b/frame/babel/src/ethereum/mod.rs @@ -71,7 +71,7 @@ where { fn into_account_id(who: H160) -> T::AccountId { let address = EthereumAddress::from(who); - T::AddressMap::find_key(Address::Ethereum(address.clone())) + T::AddressMap::find_key(VarAddress::Ethereum(address.clone())) .unwrap_or_else(|| address.into_account_truncating()) } } diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index 119757fe..be12dc3d 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -18,7 +18,7 @@ use core::marker::PhantomData; use frame_support::traits::tokens::{fungible, Fortitude, Preservation}; -use np_babel::{Address, CosmosAddress, EthereumAddress}; +use np_babel::{CosmosAddress, EthereumAddress, VarAddress}; use pallet_multimap::traits::UniqueMultimap; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -31,7 +31,7 @@ use sp_runtime::{ /// A configuration for UnifyAccount signed extension. pub trait Config: frame_system::Config + TryInto> { /// A map from account to addresses. - type AddressMap: UniqueMultimap; + type AddressMap: UniqueMultimap; /// Drain account balance when unifying accounts. type DrainBalance: DrainBalance; } @@ -63,7 +63,7 @@ impl UnifyAccount { let address = EthereumAddress::from(public.clone()); let interim = address.clone().into_account_truncating(); T::DrainBalance::drain_balance(&interim, who)?; - T::AddressMap::try_insert(who.clone(), Address::Ethereum(address)) + T::AddressMap::try_insert(who.clone(), VarAddress::Ethereum(address)) .map_err(|_| "account unification failed: ethereum")?; } #[cfg(feature = "cosmos")] @@ -71,7 +71,7 @@ impl UnifyAccount { let address = CosmosAddress::from(public); let interim = address.clone().into_account_truncating(); T::DrainBalance::drain_balance(&interim, who)?; - T::AddressMap::try_insert(who.clone(), Address::Cosmos(address)) + T::AddressMap::try_insert(who.clone(), VarAddress::Cosmos(address)) .map_err(|_| "account unification failed: cosmos")?; } } diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 1493633b..2be1adfa 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -27,7 +27,7 @@ pub mod ethereum; pub mod extensions; pub use extensions::unify_account::UnifyAccount; -pub use np_babel::Address; +pub use np_babel::VarAddress; #[cfg(feature = "pallet")] pub use pallet::*; @@ -35,7 +35,7 @@ pub use pallet::*; #[cfg(feature = "pallet")] #[frame_support::pallet] pub mod pallet { - use super::Address; + use super::VarAddress; use alloc::vec::Vec; use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; use frame_support::{ @@ -68,7 +68,7 @@ pub mod pallet { + pallet_ethereum::Config + pallet_evm::Config { - type AddressMap: UniqueMultimap; + type AddressMap: UniqueMultimap; type AssetMap: UniqueMap, DenomOf>; } @@ -217,17 +217,17 @@ pub mod pallet { })] pub fn transfer( origin: OriginFor, - dest: Address, + dest: VarAddress, #[pallet::compact] value: ::Balance, ) -> DispatchResult { let source = ensure_signed(origin.clone())?; let dest: T::AccountId = match dest { - Address::Cosmos(address) => + VarAddress::Cosmos(address) => ::AddressMapping::into_account_id(address.into()), - Address::Ethereum(address) => + VarAddress::Ethereum(address) => ::AddressMapping::into_account_id(address.into()), - Address::Polkadot(address) => address.into(), + VarAddress::Polkadot(address) => address.into(), }; pallet_balances::Pallet::::transfer(&source, &dest, value, Preserve).map(|_| ()) diff --git a/primitives/babel/src/lib.rs b/primitives/babel/src/lib.rs index 04e35333..597514fd 100644 --- a/primitives/babel/src/lib.rs +++ b/primitives/babel/src/lib.rs @@ -37,7 +37,7 @@ pub use sp_core::crypto::AccountId32; #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Address { +pub enum VarAddress { Polkadot(AccountId32), #[cfg(feature = "cosmos")] Cosmos(CosmosAddress), @@ -45,7 +45,7 @@ pub enum Address { Ethereum(EthereumAddress), } -impl Address { +impl VarAddress { pub const fn variant_count() -> u32 { let mut n = 1; if cfg!(feature = "cosmos") { diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs index 80a914ea..5f2c69f8 100644 --- a/ziggurat/node/src/chain_spec.rs +++ b/ziggurat/node/src/chain_spec.rs @@ -1,5 +1,5 @@ use cumulus_primitives_core::ParaId; -use frame_babel::Address; +use frame_babel::VarAddress; use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT, UNIT}; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; @@ -212,9 +212,12 @@ fn testnet_genesis( }) } -fn address_map(account: ecdsa::Public) -> (AccountId, Vec
) { +fn address_map(account: ecdsa::Public) -> (AccountId, Vec) { ( account.clone().into(), - vec![Address::Cosmos(account.clone().into()), Address::Ethereum(account.clone().into())], + vec![ + VarAddress::Cosmos(account.clone().into()), + VarAddress::Ethereum(account.clone().into()), + ], ) } diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 31535abc..5a1d0903 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -38,6 +38,7 @@ use frame_babel::{ cosmos::{self, precompile::Precompiles}, ethereum::{self, BabelPrecompiles, EnsureAddress, ASSET_PRECOMPILE_ADDRESS_PREFIX}, extensions::unify_account, + VarAddress, }; use frame_support::{ derive_impl, @@ -337,8 +338,8 @@ impl pallet_collator_selection::Config for Runtime { #[derive_impl(pallet_multimap::config_preludes::TestDefaultConfig)] impl pallet_multimap::Config for Runtime { type Key = AccountId; - type Value = frame_babel::Address; - type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; + type Value = VarAddress; + type CapacityPerKey = ConstU32<{ VarAddress::variant_count() }>; } impl unify_account::Config for Runtime { @@ -466,7 +467,7 @@ impl pallet_base_fee::Config for Runtime { impl pallet_multimap::Config for Runtime { type Key = AssetIdOf; type Value = DenomOf; - type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() - 1 }>; + type CapacityPerKey = ConstU32<1>; type KeyHasher = Blake2_128Concat; type ValueHasher = Blake2_128Concat; } From 5163356200ca0451711d7ad526aa0ce3b60781c0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 2 Oct 2024 23:38:32 +0900 Subject: [PATCH 116/133] chore: Replace chain_id for CosmosHub to ziggurat --- primitives/cosmos/src/traits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/cosmos/src/traits.rs b/primitives/cosmos/src/traits.rs index 8f54e740..4246b177 100644 --- a/primitives/cosmos/src/traits.rs +++ b/primitives/cosmos/src/traits.rs @@ -29,6 +29,6 @@ impl ChainInfo for CosmosHub { "cosmos" } fn chain_id() -> &'static str { - "dev" + "ziggurat" } } From aefbed01cab8fd4be66c3ce864f69413a598c7b0 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Thu, 3 Oct 2024 00:52:09 +0900 Subject: [PATCH 117/133] style: Fix inconsistent Cargo.toml --- .editorconfig | 1 - frame/babel/Cargo.toml | 16 ++++----------- frame/cosmos/Cargo.toml | 30 +++++++++++++---------------- frame/cosmos/runtime-api/Cargo.toml | 8 ++------ frame/cosmos/types/Cargo.toml | 15 ++++----------- frame/cosmos/x/auth/Cargo.toml | 3 +-- frame/cosmos/x/wasm/Cargo.toml | 4 +--- ziggurat/runtime/Cargo.toml | 12 +++--------- 8 files changed, 28 insertions(+), 61 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6d62b52f..b29741a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ max_line_length=100 insert_final_newline=true [*.toml] -indent_style=space max_line_length=off [*.md] diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index b13c6ea2..b424b463 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -8,9 +8,7 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -bech32 = { version = "0.11", default-features = false, features = [ - "alloc", -], optional = true } +bech32 = { version = "0.11", default-features = false, features = ["alloc"], optional = true } cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true } cosmwasm-std = { workspace = true, default-features = false, optional = true } cosmwasm-vm = { workspace = true, default-features = false, optional = true } @@ -41,16 +39,10 @@ pallet-evm-precompile-bn128 = { git = "https://github.com/polkadot-evm/frontier" pallet-evm-precompile-modexp = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-simple = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-multimap = { workspace = true, default-features = false } -parity-scale-codec = { version = "3.6", default-features = false, features = [ - "derive", -] } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } -scale-info = { version = "2.11", default-features = false, features = [ - "derive", -] } -serde = { version = "1.0.210", default-features = false, features = [ - "derive", -], optional = true } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } +serde = { version = "1.0.210", default-features = false, features = ["derive"], optional = true } serde-json-wasm = { version = "1.0.1", default-features = false, optional = true } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } diff --git a/frame/cosmos/Cargo.toml b/frame/cosmos/Cargo.toml index b726eaaf..4b55de46 100644 --- a/frame/cosmos/Cargo.toml +++ b/frame/cosmos/Cargo.toml @@ -14,29 +14,25 @@ frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "s pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } pallet-multimap = { workspace = true, default-features = false } -parity-scale-codec = { version = "3.6", default-features = false, features = [ - "derive", -] } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } np-cosmos = { workspace = true, default-features = false } -scale-info = { version = "2.11", default-features = false, features = [ - "derive", -] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [features] default = ["std"] std = [ - "cosmos-sdk-proto/std", - "frame-support/std", - "frame-system/std", - "pallet-cosmos-x-auth-signing/std", - "pallet-cosmos-types/std", - "pallet-multimap/std", - "parity-scale-codec/std", - "np-cosmos/std", - "scale-info/std", - "sp-core/std", - "sp-runtime/std", + "cosmos-sdk-proto/std", + "frame-support/std", + "frame-system/std", + "pallet-cosmos-x-auth-signing/std", + "pallet-cosmos-types/std", + "pallet-multimap/std", + "parity-scale-codec/std", + "np-cosmos/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", ] try-runtime = [] diff --git a/frame/cosmos/runtime-api/Cargo.toml b/frame/cosmos/runtime-api/Cargo.toml index ff1bca3e..9ea81da2 100644 --- a/frame/cosmos/runtime-api/Cargo.toml +++ b/frame/cosmos/runtime-api/Cargo.toml @@ -9,12 +9,8 @@ publish = false [dependencies] pallet-cosmos-types = { workspace = true, default-features = false } -parity-scale-codec = { version = "3.6", default-features = false, features = [ - "derive", -] } -scale-info = { version = "2.11", default-features = false, features = [ - "derive", -] } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } serde = { version = "1.0.210", default-features = false, features = ["derive"] } sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml index 06b22cc5..04f7b691 100644 --- a/frame/cosmos/types/Cargo.toml +++ b/frame/cosmos/types/Cargo.toml @@ -9,22 +9,16 @@ publish = false [dependencies] bech32 = { version = "0.11", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ - "cosmwasm", -] } +cosmos-sdk-proto = { version = "0.24", default-features = false, features = ["cosmwasm"] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } impl-trait-for-tuples = { version = "0.2.2" } -parity-scale-codec = { version = "3.6", default-features = false, features = [ - "derive", -] } -scale-info = { version = "2.11", default-features = false, features = [ - "derive", -] } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } serde = { version = "1.0.210", default-features = false, features = ["derive"] } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [dev-dependencies] -hex = { version = "0.4.3", default-features = false } +hex = "0.4" [features] default = ["std"] @@ -36,5 +30,4 @@ std = [ "scale-info/std", "serde/std", "sp-runtime/std", - "hex/std", ] diff --git a/frame/cosmos/x/auth/Cargo.toml b/frame/cosmos/x/auth/Cargo.toml index 49501715..07efe37e 100644 --- a/frame/cosmos/x/auth/Cargo.toml +++ b/frame/cosmos/x/auth/Cargo.toml @@ -21,7 +21,7 @@ sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable24 sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [dev-dependencies] -hex = { version = "0.4.3", default-features = false } +hex = "0.4" [features] default = ["std"] @@ -37,5 +37,4 @@ std = [ "sp-core/std", "sp-io/std", "sp-runtime/std", - "hex/std", ] diff --git a/frame/cosmos/x/wasm/Cargo.toml b/frame/cosmos/x/wasm/Cargo.toml index 4df16b4d..7b692be5 100644 --- a/frame/cosmos/x/wasm/Cargo.toml +++ b/frame/cosmos/x/wasm/Cargo.toml @@ -9,9 +9,7 @@ publish = false [dependencies] core2 = { version = "0.4.0", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ - "cosmwasm", -] } +cosmos-sdk-proto = { version = "0.24", default-features = false, features = ["cosmwasm"] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } hex = { version = "0.4.3", default-features = false } libflate = { version = "2.1.0", default-features = false } diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index e2315a32..5565a7ff 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -9,15 +9,11 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = [ - "derive", -] } +codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = ["derive"] } docify = "0.2" hex-literal = { version = "0.4", optional = true } log = { version = "0.4", default-features = false } -scale-info = { version = "2.11", default-features = false, features = [ - "derive", -] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } smallvec = "1.13" frame-babel = { workspace = true, features = ["pallet"] } @@ -35,9 +31,7 @@ pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = [ - "experimental", -] } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = ["experimental"] } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } From 2c756e6e325282beb632ac5998e00f124e4f1b8e Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 01:54:53 +0900 Subject: [PATCH 118/133] feat: Add chain_info rpc --- frame/cosmos/rpc/src/cosmos.rs | 10 +++++++++- frame/cosmos/runtime-api/src/lib.rs | 11 ++++++++++- primitives/cosmos/src/traits.rs | 8 ++++++++ sidecar/config/default.json | 4 ---- sidecar/src/app.ts | 12 +++++++++--- sidecar/src/constants/rpc.ts | 7 ++++++- sidecar/src/services/nodeinfo.ts | 17 +++++++++-------- sidecar/src/services/status.ts | 10 +++++----- vendor/composable/cosmwasm/src/lib.rs | 4 ++-- ziggurat/runtime/src/apis.rs | 23 +++++++++++++++++++++-- 10 files changed, 79 insertions(+), 27 deletions(-) diff --git a/frame/cosmos/rpc/src/cosmos.rs b/frame/cosmos/rpc/src/cosmos.rs index 18f144eb..08a354de 100644 --- a/frame/cosmos/rpc/src/cosmos.rs +++ b/frame/cosmos/rpc/src/cosmos.rs @@ -16,7 +16,7 @@ // limitations under the License. use crate::{internal_error, request_error}; -use cosmos_runtime_api::{CosmosRuntimeApi, SimulateError, SimulateResponse}; +use cosmos_runtime_api::{ChainInfo, CosmosRuntimeApi, SimulateError, SimulateResponse}; use futures::future::TryFutureExt; use jsonrpsee::{ core::{async_trait, RpcResult}, @@ -38,6 +38,9 @@ pub trait CosmosApi { #[method(name = "cosmos_simulate")] async fn simulate(&self, tx_bytes: Bytes, at: Option) -> RpcResult; + + #[method(name = "cosmos_chainInfo")] + async fn chain_info(&self) -> RpcResult; } pub struct Cosmos { @@ -91,4 +94,9 @@ where SimulateError::InternalError(e) => internal_error(String::from_utf8_lossy(&e)), }) } + + async fn chain_info(&self) -> RpcResult { + let best_hash = self.client.info().best_hash; + self.client.runtime_api().chain_info(best_hash).map_err(internal_error) + } } diff --git a/frame/cosmos/runtime-api/src/lib.rs b/frame/cosmos/runtime-api/src/lib.rs index b461934c..2a94dfc0 100644 --- a/frame/cosmos/runtime-api/src/lib.rs +++ b/frame/cosmos/runtime-api/src/lib.rs @@ -19,7 +19,7 @@ extern crate alloc; -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use pallet_cosmos_types::{events::CosmosEvent, gas::Gas}; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -47,9 +47,18 @@ pub enum SimulateError { pub type SimulateResult = Result; +#[derive(Clone, Decode, Encode, Debug, TypeInfo, Serialize, Deserialize)] +pub struct ChainInfo { + pub chain_id: String, + pub bech32_prefix: String, + pub name: String, + pub version: String, +} + decl_runtime_apis! { pub trait CosmosRuntimeApi { fn convert_tx(tx_bytes: Vec) -> ::Extrinsic; fn simulate(tx_bytes: Vec) -> SimulateResult; + fn chain_info() -> ChainInfo; } } diff --git a/primitives/cosmos/src/traits.rs b/primitives/cosmos/src/traits.rs index 4246b177..65af838a 100644 --- a/primitives/cosmos/src/traits.rs +++ b/primitives/cosmos/src/traits.rs @@ -19,6 +19,8 @@ pub trait ChainInfo { fn bech32_prefix() -> &'static str; fn chain_id() -> &'static str; + fn name() -> &'static str; + fn version() -> &'static str; } /// Cosmos Hub chain information. @@ -29,6 +31,12 @@ impl ChainInfo for CosmosHub { "cosmos" } fn chain_id() -> &'static str { + "ziggurat-1" + } + fn name() -> &'static str { "ziggurat" } + fn version() -> &'static str { + "1" + } } diff --git a/sidecar/config/default.json b/sidecar/config/default.json index 6c147880..b19b3933 100644 --- a/sidecar/config/default.json +++ b/sidecar/config/default.json @@ -9,10 +9,6 @@ }, "chain": { "endpoint": "ws://127.0.0.1:9944", - "network": "dev", - "moniker": "cosmos", - "name": "noir", - "version": "1", "denom": "azig" } } diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 2b7b6e58..63869ec8 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -117,6 +117,12 @@ export class App { SimulateResponse: { gas_info: 'GasInfo', events: 'Vec', + }, + ChainInfo: { + chain_id: 'String', + name: 'String', + bech32_prefix: 'String', + version: 'String', } }, rpc @@ -136,7 +142,7 @@ export class App { ); const abciService = new AbciService(this.chainApi, accountService, balanceService, txService); const distributionService = new DistributionService(); - const nodeInfoService = new NodeInfoService(this.config); + const nodeInfoService = new NodeInfoService(this.config, this.chainApi); const stakingService = new StakingService(); const statusService = new StatusService(this.config, this.chainApi); @@ -194,9 +200,9 @@ export class App { this.server.get( '/cosmos/base/tendermint/v1beta1/node_info', - (): GetNodeInfoResponse => { + async (): Promise => { const response = GetNodeInfoResponse.toJSON( - this.services.get('nodeInfo').nodeInfo() + await this.services.get('nodeInfo').nodeInfo() ); return toSnakeCase(response); } diff --git a/sidecar/src/constants/rpc.ts b/sidecar/src/constants/rpc.ts index 727972e2..95094b13 100644 --- a/sidecar/src/constants/rpc.ts +++ b/sidecar/src/constants/rpc.ts @@ -24,10 +24,15 @@ const rpc = { ], type: 'SimulateResponse', }, + chainInfo: { + description: 'Get Cosmos chain information.', + params: [], + type: 'ChainInfo', + } }, cosmwasm: { query: { - description: 'Query Cosmwasm state', + description: 'Query Cosmwasm state.', params: [ { name: 'contract', diff --git a/sidecar/src/services/nodeinfo.ts b/sidecar/src/services/nodeinfo.ts index cea9030b..63718317 100644 --- a/sidecar/src/services/nodeinfo.ts +++ b/sidecar/src/services/nodeinfo.ts @@ -2,20 +2,21 @@ import { GetNodeInfoResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1 import { ApiService } from "./service"; import { IConfig } from "config"; import Long from "long"; +import { ApiPromise } from "@polkadot/api"; export class NodeInfoService implements ApiService { config: IConfig; + chainApi: ApiPromise; - constructor(config: IConfig) { + constructor(config: IConfig, chainApi: ApiPromise) { this.config = config; + this.chainApi = chainApi; } - public nodeInfo(): GetNodeInfoResponse { + public async nodeInfo(): Promise { + const { chain_id, name, bech32_prefix, version } = (await this.chainApi.rpc['cosmos']['chainInfo']()).toJSON(); const endpoint = this.config.get('server.endpoint'); - const network = this.config.get('chain.network'); - const version = this.config.get('chain.version'); - const moniker = this.config.get('chain.moniker'); - const name = this.config.get('chain.name'); + return { defaultNodeInfo: { protocolVersion: { @@ -25,10 +26,10 @@ export class NodeInfoService implements ApiService { }, defaultNodeId: '0000000000000000000000000000000000000000', listenAddr: endpoint, - network, + network: chain_id, version, channels: new Uint8Array(Buffer.allocUnsafe(8)), - moniker, + moniker: bech32_prefix, other: { txIndex: 'off', rpcAddress: '', diff --git a/sidecar/src/services/status.ts b/sidecar/src/services/status.ts index 01d81ae6..111d1f06 100644 --- a/sidecar/src/services/status.ts +++ b/sidecar/src/services/status.ts @@ -24,10 +24,10 @@ export class StatusService implements ApiService { const blockTime = new Date(parseInt(timestamp)).toISOString(); const blockHash = hash.startsWith('0x') ? hash.slice(2) : hash; + const { chain_id, bech32_prefix, version } = (await this.chainApi.rpc['cosmos']['chainInfo']()).toJSON(); + const endpoint = this.config.get('server.endpoint'); - const network = this.config.get('chain.network'); - const version = this.config.get('chain.version'); - const moniker = this.config.get('chain.moniker'); + return { node_info: { protocol_version: { @@ -37,10 +37,10 @@ export class StatusService implements ApiService { }, id: '0000000000000000000000000000000000000000', listen_addr: endpoint, - network, + network: chain_id, version, channels: '0000000000000000', - moniker, + moniker: bech32_prefix, other: { tx_index: 'off', rpc_address: endpoint, diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index ec38d32f..489ac2c9 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -1309,8 +1309,8 @@ impl Pallet { } else { let asset = Self::cosmwasm_asset_to_native_asset(denom.clone())?; // XXX: Need a general way to handle non-unified account - if frame_system::Account::::get(&to).nonce.is_zero() && - Self::contract_exists(&to).is_err() + if frame_system::Account::::get(to).nonce.is_zero() && + Self::contract_exists(to).is_err() { return Err(Error::::InvalidAccount); } diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index d9d14a65..5c4e1547 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -24,15 +24,20 @@ // For more information, please refer to // External crates imports -use alloc::{format, string::String, vec::Vec}; +use alloc::{ + format, + string::{String, ToString}, + vec::Vec, +}; use codec::Encode; -use cosmos_runtime_api::{GasInfo, SimulateError, SimulateResponse, SimulateResult}; +use cosmos_runtime_api::{ChainInfo, GasInfo, SimulateError, SimulateResponse, SimulateResult}; use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; use frame_support::{ genesis_builder_helper::{build_state, get_preset}, traits::OnFinalize, weights::Weight, }; +use np_cosmos::traits::ChainInfo as _; use pallet_aura::Authorities; use pallet_cosmos_types::{ context::traits::Context, events::traits::EventManager, gas::traits::GasMeter, @@ -592,6 +597,20 @@ impl_runtime_apis! { events: context.event_manager().events() }) } + + fn chain_info() -> ChainInfo { + let chain_id = ::ChainInfo::chain_id().to_string(); + let name = ::ChainInfo::name().to_string(); + let bech32_prefix = ::ChainInfo::bech32_prefix().to_string(); + let version = ::ChainInfo::version().to_string(); + + ChainInfo { + chain_id, + name, + bech32_prefix, + version, + } + } } impl cosmwasm_runtime_api::CosmwasmRuntimeApi> for Runtime { From f30644575b8a9fae06cfc0cd21bc7cf2c8babe5c Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 02:00:01 +0900 Subject: [PATCH 119/133] chore: Remove suggestion chain to wallet page in sidecar --- sidecar/public/config.js | 17 ---- sidecar/public/cosmostation_logo.png | Bin 10459 -> 0 bytes sidecar/public/index.html | 126 --------------------------- sidecar/public/keplr_logo.png | Bin 21579 -> 0 bytes sidecar/src/app.ts | 4 - 5 files changed, 147 deletions(-) delete mode 100644 sidecar/public/config.js delete mode 100644 sidecar/public/cosmostation_logo.png delete mode 100644 sidecar/public/index.html delete mode 100644 sidecar/public/keplr_logo.png diff --git a/sidecar/public/config.js b/sidecar/public/config.js deleted file mode 100644 index 241ffb58..00000000 --- a/sidecar/public/config.js +++ /dev/null @@ -1,17 +0,0 @@ -const config = { - chainId: "dev", - chainName: "Noir", - addressPrefix: "cosmos", - baseDenom: "azig", // atto zig - displayDenom: "ZIG", - restURL: "http://127.0.0.1:1317", - coinType: "118", - decimals: 18, - gasRate: { - high: "1.3", - average: "1", - low: "0.8", - tiny: "0.6", - }, - sendGas: "130000000" -}; diff --git a/sidecar/public/cosmostation_logo.png b/sidecar/public/cosmostation_logo.png deleted file mode 100644 index 5d66b9db61aff2a026e3e343c90316d258c839d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10459 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 HnTPxj4DuVh diff --git a/sidecar/public/index.html b/sidecar/public/index.html deleted file mode 100644 index 804f9842..00000000 --- a/sidecar/public/index.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - Cosmos API Sidecar - - - - -
-

Welcome to Noir!

-
- -
-
- -
- - - - - diff --git a/sidecar/public/keplr_logo.png b/sidecar/public/keplr_logo.png deleted file mode 100644 index 34a73c9da03c58b8d8250f704875a5e65aa46361..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21579 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

&kmb0=3pYc!TnN&;PuxI<_-}2NvXL#qEmDqm z^^dTFCI~MC2Jx*9hKkkWJ+-`T0YE5}PBJ&Dll4ns4(9auNPuR-kdyFfS_hTAoR&vu z81T&)&g+z5aRe6LhvYFE)Fv5Z)=s*4$y)GH}268$b_Uvx`uRqK;=qaS91kf#QAU$p% z6}n;W2!(Nh$&`>Y%sR~4g%7j|Wu4issj4)5)sELmna#0KhI{QkfzJRh51l^yolM0U!Ed!=31U+ z`sx?|&I9Ym8!A&UprF3Car|Vzcl&24ue3qn#!h=qahfV2xiqW;!HspM5P~cCc^FWn zrvnH(C)bA^y?E}i9yEcvw-&Eg5JVX~g06nT)Kvl)06m^}s_+9KQw5=JArZVk->ZYo zw|vh}UwkJ$&2&cq-O{xe|IW3>^7gg!t~vuT&J%WRrj9{y+58>H-cc>WK_q%Tu1)4+ zYk35n$eDo%^FV!+zH{$Z;McA{*g22#@jOrrvbM*w1dIWGkWH~>OW7k(dJvPN`r zpfvy!LB}HqtxZX8$@j1K-E73Kx@;2u$)u<3;%mD8zUN+FKI_VhR>2;0gh157BQ-O!&I%{e9n z);yKY3Q$YeUi^Dk`QrG%YLl-taHbg1SWzXYG2$%lV>l*MHg;|*lRRv|!H8f!qJfQb zcUc`gXIzx2xLNiJInB$m%aPx@N9mewCT1GkdjPA107S`k=a?1%2*8{zrB(_I0mrEAxI@9J_qURzu9hP5$Q&23w!?}}XDU7nR;RlI(X!n0Dv^9kBR{$StzkwetGX0Kjt#<-w9}}Ua7Lzd?yA`#oePf z@q1-bCziR*;*dPDZIe5-6X_i3t^yR(U-+tz-76Ot*Vdl8e}%xccwQVp1f3ado0%2} zK!5Ex=kuT*=d5byrSJkfV0NB|&?Ak~ndfeLmxD)E#Wq1Xe&1!_#JoZ&jvWkJumzNv zw#c&`m50+tSI_j=D%*{&u;ncMkpZ-(ufO(t*Di?eT@SCWQhfRPdCw}$ zJ!_v{O^+pT22i}Qm?oe=wqsz-elEzD{X3>%RLx}+OtZO5Ffba8z-7aFh{w@6wwWft zoDA^l=zF|}lPjKq)aB9R)wpl=Wo*8|a{R4@mz&hu%9e*4catekrGEj@DSXR!HA%Su O0000 { - reply.sendFile('index.html'); - }); - this.server.get( '/cosmos/bank/v1beta1/balances/:address', async ( From bc9f477631010e9e024934a5afe0812e935caf67 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 11:05:54 +0900 Subject: [PATCH 120/133] refactor: Refactor api routes --- sidecar/src/app.ts | 277 +++------------------------ sidecar/src/constants/types.ts | 25 +++ sidecar/src/handlers/account.ts | 21 ++ sidecar/src/handlers/balance.ts | 27 +++ sidecar/src/handlers/distribution.ts | 28 +++ sidecar/src/handlers/index.ts | 6 + sidecar/src/handlers/nodeinfo.ts | 18 ++ sidecar/src/handlers/staking.ts | 41 ++++ sidecar/src/handlers/tx.ts | 38 ++++ sidecar/src/rpc/index.ts | 78 ++++++++ sidecar/src/services/balance.ts | 2 +- 11 files changed, 311 insertions(+), 250 deletions(-) create mode 100644 sidecar/src/constants/types.ts create mode 100644 sidecar/src/handlers/account.ts create mode 100644 sidecar/src/handlers/balance.ts create mode 100644 sidecar/src/handlers/distribution.ts create mode 100644 sidecar/src/handlers/index.ts create mode 100644 sidecar/src/handlers/nodeinfo.ts create mode 100644 sidecar/src/handlers/staking.ts create mode 100644 sidecar/src/handlers/tx.ts create mode 100644 sidecar/src/rpc/index.ts diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 52449061..5d8a7191 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -1,7 +1,5 @@ import fastify, { FastifyInstance, - FastifyReply, - FastifyRequest, } from "fastify"; import { ApiPromise, WsProvider } from "@polkadot/api"; import { @@ -15,44 +13,24 @@ import { TxService, AccountService, } from "./services"; -import { - AccountResponse, - BroadcastTxSyncResponse, - ResultTxSearch, -} from "./types"; import path from "path"; import fastifyStatic from "@fastify/static"; -import FastifyWebsocket, { SocketStream } from "@fastify/websocket"; +import FastifyWebsocket from "@fastify/websocket"; import { JSONRPCServer } from "json-rpc-2.0"; import { IConfig } from "config"; import { Database, open } from "lmdb"; -import querystring from "node:querystring"; import rpc from "./constants/rpc"; -import { QueryAccountRequest } from "cosmjs-types/cosmos/auth/v1beta1/query.js"; -import { - ABCIQueryResponse, - GetNodeInfoResponse, -} from "cosmjs-types/cosmos/base/tendermint/v1beta1/query.js"; -import { - QueryAllBalancesRequest, - QueryAllBalancesResponse, -} from "cosmjs-types/cosmos/bank/v1beta1/query.js"; -import { toSnakeCase } from "./utils"; -import { - QueryDelegationTotalRewardsRequest, - QueryDelegationTotalRewardsResponse, -} from "cosmjs-types/cosmos/distribution/v1beta1/query.js"; -import { - QueryDelegatorDelegationsRequest, - QueryDelegatorDelegationsResponse, - QueryDelegatorUnbondingDelegationsRequest, - QueryDelegatorUnbondingDelegationsResponse, -} from "cosmjs-types/cosmos/staking/v1beta1/query.js"; -import { - BroadcastTxResponse, - SimulateResponse, -} from "cosmjs-types/cosmos/tx/v1beta1/service.js"; import { Header } from "@polkadot/types/interfaces"; +import { types } from "./constants/types"; +import { + BalanceHandler, + AccountHandler, + DistributionHandler, + NodeInfoHandler, + TxHandler, + StakingHandler +} from "./handlers"; +import { addMethods } from "./rpc"; export class App { config: IConfig; @@ -100,31 +78,7 @@ export class App { this.chainApi = await ApiPromise.create({ provider: new WsProvider(endpoint), - types: { - GasInfo: { - gas_used: 'u64', - gas_wanted: 'u64', - }, - EventAttribute: { - key: 'Vec', - value: 'Vec', - }, - CosmosEvent: { - type: 'Vec', - attributes: 'Vec', - - }, - SimulateResponse: { - gas_info: 'GasInfo', - events: 'Vec', - }, - ChainInfo: { - chain_id: 'String', - name: 'String', - bech32_prefix: 'String', - version: 'String', - } - }, + types, rpc }); if (this.chainApi.isConnected) { @@ -165,201 +119,26 @@ export class App { }); await this.server.register(FastifyWebsocket); - this.server.get( - '/cosmos/bank/v1beta1/balances/:address', - async ( - request: FastifyRequest<{ - Params: QueryAllBalancesRequest; - }> - ): Promise => { - const { address } = request.params; - const response = QueryAllBalancesResponse.toJSON( - await this.services.get('balance').balances(address) - ); - return toSnakeCase(response); - } - ); - - this.server.get( - '/cosmos/auth/v1beta1/accounts/:address', - async ( - request: FastifyRequest<{ - Params: QueryAccountRequest; - }> - ): Promise => { - const { address } = request.params; - return await this.services - .get('account') - .accounts(address); - } - ); - - this.server.get( - '/cosmos/base/tendermint/v1beta1/node_info', - async (): Promise => { - const response = GetNodeInfoResponse.toJSON( - await this.services.get('nodeInfo').nodeInfo() - ); - return toSnakeCase(response); - } - ); - - this.server.post( - '/cosmos/tx/v1beta1/txs', - async ( - request: FastifyRequest<{ - Body: { - tx_bytes: string; - mode: number; - }; - }> - ): Promise => { - const { tx_bytes } = request.body; - const response = BroadcastTxResponse.toJSON( - await this.services.get('tx').broadcastTx(tx_bytes) - ); - return toSnakeCase(response); - } - ); - - this.server.get( - '/cosmos/staking/v1beta1/delegations/:delegatorAddr', - ( - request: FastifyRequest<{ - Params: QueryDelegatorDelegationsRequest; - }> - ): unknown => { - const { delegatorAddr } = request.params; - const response = QueryDelegatorDelegationsResponse.toJSON( - this.services - .get('staking') - .delegations(delegatorAddr) - ); - return toSnakeCase(response); - } - ); - - this.server.get( - '/cosmos/distribution/v1beta1/delegators/:delegatorAddress/rewards', - ( - request: FastifyRequest<{ - Params: QueryDelegationTotalRewardsRequest; - }> - ): unknown => { - const { delegatorAddress } = request.params; - const response = QueryDelegationTotalRewardsResponse.toJSON( - this.services - .get('distribution') - .rewards(delegatorAddress) - ); - return toSnakeCase(response); - } - ); - - this.server.get( - '/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations', - ( - request: FastifyRequest<{ - Params: QueryDelegatorUnbondingDelegationsRequest; - }> - ): unknown => { - const { delegatorAddr } = request.params; - const response = QueryDelegatorUnbondingDelegationsResponse.toJSON( - this.services - .get('staking') - .unbondingDelegations(delegatorAddr) - ); - return toSnakeCase(response); - } - ); - - this.server.post( - '/cosmos/tx/v1beta1/simulate', - async ( - request: FastifyRequest<{ - Body: { tx_bytes: string }; - }> - ): Promise => { - const { tx_bytes } = request.body; - const response = SimulateResponse.toJSON( - await this.services.get('tx').simulate(tx_bytes) - ); - return toSnakeCase(response); - } - ); + const balanceHandler = new BalanceHandler(this.services.get('balance')); + const accountHandler = new AccountHandler(this.services.get('account')); + const distributionHandler = new DistributionHandler(this.services.get('distribution')); + const nodeInfoHandler = new NodeInfoHandler(this.services.get('nodeInfo')); + const txHandler = new TxHandler(this.services.get('tx')); + const stakingHandler = new StakingHandler(this.services.get('staking')); + + this.server.get('/cosmos/bank/v1beta1/balances/:address', balanceHandler.handleGetBalance); + this.server.get('/cosmos/auth/v1beta1/accounts/:address', accountHandler.handleGetAccount); + this.server.get('/cosmos/distribution/v1beta1/delegators/:delegatorAddress/rewards', distributionHandler.handleGetDistribution); + this.server.get('/cosmos/base/tendermint/v1beta1/node_info', nodeInfoHandler.handleGetNodeInfo); + this.server.post('/cosmos/tx/v1beta1/txs', txHandler.handlePostTxs); + this.server.post('/cosmos/tx/v1beta1/simulate', txHandler.handlePostSimulate); + this.server.get('/cosmos/staking/v1beta1/delegations/:delegatorAddr', stakingHandler.handleGetStaking); + this.server.get('/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations', stakingHandler.handleGetUnbondingDelegations); } async initJsonRpcServer() { this.jsonrpc = new JSONRPCServer(); - - this.jsonrpc.addMethod('status', async (): Promise => { - return toSnakeCase( - await this.services.get('status').status() - ); - }); - - this.jsonrpc.addMethod( - 'abci_query', - async ({ path, data }): Promise => { - const result = await this.services - .get('abci') - .query(path, data); - const response = ABCIQueryResponse.toJSON(result); - return { - response, - }; - } - ); - - this.jsonrpc.addMethod( - 'broadcast_tx_sync', - async ({ tx }): Promise => { - const result = await this.services.get('tx').broadcastTx(tx); - const { code, txhash, data, rawLog, codespace } = result.txResponse; - return { - code, - data, - log: rawLog, - codespace, - hash: txhash, - }; - } - ); - - this.jsonrpc.addMethod('tx_search', ({ query }): ResultTxSearch => { - const args = querystring.parse(query); - let hash = args['tx.hash'] as string; - if (hash.includes("'")) { - hash = hash.replace(/'/gi, ''); - } - const txs = this.services.get('tx').searchTx(hash); - txs.forEach(tx => { - tx.tx_result.events.forEach(event => { - event.attributes = this.services.get('tx').encodeAttributes(event.attributes, 'utf8', 'base64'); - }); - }); - - return { - txs, - total_count: txs.length.toString(), - }; - }); - - this.server.get( - '/websocket', - { websocket: true }, - (connection: SocketStream) => { - connection.socket.on('message', async (message) => { - const request = JSON.parse(message.toString()); - const response = await this.jsonrpc.receive(request); - if (response) { - connection.socket.send( - Buffer.from(JSON.stringify(response), 'utf8') - ); - } - }); - } - ); + addMethods(this); } async initSubscribeEvents() { diff --git a/sidecar/src/constants/types.ts b/sidecar/src/constants/types.ts new file mode 100644 index 00000000..65fa4796 --- /dev/null +++ b/sidecar/src/constants/types.ts @@ -0,0 +1,25 @@ +export const types = { + GasInfo: { + gas_used: 'u64', + gas_wanted: 'u64', + }, + EventAttribute: { + key: 'Vec', + value: 'Vec', + }, + CosmosEvent: { + type: 'Vec', + attributes: 'Vec', + + }, + SimulateResponse: { + gas_info: 'GasInfo', + events: 'Vec', + }, + ChainInfo: { + chain_id: 'String', + name: 'String', + bech32_prefix: 'String', + version: 'String', + } +}; diff --git a/sidecar/src/handlers/account.ts b/sidecar/src/handlers/account.ts new file mode 100644 index 00000000..656f1b32 --- /dev/null +++ b/sidecar/src/handlers/account.ts @@ -0,0 +1,21 @@ +import { FastifyRequest } from "fastify"; +import { AccountService } from "../services"; +import { AccountResponse } from "../types"; +import { QueryAccountRequest } from "cosmjs-types/cosmos/auth/v1beta1/query"; + +export class AccountHandler { + accountService: AccountService; + + constructor(accountService: AccountService) { + this.accountService = accountService; + } + + async handleGetAccount( + request: FastifyRequest<{ + Params: QueryAccountRequest; + }> + ): Promise { + const { address } = request.params; + return this.accountService.accounts(address); + } +} diff --git a/sidecar/src/handlers/balance.ts b/sidecar/src/handlers/balance.ts new file mode 100644 index 00000000..3d8fd210 --- /dev/null +++ b/sidecar/src/handlers/balance.ts @@ -0,0 +1,27 @@ +import { + QueryAllBalancesRequest, + QueryAllBalancesResponse +} from "cosmjs-types/cosmos/bank/v1beta1/query"; +import { FastifyRequest } from "fastify"; +import { BalanceService } from "../services"; +import { toSnakeCase } from "../utils"; + +export class BalanceHandler { + balanceService: BalanceService; + + constructor(balanceService: BalanceService) { + this.balanceService = balanceService + } + + async handleGetBalance( + request: FastifyRequest<{ + Params: QueryAllBalancesRequest; + }> + ): Promise { + const { address } = request.params; + const response = QueryAllBalancesResponse.toJSON( + await this.balanceService.balances(address) + ); + return toSnakeCase(response); + } +} diff --git a/sidecar/src/handlers/distribution.ts b/sidecar/src/handlers/distribution.ts new file mode 100644 index 00000000..f247daf9 --- /dev/null +++ b/sidecar/src/handlers/distribution.ts @@ -0,0 +1,28 @@ +import { FastifyRequest } from "fastify"; +import { DistributionService } from "../services"; +import { toSnakeCase } from "../utils"; +import { + QueryDelegationTotalRewardsRequest, + QueryDelegationTotalRewardsResponse +} from "cosmjs-types/cosmos/distribution/v1beta1/query"; + +export class DistributionHandler { + distributionService: DistributionService; + + constructor(distributionService: DistributionService) { + this.distributionService = distributionService; + } + + handleGetDistribution( + request: FastifyRequest<{ + Params: QueryDelegationTotalRewardsRequest; + }> + ): unknown { + const { delegatorAddress } = request.params; + const response = QueryDelegationTotalRewardsResponse.toJSON( + this.distributionService + .rewards(delegatorAddress) + ); + return toSnakeCase(response); + } +} diff --git a/sidecar/src/handlers/index.ts b/sidecar/src/handlers/index.ts new file mode 100644 index 00000000..1c4707f5 --- /dev/null +++ b/sidecar/src/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './balance'; +export * from './account'; +export * from './nodeinfo'; +export * from './tx'; +export * from './distribution'; +export * from './staking'; diff --git a/sidecar/src/handlers/nodeinfo.ts b/sidecar/src/handlers/nodeinfo.ts new file mode 100644 index 00000000..94daccfe --- /dev/null +++ b/sidecar/src/handlers/nodeinfo.ts @@ -0,0 +1,18 @@ +import { NodeInfoService } from "../services"; +import { GetNodeInfoResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query"; +import { toSnakeCase } from "../utils"; + +export class NodeInfoHandler { + nodeInfoService: NodeInfoService; + + constructor(nodeInfoService: NodeInfoService) { + this.nodeInfoService = nodeInfoService; + } + + async handleGetNodeInfo(): Promise { + const response = GetNodeInfoResponse.toJSON( + await this.nodeInfoService.nodeInfo() + ); + return toSnakeCase(response); + } +} diff --git a/sidecar/src/handlers/staking.ts b/sidecar/src/handlers/staking.ts new file mode 100644 index 00000000..394996f1 --- /dev/null +++ b/sidecar/src/handlers/staking.ts @@ -0,0 +1,41 @@ +import { FastifyRequest } from "fastify"; +import { + QueryDelegatorDelegationsRequest, + QueryDelegatorDelegationsResponse, + QueryDelegatorUnbondingDelegationsRequest, + QueryDelegatorUnbondingDelegationsResponse +} from "cosmjs-types/cosmos/staking/v1beta1/query"; +import { StakingService } from "../services"; +import { toSnakeCase } from "../utils"; + +export class StakingHandler { + stakingService: StakingService; + + constructor(stakingService: StakingService) { + this.stakingService = stakingService; + } + + handleGetStaking( + request: FastifyRequest<{ + Params: QueryDelegatorDelegationsRequest; + }> + ): unknown { + const { delegatorAddr } = request.params; + const response = QueryDelegatorDelegationsResponse.toJSON( + this.stakingService.delegations(delegatorAddr) + ); + return toSnakeCase(response); + } + + handleGetUnbondingDelegations( + request: FastifyRequest<{ + Params: QueryDelegatorUnbondingDelegationsRequest; + }> + ): unknown { + const { delegatorAddr } = request.params; + const response = QueryDelegatorUnbondingDelegationsResponse.toJSON( + this.stakingService.unbondingDelegations(delegatorAddr) + ); + return toSnakeCase(response); + } +} diff --git a/sidecar/src/handlers/tx.ts b/sidecar/src/handlers/tx.ts new file mode 100644 index 00000000..04d8f9cc --- /dev/null +++ b/sidecar/src/handlers/tx.ts @@ -0,0 +1,38 @@ +import { FastifyRequest } from "fastify"; +import { toSnakeCase } from "../utils"; +import { BroadcastTxResponse, SimulateResponse } from "cosmjs-types/cosmos/tx/v1beta1/service"; +import { TxService } from "../services"; + +export class TxHandler { + txService: TxService; + + constructor(txService: TxService) { + this.txService = txService; + } + + async handlePostTxs( + request: FastifyRequest<{ + Body: { + tx_bytes: string; + mode: number; + }; + }> + ): Promise { + const { tx_bytes } = request.body; + const response = BroadcastTxResponse.toJSON( + await this.txService.broadcastTx(tx_bytes) + ); + return toSnakeCase(response); + } + + async handlePostSimulate(request: FastifyRequest<{ + Body: { tx_bytes: string }; + }> + ): Promise { + const { tx_bytes } = request.body; + const response = SimulateResponse.toJSON( + await this.txService.simulate(tx_bytes) + ); + return toSnakeCase(response); + } +} diff --git a/sidecar/src/rpc/index.ts b/sidecar/src/rpc/index.ts new file mode 100644 index 00000000..135ffbde --- /dev/null +++ b/sidecar/src/rpc/index.ts @@ -0,0 +1,78 @@ +import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query"; +import { App } from "../app"; +import { AbciService, StatusService, TxService } from "../services"; +import { toSnakeCase } from "../utils"; +import { BroadcastTxSyncResponse, ResultTxSearch } from "../types"; +import querystring from "node:querystring"; +import { SocketStream } from "@fastify/websocket"; + +export function addMethods(context: App) { + context.jsonrpc.addMethod('status', async (): Promise => { + return toSnakeCase( + await context.services.get('status').status() + ); + }); + + context.jsonrpc.addMethod( + 'abci_query', + async ({ path, data }): Promise => { + const result = await context.services + .get('abci') + .query(path, data); + const response = ABCIQueryResponse.toJSON(result); + return { + response, + }; + } + ); + + context.jsonrpc.addMethod( + 'broadcast_tx_sync', + async ({ tx }): Promise => { + const result = await context.services.get('tx').broadcastTx(tx); + const { code, txhash, data, rawLog, codespace } = result.txResponse; + return { + code, + data, + log: rawLog, + codespace, + hash: txhash, + }; + } + ); + + context.jsonrpc.addMethod('tx_search', ({ query }): ResultTxSearch => { + const args = querystring.parse(query); + let hash = args['tx.hash'] as string; + if (hash.includes("'")) { + hash = hash.replace(/'/gi, ''); + } + const txs = context.services.get('tx').searchTx(hash); + txs.forEach(tx => { + tx.tx_result.events.forEach(event => { + event.attributes = context.services.get('tx').encodeAttributes(event.attributes, 'utf8', 'base64'); + }); + }); + + return { + txs, + total_count: txs.length.toString(), + }; + }); + + context.server.get( + '/websocket', + { websocket: true }, + (connection: SocketStream) => { + connection.socket.on('message', async (message) => { + const request = JSON.parse(message.toString()); + const response = await this.jsonrpc.receive(request); + if (response) { + connection.socket.send( + Buffer.from(JSON.stringify(response), 'utf8') + ); + } + }); + } + ); +} diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index fb01c5dd..63287e2c 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -82,7 +82,7 @@ export class BalanceService implements ApiService { const assets = []; const metadata = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.metadata.entries(); - for (const [{ args: [assetId] }, value] of metadata) { + for (const [{ args: [assetId] }] of metadata) { const asset = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.account(assetId.toString(), origin); if (!asset.isEmpty) { From 783e87e4602b92c5b325ae2a2d14a481cf8a95b0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 12:14:54 +0900 Subject: [PATCH 121/133] refactor: Refactor chain api --- sidecar/src/app.ts | 36 +++++++++++-------------------- sidecar/src/constants/rpc.ts | 4 +--- sidecar/src/services/abci.ts | 32 ++++++++++++++------------- sidecar/src/services/account.ts | 14 ++++++------ sidecar/src/services/balance.ts | 24 +++++++++++---------- sidecar/src/services/chain.ts | 37 ++++++++++++++++++++++++++++++++ sidecar/src/services/index.ts | 1 + sidecar/src/services/nodeinfo.ts | 13 ++++++----- sidecar/src/services/status.ts | 19 +++++++++------- sidecar/src/services/tx.ts | 22 ++++++++++--------- sidecar/src/utils/index.ts | 8 ++++++- 11 files changed, 127 insertions(+), 83 deletions(-) create mode 100644 sidecar/src/services/chain.ts diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 5d8a7191..b6b4d686 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -1,7 +1,6 @@ import fastify, { FastifyInstance, } from "fastify"; -import { ApiPromise, WsProvider } from "@polkadot/api"; import { AbciService, ApiServices, @@ -12,6 +11,7 @@ import { StatusService, TxService, AccountService, + ChainService, } from "./services"; import path from "path"; import fastifyStatic from "@fastify/static"; @@ -19,9 +19,7 @@ import FastifyWebsocket from "@fastify/websocket"; import { JSONRPCServer } from "json-rpc-2.0"; import { IConfig } from "config"; import { Database, open } from "lmdb"; -import rpc from "./constants/rpc"; import { Header } from "@polkadot/types/interfaces"; -import { types } from "./constants/types"; import { BalanceHandler, AccountHandler, @@ -38,7 +36,6 @@ export class App { server: FastifyInstance; services: ApiServices; jsonrpc: JSONRPCServer; - chainApi: ApiPromise; constructor(config: IConfig) { this.config = config; @@ -74,32 +71,22 @@ export class App { async initApiServices() { const endpoint = this.config.get('chain.endpoint'); - console.debug(`Try connecting to chain RPC. endpoint: ${endpoint}`); + const chainService = new ChainService(endpoint); - this.chainApi = await ApiPromise.create({ - provider: new WsProvider(endpoint), - types, - rpc - }); - if (this.chainApi.isConnected) { - console.debug('Chain RPC connected'); - } else { - console.error('Failed to connect with chain RPC'); - } - - const accountService = new AccountService(this.chainApi); - const txService = new TxService(this.db, this.chainApi); + const accountService = new AccountService(chainService); + const txService = new TxService(this.db, chainService); const balanceService = new BalanceService( this.config, - this.chainApi, + chainService, accountService ); - const abciService = new AbciService(this.chainApi, accountService, balanceService, txService); + const abciService = new AbciService(chainService, accountService, balanceService, txService); const distributionService = new DistributionService(); - const nodeInfoService = new NodeInfoService(this.config, this.chainApi); + const nodeInfoService = new NodeInfoService(this.config, chainService); const stakingService = new StakingService(); - const statusService = new StatusService(this.config, this.chainApi); + const statusService = new StatusService(this.config, chainService); + this.services.set('chain', chainService); this.services.set('abci', abciService); this.services.set('account', accountService); this.services.set('balance', balanceService); @@ -142,9 +129,10 @@ export class App { } async initSubscribeEvents() { - await this.chainApi.rpc.chain.subscribeNewHeads( + const chainApi = await this.services.get('chain').getChainApi(); + await chainApi.rpc.chain.subscribeNewHeads( async (header: Header) => { - const signedBlock = await this.chainApi.rpc.chain.getBlock(header.hash); + const signedBlock = await chainApi.rpc.chain.getBlock(header.hash); signedBlock.block.extrinsics.forEach( async ({ method: { args, method, section } }, index) => { diff --git a/sidecar/src/constants/rpc.ts b/sidecar/src/constants/rpc.ts index 95094b13..352f22aa 100644 --- a/sidecar/src/constants/rpc.ts +++ b/sidecar/src/constants/rpc.ts @@ -1,4 +1,4 @@ -const rpc = { +export const rpc = { cosmos: { broadcastTx: { description: 'Broadcast cosmos transaction.', @@ -55,5 +55,3 @@ const rpc = { }, } }; - -export default rpc; diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index 9dd2c07c..761c90b2 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -7,7 +7,6 @@ import { AccountService } from "./account"; import { PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { BaseAccount } from "cosmjs-types/cosmos/auth/v1beta1/auth.js"; import Long from "long"; -import { ApiPromise } from "@polkadot/api"; import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query.js"; import { SimulateRequest, SimulateResponse } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; import { TxService } from "./tx"; @@ -16,15 +15,16 @@ import { convertToCodespace } from "../constants/codespace"; import { encodeTo } from "../utils"; import { BalanceService } from "./balance"; import { QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; +import { ChainService } from "./chain"; export class AbciService implements ApiService { - chainApi: ApiPromise; + chainService: ChainService; accountService: AccountService; balanceService: BalanceService; txService: TxService; - constructor(chainApi: ApiPromise, accountService: AccountService, balanceService: BalanceService, txService: TxService) { - this.chainApi = chainApi; + constructor(chainService: ChainService, accountService: AccountService, balanceService: BalanceService, txService: TxService) { + this.chainService = chainService; this.accountService = accountService; this.balanceService = balanceService; this.txService = txService; @@ -34,13 +34,15 @@ export class AbciService implements ApiService { console.debug('query'); console.debug(`path: ${path}`); + const chainApi = await this.chainService.getChainApi(); + if (path === '/cosmos.auth.v1beta1.Query/Account') { const address = QueryAccountRequest.decode( Buffer.from(data, 'hex') ).address; - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await chainApi.query.system.number(); + const blockHash = await chainApi.rpc.chain.getBlockHash(height.toString()); const { account } = await this.accountService.accounts(address, blockHash.toString()); const pubkey: PubKey = { @@ -75,8 +77,8 @@ export class AbciService implements ApiService { codespace: '', }; } else if (path === '/cosmos.bank.v1beta1.Query/AllBalances') { - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await chainApi.query.system.number(); + const blockHash = await chainApi.rpc.chain.getBlockHash(height.toString()); const { address } = QueryAllBalancesRequest.decode( Buffer.from(data, 'hex') @@ -96,8 +98,8 @@ export class AbciService implements ApiService { codespace: '', }; } else if (path === '/cosmos.bank.v1beta1.Query/Balance') { - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await chainApi.query.system.number(); + const blockHash = await chainApi.rpc.chain.getBlockHash(height.toString()); const { address, denom } = QueryBalanceRequest.decode( Buffer.from(data, 'hex') @@ -117,8 +119,8 @@ export class AbciService implements ApiService { codespace: '', }; } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await chainApi.query.system.number(); + const blockHash = await chainApi.rpc.chain.getBlockHash(height.toString()); try { const req = SimulateRequest.decode(Buffer.from(data, 'hex')); const res = await this.txService.simulate(Buffer.from(req.txBytes).toString('base64'), blockHash.toString()); @@ -161,10 +163,10 @@ export class AbciService implements ApiService { } }; - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await chainApi.query.system.number(); + const blockHash = await chainApi.rpc.chain.getBlockHash(height.toString()); - const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${encodeTo(JSON.stringify(msg), 'utf8', 'hex')}`, blockHash.toString()); + const response = await chainApi.rpc['cosmwasm']['query'](address, gas, `0x${encodeTo(JSON.stringify(msg), 'utf8', 'hex')}`, blockHash.toString()); const stateResponse = QuerySmartContractStateResponse.fromPartial({ data: Uint8Array.from(Buffer.from(response, 'hex')) }); return { diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 6829782d..abb1b23a 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -1,4 +1,3 @@ -import { ApiPromise } from "@polkadot/api"; import { AccountResponse } from "../types"; import { fromBech32 } from "@cosmjs/encoding"; import { Codec } from "@polkadot/types/types/index.js"; @@ -7,12 +6,13 @@ import { blake2AsU8a } from "@polkadot/util-crypto"; import { ApiService } from "./service"; import Dummy from "../constants/dummy"; import { AccountInfo } from "@polkadot/types/interfaces"; +import { ChainService } from "./chain"; export class AccountService implements ApiService { - chainApi: ApiPromise; + chainService: ChainService; - constructor(chainApi: ApiPromise) { - this.chainApi = chainApi; + constructor(chainService: ChainService) { + this.chainService = chainService; } public async accounts(address: string, blockHash?: string): Promise { @@ -23,7 +23,8 @@ export class AccountService implements ApiService { if (!origin) { origin = this.interim(address); } - const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); + const chainApi = await this.chainService.getChainApi(); + const account = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query['system']['account'](origin); if (account) { const { nonce } = account.toJSON() as unknown as AccountInfo; @@ -43,7 +44,8 @@ export class AccountService implements ApiService { public async origin(address: string): Promise { const { data } = fromBech32(address); - return this.chainApi.query['addressMap']['index']( + const chainApi = await this.chainService.getChainApi(); + return chainApi.query['addressMap']['index']( Buffer.concat([Buffer.from([0x01]), data]) ); } diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 63287e2c..a8970dde 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -1,4 +1,3 @@ -import { ApiPromise } from "@polkadot/api"; import { AccountService } from "./account"; import { ApiService } from "./service"; import { IConfig } from "config"; @@ -6,25 +5,27 @@ import { QueryAllBalancesResponse, QueryBalanceResponse } from "cosmjs-types/cos import Long from "long"; import { AccountInfo } from "@polkadot/types/interfaces"; import { encodeTo } from "../utils"; +import { ChainService } from "./chain"; export class BalanceService implements ApiService { config: IConfig; - chainApi: ApiPromise; + chainService: ChainService; accountService: AccountService; constructor( config: IConfig, - chainApi: ApiPromise, + chainService: ChainService, accountService: AccountService ) { this.config = config; - this.chainApi = chainApi; + this.chainService = chainService; this.accountService = accountService; } public async balance(address: string, denom: string, blockHash?: string): Promise { console.debug('balance'); + const chainApi = await this.chainService.getChainApi(); const originRaw = await this.accountService.origin(address); let amount = '0'; let origin = originRaw.toString(); @@ -34,7 +35,7 @@ export class BalanceService implements ApiService { const nativeDenom = this.config.get('chain.denom'); if (nativeDenom === denom) { - const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); + const account = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query['system']['account'](origin); if (account) { const { data } = account.toJSON() as unknown as AccountInfo; amount = BigInt(data.free.toString()).toString(); @@ -47,8 +48,8 @@ export class BalanceService implements ApiService { } } } else { - const assetId = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assetMap.index(denom); - const asset = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.account(assetId.toString(), origin); + const assetId = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query.assetMap.index(denom); + const asset = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query.assets.account(assetId.toString(), origin); if (!asset.isEmpty) { const amount = BigInt(asset.toJSON()['balance']).toString(); console.debug(`denom: ${denom}, amount: ${amount}`); @@ -66,13 +67,14 @@ export class BalanceService implements ApiService { public async balances(address: string, blockHash?: string): Promise { console.debug('balances'); + const chainApi = await this.chainService.getChainApi(); const originRaw = await this.accountService.origin(address); let amount = '0'; let origin = originRaw.toString(); if (!origin) { origin = this.accountService.interim(address); } - const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); + const account = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query['system']['account'](origin); if (account) { const { data } = account.toJSON() as unknown as AccountInfo; amount = BigInt(data.free.toString()).toString(); @@ -81,12 +83,12 @@ export class BalanceService implements ApiService { const nativeBalance = { denom, amount }; const assets = []; - const metadata = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.metadata.entries(); + const metadata = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query.assets.metadata.entries(); for (const [{ args: [assetId] }] of metadata) { - const asset = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assets.account(assetId.toString(), origin); + const asset = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query.assets.account(assetId.toString(), origin); if (!asset.isEmpty) { - const assetDenom = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query.assetMap.map(assetId); + const assetDenom = await (await (blockHash ? chainApi.at(blockHash) : chainApi)).query.assetMap.map(assetId); if (!assetDenom.isEmpty) { const denomSet = assetDenom.toJSON(); diff --git a/sidecar/src/services/chain.ts b/sidecar/src/services/chain.ts new file mode 100644 index 00000000..d61d050f --- /dev/null +++ b/sidecar/src/services/chain.ts @@ -0,0 +1,37 @@ +import { rpc } from "../constants/rpc"; +import { types } from "../constants/types"; +import { sleep } from "../utils"; +import { ApiService } from "./service"; +import { ApiPromise, WsProvider } from "@polkadot/api"; + +export class ChainService implements ApiService { + endpoint: string; + chainApi: ApiPromise; + + constructor(endpoint: string) { + this.endpoint = endpoint; + + ApiPromise.create({ + provider: new WsProvider(this.endpoint), + types, + rpc + }).then((chainApi: ApiPromise) => this.chainApi = chainApi); + } + + public async getChainApi(): Promise { + while (!this.chainApi || !this.chainApi.isConnected) { + console.debug(`Try connecting to chain RPC. endpoint: ${this.endpoint}`); + + this.chainApi = await ApiPromise.create({ + provider: new WsProvider(this.endpoint), + types, + rpc + }); + + await sleep(1000); + } + console.debug('Chain RPC connected'); + + return this.chainApi; + } +} diff --git a/sidecar/src/services/index.ts b/sidecar/src/services/index.ts index 0111fd04..678b97cf 100644 --- a/sidecar/src/services/index.ts +++ b/sidecar/src/services/index.ts @@ -1,6 +1,7 @@ export * from "./abci"; export * from "./account"; export * from "./balance"; +export * from "./chain"; export * from "./distribution"; export * from "./nodeinfo"; export * from "./service"; diff --git a/sidecar/src/services/nodeinfo.ts b/sidecar/src/services/nodeinfo.ts index 63718317..803b9788 100644 --- a/sidecar/src/services/nodeinfo.ts +++ b/sidecar/src/services/nodeinfo.ts @@ -2,19 +2,22 @@ import { GetNodeInfoResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1 import { ApiService } from "./service"; import { IConfig } from "config"; import Long from "long"; -import { ApiPromise } from "@polkadot/api"; +import { ChainService } from "./chain"; export class NodeInfoService implements ApiService { config: IConfig; - chainApi: ApiPromise; + chainService: ChainService; - constructor(config: IConfig, chainApi: ApiPromise) { + constructor(config: IConfig, chainService: ChainService) { this.config = config; - this.chainApi = chainApi; + this.chainService = chainService; } public async nodeInfo(): Promise { - const { chain_id, name, bech32_prefix, version } = (await this.chainApi.rpc['cosmos']['chainInfo']()).toJSON(); + console.debug('nodeInfo'); + + const chainApi = await this.chainService.getChainApi(); + const { chain_id, name, bech32_prefix, version } = (await chainApi.rpc['cosmos']['chainInfo']()).toJSON(); const endpoint = this.config.get('server.endpoint'); return { diff --git a/sidecar/src/services/status.ts b/sidecar/src/services/status.ts index 111d1f06..e8b174be 100644 --- a/sidecar/src/services/status.ts +++ b/sidecar/src/services/status.ts @@ -1,30 +1,33 @@ -import { ApiPromise } from "@polkadot/api"; import { ResultStatus } from "../types"; import { ApiService } from "./service"; import { IConfig } from "config"; import Long from "long"; import { Block } from "@polkadot/types/interfaces"; +import { ChainService } from "./chain"; export class StatusService implements ApiService { config: IConfig; - chainApi: ApiPromise; + chainService: ChainService; - constructor(config: IConfig, chainApi: ApiPromise) { + constructor(config: IConfig, chainService: ChainService) { this.config = config; - this.chainApi = chainApi; + this.chainService = chainService; } public async status(): Promise { - const hash = (await this.chainApi.rpc.chain.getFinalizedHead()).toString(); - const { block } = (await this.chainApi.rpc.chain.getBlock(hash)).toJSON(); + console.debug('status'); + + const chainApi = await this.chainService.getChainApi(); + const hash = (await chainApi.rpc.chain.getFinalizedHead()).toString(); + const { block } = (await chainApi.rpc.chain.getBlock(hash)).toJSON(); const blockNumber = (block as unknown as Block).header.number.toString(); const timestamp = ( - await (await this.chainApi.at(hash)).query.timestamp.now() + await (await chainApi.at(hash)).query.timestamp.now() ).toString(); const blockTime = new Date(parseInt(timestamp)).toISOString(); const blockHash = hash.startsWith('0x') ? hash.slice(2) : hash; - const { chain_id, bech32_prefix, version } = (await this.chainApi.rpc['cosmos']['chainInfo']()).toJSON(); + const { chain_id, bech32_prefix, version } = (await chainApi.rpc['cosmos']['chainInfo']()).toJSON(); const endpoint = this.config.get('server.endpoint'); diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index f37b13fd..21f4bac3 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -1,4 +1,3 @@ -import { ApiPromise } from "@polkadot/api"; import { ResultTx } from "../types"; import { ApiService } from "./service"; import { Database } from "lmdb"; @@ -9,30 +8,30 @@ import { import Long from "long"; import { createHash } from "crypto"; import { convertToCodespace } from "../constants/codespace"; -import { encodeTo } from "../utils"; +import { encodeTo, sleep } from "../utils"; import { Event as CosmosEvent } from "cosmjs-types/tendermint/abci/types"; import { Header } from "@polkadot/types/interfaces"; +import { ChainService } from "./chain"; export type TransactResult = { codespace: string, code: number, gasWanted: number, gasUsed: number, events: CosmosEvent[] }; export class TxService implements ApiService { - chainApi: ApiPromise; + chainService: ChainService; db: Database; - constructor(db: Database, chainApi: ApiPromise) { - this.chainApi = chainApi; + constructor(db: Database, chainService: ChainService) { + this.chainService = chainService; this.db = db; } public async broadcastTx(txBytes: string): Promise { console.debug('broadcastTx'); - let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](`0x${encodeTo(txBytes, 'base64', 'hex')}`)).toString(); + const chainApi = await this.chainService.getChainApi(); + let txHash = (await chainApi.rpc['cosmos']['broadcastTx'](`0x${encodeTo(txBytes, 'base64', 'hex')}`)).toString(); txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; console.debug(`txHash: ${txHash}`); - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - while (true) { const txs = this.searchTx(txHash); if (txs.length > 0) { @@ -114,9 +113,11 @@ export class TxService implements ApiService { header: Header, extrinsicIndex: number ): Promise { + const chainApi = await this.chainService.getChainApi(); + /* eslint-disable @typescript-eslint/no-explicit-any */ const events = (await ( - await this.chainApi.at(header.hash) + await chainApi.at(header.hash) ).query.system.events()) as any; const result = events @@ -153,8 +154,9 @@ export class TxService implements ApiService { public async simulate(txBytes: string, blockHash?: string): Promise { console.debug('simulate'); + const chainApi = await this.chainService.getChainApi(); const txRaw = `0x${encodeTo(txBytes, 'base64', 'hex')}`; - const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); + const { gas_info, events } = (await chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); console.debug(`gasInfo: ${JSON.stringify(gas_info)}`); diff --git a/sidecar/src/utils/index.ts b/sidecar/src/utils/index.ts index dc27b635..55afd2ff 100644 --- a/sidecar/src/utils/index.ts +++ b/sidecar/src/utils/index.ts @@ -23,4 +23,10 @@ export function toSnakeCase(input: unknown): unknown { export const encodeTo = (value: string, from: BufferEncoding, to: BufferEncoding): string => { return Buffer.from(from === 'hex' && value.startsWith('0x') ? value.slice(2) : value, from).toString(to); -} \ No newline at end of file +} + +export const sleep = (ms: number) => { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +}; \ No newline at end of file From a7c94218a25520a53380452533eb5a54ac95ad3b Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 14:31:37 +0900 Subject: [PATCH 122/133] fix: Fix typo --- frame/babel/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 2be1adfa..83a26bf6 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -178,7 +178,7 @@ pub mod pallet { symbol: Vec, denom: Vec, decimals: u8, - is_frozon: bool, + is_frozen: bool, is_sufficient: bool, owner: AccountIdLookupOf, #[pallet::compact] min_balance: ::Balance, @@ -198,7 +198,7 @@ pub mod pallet { symbol, name, decimals, - is_frozon, + is_frozen, )?; let id: ::AssetId = id.into(); let denom: DenomOf = From 980fdb33ec5c28dcdc31f46189f329ffca8051e6 Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 15:49:57 +0900 Subject: [PATCH 123/133] refactor: Refactor rpc handlers --- sidecar/src/app.ts | 33 +++++++++----- sidecar/src/handlers/index.ts | 1 + sidecar/src/handlers/ws.ts | 22 ++++++++++ sidecar/src/rpc/abci_query.ts | 18 ++++++++ sidecar/src/rpc/index.ts | 81 ++--------------------------------- sidecar/src/rpc/status.ts | 16 +++++++ sidecar/src/rpc/tx.ts | 42 ++++++++++++++++++ 7 files changed, 125 insertions(+), 88 deletions(-) create mode 100644 sidecar/src/handlers/ws.ts create mode 100644 sidecar/src/rpc/abci_query.ts create mode 100644 sidecar/src/rpc/status.ts create mode 100644 sidecar/src/rpc/tx.ts diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index b6b4d686..8d6c43b8 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -13,8 +13,6 @@ import { AccountService, ChainService, } from "./services"; -import path from "path"; -import fastifyStatic from "@fastify/static"; import FastifyWebsocket from "@fastify/websocket"; import { JSONRPCServer } from "json-rpc-2.0"; import { IConfig } from "config"; @@ -26,9 +24,14 @@ import { DistributionHandler, NodeInfoHandler, TxHandler, - StakingHandler + StakingHandler, + WebsocketHandler } from "./handlers"; -import { addMethods } from "./rpc"; +import { + StatusRpcHandler, + AbciRpcHandler, + TxRpcHandler +} from "./rpc"; export class App { config: IConfig; @@ -47,8 +50,8 @@ export class App { await this.initDatabase(); await this.initApiServices(); - await this.initApiServer(); await this.initJsonRpcServer(); + await this.initApiServer(); await this.initSubscribeEvents(); } @@ -70,6 +73,8 @@ export class App { } async initApiServices() { + console.debug('Initialize API Services'); + const endpoint = this.config.get('chain.endpoint'); const chainService = new ChainService(endpoint); @@ -98,12 +103,10 @@ export class App { } async initApiServer() { + console.debug('Initialize API Server'); + const logger = this.config.get('server.logger'); this.server = fastify({ logger }); - const __dirname = path.resolve(); - this.server.register(fastifyStatic, { - root: path.join(__dirname, 'public'), - }); await this.server.register(FastifyWebsocket); const balanceHandler = new BalanceHandler(this.services.get('balance')); @@ -112,6 +115,7 @@ export class App { const nodeInfoHandler = new NodeInfoHandler(this.services.get('nodeInfo')); const txHandler = new TxHandler(this.services.get('tx')); const stakingHandler = new StakingHandler(this.services.get('staking')); + const wsHandler = new WebsocketHandler(this.jsonrpc); this.server.get('/cosmos/bank/v1beta1/balances/:address', balanceHandler.handleGetBalance); this.server.get('/cosmos/auth/v1beta1/accounts/:address', accountHandler.handleGetAccount); @@ -121,11 +125,20 @@ export class App { this.server.post('/cosmos/tx/v1beta1/simulate', txHandler.handlePostSimulate); this.server.get('/cosmos/staking/v1beta1/delegations/:delegatorAddr', stakingHandler.handleGetStaking); this.server.get('/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations', stakingHandler.handleGetUnbondingDelegations); + this.server.get('/websocket', { websocket: true }, wsHandler.handlerMessage); } async initJsonRpcServer() { this.jsonrpc = new JSONRPCServer(); - addMethods(this); + + const statusHandler = new StatusRpcHandler(this.services.get('status')); + const abciHandler = new AbciRpcHandler(this.services.get('abci')); + const txHandler = new TxRpcHandler(this.services.get('tx')); + + this.jsonrpc.addMethod('status', statusHandler.status); + this.jsonrpc.addMethod('abci_query', abciHandler.abciQuery); + this.jsonrpc.addMethod('broadcast_tx_sync', txHandler.broadcastTxSync); + this.jsonrpc.addMethod('tx_search', txHandler.txSearch); } async initSubscribeEvents() { diff --git a/sidecar/src/handlers/index.ts b/sidecar/src/handlers/index.ts index 1c4707f5..96c621e2 100644 --- a/sidecar/src/handlers/index.ts +++ b/sidecar/src/handlers/index.ts @@ -4,3 +4,4 @@ export * from './nodeinfo'; export * from './tx'; export * from './distribution'; export * from './staking'; +export * from './ws'; \ No newline at end of file diff --git a/sidecar/src/handlers/ws.ts b/sidecar/src/handlers/ws.ts new file mode 100644 index 00000000..1def5da4 --- /dev/null +++ b/sidecar/src/handlers/ws.ts @@ -0,0 +1,22 @@ +import { JSONRPCServer } from "json-rpc-2.0"; +import { SocketStream } from "@fastify/websocket"; + +export class WebsocketHandler { + jsonrpc: JSONRPCServer; + + constructor(jsonrpc: JSONRPCServer) { + this.jsonrpc = jsonrpc; + } + + handlerMessage(connection: SocketStream) { + connection.socket.on('message', async (message) => { + const request = JSON.parse(message.toString()); + const response = await this.jsonrpc.receive(request); + if (response) { + connection.socket.send( + Buffer.from(JSON.stringify(response), 'utf8') + ); + } + }); + } +} diff --git a/sidecar/src/rpc/abci_query.ts b/sidecar/src/rpc/abci_query.ts new file mode 100644 index 00000000..3c8f407f --- /dev/null +++ b/sidecar/src/rpc/abci_query.ts @@ -0,0 +1,18 @@ +import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query"; +import { AbciService } from "../services"; + +export class AbciRpcHandler { + abciService: AbciService; + + constructor(abciService: AbciService) { + this.abciService = abciService; + } + + async abciQuery({ path, data }): Promise { + const result = await this.abciService.query(path, data); + const response = ABCIQueryResponse.toJSON(result); + return { + response, + }; + } +} \ No newline at end of file diff --git a/sidecar/src/rpc/index.ts b/sidecar/src/rpc/index.ts index 135ffbde..9bc441b8 100644 --- a/sidecar/src/rpc/index.ts +++ b/sidecar/src/rpc/index.ts @@ -1,78 +1,3 @@ -import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query"; -import { App } from "../app"; -import { AbciService, StatusService, TxService } from "../services"; -import { toSnakeCase } from "../utils"; -import { BroadcastTxSyncResponse, ResultTxSearch } from "../types"; -import querystring from "node:querystring"; -import { SocketStream } from "@fastify/websocket"; - -export function addMethods(context: App) { - context.jsonrpc.addMethod('status', async (): Promise => { - return toSnakeCase( - await context.services.get('status').status() - ); - }); - - context.jsonrpc.addMethod( - 'abci_query', - async ({ path, data }): Promise => { - const result = await context.services - .get('abci') - .query(path, data); - const response = ABCIQueryResponse.toJSON(result); - return { - response, - }; - } - ); - - context.jsonrpc.addMethod( - 'broadcast_tx_sync', - async ({ tx }): Promise => { - const result = await context.services.get('tx').broadcastTx(tx); - const { code, txhash, data, rawLog, codespace } = result.txResponse; - return { - code, - data, - log: rawLog, - codespace, - hash: txhash, - }; - } - ); - - context.jsonrpc.addMethod('tx_search', ({ query }): ResultTxSearch => { - const args = querystring.parse(query); - let hash = args['tx.hash'] as string; - if (hash.includes("'")) { - hash = hash.replace(/'/gi, ''); - } - const txs = context.services.get('tx').searchTx(hash); - txs.forEach(tx => { - tx.tx_result.events.forEach(event => { - event.attributes = context.services.get('tx').encodeAttributes(event.attributes, 'utf8', 'base64'); - }); - }); - - return { - txs, - total_count: txs.length.toString(), - }; - }); - - context.server.get( - '/websocket', - { websocket: true }, - (connection: SocketStream) => { - connection.socket.on('message', async (message) => { - const request = JSON.parse(message.toString()); - const response = await this.jsonrpc.receive(request); - if (response) { - connection.socket.send( - Buffer.from(JSON.stringify(response), 'utf8') - ); - } - }); - } - ); -} +export * from './abci_query'; +export * from './status'; +export * from './tx'; \ No newline at end of file diff --git a/sidecar/src/rpc/status.ts b/sidecar/src/rpc/status.ts new file mode 100644 index 00000000..729c9e38 --- /dev/null +++ b/sidecar/src/rpc/status.ts @@ -0,0 +1,16 @@ +import { StatusService } from "../services"; +import { toSnakeCase } from "../utils"; + +export class StatusRpcHandler { + statusService: StatusService; + + constructor(statusService: StatusService) { + this.statusService = statusService; + } + + async status() { + return toSnakeCase( + await this.statusService.status() + ); + } +} \ No newline at end of file diff --git a/sidecar/src/rpc/tx.ts b/sidecar/src/rpc/tx.ts new file mode 100644 index 00000000..2ed1854c --- /dev/null +++ b/sidecar/src/rpc/tx.ts @@ -0,0 +1,42 @@ +import { TxService } from "../services"; +import { BroadcastTxSyncResponse, ResultTxSearch } from "../types"; +import querystring from "querystring"; + +export class TxRpcHandler { + txService: TxService; + + constructor(txService: TxService) { + this.txService = txService; + } + + async broadcastTxSync({ tx }): Promise { + const result = await this.txService.broadcastTx(tx); + const { code, txhash, data, rawLog, codespace } = result.txResponse; + return { + code, + data, + log: rawLog, + codespace, + hash: txhash, + }; + } + + txSearch({ query }): ResultTxSearch { + const args = querystring.parse(query); + let hash = args['tx.hash'] as string; + if (hash.includes("'")) { + hash = hash.replace(/'/gi, ''); + } + const txs = this.txService.searchTx(hash); + txs.forEach(tx => { + tx.tx_result.events.forEach(event => { + event.attributes = this.txService.encodeAttributes(event.attributes, 'utf8', 'base64'); + }); + }); + + return { + txs, + total_count: txs.length.toString(), + }; + } +} \ No newline at end of file From 89ace7003c7b33e69b7f7a9d3f60f7e27c3e1c1d Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 16:27:15 +0900 Subject: [PATCH 124/133] fix: Fix handler bind issue --- sidecar/src/app.ts | 8 ++++---- sidecar/src/handlers/account.ts | 4 ++-- sidecar/src/handlers/balance.ts | 7 ++++--- sidecar/src/handlers/distribution.ts | 4 ++-- sidecar/src/handlers/nodeinfo.ts | 2 +- sidecar/src/handlers/staking.ts | 8 ++++---- sidecar/src/handlers/tx.ts | 8 ++++---- sidecar/src/handlers/ws.ts | 2 +- sidecar/src/rpc/abci_query.ts | 2 +- sidecar/src/rpc/status.ts | 2 +- sidecar/src/rpc/tx.ts | 4 ++-- 11 files changed, 26 insertions(+), 25 deletions(-) diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 8d6c43b8..738f15b9 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -27,10 +27,10 @@ import { StakingHandler, WebsocketHandler } from "./handlers"; -import { - StatusRpcHandler, - AbciRpcHandler, - TxRpcHandler +import { + StatusRpcHandler, + AbciRpcHandler, + TxRpcHandler } from "./rpc"; export class App { diff --git a/sidecar/src/handlers/account.ts b/sidecar/src/handlers/account.ts index 656f1b32..f6a3be15 100644 --- a/sidecar/src/handlers/account.ts +++ b/sidecar/src/handlers/account.ts @@ -10,11 +10,11 @@ export class AccountHandler { this.accountService = accountService; } - async handleGetAccount( + handleGetAccount = async ( request: FastifyRequest<{ Params: QueryAccountRequest; }> - ): Promise { + ): Promise => { const { address } = request.params; return this.accountService.accounts(address); } diff --git a/sidecar/src/handlers/balance.ts b/sidecar/src/handlers/balance.ts index 3d8fd210..9be5f18a 100644 --- a/sidecar/src/handlers/balance.ts +++ b/sidecar/src/handlers/balance.ts @@ -10,15 +10,16 @@ export class BalanceHandler { balanceService: BalanceService; constructor(balanceService: BalanceService) { - this.balanceService = balanceService + this.balanceService = balanceService; } - async handleGetBalance( + handleGetBalance = async ( request: FastifyRequest<{ Params: QueryAllBalancesRequest; }> - ): Promise { + ): Promise => { const { address } = request.params; + const response = QueryAllBalancesResponse.toJSON( await this.balanceService.balances(address) ); diff --git a/sidecar/src/handlers/distribution.ts b/sidecar/src/handlers/distribution.ts index f247daf9..087cb018 100644 --- a/sidecar/src/handlers/distribution.ts +++ b/sidecar/src/handlers/distribution.ts @@ -13,11 +13,11 @@ export class DistributionHandler { this.distributionService = distributionService; } - handleGetDistribution( + handleGetDistribution = ( request: FastifyRequest<{ Params: QueryDelegationTotalRewardsRequest; }> - ): unknown { + ): unknown => { const { delegatorAddress } = request.params; const response = QueryDelegationTotalRewardsResponse.toJSON( this.distributionService diff --git a/sidecar/src/handlers/nodeinfo.ts b/sidecar/src/handlers/nodeinfo.ts index 94daccfe..972667bb 100644 --- a/sidecar/src/handlers/nodeinfo.ts +++ b/sidecar/src/handlers/nodeinfo.ts @@ -9,7 +9,7 @@ export class NodeInfoHandler { this.nodeInfoService = nodeInfoService; } - async handleGetNodeInfo(): Promise { + handleGetNodeInfo = async (): Promise => { const response = GetNodeInfoResponse.toJSON( await this.nodeInfoService.nodeInfo() ); diff --git a/sidecar/src/handlers/staking.ts b/sidecar/src/handlers/staking.ts index 394996f1..c940e3e4 100644 --- a/sidecar/src/handlers/staking.ts +++ b/sidecar/src/handlers/staking.ts @@ -15,11 +15,11 @@ export class StakingHandler { this.stakingService = stakingService; } - handleGetStaking( + handleGetStaking = ( request: FastifyRequest<{ Params: QueryDelegatorDelegationsRequest; }> - ): unknown { + ): unknown => { const { delegatorAddr } = request.params; const response = QueryDelegatorDelegationsResponse.toJSON( this.stakingService.delegations(delegatorAddr) @@ -27,11 +27,11 @@ export class StakingHandler { return toSnakeCase(response); } - handleGetUnbondingDelegations( + handleGetUnbondingDelegations = ( request: FastifyRequest<{ Params: QueryDelegatorUnbondingDelegationsRequest; }> - ): unknown { + ): unknown => { const { delegatorAddr } = request.params; const response = QueryDelegatorUnbondingDelegationsResponse.toJSON( this.stakingService.unbondingDelegations(delegatorAddr) diff --git a/sidecar/src/handlers/tx.ts b/sidecar/src/handlers/tx.ts index 04d8f9cc..a35edefd 100644 --- a/sidecar/src/handlers/tx.ts +++ b/sidecar/src/handlers/tx.ts @@ -10,14 +10,14 @@ export class TxHandler { this.txService = txService; } - async handlePostTxs( + handlePostTxs = async ( request: FastifyRequest<{ Body: { tx_bytes: string; mode: number; }; }> - ): Promise { + ): Promise => { const { tx_bytes } = request.body; const response = BroadcastTxResponse.toJSON( await this.txService.broadcastTx(tx_bytes) @@ -25,10 +25,10 @@ export class TxHandler { return toSnakeCase(response); } - async handlePostSimulate(request: FastifyRequest<{ + handlePostSimulate = async (request: FastifyRequest<{ Body: { tx_bytes: string }; }> - ): Promise { + ): Promise => { const { tx_bytes } = request.body; const response = SimulateResponse.toJSON( await this.txService.simulate(tx_bytes) diff --git a/sidecar/src/handlers/ws.ts b/sidecar/src/handlers/ws.ts index 1def5da4..d8970dce 100644 --- a/sidecar/src/handlers/ws.ts +++ b/sidecar/src/handlers/ws.ts @@ -8,7 +8,7 @@ export class WebsocketHandler { this.jsonrpc = jsonrpc; } - handlerMessage(connection: SocketStream) { + handlerMessage = (connection: SocketStream) => { connection.socket.on('message', async (message) => { const request = JSON.parse(message.toString()); const response = await this.jsonrpc.receive(request); diff --git a/sidecar/src/rpc/abci_query.ts b/sidecar/src/rpc/abci_query.ts index 3c8f407f..73738de7 100644 --- a/sidecar/src/rpc/abci_query.ts +++ b/sidecar/src/rpc/abci_query.ts @@ -8,7 +8,7 @@ export class AbciRpcHandler { this.abciService = abciService; } - async abciQuery({ path, data }): Promise { + abciQuery = async ({ path, data }): Promise => { const result = await this.abciService.query(path, data); const response = ABCIQueryResponse.toJSON(result); return { diff --git a/sidecar/src/rpc/status.ts b/sidecar/src/rpc/status.ts index 729c9e38..7534a2b7 100644 --- a/sidecar/src/rpc/status.ts +++ b/sidecar/src/rpc/status.ts @@ -8,7 +8,7 @@ export class StatusRpcHandler { this.statusService = statusService; } - async status() { + status = async () => { return toSnakeCase( await this.statusService.status() ); diff --git a/sidecar/src/rpc/tx.ts b/sidecar/src/rpc/tx.ts index 2ed1854c..cc48fbe8 100644 --- a/sidecar/src/rpc/tx.ts +++ b/sidecar/src/rpc/tx.ts @@ -9,7 +9,7 @@ export class TxRpcHandler { this.txService = txService; } - async broadcastTxSync({ tx }): Promise { + broadcastTxSync = async ({ tx }): Promise => { const result = await this.txService.broadcastTx(tx); const { code, txhash, data, rawLog, codespace } = result.txResponse; return { @@ -21,7 +21,7 @@ export class TxRpcHandler { }; } - txSearch({ query }): ResultTxSearch { + txSearch = ({ query }): ResultTxSearch => { const args = querystring.parse(query); let hash = args['tx.hash'] as string; if (hash.includes("'")) { From a27d280d477985f96abe314bf638a267d482af5c Mon Sep 17 00:00:00 2001 From: code0xff Date: Fri, 4 Oct 2024 20:29:17 +0900 Subject: [PATCH 125/133] fix: Add wildcard for cors --- sidecar/src/app.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 738f15b9..d5d9f9e5 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -109,6 +109,14 @@ export class App { this.server = fastify({ logger }); await this.server.register(FastifyWebsocket); + this.server.addHook('preHandler', (req, res, done) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', '*'); + res.header('Access-Control-Allow-Headers', '*'); + + done(); + }); + const balanceHandler = new BalanceHandler(this.services.get('balance')); const accountHandler = new AccountHandler(this.services.get('account')); const distributionHandler = new DistributionHandler(this.services.get('distribution')); From e208e3e04b4e87bdcb782b9f3c43164912090f94 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 5 Oct 2024 00:52:16 +0900 Subject: [PATCH 126/133] fix: Remove waiting for tx result in broadcastTx --- sidecar/src/services/tx.ts | 51 ++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 21f4bac3..bb89e727 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -32,35 +32,28 @@ export class TxService implements ApiService { txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; console.debug(`txHash: ${txHash}`); - while (true) { - const txs = this.searchTx(txHash); - if (txs.length > 0) { - const tx = txs.at(0); - return { - txResponse: { - height: Long.fromString(tx.height), - txhash: txHash.toUpperCase(), - codespace: tx.tx_result.codespace, - code: tx.tx_result.code, - data: tx.tx_result.data, - rawLog: '', - logs: [], - info: tx.tx_result.info, - gasWanted: Long.fromString(tx.tx_result.gas_wanted), - gasUsed: Long.fromString(tx.tx_result.gas_used), - tx: { - typeUrl: '', - value: new Uint8Array(), - }, - timestamp: '', - events: tx.tx_result.events, - }, - }; - } else { - console.debug('Waiting for events...'); - await sleep(1000); - } - } + const height = await chainApi.query.system.number(); + + return { + txResponse: { + height: Long.fromString(height.toString()), + txhash: txHash.toUpperCase(), + codespace: '', + code: 0, + data: '', + rawLog: '', + logs: [], + info: '', + gasWanted: Long.fromNumber(0), + gasUsed: Long.fromNumber(0), + tx: { + typeUrl: '', + value: new Uint8Array(), + }, + timestamp: '', + events: [], + }, + }; } public searchTx(hash: string): ResultTx[] { From ce22879f74d211904006c33a1071a9590ef77243 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 07:41:59 +0900 Subject: [PATCH 127/133] style: Fix indent --- ziggurat/runtime/src/apis.rs | 49 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 5c4e1547..44c0e2c2 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -276,33 +276,32 @@ impl_runtime_apis! { None }; - // Estimated encoded transaction size must be based on the heaviest transaction - // type (EIP1559Transaction) to be compatible with all transaction types. - let mut estimated_transaction_len = data.len() + - // pallet ethereum index: 1 - // transact call index: 1 - // Transaction enum variant: 1 - // chain_id 8 bytes - // nonce: 32 - // max_priority_fee_per_gas: 32 - // max_fee_per_gas: 32 - // gas_limit: 32 - // action: 21 (enum varianrt + call address) - // value: 32 - // access_list: 1 (empty vec size) - // 65 bytes signature - 258; - - if access_list.is_some() { - estimated_transaction_len += access_list.encoded_size(); - } + // Estimated encoded transaction size must be based on the heaviest transaction + // type (EIP1559Transaction) to be compatible with all transaction types. + let mut estimated_transaction_len = data.len() + + // pallet ethereum index: 1 + // transact call index: 1 + // Transaction enum variant: 1 + // chain_id 8 bytes + // nonce: 32 + // max_priority_fee_per_gas: 32 + // max_fee_per_gas: 32 + // gas_limit: 32 + // action: 21 (enum varianrt + call address) + // value: 32 + // access_list: 1 (empty vec size) + // 65 bytes signature + 258; + if access_list.is_some() { + estimated_transaction_len += access_list.encoded_size(); + } - let gas_limit = if gas_limit > U256::from(u64::MAX) { - u64::MAX - } else { - gas_limit.low_u64() - }; + let gas_limit = if gas_limit > U256::from(u64::MAX) { + u64::MAX + } else { + gas_limit.low_u64() + }; let without_base_extrinsic_weight = true; let (weight_limit, proof_size_base_cost) = From d0cbe7191d5857f0d24b318ede424a975f70813f Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 09:04:26 +0900 Subject: [PATCH 128/133] feat: Use geth estimateGas algorithm --- ziggurat/node/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml index 66cf3019..b1705c47 100644 --- a/ziggurat/node/Cargo.toml +++ b/ziggurat/node/Cargo.toml @@ -30,7 +30,7 @@ ziggurat-runtime = { path = "../runtime", default-features = true } fc-api = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } fc-db = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } fc-mapping-sync = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["sql"] } -fc-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["txpool"] } +fc-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["rpc-binary-search-estimate", "txpool"] } fc-rpc-core = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } fp-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } From eed75d605db9278fc9299e1328d702845912db82 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 10:14:41 +0900 Subject: [PATCH 129/133] build: Extract ziggurat implementation --- Cargo.toml | 2 - ziggurat/node/Cargo.toml | 108 --- ziggurat/node/build.rs | 7 - ziggurat/node/src/chain_spec.rs | 223 ------ ziggurat/node/src/cli.rs | 114 ---- ziggurat/node/src/command.rs | 389 ----------- ziggurat/node/src/eth/backend.rs | 79 --- ziggurat/node/src/eth/config.rs | 71 -- ziggurat/node/src/eth/mod.rs | 205 ------ ziggurat/node/src/eth/rpc.rs | 247 ------- ziggurat/node/src/main.rs | 14 - ziggurat/node/src/rpc.rs | 64 -- ziggurat/node/src/service.rs | 484 -------------- ziggurat/runtime/Cargo.toml | 231 ------- ziggurat/runtime/build.rs | 17 - ziggurat/runtime/src/apis.rs | 632 ------------------ ziggurat/runtime/src/benchmarks.rs | 36 - ziggurat/runtime/src/call.rs | 228 ------- ziggurat/runtime/src/configs/mod.rs | 618 ----------------- ziggurat/runtime/src/configs/xcm_config.rs | 193 ------ ziggurat/runtime/src/lib.rs | 333 --------- ziggurat/runtime/src/weights/block_weights.rs | 53 -- .../runtime/src/weights/extrinsic_weights.rs | 53 -- ziggurat/runtime/src/weights/mod.rs | 27 - .../runtime/src/weights/paritydb_weights.rs | 63 -- .../runtime/src/weights/rocksdb_weights.rs | 63 -- ziggurat/zombienet.toml | 18 - 27 files changed, 4572 deletions(-) delete mode 100644 ziggurat/node/Cargo.toml delete mode 100644 ziggurat/node/build.rs delete mode 100644 ziggurat/node/src/chain_spec.rs delete mode 100644 ziggurat/node/src/cli.rs delete mode 100644 ziggurat/node/src/command.rs delete mode 100644 ziggurat/node/src/eth/backend.rs delete mode 100644 ziggurat/node/src/eth/config.rs delete mode 100644 ziggurat/node/src/eth/mod.rs delete mode 100644 ziggurat/node/src/eth/rpc.rs delete mode 100644 ziggurat/node/src/main.rs delete mode 100644 ziggurat/node/src/rpc.rs delete mode 100644 ziggurat/node/src/service.rs delete mode 100644 ziggurat/runtime/Cargo.toml delete mode 100644 ziggurat/runtime/build.rs delete mode 100644 ziggurat/runtime/src/apis.rs delete mode 100644 ziggurat/runtime/src/benchmarks.rs delete mode 100644 ziggurat/runtime/src/call.rs delete mode 100644 ziggurat/runtime/src/configs/mod.rs delete mode 100644 ziggurat/runtime/src/configs/xcm_config.rs delete mode 100644 ziggurat/runtime/src/lib.rs delete mode 100644 ziggurat/runtime/src/weights/block_weights.rs delete mode 100644 ziggurat/runtime/src/weights/extrinsic_weights.rs delete mode 100644 ziggurat/runtime/src/weights/mod.rs delete mode 100644 ziggurat/runtime/src/weights/paritydb_weights.rs delete mode 100644 ziggurat/runtime/src/weights/rocksdb_weights.rs delete mode 100644 ziggurat/zombienet.toml diff --git a/Cargo.toml b/Cargo.toml index fbad4123..273f9bd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,6 @@ members = [ "vendor/composable/cosmwasm/runtime-api", "vendor/moonbeam/precompiles/assets-erc20", "vendor/moonbeam/precompiles/balances-erc20", - "ziggurat/node", - "ziggurat/runtime", ] [workspace.dependencies] diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml deleted file mode 100644 index b1705c47..00000000 --- a/ziggurat/node/Cargo.toml +++ /dev/null @@ -1,108 +0,0 @@ -[package] -name = "ziggurat-node" -version = "0.1.0" -description = "Ziggurat node (Babel testnet)" -authors = ["Haderech Pte. Ltd."] -edition = "2021" -license = "GPL-3.0-or-later" -repository = "https://github.com/noirhq/noir.git" -publish = false - -[[bin]] -name = "ziggurat" -path = "src/main.rs" - -[dependencies] -clap = { version = "4.5", features = ["derive"] } -codec = { package = "parity-scale-codec", version = "3.6" } -color-print = "0.3" -const-hex = "1.12" -docify = "0.2" -futures = "0.3" -jsonrpsee = { version = "0.23", features = ["server"] } -log = "0.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -frame-babel = { workspace = true } -ziggurat-runtime = { path = "../runtime", default-features = true } - -fc-api = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } -fc-db = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } -fc-mapping-sync = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["sql"] } -fc-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", features = ["rpc-binary-search-estimate", "txpool"] } -fc-rpc-core = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } -fp-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } -pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } - -frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-chain-spec = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-offchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-network = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-network-sync = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-rpc-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-sysinfo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-telemetry = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-tracing = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -prometheus-endpoint = { package = "substrate-prometheus-endpoint", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -polkadot-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -polkadot-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-collator = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-consensus-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-consensus-proposer = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-client-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-relay-chain-interface = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } -cumulus-test-relay-sproof-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } - -cosmos-rpc = { workspace = true } -cosmos-runtime-api = { workspace = true } -cosmwasm-rpc = { workspace = true } -cosmwasm-runtime-api = { workspace = true } - -[build-dependencies] -docify = "0.2" -substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407" } - -[features] -default = [] -runtime-benchmarks = [ - "cumulus-primitives-core/runtime-benchmarks", - "frame-benchmarking-cli/runtime-benchmarks", - "frame-benchmarking/runtime-benchmarks", - "polkadot-cli/runtime-benchmarks", - "polkadot-primitives/runtime-benchmarks", - "sc-service/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", - "ziggurat-runtime/runtime-benchmarks", -] -try-runtime = [ - "polkadot-cli/try-runtime", - "sp-runtime/try-runtime", - "ziggurat-runtime/try-runtime", -] diff --git a/ziggurat/node/build.rs b/ziggurat/node/build.rs deleted file mode 100644 index e3bfe311..00000000 --- a/ziggurat/node/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; - -fn main() { - generate_cargo_keys(); - - rerun_if_git_head_changed(); -} diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs deleted file mode 100644 index 5f2c69f8..00000000 --- a/ziggurat/node/src/chain_spec.rs +++ /dev/null @@ -1,223 +0,0 @@ -use cumulus_primitives_core::ParaId; -use frame_babel::VarAddress; -use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT, UNIT}; -use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; -use sc_service::ChainType; -use serde::{Deserialize, Serialize}; -use sp_core::{crypto::Ss58Codec, ecdsa, sr25519, Pair, Public}; -use sp_runtime::traits::{IdentifyAccount, Verify}; -use ziggurat_runtime as runtime; - -/// Specialized `ChainSpec` for the normal parachain runtime. -pub type ChainSpec = sc_service::GenericChainSpec; - -/// The default XCM version to set in genesis config. -const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; - -/// Helper function to generate a crypto pair from seed -pub fn get_from_seed(seed: &str) -> ::Public { - TPublic::Pair::from_string(&format!("//{}", seed), None) - .expect("static values are valid; qed") - .public() -} - -/// The extensions for the [`ChainSpec`]. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChainSpecGroup, ChainSpecExtension)] -pub struct Extensions { - /// The relay chain of the Parachain. - #[serde(alias = "relayChain", alias = "RelayChain")] - pub relay_chain: String, - /// The id of the Parachain. - #[serde(alias = "paraId", alias = "ParaId")] - pub para_id: u32, -} - -impl Extensions { - /// Try to get the extension from the given `ChainSpec`. - pub fn try_get(chain_spec: &dyn sc_service::ChainSpec) -> Option<&Self> { - sc_chain_spec::get_extension(chain_spec.extensions()) - } -} - -type AccountPublic = ::Signer; - -/// Generate collator keys from seed. -/// -/// This function's return type must always match the session keys of the chain in tuple format. -pub fn get_collator_keys_from_seed(seed: &str) -> AuraId { - get_from_seed::(seed) -} - -/// Helper function to generate an account ID from seed -pub fn get_account_id_from_seed(seed: &str) -> AccountId -where - AccountPublic: From<::Public>, -{ - AccountPublic::from(get_from_seed::(seed)).into_account() -} - -/// Generate the session keys from individual elements. -/// -/// The input must be a tuple of individual keys (a single arg for now since we have just one key). -pub fn template_session_keys(keys: AuraId) -> runtime::SessionKeys { - runtime::SessionKeys { aura: keys } -} - -pub fn development_config() -> ChainSpec { - // Give your base currency a unit name and decimal places - let mut properties = sc_chain_spec::Properties::new(); - properties.insert("tokenSymbol".into(), "ZIG".into()); - properties.insert("tokenDecimals".into(), 18.into()); - properties.insert("ss58Format".into(), 42.into()); - - ChainSpec::builder( - runtime::WASM_BINARY.expect("WASM binary was not built, please build it!"), - Extensions { - relay_chain: "paseo-local".into(), - // You MUST set this to the correct network! - para_id: 2000, - }, - ) - .with_name("Development") - .with_id("dev") - .with_chain_type(ChainType::Development) - .with_genesis_config_patch(testnet_genesis( - // initial collators. - vec![ - ( - get_account_id_from_seed::("Alice"), - get_collator_keys_from_seed("Alice"), - ), - ( - get_account_id_from_seed::("Bob"), - get_collator_keys_from_seed("Bob"), - ), - ], - vec![ - get_account_id_from_seed::("Alice"), - get_account_id_from_seed::("Bob"), - get_account_id_from_seed::("Charlie"), - get_account_id_from_seed::("Dave"), - get_account_id_from_seed::("Eve"), - get_account_id_from_seed::("Ferdie"), - get_account_id_from_seed::("Alice//stash"), - get_account_id_from_seed::("Bob//stash"), - get_account_id_from_seed::("Charlie//stash"), - get_account_id_from_seed::("Dave//stash"), - get_account_id_from_seed::("Eve//stash"), - get_account_id_from_seed::("Ferdie//stash"), - get_account_id_from_seed::("Alice"), - AccountId::from_string("5HSVzaktLBoAFZ89RQHEWrJizmSuXrbpGm7favc99p5qt6UM").unwrap(), - ], - get_account_id_from_seed::("Alice"), - 2000.into(), - )) - .build() -} - -pub fn local_testnet_config() -> ChainSpec { - // Give your base currency a unit name and decimal places - let mut properties = sc_chain_spec::Properties::new(); - properties.insert("tokenSymbol".into(), "ZIG".into()); - properties.insert("tokenDecimals".into(), 18.into()); - properties.insert("ss58Format".into(), 42.into()); - - #[allow(deprecated)] - ChainSpec::builder( - runtime::WASM_BINARY.expect("WASM binary was not built, please build it!"), - Extensions { - relay_chain: "paseo-local".into(), - // You MUST set this to the correct network! - para_id: 2000, - }, - ) - .with_name("Local Testnet") - .with_id("local_testnet") - .with_chain_type(ChainType::Local) - .with_genesis_config_patch(testnet_genesis( - // initial collators. - vec![ - ( - get_account_id_from_seed::("Alice"), - get_collator_keys_from_seed("Alice"), - ), - ( - get_account_id_from_seed::("Bob"), - get_collator_keys_from_seed("Bob"), - ), - ], - vec![ - get_account_id_from_seed::("Alice"), - get_account_id_from_seed::("Bob"), - get_account_id_from_seed::("Charlie"), - get_account_id_from_seed::("Dave"), - get_account_id_from_seed::("Eve"), - get_account_id_from_seed::("Ferdie"), - get_account_id_from_seed::("Alice//stash"), - get_account_id_from_seed::("Bob//stash"), - get_account_id_from_seed::("Charlie//stash"), - get_account_id_from_seed::("Dave//stash"), - get_account_id_from_seed::("Eve//stash"), - get_account_id_from_seed::("Ferdie//stash"), - get_account_id_from_seed::("Alice"), - AccountId::from_string("5HSVzaktLBoAFZ89RQHEWrJizmSuXrbpGm7favc99p5qt6UM").unwrap(), - ], - get_account_id_from_seed::("Alice"), - 2000.into(), - )) - .with_protocol_id("ziggurat-local") - .with_properties(properties) - .build() -} - -fn testnet_genesis( - invulnerables: Vec<(AccountId, AuraId)>, - endowed_accounts: Vec, - root: AccountId, - id: ParaId, -) -> serde_json::Value { - serde_json::json!({ - "addressMap": { - "multimap": vec![ - address_map(get_from_seed::("Alice")), - address_map(ecdsa::Public::from(const_hex::const_decode_to_array(b"0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f").unwrap())), - ], - }, - "balances": { - "balances": endowed_accounts.iter().cloned().map(|k| (k, 1_000_000 * UNIT)).collect::>(), - }, - "parachainInfo": { - "parachainId": id, - }, - "collatorSelection": { - "invulnerables": invulnerables.iter().cloned().map(|(acc, _)| acc).collect::>(), - "candidacyBond": EXISTENTIAL_DEPOSIT * 16, - }, - "session": { - "keys": invulnerables - .into_iter() - .map(|(acc, aura)| { - ( - acc.clone(), // account id - acc, // validator id - template_session_keys(aura), // session keys - ) - }) - .collect::>(), - }, - "polkadotXcm": { - "safeXcmVersion": Some(SAFE_XCM_VERSION), - }, - "sudo": { "key": Some(root) } - }) -} - -fn address_map(account: ecdsa::Public) -> (AccountId, Vec) { - ( - account.clone().into(), - vec![ - VarAddress::Cosmos(account.clone().into()), - VarAddress::Ethereum(account.clone().into()), - ], - ) -} diff --git a/ziggurat/node/src/cli.rs b/ziggurat/node/src/cli.rs deleted file mode 100644 index ccf36d91..00000000 --- a/ziggurat/node/src/cli.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::path::PathBuf; - -/// Sub-commands supported by the collator. -#[allow(clippy::large_enum_variant)] -#[derive(Debug, clap::Subcommand)] -pub enum Subcommand { - /// Build a chain specification. - BuildSpec(sc_cli::BuildSpecCmd), - - /// Validate blocks. - CheckBlock(sc_cli::CheckBlockCmd), - - /// Export blocks. - ExportBlocks(sc_cli::ExportBlocksCmd), - - /// Export the state of a given block into a chain spec. - ExportState(sc_cli::ExportStateCmd), - - /// Import blocks. - ImportBlocks(sc_cli::ImportBlocksCmd), - - /// Revert the chain to a previous state. - Revert(sc_cli::RevertCmd), - - /// Remove the whole chain. - PurgeChain(cumulus_client_cli::PurgeChainCmd), - - /// Export the genesis head data of the parachain. - /// - /// Head data is the encoded block header. - #[command(alias = "export-genesis-state")] - ExportGenesisHead(cumulus_client_cli::ExportGenesisHeadCommand), - - /// Export the genesis wasm of the parachain. - ExportGenesisWasm(cumulus_client_cli::ExportGenesisWasmCommand), - - /// Sub-commands concerned with benchmarking. - /// The pallet benchmarking moved to the `pallet` sub-command. - #[command(subcommand)] - Benchmark(frame_benchmarking_cli::BenchmarkCmd), -} - -const AFTER_HELP_EXAMPLE: &str = color_print::cstr!( - r#"Examples: - ziggurat build-spec --disable-default-bootnode > ziggurat.plain.json - Export a chainspec for a local testnet in json format. - ziggurat --chain ziggurat.plain.json --tmp -- --chain paseo-local - Launch a full node with chain specification loaded from ziggurat.plain.json. - ziggurat - Launch a full node with default parachain local-testnet and relay chain paseo-local. - ziggurat --collator - Launch a collator with default parachain local-testnet and relay chain paseo-local. - "# -); -#[derive(Debug, clap::Parser)] -#[command( - propagate_version = true, - args_conflicts_with_subcommands = true, - subcommand_negates_reqs = true -)] -#[clap(after_help = AFTER_HELP_EXAMPLE)] -pub struct Cli { - #[command(subcommand)] - pub subcommand: Option, - - #[command(flatten)] - pub run: cumulus_client_cli::RunCmd, - - #[command(flatten)] - pub eth_config: crate::eth::Configuration, - - /// Disable automatic hardware benchmarks. - /// - /// By default these benchmarks are automatically ran at startup and measure - /// the CPU speed, the memory bandwidth and the disk speed. - /// - /// The results are then printed out in the logs, and also sent as part of - /// telemetry, if telemetry is enabled. - #[arg(long)] - pub no_hardware_benchmarks: bool, - - /// Relay chain arguments - #[arg(raw = true)] - pub relay_chain_args: Vec, -} - -#[derive(Debug)] -pub struct RelayChainCli { - /// The actual relay chain cli object. - pub base: polkadot_cli::RunCmd, - - /// Optional chain id that should be passed to the relay chain. - pub chain_id: Option, - - /// The base path that should be used by the relay chain. - pub base_path: Option, -} - -impl RelayChainCli { - /// Parse the relay chain CLI parameters using the para chain `Configuration`. - pub fn new<'a>( - para_config: &sc_service::Configuration, - relay_chain_args: impl Iterator, - ) -> Self { - let extension = crate::chain_spec::Extensions::try_get(&*para_config.chain_spec); - let chain_id = extension.map(|e| e.relay_chain.clone()); - let base_path = para_config.base_path.path().join("polkadot"); - Self { - base_path: Some(base_path), - chain_id, - base: clap::Parser::parse_from(relay_chain_args), - } - } -} diff --git a/ziggurat/node/src/command.rs b/ziggurat/node/src/command.rs deleted file mode 100644 index 0acacfe8..00000000 --- a/ziggurat/node/src/command.rs +++ /dev/null @@ -1,389 +0,0 @@ -use std::net::SocketAddr; - -use cumulus_client_service::storage_proof_size::HostFunctions as ReclaimHostFunctions; -use cumulus_primitives_core::ParaId; -use frame_benchmarking_cli::{BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE}; -use log::info; -use sc_cli::{ - ChainSpec, CliConfiguration, DefaultConfigurationValues, ImportParams, KeystoreParams, - NetworkParams, Result, SharedParams, SubstrateCli, -}; -use sc_service::config::{BasePath, PrometheusConfig}; -use ziggurat_runtime::Block; - -use crate::{ - chain_spec, - cli::{Cli, RelayChainCli, Subcommand}, - service::new_partial, -}; - -const IMPL_NAME: &str = "Ziggurat Node"; - -fn load_spec(id: &str) -> std::result::Result, String> { - Ok(match id { - "dev" => Box::new(chain_spec::development_config()), - "" | "local" => Box::new(chain_spec::local_testnet_config()), - path => Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?), - }) -} - -impl SubstrateCli for Cli { - fn impl_name() -> String { - IMPL_NAME.into() - } - - fn impl_version() -> String { - env!("SUBSTRATE_CLI_IMPL_VERSION").into() - } - - fn description() -> String { - format!( - "{}\n\nThe command-line arguments provided first will be \ - passed to the parachain node, while the arguments provided after -- will be passed \ - to the relay chain node.\n\n\ - {} -- ", - IMPL_NAME, - Self::executable_name() - ) - } - - fn author() -> String { - env!("CARGO_PKG_AUTHORS").into() - } - - fn support_url() -> String { - "https://github.com/noirhq/noir/issues/new".into() - } - - fn copyright_start_year() -> i32 { - 2021 - } - - fn load_spec(&self, id: &str) -> std::result::Result, String> { - load_spec(id) - } -} - -impl SubstrateCli for RelayChainCli { - fn impl_name() -> String { - IMPL_NAME.into() - } - - fn impl_version() -> String { - env!("SUBSTRATE_CLI_IMPL_VERSION").into() - } - - fn description() -> String { - format!( - "{}\n\nThe command-line arguments provided first will be \ - passed to the parachain node, while the arguments provided after -- will be passed \ - to the relay chain node.\n\n\ - {} -- ", - IMPL_NAME, - Self::executable_name() - ) - } - - fn author() -> String { - env!("CARGO_PKG_AUTHORS").into() - } - - fn support_url() -> String { - "https://github.com/noirhq/noir/issues/new".into() - } - - fn copyright_start_year() -> i32 { - 2021 - } - - fn load_spec(&self, id: &str) -> std::result::Result, String> { - polkadot_cli::Cli::from_iter([RelayChainCli::executable_name()].iter()).load_spec(id) - } -} - -macro_rules! construct_async_run { - (|$components:ident, $cli:ident, $cmd:ident, $config:ident| $( $code:tt )* ) => {{ - let runner = $cli.create_runner($cmd)?; - runner.async_run(|$config| { - let $components = new_partial(&$config)?; - let task_manager = $components.task_manager; - { $( $code )* }.map(|v| (v, task_manager)) - }) - }} -} - -/// Parse command line arguments into service configuration. -pub fn run() -> Result<()> { - let cli = Cli::from_args(); - - match &cli.subcommand { - Some(Subcommand::BuildSpec(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) - }, - Some(Subcommand::CheckBlock(cmd)) => { - construct_async_run!(|components, cli, cmd, config| { - Ok(cmd.run(components.client, components.import_queue)) - }) - }, - Some(Subcommand::ExportBlocks(cmd)) => { - construct_async_run!(|components, cli, cmd, config| { - Ok(cmd.run(components.client, config.database)) - }) - }, - Some(Subcommand::ExportState(cmd)) => { - construct_async_run!(|components, cli, cmd, config| { - Ok(cmd.run(components.client, config.chain_spec)) - }) - }, - Some(Subcommand::ImportBlocks(cmd)) => { - construct_async_run!(|components, cli, cmd, config| { - Ok(cmd.run(components.client, components.import_queue)) - }) - }, - Some(Subcommand::Revert(cmd)) => { - construct_async_run!(|components, cli, cmd, config| { - Ok(cmd.run(components.client, components.backend, None)) - }) - }, - Some(Subcommand::PurgeChain(cmd)) => { - let runner = cli.create_runner(cmd)?; - - runner.sync_run(|config| { - let polkadot_cli = RelayChainCli::new( - &config, - [RelayChainCli::executable_name()].iter().chain(cli.relay_chain_args.iter()), - ); - - let polkadot_config = SubstrateCli::create_configuration( - &polkadot_cli, - &polkadot_cli, - config.tokio_handle.clone(), - ) - .map_err(|err| format!("Relay chain argument error: {}", err))?; - - cmd.run(config, polkadot_config) - }) - }, - Some(Subcommand::ExportGenesisHead(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| { - let partials = new_partial(&config)?; - - cmd.run(partials.client) - }) - }, - Some(Subcommand::ExportGenesisWasm(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|_config| { - let spec = cli.load_spec(&cmd.shared_params.chain.clone().unwrap_or_default())?; - cmd.run(&*spec) - }) - }, - Some(Subcommand::Benchmark(cmd)) => { - let runner = cli.create_runner(cmd)?; - // Switch on the concrete benchmark sub-command- - match cmd { - BenchmarkCmd::Pallet(cmd) => - if cfg!(feature = "runtime-benchmarks") { - runner.sync_run(|config| cmd.run_with_spec::, ReclaimHostFunctions>(Some(config.chain_spec))) - } else { - Err("Benchmarking wasn't enabled when building the node. \ - You can enable it with `--features runtime-benchmarks`." - .into()) - }, - BenchmarkCmd::Block(cmd) => runner.sync_run(|config| { - let partials = new_partial(&config)?; - cmd.run(partials.client) - }), - #[cfg(not(feature = "runtime-benchmarks"))] - BenchmarkCmd::Storage(_) => Err(sc_cli::Error::Input( - "Compile with --features=runtime-benchmarks \ - to enable storage benchmarks." - .into(), - )), - #[cfg(feature = "runtime-benchmarks")] - BenchmarkCmd::Storage(cmd) => runner.sync_run(|config| { - let partials = new_partial(&config)?; - let db = partials.backend.expose_db(); - let storage = partials.backend.expose_storage(); - cmd.run(config, partials.client.clone(), db, storage) - }), - BenchmarkCmd::Machine(cmd) => - runner.sync_run(|config| cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone())), - // NOTE: this allows the Client to leniently implement - // new benchmark commands without requiring a companion MR. - #[allow(unreachable_patterns)] - _ => Err("Benchmarking sub-command unsupported".into()), - } - }, - None => { - let runner = cli.create_runner(&cli.run.normalize())?; - let collator_options = cli.run.collator_options(); - - runner.run_node_until_exit(|config| async move { - let hwbench = (!cli.no_hardware_benchmarks) - .then_some(config.database.path().map(|database_path| { - let _ = std::fs::create_dir_all(database_path); - sc_sysinfo::gather_hwbench(Some(database_path)) - })) - .flatten(); - - let para_id = chain_spec::Extensions::try_get(&*config.chain_spec) - .map(|e| e.para_id) - .ok_or("Could not find parachain ID in chain-spec.")?; - - let polkadot_cli = RelayChainCli::new( - &config, - [RelayChainCli::executable_name()].iter().chain(cli.relay_chain_args.iter()), - ); - - let id = ParaId::from(para_id); - - let tokio_handle = config.tokio_handle.clone(); - let polkadot_config = - SubstrateCli::create_configuration(&polkadot_cli, &polkadot_cli, tokio_handle) - .map_err(|err| format!("Relay chain argument error: {}", err))?; - - info!("Is collating: {}", if config.role.is_authority() { "yes" } else { "no" }); - - crate::service::start_parachain_node( - config, - polkadot_config, - cli.eth_config, - collator_options, - id, - hwbench, - ) - .await - .map(|r| r.0) - .map_err(Into::into) - }) - }, - } -} - -impl DefaultConfigurationValues for RelayChainCli { - fn p2p_listen_port() -> u16 { - 30334 - } - - fn rpc_listen_port() -> u16 { - 9945 - } - - fn prometheus_listen_port() -> u16 { - 9616 - } -} - -impl CliConfiguration for RelayChainCli { - fn shared_params(&self) -> &SharedParams { - self.base.base.shared_params() - } - - fn import_params(&self) -> Option<&ImportParams> { - self.base.base.import_params() - } - - fn network_params(&self) -> Option<&NetworkParams> { - self.base.base.network_params() - } - - fn keystore_params(&self) -> Option<&KeystoreParams> { - self.base.base.keystore_params() - } - - fn base_path(&self) -> Result> { - Ok(self - .shared_params() - .base_path()? - .or_else(|| self.base_path.clone().map(Into::into))) - } - - fn rpc_addr(&self, default_listen_port: u16) -> Result> { - self.base.base.rpc_addr(default_listen_port) - } - - fn prometheus_config( - &self, - default_listen_port: u16, - chain_spec: &Box, - ) -> Result> { - self.base.base.prometheus_config(default_listen_port, chain_spec) - } - - fn init( - &self, - _support_url: &String, - _impl_version: &String, - _logger_hook: F, - _config: &sc_service::Configuration, - ) -> Result<()> - where - F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration), - { - unreachable!("PolkadotCli is never initialized; qed"); - } - - fn chain_id(&self, is_dev: bool) -> Result { - let chain_id = self.base.base.chain_id(is_dev)?; - - Ok(if chain_id.is_empty() { self.chain_id.clone().unwrap_or_default() } else { chain_id }) - } - - fn role(&self, is_dev: bool) -> Result { - self.base.base.role(is_dev) - } - - fn transaction_pool(&self, is_dev: bool) -> Result { - self.base.base.transaction_pool(is_dev) - } - - fn trie_cache_maximum_size(&self) -> Result> { - self.base.base.trie_cache_maximum_size() - } - - fn rpc_methods(&self) -> Result { - self.base.base.rpc_methods() - } - - fn rpc_max_connections(&self) -> Result { - self.base.base.rpc_max_connections() - } - - fn rpc_cors(&self, is_dev: bool) -> Result>> { - self.base.base.rpc_cors(is_dev) - } - - fn default_heap_pages(&self) -> Result> { - self.base.base.default_heap_pages() - } - - fn force_authoring(&self) -> Result { - self.base.base.force_authoring() - } - - fn disable_grandpa(&self) -> Result { - self.base.base.disable_grandpa() - } - - fn max_runtime_instances(&self) -> Result> { - self.base.base.max_runtime_instances() - } - - fn announce_block(&self) -> Result { - self.base.base.announce_block() - } - - fn telemetry_endpoints( - &self, - chain_spec: &Box, - ) -> Result> { - self.base.base.telemetry_endpoints(chain_spec) - } - - fn node_name(&self) -> Result { - self.base.base.node_name() - } -} diff --git a/ziggurat/node/src/eth/backend.rs b/ziggurat/node/src/eth/backend.rs deleted file mode 100644 index 4d003afe..00000000 --- a/ziggurat/node/src/eth/backend.rs +++ /dev/null @@ -1,79 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ziggurat_runtime::opaque::Block; - -use fc_db::Backend; -use fc_rpc::StorageOverrideHandler; -use fp_rpc::EthereumRuntimeRPCApi; -use sc_client_api::{HeaderBackend, StorageProvider}; -use sc_service::Configuration; -use sp_api::ProvideRuntimeApi; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use crate::{eth::FrontierBackendType, service::ParachainBackend}; - -pub fn db_config_dir(config: &Configuration) -> PathBuf { - config.base_path.config_dir(config.chain_spec.id()) -} - -pub fn open_frontier_backend( - client: Arc, - config: &Configuration, - eth_config: &super::Configuration, -) -> Result, String> -where - C: HeaderBackend - + ProvideRuntimeApi - + StorageProvider - + 'static, - C::Api: EthereumRuntimeRPCApi, -{ - Ok(match eth_config.frontier_backend_type { - FrontierBackendType::KeyValue => Backend::KeyValue(Arc::new(fc_db::kv::Backend::open( - client.clone(), - &config.database, - &db_config_dir(config), - )?)), - FrontierBackendType::Sql => { - let overrides = Arc::new(StorageOverrideHandler::new(client.clone())); - let db_path = db_config_dir(config).join("sql"); - std::fs::create_dir_all(&db_path).expect("failed creating sql db directory"); - let backend = futures::executor::block_on(fc_db::sql::Backend::new( - fc_db::sql::BackendConfig::Sqlite(fc_db::sql::SqliteBackendConfig { - path: Path::new("sqlite:///") - .join(db_path) - .join("frontier.db3") - .to_str() - .unwrap(), - create_if_missing: true, - thread_count: eth_config.frontier_sql_backend_thread_count, - cache_size: eth_config.frontier_sql_backend_cache_size, - }), - eth_config.frontier_sql_backend_pool_size, - std::num::NonZeroU32::new(eth_config.frontier_sql_backend_num_ops_timeout), - overrides, - )) - .unwrap_or_else(|err| panic!("failed creating sql backend: {:?}", err)); - Backend::Sql(Arc::new(backend)) - }, - }) -} diff --git a/ziggurat/node/src/eth/config.rs b/ziggurat/node/src/eth/config.rs deleted file mode 100644 index 8241cd50..00000000 --- a/ziggurat/node/src/eth/config.rs +++ /dev/null @@ -1,71 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -/// Available frontier backend types. -#[derive(Debug, Copy, Clone, Default, clap::ValueEnum)] -pub enum FrontierBackendType { - /// Either RocksDb or ParityDb as per inherited from the global backend settings. - #[default] - KeyValue, - /// Sql database with custom log indexing. - Sql, -} - -#[derive(Debug, clap::Parser)] -pub struct Configuration { - /// Maximum number of logs in a query. - #[arg(long, default_value = "10000")] - pub max_past_logs: u32, - - /// Maximum fee history cache size. - #[arg(long, default_value = "2048")] - pub fee_history_limit: u64, - - /// The dynamic-fee pallet target gas price set by block author - #[arg(long, default_value = "1")] - pub target_gas_price: u64, - - /// Size in bytes of the LRU cache for block data. - #[arg(long, default_value = "50")] - pub eth_log_block_cache: usize, - - /// Size in bytes of the LRU cache for transactions statuses data. - #[arg(long, default_value = "50")] - pub eth_statuses_cache: usize, - - /// Sets the frontier backend type (KeyValue or Sql) - #[arg(long, value_enum, ignore_case = true, default_value_t = FrontierBackendType::default())] - pub frontier_backend_type: FrontierBackendType, - - // Sets the SQL backend's pool size. - #[arg(long, default_value = "100")] - pub frontier_sql_backend_pool_size: u32, - - /// Sets the SQL backend's query timeout in number of VM ops. - #[arg(long, default_value = "10000000")] - pub frontier_sql_backend_num_ops_timeout: u32, - - /// Sets the SQL backend's auxiliary thread limit. - #[arg(long, default_value = "4")] - pub frontier_sql_backend_thread_count: u32, - - /// Sets the SQL backend's query timeout in number of VM ops. - /// Default value is 200MB. - #[arg(long, default_value = "209715200")] - pub frontier_sql_backend_cache_size: u64, -} diff --git a/ziggurat/node/src/eth/mod.rs b/ziggurat/node/src/eth/mod.rs deleted file mode 100644 index 0a5a7ec1..00000000 --- a/ziggurat/node/src/eth/mod.rs +++ /dev/null @@ -1,205 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -mod backend; -mod config; -pub mod rpc; - -pub use backend::*; -pub use config::*; - -use fc_mapping_sync::{kv::MappingSyncWorker, SyncStrategy}; -use fc_rpc::{EthTask, StorageOverride}; -use fc_rpc_core::types::{FeeHistoryCache, FeeHistoryCacheLimit, FilterPool}; -use fp_rpc::EthereumRuntimeRPCApi; -use futures::prelude::*; -use jsonrpsee::RpcModule; -use sc_client_api::{ - backend::{Backend, StateBackend, StorageProvider}, - blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}, - client::BlockchainEvents, - BlockOf, -}; -use sc_network_sync::SyncingService; -use sc_rpc_api::DenyUnsafe; -use sc_service::{Error as ServiceError, TaskManager}; -use sp_api::ProvideRuntimeApi; -use sp_block_builder::BlockBuilder; -use sp_core::H256; -use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Header as HeaderT}; -use std::{ - collections::BTreeMap, - sync::{Arc, Mutex}, - time::Duration, -}; - -pub struct SpawnTasksParams<'a, B: BlockT, C, BE> { - pub config: sc_service::Configuration, - pub rpc_builder: Box Result, ServiceError>>, - pub task_manager: &'a mut TaskManager, - pub client: Arc, - pub substrate_backend: Arc, - pub frontier_backend: Arc>, - pub filter_pool: Option, - pub storage_override: Arc>, - pub fee_history_cache: FeeHistoryCache, - pub fee_history_cache_limit: FeeHistoryCacheLimit, - pub sync: Arc>, - pub pubsub_notification_sinks: Arc< - fc_mapping_sync::EthereumBlockNotificationSinks< - fc_mapping_sync::EthereumBlockNotification, - >, - >, -} - -pub fn spawn_tasks( - params: SpawnTasksParams, -) -> Result -where - C: ProvideRuntimeApi + BlockOf, - C: HeaderBackend + HeaderMetadata + 'static, - C: BlockchainEvents + StorageProvider, - C: Send + Sync + 'static, - C::Api: EthereumRuntimeRPCApi, - C::Api: BlockBuilder, - B: BlockT + Send + Sync + 'static, - B::Header: HeaderT, - BE: Backend + 'static, - BE::State: StateBackend, -{ - let SpawnTasksParams { - mut config, - rpc_builder, - task_manager, - client, - substrate_backend, - frontier_backend, - filter_pool, - storage_override, - fee_history_cache, - fee_history_cache_limit, - sync, - pubsub_notification_sinks, - } = params; - - let rpc_port = config.rpc_addr.map(|addr| addr.port()).unwrap_or(config.rpc_port); - let prometheus_config = config.prometheus_config.take(); - - // TODO: Make the Ethereum RPC port configurable. - let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(8545)); - - let rpc = sc_service::start_rpc_servers( - &config, - rpc_builder, - Some(Box::new(fc_rpc::EthereumSubIdProvider)), - ); - if rpc.is_ok() { - log::info!( - "Ethereum RPC started: {}", - config.rpc_addr.as_ref().map(|addr| addr.port()).unwrap_or(0) - ); - } else { - log::warn!("Ethereum RPC not started"); - } - task_manager.keep_alive(rpc); - - // Spawn main mapping sync worker background task. - match *frontier_backend { - fc_db::Backend::KeyValue(ref b) => { - task_manager.spawn_essential_handle().spawn( - "frontier-mapping-sync-worker", - Some("frontier"), - MappingSyncWorker::new( - client.import_notification_stream(), - Duration::new(6, 0), - client.clone(), - substrate_backend.clone(), - storage_override.clone(), - b.clone(), - 3, - 0, - SyncStrategy::Parachain, - sync.clone(), - pubsub_notification_sinks.clone(), - ) - .for_each(|()| future::ready(())), - ); - }, - fc_db::Backend::Sql(ref b) => { - task_manager.spawn_essential_handle().spawn_blocking( - "frontier-mapping-sync-worker", - Some("frontier"), - fc_mapping_sync::sql::SyncWorker::run( - client.clone(), - substrate_backend.clone(), - b.clone(), - client.import_notification_stream(), - fc_mapping_sync::sql::SyncWorkerConfig { - read_notification_timeout: Duration::from_secs(10), - check_indexed_blocks_interval: Duration::from_secs(60), - }, - fc_mapping_sync::SyncStrategy::Parachain, - sync.clone(), - pubsub_notification_sinks.clone(), - ), - ); - }, - } - - // Spawn Frontier EthFilterApi maintenance task. - if let Some(filter_pool) = filter_pool { - // Each filter is allowed to stay in the pool for 100 blocks. - const FILTER_RETAIN_THRESHOLD: u64 = 100; - task_manager.spawn_essential_handle().spawn( - "frontier-filter-pool", - Some("frontier"), - EthTask::filter_pool_task(client.clone(), filter_pool, FILTER_RETAIN_THRESHOLD), - ); - } - - // Spawn Frontier FeeHistory cache maintenance task. - task_manager.spawn_essential_handle().spawn( - "frontier-fee-history", - Some("frontier"), - EthTask::fee_history_task( - client.clone(), - storage_override.clone(), - fee_history_cache, - fee_history_cache_limit, - ), - ); - - let _ = config.rpc_addr.as_mut().map(|addr| addr.set_port(rpc_port)); - config.prometheus_config = prometheus_config; - - Ok(config) -} - -pub struct PartialComponents { - pub filter_pool: Option, - pub fee_history_cache: FeeHistoryCache, - pub fee_history_cache_limit: FeeHistoryCacheLimit, -} - -pub fn new_partial(config: &Configuration) -> Result { - Ok(PartialComponents { - filter_pool: Some(Arc::new(Mutex::new(BTreeMap::new()))), - fee_history_cache: Arc::new(Mutex::new(BTreeMap::new())), - fee_history_cache_limit: config.fee_history_limit, - }) -} diff --git a/ziggurat/node/src/eth/rpc.rs b/ziggurat/node/src/eth/rpc.rs deleted file mode 100644 index 1e908344..00000000 --- a/ziggurat/node/src/eth/rpc.rs +++ /dev/null @@ -1,247 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ziggurat_runtime::opaque::Block; - -use cumulus_primitives_core::PersistedValidationData; -use cumulus_primitives_parachain_inherent::ParachainInherentData; -use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; -use fc_rpc::{EthBlockDataCacheTask, StorageOverride}; -use fc_rpc_core::types::{FeeHistoryCache, FeeHistoryCacheLimit, FilterPool}; -use fp_rpc::{ConvertTransactionRuntimeApi, EthereumRuntimeRPCApi}; -use jsonrpsee::RpcModule; -use sc_client_api::{ - client::BlockchainEvents, AuxStore, Backend, HeaderBackend, StorageProvider, UsageProvider, -}; -use sc_network::service::traits::NetworkService; -use sc_network_sync::SyncingService; -use sc_rpc::SubscriptionTaskExecutor; -use sc_transaction_pool::{ChainApi, Pool}; -use sc_transaction_pool_api::TransactionPool; -use sp_api::{CallApiAt, ProvideRuntimeApi}; -use sp_block_builder::BlockBuilder; -use sp_consensus_aura::{sr25519::AuthorityId as AuraId, AuraApi}; -use sp_core::H256; -use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; - -pub struct EthConfig(PhantomData<(C, BE)>); - -impl fc_rpc::EthConfig for EthConfig -where - C: StorageProvider + Send + Sync + 'static, - BE: Backend + 'static, -{ - type EstimateGasAdapter = (); - type RuntimeStorageOverride = - fc_rpc::frontier_backend_client::SystemAccountId20StorageOverride; -} - -/// Extra dependencies for Ethereum compatibility. -pub struct FullDeps { - /// The client instance to use. - pub client: Arc, - /// Transaction pool instance. - pub pool: Arc

, - /// Graph pool instance. - pub graph: Arc>, - /// The Node authority flag - pub is_authority: bool, - /// Network service - pub network: Arc, - /// Chain syncing service - pub sync: Arc>, - /// Frontier Backend. - pub frontier_backend: Arc>, - /// Ethereum data access overrides. - pub storage_override: Arc>, - /// Cache for Ethereum block data. - pub block_data_cache: Arc>, - /// EthFilterApi pool. - pub filter_pool: Option, - /// Maximum number of logs in a query. - pub max_past_logs: u32, - /// Fee history cache. - pub fee_history_cache: FeeHistoryCache, - /// Maximum fee history cache size. - pub fee_history_cache_limit: FeeHistoryCacheLimit, - /// Mandated parent hashes for a given block hash. - pub forced_parent_hashes: Option>, - /// - pub subscription_task_executor: SubscriptionTaskExecutor, - /// - pub pubsub_notification_sinks: Arc< - fc_mapping_sync::EthereumBlockNotificationSinks< - fc_mapping_sync::EthereumBlockNotification, - >, - >, -} - -/// Instantiate Ethereum-compatible RPC extensions. -pub fn create_full( - deps: FullDeps, -) -> Result, Box> -where - C: CallApiAt - + ProvideRuntimeApi - + AuxStore - + StorageProvider - + UsageProvider - + HeaderBackend - + BlockchainEvents - + 'static, - C::Api: AuraApi - + EthereumRuntimeRPCApi - + BlockBuilder - + ConvertTransactionRuntimeApi, - P: TransactionPool + 'static, - A: ChainApi + 'static, - BE: Backend + 'static, -{ - use fc_rpc::{ - pending::AuraConsensusDataProvider, Debug, DebugApiServer, Eth, EthApiServer, EthFilter, - EthFilterApiServer, EthPubSub, EthPubSubApiServer, Net, NetApiServer, TxPool, - TxPoolApiServer, Web3, Web3ApiServer, - }; - - let mut io = RpcModule::new(()); - let FullDeps { - client, - pool, - graph, - is_authority, - network, - sync, - frontier_backend, - storage_override, - block_data_cache, - filter_pool, - max_past_logs, - fee_history_cache, - fee_history_cache_limit, - forced_parent_hashes, - subscription_task_executor, - pubsub_notification_sinks, - } = deps; - - let signers = Vec::new(); - - enum Never {} - impl fp_rpc::ConvertTransaction for Never { - fn convert_transaction(&self, _transaction: pallet_ethereum::Transaction) -> T { - // The Never type is not instantiable, but this method requires the type to be - // instantiated to be called (`&self` parameter), so if the code compiles we - // have the guarantee that this function will never be called. - unreachable!() - } - } - let convert_transaction: Option = None; - - let pending_create_inherent_data_providers = move |_, _| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - // Create a dummy parachain inherent data provider which is required to pass - // the checks by the para chain system. We use dummy values because in the 'pending - // context' neither do we have access to the real values nor do we need them. - let (relay_parent_storage_root, relay_chain_state) = - RelayStateSproofBuilder::default().into_state_root_and_proof(); - let vfp = PersistedValidationData { - // This is a hack to make - // `cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases` happy. Relay - // parent number can't be bigger than u32::MAX. - relay_parent_number: u32::MAX, - relay_parent_storage_root, - ..Default::default() - }; - let parachain_inherent_data = ParachainInherentData { - validation_data: vfp, - relay_chain_state, - downward_messages: Default::default(), - horizontal_messages: Default::default(), - }; - Ok((timestamp, parachain_inherent_data)) - }; - - io.merge( - Eth::<_, _, _, _, _, _, _, EthConfig<_, _>>::new( - client.clone(), - pool.clone(), - graph.clone(), - convert_transaction, - sync.clone(), - signers, - storage_override.clone(), - frontier_backend.clone(), - is_authority, - block_data_cache.clone(), - fee_history_cache, - fee_history_cache_limit, - 10, /* execute_gas_limit_multiplier */ - forced_parent_hashes, - pending_create_inherent_data_providers, - Some(Box::new(AuraConsensusDataProvider::new(client.clone()))), - ) - .replace_config::>() - .into_rpc(), - )?; - - if let Some(filter_pool) = filter_pool { - io.merge( - EthFilter::new( - client.clone(), - frontier_backend.clone(), - graph.clone(), - filter_pool, - 500_usize, /* max stored filters */ - max_past_logs, - block_data_cache.clone(), - ) - .into_rpc(), - )?; - } - - io.merge( - Net::new( - client.clone(), - network.clone(), - // Whether to format the `peer_count` response as Hex (default) or not. - true, - ) - .into_rpc(), - )?; - - io.merge(Web3::new(client.clone()).into_rpc())?; - - io.merge( - EthPubSub::new( - pool, - client.clone(), - sync.clone(), - subscription_task_executor, - storage_override.clone(), - pubsub_notification_sinks.clone(), - ) - .into_rpc(), - )?; - - io.merge( - Debug::new(client.clone(), frontier_backend, storage_override, block_data_cache).into_rpc(), - )?; - - io.merge(TxPool::new(client, graph).into_rpc())?; - - Ok(io) -} diff --git a/ziggurat/node/src/main.rs b/ziggurat/node/src/main.rs deleted file mode 100644 index 9b660805..00000000 --- a/ziggurat/node/src/main.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Substrate Parachain Node Template CLI - -#![warn(missing_docs)] - -mod chain_spec; -mod cli; -mod command; -mod eth; -mod rpc; -mod service; - -fn main() -> sc_cli::Result<()> { - command::run() -} diff --git a/ziggurat/node/src/rpc.rs b/ziggurat/node/src/rpc.rs deleted file mode 100644 index 874895b7..00000000 --- a/ziggurat/node/src/rpc.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! A collection of node-specific RPC methods. -//! Substrate provides the `sc-rpc` crate, which defines the core RPC layer -//! used by Substrate nodes. This file extends those RPC definitions with -//! capabilities that are specific to this project's runtime configuration. - -#![warn(missing_docs)] - -use std::sync::Arc; - -use ziggurat_runtime::{opaque::Block, AccountId, Balance, Nonce}; - -pub use sc_rpc::DenyUnsafe; -use sc_transaction_pool_api::TransactionPool; -use sp_api::ProvideRuntimeApi; -use sp_block_builder::BlockBuilder; -use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; - -/// A type representing all RPC extensions. -pub type RpcExtension = jsonrpsee::RpcModule<()>; - -/// Full client dependencies -pub struct FullDeps { - /// The client instance to use. - pub client: Arc, - /// Transaction pool instance. - pub pool: Arc

, - /// Whether to deny unsafe calls - pub deny_unsafe: DenyUnsafe, -} - -/// Instantiate all RPC extensions. -pub fn create_full( - deps: FullDeps, -) -> Result> -where - C: ProvideRuntimeApi - + HeaderBackend - + HeaderMetadata - + Send - + Sync - + 'static, - C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, - C::Api: substrate_frame_rpc_system::AccountNonceApi, - C::Api: BlockBuilder, - P: TransactionPool + Sync + Send + 'static, - C::Api: cosmos_runtime_api::CosmosRuntimeApi, - C::Api: cosmwasm_runtime_api::CosmwasmRuntimeApi>, -{ - use cosmos_rpc::cosmos::{Cosmos, CosmosApiServer}; - use cosmwasm_rpc::{Cosmwasm, CosmwasmApiServer}; - use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; - use substrate_frame_rpc_system::{System, SystemApiServer}; - - let mut module = RpcExtension::new(()); - let FullDeps { client, pool, deny_unsafe } = deps; - - module.merge(System::new(client.clone(), pool.clone(), deny_unsafe).into_rpc())?; - module.merge(TransactionPayment::new(client.clone()).into_rpc())?; - - module.merge(Cosmos::new(client.clone(), pool).into_rpc())?; - module.merge(Cosmwasm::new(client).into_rpc())?; - - Ok(module) -} diff --git a/ziggurat/node/src/service.rs b/ziggurat/node/src/service.rs deleted file mode 100644 index 9752836b..00000000 --- a/ziggurat/node/src/service.rs +++ /dev/null @@ -1,484 +0,0 @@ -//! Service and ServiceFactory implementation. Specialized wrapper over substrate service. - -// std -use std::{sync::Arc, time::Duration}; - -use cumulus_client_cli::CollatorOptions; -// Local Runtime Types -use ziggurat_runtime::{ - apis::RuntimeApi, - opaque::{Block, Hash}, -}; - -use crate::eth; -use fc_rpc::StorageOverrideHandler; - -// Cumulus Imports -use cumulus_client_collator::service::CollatorService; -#[docify::export(lookahead_collator)] -use cumulus_client_consensus_aura::collators::lookahead::{self as aura, Params as AuraParams}; -use cumulus_client_consensus_common::ParachainBlockImport as TParachainBlockImport; -use cumulus_client_consensus_proposer::Proposer; -use cumulus_client_service::{ - build_network, build_relay_chain_interface, prepare_node_config, start_relay_chain_tasks, - BuildNetworkParams, CollatorSybilResistance, DARecoveryProfile, ParachainHostFunctions, - StartRelayChainTasksParams, -}; -#[docify::export(cumulus_primitives)] -use cumulus_primitives_core::{ - relay_chain::{CollatorPair, ValidationCode}, - ParaId, -}; -use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface}; - -// Substrate Imports -use frame_benchmarking_cli::SUBSTRATE_REFERENCE_HARDWARE; -use prometheus_endpoint::Registry; -use sc_client_api::Backend; -use sc_consensus::ImportQueue; -use sc_executor::{HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY}; -use sc_network::NetworkBlock; -use sc_service::{Configuration, PartialComponents, TFullBackend, TFullClient, TaskManager}; -use sc_telemetry::{Telemetry, TelemetryHandle, TelemetryWorker, TelemetryWorkerHandle}; -use sc_transaction_pool_api::OffchainTransactionPoolFactory; -use sp_keystore::KeystorePtr; - -#[docify::export(wasm_executor)] -type ParachainExecutor = WasmExecutor; - -type ParachainClient = TFullClient; - -pub type ParachainBackend = TFullBackend; - -type ParachainBlockImport = TParachainBlockImport, ParachainBackend>; - -/// Assembly of PartialComponents (enough to run chain ops subcommands) -pub type Service = PartialComponents< - ParachainClient, - ParachainBackend, - (), - sc_consensus::DefaultImportQueue, - sc_transaction_pool::FullPool, - (ParachainBlockImport, Option, Option), ->; - -/// Starts a `ServiceBuilder` for a full service. -/// -/// Use this macro if you don't actually need the full service, but just the builder in order to -/// be able to perform chain operations. -#[docify::export(component_instantiation)] -pub fn new_partial(config: &Configuration) -> Result { - let telemetry = config - .telemetry_endpoints - .clone() - .filter(|x| !x.is_empty()) - .map(|endpoints| -> Result<_, sc_telemetry::Error> { - let worker = TelemetryWorker::new(16)?; - let telemetry = worker.handle().new_telemetry(endpoints); - Ok((worker, telemetry)) - }) - .transpose()?; - - let heap_pages = config - .default_heap_pages - .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static { extra_pages: h as _ }); - - let executor = ParachainExecutor::builder() - .with_execution_method(config.wasm_method) - .with_onchain_heap_alloc_strategy(heap_pages) - .with_offchain_heap_alloc_strategy(heap_pages) - .with_max_runtime_instances(config.max_runtime_instances) - .with_runtime_cache_size(config.runtime_cache_size) - .build(); - - let (client, backend, keystore_container, task_manager) = - sc_service::new_full_parts_record_import::( - config, - telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), - executor, - true, - )?; - let client = Arc::new(client); - - let telemetry_worker_handle = telemetry.as_ref().map(|(worker, _)| worker.handle()); - - let telemetry = telemetry.map(|(worker, telemetry)| { - task_manager.spawn_handle().spawn("telemetry", None, worker.run()); - telemetry - }); - - let transaction_pool = sc_transaction_pool::BasicPool::new_full( - config.transaction_pool.clone(), - config.role.is_authority().into(), - config.prometheus_registry(), - task_manager.spawn_essential_handle(), - client.clone(), - ); - - let block_import = ParachainBlockImport::new(client.clone(), backend.clone()); - - let import_queue = build_import_queue( - client.clone(), - block_import.clone(), - config, - telemetry.as_ref().map(|telemetry| telemetry.handle()), - &task_manager, - ); - - Ok(PartialComponents { - backend, - client, - import_queue, - keystore_container, - task_manager, - transaction_pool, - select_chain: (), - other: (block_import, telemetry, telemetry_worker_handle), - }) -} - -/// Build the import queue for the parachain runtime. -fn build_import_queue( - client: Arc, - block_import: ParachainBlockImport, - config: &Configuration, - telemetry: Option, - task_manager: &TaskManager, -) -> sc_consensus::DefaultImportQueue { - cumulus_client_consensus_aura::equivocation_import_queue::fully_verifying_import_queue::< - sp_consensus_aura::sr25519::AuthorityPair, - _, - _, - _, - _, - >( - client, - block_import, - move |_, _| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - Ok(timestamp) - }, - &task_manager.spawn_essential_handle(), - config.prometheus_registry(), - telemetry, - ) -} - -#[allow(clippy::too_many_arguments)] -fn start_consensus( - client: Arc, - backend: Arc, - block_import: ParachainBlockImport, - prometheus_registry: Option<&Registry>, - telemetry: Option, - task_manager: &TaskManager, - relay_chain_interface: Arc, - transaction_pool: Arc>, - keystore: KeystorePtr, - relay_chain_slot_duration: Duration, - para_id: ParaId, - collator_key: CollatorPair, - overseer_handle: OverseerHandle, - announce_block: Arc>) + Send + Sync>, -) -> Result<(), sc_service::Error> { - let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( - task_manager.spawn_handle(), - client.clone(), - transaction_pool, - prometheus_registry, - telemetry.clone(), - ); - - let proposer = Proposer::new(proposer_factory); - - let collator_service = CollatorService::new( - client.clone(), - Arc::new(task_manager.spawn_handle()), - announce_block, - client.clone(), - ); - - let params = AuraParams { - create_inherent_data_providers: move |_, ()| async move { Ok(()) }, - block_import, - para_client: client.clone(), - para_backend: backend, - relay_client: relay_chain_interface, - code_hash_provider: move |block_hash| { - client.code_at(block_hash).ok().map(|c| ValidationCode::from(c).hash()) - }, - keystore, - collator_key, - para_id, - overseer_handle, - relay_chain_slot_duration, - proposer, - collator_service, - authoring_duration: Duration::from_millis(2000), - reinitialize: false, - }; - let fut = aura::run::( - params, - ); - task_manager.spawn_essential_handle().spawn("aura", None, fut); - - Ok(()) -} - -/// Start a node with the given parachain `Configuration` and relay chain `Configuration`. -#[sc_tracing::logging::prefix_logs_with("Parachain")] -pub async fn start_parachain_node( - parachain_config: Configuration, - polkadot_config: Configuration, - eth_config: eth::Configuration, - collator_options: CollatorOptions, - para_id: ParaId, - hwbench: Option, -) -> sc_service::error::Result<(TaskManager, Arc)> { - let parachain_config = prepare_node_config(parachain_config); - - let params = new_partial(¶chain_config)?; - let (block_import, mut telemetry, telemetry_worker_handle) = params.other; - let net_config = sc_network::config::FullNetworkConfiguration::< - _, - _, - sc_network::NetworkWorker, - >::new(¶chain_config.network); - - let client = params.client.clone(); - let backend = params.backend.clone(); - let mut task_manager = params.task_manager; - - let (relay_chain_interface, collator_key) = build_relay_chain_interface( - polkadot_config, - ¶chain_config, - telemetry_worker_handle, - &mut task_manager, - collator_options.clone(), - hwbench.clone(), - ) - .await - .map_err(|e| sc_service::Error::Application(Box::new(e) as Box<_>))?; - - let validator = parachain_config.role.is_authority(); - let prometheus_registry = parachain_config.prometheus_registry().cloned(); - let transaction_pool = params.transaction_pool.clone(); - let import_queue_service = params.import_queue.service(); - - // NOTE: because we use Aura here explicitly, we can use `CollatorSybilResistance::Resistant` - // when starting the network. - let (network, system_rpc_tx, tx_handler_controller, start_network, sync_service) = - build_network(BuildNetworkParams { - parachain_config: ¶chain_config, - net_config, - client: client.clone(), - transaction_pool: transaction_pool.clone(), - para_id, - spawn_handle: task_manager.spawn_handle(), - relay_chain_interface: relay_chain_interface.clone(), - import_queue: params.import_queue, - sybil_resistance_level: CollatorSybilResistance::Resistant, // because of Aura - }) - .await?; - - if parachain_config.offchain_worker.enabled { - use futures::FutureExt; - - task_manager.spawn_handle().spawn( - "offchain-workers-runner", - "offchain-work", - sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { - runtime_api_provider: client.clone(), - keystore: Some(params.keystore_container.keystore()), - offchain_db: backend.offchain_storage(), - transaction_pool: Some(OffchainTransactionPoolFactory::new( - transaction_pool.clone(), - )), - network_provider: Arc::new(network.clone()), - is_validator: parachain_config.role.is_authority(), - enable_http_requests: false, - custom_extensions: move |_| vec![], - }) - .run(client.clone(), task_manager.spawn_handle()) - .boxed(), - ); - } - - let eth::PartialComponents { filter_pool, fee_history_cache, fee_history_cache_limit } = - eth::new_partial(ð_config)?; - - let frontier_backend = - Arc::new(eth::open_frontier_backend(client.clone(), ¶chain_config, ð_config)?); - let storage_override = Arc::new(StorageOverrideHandler::new(client.clone())); - let pubsub_notification_sinks = Arc::new(fc_mapping_sync::EthereumBlockNotificationSinks::< - fc_mapping_sync::EthereumBlockNotification, - >::default()); - - let eth_rpc_builder = { - let client = client.clone(); - let pool = transaction_pool.clone(); - let network = network.clone(); - let sync_service = sync_service.clone(); - let subscription_task_executor = Arc::new(task_manager.spawn_handle()); - let is_authority = validator; - let max_past_logs = eth_config.max_past_logs; - let filter_pool = filter_pool.clone(); - let frontier_backend = frontier_backend.clone(); - let pubsub_notificatino_sinks = pubsub_notification_sinks.clone(); - let storage_override = storage_override.clone(); - let fee_history_cache = fee_history_cache.clone(); - let block_data_cache = Arc::new(fc_rpc::EthBlockDataCacheTask::new( - task_manager.spawn_handle(), - storage_override.clone(), - eth_config.eth_log_block_cache, - eth_config.eth_statuses_cache, - None, - )); - - Box::new(move |_deny_unsafe| { - eth::rpc::create_full(eth::rpc::FullDeps { - client: client.clone(), - pool: pool.clone(), - graph: pool.pool().clone(), - is_authority, - network: network.clone(), - sync: sync_service.clone(), - frontier_backend: match *frontier_backend { - fc_db::Backend::KeyValue(ref b) => b.clone(), - fc_db::Backend::Sql(ref b) => b.clone(), - }, - storage_override: storage_override.clone(), - block_data_cache: block_data_cache.clone(), - filter_pool: filter_pool.clone(), - max_past_logs, - fee_history_cache: fee_history_cache.clone(), - fee_history_cache_limit, - forced_parent_hashes: None, - subscription_task_executor: subscription_task_executor.clone(), - pubsub_notification_sinks: pubsub_notificatino_sinks.clone(), - }) - .map_err(Into::into) - }) - }; - - let parachain_config = eth::spawn_tasks(eth::SpawnTasksParams { - config: parachain_config, - rpc_builder: eth_rpc_builder, - task_manager: &mut task_manager, - client: client.clone(), - substrate_backend: backend.clone(), - frontier_backend, - filter_pool, - storage_override, - fee_history_cache, - fee_history_cache_limit, - sync: sync_service.clone(), - pubsub_notification_sinks, - })?; - - let rpc_builder = { - let client = client.clone(); - let transaction_pool = transaction_pool.clone(); - - Box::new(move |deny_unsafe, _| { - let deps = crate::rpc::FullDeps { - client: client.clone(), - pool: transaction_pool.clone(), - deny_unsafe, - }; - - crate::rpc::create_full(deps).map_err(Into::into) - }) - }; - - sc_service::spawn_tasks(sc_service::SpawnTasksParams { - rpc_builder, - client: client.clone(), - transaction_pool: transaction_pool.clone(), - task_manager: &mut task_manager, - config: parachain_config, - keystore: params.keystore_container.keystore(), - backend: backend.clone(), - network, - sync_service: sync_service.clone(), - system_rpc_tx, - tx_handler_controller, - telemetry: telemetry.as_mut(), - })?; - - if let Some(hwbench) = hwbench { - sc_sysinfo::print_hwbench(&hwbench); - // Here you can check whether the hardware meets your chains' requirements. Putting a link - // in there and swapping out the requirements for your own are probably a good idea. The - // requirements for a para-chain are dictated by its relay-chain. - match SUBSTRATE_REFERENCE_HARDWARE.check_hardware(&hwbench) { - Err(err) if validator => { - log::warn!( - "⚠️ The hardware does not meet the minimal requirements {} for role 'Authority'.", - err - ); - }, - _ => {}, - } - - if let Some(ref mut telemetry) = telemetry { - let telemetry_handle = telemetry.handle(); - task_manager.spawn_handle().spawn( - "telemetry_hwbench", - None, - sc_sysinfo::initialize_hwbench_telemetry(telemetry_handle, hwbench), - ); - } - } - - let announce_block = { - let sync_service = sync_service.clone(); - Arc::new(move |hash, data| sync_service.announce_block(hash, data)) - }; - - let relay_chain_slot_duration = Duration::from_secs(6); - - let overseer_handle = relay_chain_interface - .overseer_handle() - .map_err(|e| sc_service::Error::Application(Box::new(e)))?; - - start_relay_chain_tasks(StartRelayChainTasksParams { - client: client.clone(), - announce_block: announce_block.clone(), - para_id, - relay_chain_interface: relay_chain_interface.clone(), - task_manager: &mut task_manager, - da_recovery_profile: if validator { - DARecoveryProfile::Collator - } else { - DARecoveryProfile::FullNode - }, - import_queue: import_queue_service, - relay_chain_slot_duration, - recovery_handle: Box::new(overseer_handle.clone()), - sync_service: sync_service.clone(), - })?; - - if validator { - start_consensus( - client.clone(), - backend, - block_import, - prometheus_registry.as_ref(), - telemetry.as_ref().map(|t| t.handle()), - &task_manager, - relay_chain_interface, - transaction_pool, - params.keystore_container.keystore(), - relay_chain_slot_duration, - para_id, - collator_key.expect("Command line arguments do not allow this. qed"), - overseer_handle, - announce_block, - )?; - } - - start_network.start_network(); - - Ok((task_manager, client)) -} diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml deleted file mode 100644 index 5565a7ff..00000000 --- a/ziggurat/runtime/Cargo.toml +++ /dev/null @@ -1,231 +0,0 @@ -[package] -name = "ziggurat-runtime" -version = "0.1.0" -description = "Ziggurat runtime (Babel testnet)" -authors = ["Haderech Pte. Ltd."] -edition = "2021" -license = "GPL-3.0-or-later" -repository = "https://github.com/noirhq/noir.git" -publish = false - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.6", default-features = false, features = ["derive"] } -docify = "0.2" -hex-literal = { version = "0.4", optional = true } -log = { version = "0.4", default-features = false } -scale-info = { version = "2.11", default-features = false, features = ["derive"] } -smallvec = "1.13" - -frame-babel = { workspace = true, features = ["pallet"] } -np-ethereum = { workspace = true } -np-runtime = { workspace = true, features = ["serde"] } -pallet-multimap = { workspace = true } - -fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } -fp-rpc = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } -fp-self-contained = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } -pallet-base-fee = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } -pallet-ethereum = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } -pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } - -frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } -frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, features = ["experimental"] } -frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } -frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -frame-try-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } -pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-message-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-version = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -polkadot-parachain-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-pallet-aura-ext = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-pallet-parachain-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-pallet-session-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-pallet-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-pallet-xcmp-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-primitives-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-primitives-utility = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -cumulus-primitives-storage-weight-reclaim = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -parachains-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -parachain-info = { package = "staging-parachain-info", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } - -cosmos-runtime-api = { workspace = true, default-features = false } -cosmos-sdk-proto = { version = "0.24", default-features = false } -cosmwasm-runtime-api = { workspace = true, default-features = false } -np-cosmos = { workspace = true, default-features = false } -pallet-cosmos = { workspace = true, default-features = false } -pallet-cosmos-types = { workspace = true, default-features = false } -pallet-cosmos-x-auth = { workspace = true, default-features = false } -pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } -pallet-cosmwasm = { workspace = true, default-features = false } - -[build-dependencies] -docify = "0.2" -substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", optional = true } - -[features] -default = ["std"] -std = [ - "codec/std", - "cosmos-sdk-proto/std", - "cumulus-pallet-aura-ext/std", - "cumulus-pallet-parachain-system/std", - "cumulus-pallet-session-benchmarking/std", - "cumulus-pallet-xcm/std", - "cumulus-pallet-xcmp-queue/std", - "cumulus-primitives-aura/std", - "cumulus-primitives-core/std", - "cumulus-primitives-storage-weight-reclaim/std", - "cumulus-primitives-utility/std", - "fp-evm/std", - "fp-rpc/std", - "fp-self-contained/std", - "frame-babel/std", - "frame-benchmarking?/std", - "frame-executive/std", - "frame-metadata-hash-extension/std", - "frame-support/std", - "frame-system-benchmarking?/std", - "frame-system-rpc-runtime-api/std", - "frame-system/std", - "frame-try-runtime?/std", - "log/std", - "np-cosmos/std", - "np-ethereum/std", - "np-runtime/std", - "pallet-assets/std", - "pallet-aura/std", - "pallet-authorship/std", - "pallet-balances/std", - "pallet-base-fee/std", - "pallet-collator-selection/std", - "pallet-cosmos/std", - "pallet-cosmos-types/std", - "pallet-cosmos-x-auth/std", - "pallet-cosmos-x-auth-signing/std", - "pallet-cosmwasm/std", - "pallet-ethereum/std", - "pallet-evm/std", - "pallet-multimap/std", - "pallet-message-queue/std", - "pallet-session/std", - "pallet-sudo/std", - "pallet-timestamp/std", - "pallet-transaction-payment-rpc-runtime-api/std", - "pallet-transaction-payment/std", - "pallet-xcm/std", - "parachain-info/std", - "parachains-common/std", - "polkadot-parachain-primitives/std", - "polkadot-runtime-common/std", - "scale-info/std", - "sp-api/std", - "sp-block-builder/std", - "sp-consensus-aura/std", - "sp-core/std", - "sp-genesis-builder/std", - "sp-inherents/std", - "sp-offchain/std", - "sp-runtime/std", - "sp-session/std", - "sp-transaction-pool/std", - "sp-version/std", - "substrate-wasm-builder", - "xcm-builder/std", - "xcm-executor/std", - "xcm/std", -] - -runtime-benchmarks = [ - "cumulus-pallet-parachain-system/runtime-benchmarks", - "cumulus-pallet-session-benchmarking/runtime-benchmarks", - "cumulus-pallet-xcmp-queue/runtime-benchmarks", - "cumulus-primitives-core/runtime-benchmarks", - "cumulus-primitives-utility/runtime-benchmarks", - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system-benchmarking/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "hex-literal", - "pallet-balances/runtime-benchmarks", - "pallet-collator-selection/runtime-benchmarks", - "pallet-ethereum/runtime-benchmarks", - "pallet-evm/runtime-benchmarks", - "pallet-message-queue/runtime-benchmarks", - "pallet-sudo/runtime-benchmarks", - "pallet-timestamp/runtime-benchmarks", - "pallet-xcm/runtime-benchmarks", - "parachains-common/runtime-benchmarks", - "polkadot-parachain-primitives/runtime-benchmarks", - "polkadot-runtime-common/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", - "xcm-builder/runtime-benchmarks", - "xcm-executor/runtime-benchmarks", -] - -try-runtime = [ - "cumulus-pallet-aura-ext/try-runtime", - "cumulus-pallet-parachain-system/try-runtime", - "cumulus-pallet-xcm/try-runtime", - "cumulus-pallet-xcmp-queue/try-runtime", - "frame-executive/try-runtime", - "frame-support/try-runtime", - "frame-system/try-runtime", - "frame-try-runtime/try-runtime", - "pallet-aura/try-runtime", - "pallet-authorship/try-runtime", - "pallet-balances/try-runtime", - "pallet-base-fee/try-runtime", - "pallet-collator-selection/try-runtime", - "pallet-ethereum/try-runtime", - "pallet-evm/try-runtime", - "pallet-message-queue/try-runtime", - "pallet-session/try-runtime", - "pallet-sudo/try-runtime", - "pallet-timestamp/try-runtime", - "pallet-transaction-payment/try-runtime", - "pallet-xcm/try-runtime", - "parachain-info/try-runtime", - "polkadot-runtime-common/try-runtime", - "sp-runtime/try-runtime", -] - -# Enable the metadata hash generation. -# -# This is hidden behind a feature because it increases the compile time. -# The wasm binary needs to be compiled twice, once to fetch the metadata, -# generate the metadata hash and then a second time with the -# `RUNTIME_METADATA_HASH` environment variable set for the `CheckMetadataHash` -# extension. -metadata-hash = ["substrate-wasm-builder/metadata-hash"] - -# A convenience feature for enabling things when doing a build -# for an on-chain release. -on-chain-release-build = ["metadata-hash"] diff --git a/ziggurat/runtime/build.rs b/ziggurat/runtime/build.rs deleted file mode 100644 index 4f33752c..00000000 --- a/ziggurat/runtime/build.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[cfg(all(feature = "std", feature = "metadata-hash"))] -#[docify::export(template_enable_metadata_hash)] -fn main() { - substrate_wasm_builder::WasmBuilder::init_with_defaults() - .enable_metadata_hash("UNIT", 12) - .build(); -} - -#[cfg(all(feature = "std", not(feature = "metadata-hash")))] -fn main() { - substrate_wasm_builder::WasmBuilder::build_using_defaults(); -} - -/// The wasm builder is deactivated when compiling -/// this crate for wasm to speed up the compilation. -#[cfg(not(feature = "std"))] -fn main() {} diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs deleted file mode 100644 index 44c0e2c2..00000000 --- a/ziggurat/runtime/src/apis.rs +++ /dev/null @@ -1,632 +0,0 @@ -// This is free and unencumbered software released into the public domain. -// -// Anyone is free to copy, modify, publish, use, compile, sell, or -// distribute this software, either in source code form or as a compiled -// binary, for any purpose, commercial or non-commercial, and by any -// means. -// -// In jurisdictions that recognize copyright laws, the author or authors -// of this software dedicate any and all copyright interest in the -// software to the public domain. We make this dedication for the benefit -// of the public at large and to the detriment of our heirs and -// successors. We intend this dedication to be an overt act of -// relinquishment in perpetuity of all present and future rights to this -// software under copyright law. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -// For more information, please refer to - -// External crates imports -use alloc::{ - format, - string::{String, ToString}, - vec::Vec, -}; -use codec::Encode; -use cosmos_runtime_api::{ChainInfo, GasInfo, SimulateError, SimulateResponse, SimulateResult}; -use cosmos_sdk_proto::{cosmos::tx::v1beta1::Tx, traits::Message}; -use frame_support::{ - genesis_builder_helper::{build_state, get_preset}, - traits::OnFinalize, - weights::Weight, -}; -use np_cosmos::traits::ChainInfo as _; -use pallet_aura::Authorities; -use pallet_cosmos_types::{ - context::traits::Context, events::traits::EventManager, gas::traits::GasMeter, - handler::AnteDecorator, -}; -use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; -use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; -use sp_api::impl_runtime_apis; -use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H160, H256, U256}; -use sp_runtime::{ - traits::{Block as BlockT, Convert}, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, Permill, SaturatedConversion, -}; -use sp_version::RuntimeVersion; - -// Local module imports -use super::{ - AccountId, Balance, Block, ConsensusHook, Ethereum, Executive, InherentDataExt, Nonce, - ParachainSystem, Runtime, RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, - TransactionPayment, UncheckedExtrinsic, SLOT_DURATION, VERSION, -}; - -// we move some impls outside so we can easily use them with `docify`. -impl Runtime { - #[docify::export] - fn impl_slot_duration() -> sp_consensus_aura::SlotDuration { - sp_consensus_aura::SlotDuration::from_millis(SLOT_DURATION) - } - - #[docify::export] - fn impl_can_build_upon( - included_hash: ::Hash, - slot: cumulus_primitives_aura::Slot, - ) -> bool { - ConsensusHook::can_build_upon(included_hash, slot) - } -} - -impl_runtime_apis! { - impl sp_consensus_aura::AuraApi for Runtime { - fn slot_duration() -> sp_consensus_aura::SlotDuration { - Runtime::impl_slot_duration() - } - - fn authorities() -> Vec { - Authorities::::get().into_inner() - } - } - - impl cumulus_primitives_aura::AuraUnincludedSegmentApi for Runtime { - fn can_build_upon( - included_hash: ::Hash, - slot: cumulus_primitives_aura::Slot, - ) -> bool { - Runtime::impl_can_build_upon(included_hash, slot) - } - } - - impl sp_api::Core for Runtime { - fn version() -> RuntimeVersion { - VERSION - } - - fn execute_block(block: Block) { - Executive::execute_block(block) - } - - fn initialize_block(header: &::Header) -> sp_runtime::ExtrinsicInclusionMode { - Executive::initialize_block(header) - } - } - - impl sp_api::Metadata for Runtime { - fn metadata() -> OpaqueMetadata { - OpaqueMetadata::new(Runtime::metadata().into()) - } - - fn metadata_at_version(version: u32) -> Option { - Runtime::metadata_at_version(version) - } - - fn metadata_versions() -> Vec { - Runtime::metadata_versions() - } - } - - impl sp_block_builder::BlockBuilder for Runtime { - fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { - Executive::apply_extrinsic(extrinsic) - } - - fn finalize_block() -> ::Header { - Executive::finalize_block() - } - - fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { - data.create_extrinsics() - } - - fn check_inherents( - block: Block, - data: sp_inherents::InherentData, - ) -> sp_inherents::CheckInherentsResult { - data.check_extrinsics(&block) - } - } - - impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { - fn validate_transaction( - source: TransactionSource, - tx: ::Extrinsic, - block_hash: ::Hash, - ) -> TransactionValidity { - Executive::validate_transaction(source, tx, block_hash) - } - } - - impl sp_offchain::OffchainWorkerApi for Runtime { - fn offchain_worker(header: &::Header) { - Executive::offchain_worker(header) - } - } - - impl sp_session::SessionKeys for Runtime { - fn generate_session_keys(seed: Option>) -> Vec { - SessionKeys::generate(seed) - } - - fn decode_session_keys( - encoded: Vec, - ) -> Option, KeyTypeId)>> { - SessionKeys::decode_into_raw_public_keys(&encoded) - } - } - - impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { - fn account_nonce(account: AccountId) -> Nonce { - System::account_nonce(account) - } - } - - impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi for Runtime { - fn query_info( - uxt: ::Extrinsic, - len: u32, - ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { - TransactionPayment::query_info(uxt, len) - } - fn query_fee_details( - uxt: ::Extrinsic, - len: u32, - ) -> pallet_transaction_payment::FeeDetails { - TransactionPayment::query_fee_details(uxt, len) - } - fn query_weight_to_fee(weight: Weight) -> Balance { - TransactionPayment::weight_to_fee(weight) - } - fn query_length_to_fee(length: u32) -> Balance { - TransactionPayment::length_to_fee(length) - } - } - - impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi - for Runtime - { - fn query_call_info( - call: RuntimeCall, - len: u32, - ) -> pallet_transaction_payment::RuntimeDispatchInfo { - TransactionPayment::query_call_info(call, len) - } - fn query_call_fee_details( - call: RuntimeCall, - len: u32, - ) -> pallet_transaction_payment::FeeDetails { - TransactionPayment::query_call_fee_details(call, len) - } - fn query_weight_to_fee(weight: Weight) -> Balance { - TransactionPayment::weight_to_fee(weight) - } - fn query_length_to_fee(length: u32) -> Balance { - TransactionPayment::length_to_fee(length) - } - } - - impl fp_rpc::EthereumRuntimeRPCApi for Runtime { - fn chain_id() -> u64 { - ::ChainId::get() - } - - fn account_basic(address: H160) -> EVMAccount { - let (account, _) = pallet_evm::Pallet::::account_basic(&address); - account - } - - fn gas_price() -> U256 { - let (gas_price, _) = ::FeeCalculator::min_gas_price(); - gas_price - } - - fn account_code_at(address: H160) -> Vec { - pallet_evm::AccountCodes::::get(address) - } - - fn author() -> H160 { - >::find_author() - } - - fn storage_at(address: H160, index: U256) -> H256 { - let mut tmp = [0u8; 32]; - index.to_big_endian(&mut tmp); - pallet_evm::AccountStorages::::get(address, H256::from_slice(&tmp[..])) - } - - fn call( - from: H160, - to: H160, - data: Vec, - value: U256, - gas_limit: U256, - max_fee_per_gas: Option, - max_priority_fee_per_gas: Option, - nonce: Option, - estimate: bool, - access_list: Option)>>, - ) -> Result { - use pallet_evm::GasWeightMapping as _; - - let config = if estimate { - let mut config = ::config().clone(); - config.estimate = true; - Some(config) - } else { - None - }; - - // Estimated encoded transaction size must be based on the heaviest transaction - // type (EIP1559Transaction) to be compatible with all transaction types. - let mut estimated_transaction_len = data.len() + - // pallet ethereum index: 1 - // transact call index: 1 - // Transaction enum variant: 1 - // chain_id 8 bytes - // nonce: 32 - // max_priority_fee_per_gas: 32 - // max_fee_per_gas: 32 - // gas_limit: 32 - // action: 21 (enum varianrt + call address) - // value: 32 - // access_list: 1 (empty vec size) - // 65 bytes signature - 258; - - if access_list.is_some() { - estimated_transaction_len += access_list.encoded_size(); - } - - let gas_limit = if gas_limit > U256::from(u64::MAX) { - u64::MAX - } else { - gas_limit.low_u64() - }; - let without_base_extrinsic_weight = true; - - let (weight_limit, proof_size_base_cost) = - match ::GasWeightMapping::gas_to_weight( - gas_limit, - without_base_extrinsic_weight - ) { - weight_limit if weight_limit.proof_size() > 0 => { - (Some(weight_limit), Some(estimated_transaction_len as u64)) - } - _ => (None, None), - }; - - ::Runner::call( - from, - to, - data, - value, - gas_limit.saturated_into(), - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list.unwrap_or_default(), - false, - true, - weight_limit, - proof_size_base_cost, - config.as_ref().unwrap_or(::config()), - ).map_err(|err| err.error.into()) - } - - fn create( - from: H160, - data: Vec, - value: U256, - gas_limit: U256, - max_fee_per_gas: Option, - max_priority_fee_per_gas: Option, - nonce: Option, - estimate: bool, - access_list: Option)>>, - ) -> Result { - use pallet_evm::GasWeightMapping as _; - - let config = if estimate { - let mut config = ::config().clone(); - config.estimate = true; - Some(config) - } else { - None - }; - - - let mut estimated_transaction_len = data.len() + - // from: 20 - // value: 32 - // gas_limit: 32 - // nonce: 32 - // 1 byte transaction action variant - // chain id 8 bytes - // 65 bytes signature - 190; - - if max_fee_per_gas.is_some() { - estimated_transaction_len += 32; - } - if max_priority_fee_per_gas.is_some() { - estimated_transaction_len += 32; - } - if access_list.is_some() { - estimated_transaction_len += access_list.encoded_size(); - } - - - let gas_limit = if gas_limit > U256::from(u64::MAX) { - u64::MAX - } else { - gas_limit.low_u64() - }; - let without_base_extrinsic_weight = true; - - let (weight_limit, proof_size_base_cost) = - match ::GasWeightMapping::gas_to_weight( - gas_limit, - without_base_extrinsic_weight - ) { - weight_limit if weight_limit.proof_size() > 0 => { - (Some(weight_limit), Some(estimated_transaction_len as u64)) - } - _ => (None, None), - }; - - ::Runner::create( - from, - data, - value, - gas_limit.saturated_into(), - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list.unwrap_or_default(), - false, - true, - weight_limit, - proof_size_base_cost, - config.as_ref().unwrap_or(::config()), - ).map_err(|err| err.error.into()) - } - - fn current_transaction_statuses() -> Option> { - pallet_ethereum::CurrentTransactionStatuses::::get() - } - - fn current_block() -> Option { - pallet_ethereum::CurrentBlock::::get() - } - - fn current_receipts() -> Option> { - pallet_ethereum::CurrentReceipts::::get() - } - - fn current_all() -> ( - Option, - Option>, - Option> - ) { - ( - pallet_ethereum::CurrentBlock::::get(), - pallet_ethereum::CurrentReceipts::::get(), - pallet_ethereum::CurrentTransactionStatuses::::get() - ) - } - - fn extrinsic_filter( - xts: Vec<::Extrinsic>, - ) -> Vec { - xts.into_iter().filter_map(|xt| match xt.0.function { - RuntimeCall::Ethereum(pallet_ethereum::Call::transact { transaction }) => Some(transaction), - _ => None - }).collect::>() - } - - fn elasticity() -> Option { - Some(pallet_base_fee::Elasticity::::get()) - } - - fn gas_limit_multiplier_support() {} - - fn pending_block( - xts: Vec<::Extrinsic>, - ) -> (Option, Option>) { - for ext in xts.into_iter() { - let _ = Executive::apply_extrinsic(ext); - } - - Ethereum::on_finalize(System::block_number() + 1); - - ( - pallet_ethereum::CurrentBlock::::get(), - pallet_ethereum::CurrentTransactionStatuses::::get() - ) - } - - fn initialize_pending_block(header: &::Header) { - Executive::initialize_block(header); - } - } - - impl fp_rpc::ConvertTransactionRuntimeApi for Runtime { - fn convert_transaction(transaction: EthereumTransaction) -> ::Extrinsic { - UncheckedExtrinsic::new_unsigned( - pallet_ethereum::Call::::transact { transaction }.into(), - ) - } - } - - impl cumulus_primitives_core::CollectCollationInfo for Runtime { - fn collect_collation_info(header: &::Header) -> cumulus_primitives_core::CollationInfo { - ParachainSystem::collect_collation_info(header) - } - } - - #[cfg(feature = "try-runtime")] - impl frame_try_runtime::TryRuntime for Runtime { - fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { - use super::configs::RuntimeBlockWeights; - - let weight = Executive::try_runtime_upgrade(checks).unwrap(); - (weight, RuntimeBlockWeights::get().max_block) - } - - fn execute_block( - block: Block, - state_root_check: bool, - signature_check: bool, - select: frame_try_runtime::TryStateSelect, - ) -> Weight { - // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to - // have a backtrace here. - Executive::try_execute_block(block, state_root_check, signature_check, select).unwrap() - } - } - - #[cfg(feature = "runtime-benchmarks")] - impl frame_benchmarking::Benchmark for Runtime { - fn benchmark_metadata(extra: bool) -> ( - Vec, - Vec, - ) { - use frame_benchmarking::{Benchmarking, BenchmarkList}; - use frame_support::traits::StorageInfoTrait; - use frame_system_benchmarking::Pallet as SystemBench; - use cumulus_pallet_session_benchmarking::Pallet as SessionBench; - use super::*; - - let mut list = Vec::::new(); - list_benchmarks!(list, extra); - - let storage_info = AllPalletsWithSystem::storage_info(); - (list, storage_info) - } - - fn dispatch_benchmark( - config: frame_benchmarking::BenchmarkConfig - ) -> Result, sp_runtime::RuntimeString> { - use frame_benchmarking::{BenchmarkError, Benchmarking, BenchmarkBatch}; - use super::*; - - use frame_system_benchmarking::Pallet as SystemBench; - impl frame_system_benchmarking::Config for Runtime { - fn setup_set_code_requirements(code: &Vec) -> Result<(), BenchmarkError> { - ParachainSystem::initialize_for_set_code_benchmark(code.len() as u32); - Ok(()) - } - - fn verify_set_code() { - System::assert_last_event(cumulus_pallet_parachain_system::Event::::ValidationFunctionStored.into()); - } - } - - use cumulus_pallet_session_benchmarking::Pallet as SessionBench; - impl cumulus_pallet_session_benchmarking::Config for Runtime {} - - use frame_support::traits::WhitelistedStorageKeys; - let whitelist = AllPalletsWithSystem::whitelisted_storage_keys(); - - let mut batches = Vec::::new(); - let params = (&config, &whitelist); - add_benchmarks!(params, batches); - - if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } - Ok(batches) - } - } - - impl sp_genesis_builder::GenesisBuilder for Runtime { - fn build_state(config: Vec) -> sp_genesis_builder::Result { - build_state::(config) - } - - fn get_preset(id: &Option) -> Option> { - get_preset::(id, |_| None) - } - - fn preset_names() -> Vec { - Default::default() - } - } - - impl cosmos_runtime_api::CosmosRuntimeApi for Runtime { - fn convert_tx(tx_bytes: Vec) -> ::Extrinsic { - UncheckedExtrinsic::new_unsigned( - pallet_cosmos::Call::::transact { tx_bytes }.into(), - ) - } - - fn simulate(tx_bytes: Vec) -> SimulateResult { - let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| SimulateError::InvalidTx)?; - - ::AnteHandler::ante_handle(&tx, true) - .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; - - let mut context = ::Context::new(::SimulationGasLimit::get()); - pallet_cosmos::Pallet::::run_tx(&mut context, &tx) - .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; - - Ok(SimulateResponse { - gas_info: GasInfo { - gas_wanted: 0, - gas_used: context.gas_meter().consumed_gas() - }, - events: context.event_manager().events() - }) - } - - fn chain_info() -> ChainInfo { - let chain_id = ::ChainInfo::chain_id().to_string(); - let name = ::ChainInfo::name().to_string(); - let bech32_prefix = ::ChainInfo::bech32_prefix().to_string(); - let version = ::ChainInfo::version().to_string(); - - ChainInfo { - chain_id, - name, - bech32_prefix, - version, - } - } - } - - impl cosmwasm_runtime_api::CosmwasmRuntimeApi> for Runtime { - fn query( - contract: String, - gas: u64, - query_request: Vec, - ) -> Result, Vec>{ - let contract = ::AccountToAddr::convert(contract.clone()).map_err(|_| "Not exist contract".as_bytes().to_vec())?; - match pallet_cosmwasm::query::( - contract, - gas, - query_request, - ) { - Ok(response) => Ok(response.into()), - Err(err) => Err(format!("{:?}", err).into_bytes()) - } - } - } -} diff --git a/ziggurat/runtime/src/benchmarks.rs b/ziggurat/runtime/src/benchmarks.rs deleted file mode 100644 index 9fbf1ad8..00000000 --- a/ziggurat/runtime/src/benchmarks.rs +++ /dev/null @@ -1,36 +0,0 @@ -// This is free and unencumbered software released into the public domain. -// -// Anyone is free to copy, modify, publish, use, compile, sell, or -// distribute this software, either in source code form or as a compiled -// binary, for any purpose, commercial or non-commercial, and by any -// means. -// -// In jurisdictions that recognize copyright laws, the author or authors -// of this software dedicate any and all copyright interest in the -// software to the public domain. We make this dedication for the benefit -// of the public at large and to the detriment of our heirs and -// successors. We intend this dedication to be an overt act of -// relinquishment in perpetuity of all present and future rights to this -// software under copyright law. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -// For more information, please refer to - -frame_benchmarking::define_benchmarks!( - [frame_system, SystemBench::] - [pallet_balances, Balances] - [pallet_session, SessionBench::] - [pallet_timestamp, Timestamp] - [pallet_message_queue, MessageQueue] - [pallet_sudo, Sudo] - [pallet_collator_selection, CollatorSelection] - [cumulus_pallet_parachain_system, ParachainSystem] - [cumulus_pallet_xcmp_queue, XcmpQueue] -); diff --git a/ziggurat/runtime/src/call.rs b/ziggurat/runtime/src/call.rs deleted file mode 100644 index cfe02df0..00000000 --- a/ziggurat/runtime/src/call.rs +++ /dev/null @@ -1,228 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use cosmos_sdk_proto::{ - cosmos::{crypto::secp256k1, tx::v1beta1::Tx}, - prost::Message, -}; -use fp_evm::TransactionValidationError; -use fp_self_contained::SelfContainedCall; -use frame_babel::{ethereum::TransactionExt, UnifyAccount}; -use np_cosmos::Address as CosmosAddress; -use np_ethereum::Address as EthereumAddress; -use pallet_cosmos_types::{any_match, tx_msgs::FeeTx}; -use sp_core::ecdsa; -use sp_runtime::{ - traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf}, - transaction_validity::{ - InvalidTransaction, TransactionValidity, TransactionValidityError, UnknownTransaction, - }, - DispatchResultWithInfo, -}; - -use super::{AccountId, Runtime, RuntimeCall, RuntimeOrigin}; - -impl SelfContainedCall for RuntimeCall { - type SignedInfo = AccountId; - - fn is_self_contained(&self) -> bool { - match self { - RuntimeCall::Ethereum(call) => call.is_self_contained(), - RuntimeCall::Cosmos(call) => call.is_self_contained(), - _ => false, - } - } - - fn check_self_contained(&self) -> Option> { - match self { - RuntimeCall::Ethereum(call) => { - if let pallet_ethereum::Call::transact { transaction } = call { - let check = || { - let origin = transaction.recover_key().map(Self::SignedInfo::from).ok_or( - InvalidTransaction::Custom( - TransactionValidationError::InvalidSignature as u8, - ), - )?; - Ok(origin) - }; - Some(check()) - } else { - None - } - }, - RuntimeCall::Cosmos(call) => - if let pallet_cosmos::Call::transact { tx_bytes } = call { - let check = || { - let tx = - Tx::decode(&mut &tx_bytes[..]).map_err(|_| InvalidTransaction::Call)?; - let fee_payer = tx.fee_payer().ok_or(InvalidTransaction::Call)?; - let signer_infos = - tx.auth_info.ok_or(InvalidTransaction::Call)?.signer_infos; - - let public_key = if fee_payer.is_empty() { - signer_infos.first() - } else { - signer_infos.last() - } - .and_then(|signer_info| signer_info.public_key.as_ref()) - .ok_or(InvalidTransaction::Call)?; - - let origin = any_match!( - public_key, { - secp256k1::PubKey => { - let pubkey = secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| InvalidTransaction::BadSigner)?; - ecdsa::Public::try_from(pubkey.key.as_ref()).map(Self::SignedInfo::from).map_err(|_| InvalidTransaction::BadSigner) - } - }, - Err(InvalidTransaction::BadSigner) - )?; - - Ok(origin) - }; - Some(check()) - } else { - None - }, - _ => None, - } - } - - fn validate_self_contained( - &self, - info: &Self::SignedInfo, - dispatch_info: &DispatchInfoOf, - len: usize, - ) -> Option { - match self { - RuntimeCall::Ethereum(call) => { - if let pallet_ethereum::Call::transact { transaction } = call { - if transaction.nonce() == 0 { - match UnifyAccount::::unify_ecdsa(info) { - Ok(_) => (), - Err(_) => - return Some(Err(TransactionValidityError::Unknown( - UnknownTransaction::CannotLookup, - ))), - } - } - } - let public: ecdsa::Public = info.clone().try_into().unwrap(); - let address: EthereumAddress = public.into(); - call.validate_self_contained(&address.into(), dispatch_info, len) - }, - RuntimeCall::Cosmos(call) => { - if let pallet_cosmos::Call::transact { tx_bytes } = call { - match Self::unify_cosmos_account(tx_bytes) { - Ok(_) => (), - Err(e) => return Some(Err(e)), - }; - }; - let public: ecdsa::Public = info.clone().try_into().unwrap(); - let address: CosmosAddress = public.into(); - call.validate_self_contained(&address.into(), dispatch_info, len) - }, - _ => None, - } - } - - fn pre_dispatch_self_contained( - &self, - info: &Self::SignedInfo, - dispatch_info: &DispatchInfoOf, - len: usize, - ) -> Option> { - match self { - RuntimeCall::Ethereum(call) => { - if let pallet_ethereum::Call::transact { transaction } = &call { - if transaction.nonce() == 0 { - match UnifyAccount::::unify_ecdsa(info) { - Ok(_) => (), - Err(_) => - return Some(Err(TransactionValidityError::Unknown( - UnknownTransaction::CannotLookup, - ))), - } - } - } - let public: ecdsa::Public = info.clone().try_into().unwrap(); - let address: EthereumAddress = public.into(); - call.pre_dispatch_self_contained(&address.into(), dispatch_info, len) - }, - RuntimeCall::Cosmos(call) => { - if let pallet_cosmos::Call::transact { tx_bytes } = call { - match Self::unify_cosmos_account(tx_bytes) { - Ok(_) => (), - Err(e) => return Some(Err(e)), - }; - }; - call.pre_dispatch_self_contained(dispatch_info, len) - }, - _ => None, - } - } - - fn apply_self_contained( - self, - info: Self::SignedInfo, - ) -> Option>> { - match self { - call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { - let public: ecdsa::Public = info.clone().try_into().unwrap(); - let address: EthereumAddress = public.into(); - Some(call.dispatch(RuntimeOrigin::from( - pallet_ethereum::RawOrigin::EthereumTransaction(address.into()), - ))) - }, - call @ RuntimeCall::Cosmos(pallet_cosmos::Call::transact { .. }) => { - let public: ecdsa::Public = info.clone().try_into().unwrap(); - let address: CosmosAddress = public.into(); - Some(call.dispatch(RuntimeOrigin::from( - pallet_cosmos::RawOrigin::CosmosTransaction(address.into()), - ))) - }, - _ => None, - } - } -} - -impl RuntimeCall { - fn unify_cosmos_account(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { - let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; - let signer_infos = &tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?.signer_infos; - - for signer_info in signer_infos.iter() { - if signer_info.sequence == 0 { - let public_key = signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; - let signer = any_match!( - public_key, { - secp256k1::PubKey => { - let pubkey = secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| InvalidTransaction::BadSigner)?; - ecdsa::Public::try_from(pubkey.key.as_ref()).map(AccountId::from).map_err(|_| InvalidTransaction::BadSigner) - } - }, - Err(InvalidTransaction::BadSigner) - )?; - - UnifyAccount::::unify_ecdsa(&signer) - .map_err(|_| InvalidTransaction::Call)?; - } - } - - Ok(()) - } -} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs deleted file mode 100644 index 5a1d0903..00000000 --- a/ziggurat/runtime/src/configs/mod.rs +++ /dev/null @@ -1,618 +0,0 @@ -// This is free and unencumbered software released into the public domain. -// -// Anyone is free to copy, modify, publish, use, compile, sell, or -// distribute this software, either in source code form or as a compiled -// binary, for any purpose, commercial or non-commercial, and by any -// means. -// -// In jurisdictions that recognize copyright laws, the author or authors -// of this software dedicate any and all copyright interest in the -// software to the public domain. We make this dedication for the benefit -// of the public at large and to the detriment of our heirs and -// successors. We intend this dedication to be an overt act of -// relinquishment in perpetuity of all present and future rights to this -// software under copyright law. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -// For more information, please refer to - -mod xcm_config; - -// Substrate and Polkadot dependencies -use alloc::{ - string::{String, ToString}, - vec, - vec::Vec, -}; -use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; -use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; -use fp_evm::weight_per_gas; -use frame_babel::{ - cosmos::{self, precompile::Precompiles}, - ethereum::{self, BabelPrecompiles, EnsureAddress, ASSET_PRECOMPILE_ADDRESS_PREFIX}, - extensions::unify_account, - VarAddress, -}; -use frame_support::{ - derive_impl, - dispatch::DispatchClass, - parameter_types, - traits::{ - ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, NeverEnsureOrigin, - TransformOrigin, VariantCountOf, - }, - weights::{ConstantMultiplier, Weight}, - Blake2_128Concat, PalletId, -}; -use frame_system::{ - limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureSigned, -}; -use pallet_assets::pallet::Instance2; -use pallet_cosmos::{ - config_preludes::{ - MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, - SimulationGasLimit, TxSigLimit, WeightToGas, - }, - types::{AssetIdOf, DenomOf}, -}; -use pallet_cosmos_types::{coin::DecCoin, context}; -use pallet_cosmos_x_auth_signing::{ - sign_mode_handler::SignModeHandler, sign_verifiable_tx::SigVerifiableTx, -}; -use pallet_cosmwasm::instrument::CostRules; -use pallet_ethereum::PostLogContent; -use pallet_multimap::traits::UniqueMap; -use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; -use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; -use polkadot_runtime_common::{ - xcm_sender::NoPriceForMessageDelivery, BlockHashCount, SlowAdjustingFeeUpdate, -}; -use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{ConstU128, H160, U256}; -use sp_runtime::{traits::TryConvert, BoundedVec, Perbill, Permill}; -use sp_version::RuntimeVersion; -use xcm::latest::prelude::BodyId; - -// Local module imports -use super::{ - weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}, - AccountId, AddressMap, AssetId, AssetMap, Assets, Aura, Balance, Balances, BaseFee, Block, - BlockNumber, CollatorSelection, ConsensusHook, Hash, Instance1, MessageQueue, Nonce, - PalletInfo, ParachainSystem, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, System, Timestamp, - WeightToFee, XcmpQueue, AVERAGE_ON_INITIALIZE_RATIO, EXISTENTIAL_DEPOSIT, HOURS, - MAXIMUM_BLOCK_WEIGHT, MICROUNIT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, -}; -use xcm_config::{RelayLocation, XcmOriginToTransactDispatchOrigin}; - -parameter_types! { - pub const Version: RuntimeVersion = VERSION; - - // This part is copied from Substrate's `bin/node/runtime/src/lib.rs`. - // The `RuntimeBlockLength` and `RuntimeBlockWeights` exist here because the - // `DeletionWeightLimit` and `DeletionQueueDepth` depend on those to parameterize - // the lazy contract deletion. - pub RuntimeBlockLength: BlockLength = - BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); - pub RuntimeBlockWeights: BlockWeights = BlockWeights::builder() - .base_block(BlockExecutionWeight::get()) - .for_class(DispatchClass::all(), |weights| { - weights.base_extrinsic = ExtrinsicBaseWeight::get(); - }) - .for_class(DispatchClass::Normal, |weights| { - weights.max_total = Some(NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT); - }) - .for_class(DispatchClass::Operational, |weights| { - weights.max_total = Some(MAXIMUM_BLOCK_WEIGHT); - // Operational transactions have some extra reserved space, so that they - // are included even if block reached `MAXIMUM_BLOCK_WEIGHT`. - weights.reserved = Some( - MAXIMUM_BLOCK_WEIGHT - NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT - ); - }) - .avg_block_initialization(AVERAGE_ON_INITIALIZE_RATIO) - .build_or_panic(); - pub const SS58Prefix: u16 = 42; -} - -/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from -/// [`ParaChainDefaultConfig`](`struct@frame_system::config_preludes::ParaChainDefaultConfig`), -/// but overridden as needed. -#[derive_impl(frame_system::config_preludes::ParaChainDefaultConfig)] -impl frame_system::Config for Runtime { - /// The identifier used to distinguish between accounts. - type AccountId = AccountId; - /// The lookup mechanism to get account ID from whatever is passed in dispatchers. - type Lookup = sp_runtime::traits::AccountIdLookup; - /// The index type for storing how many extrinsics an account has signed. - type Nonce = Nonce; - /// The type for hashing blocks and tries. - type Hash = Hash; - /// The block type. - type Block = Block; - /// Maximum number of block number to block hash mappings to keep (oldest pruned first). - type BlockHashCount = BlockHashCount; - /// Runtime version. - type Version = Version; - /// The data to be stored in an account. - type AccountData = pallet_balances::AccountData; - /// The weight of database operations that the runtime can invoke. - type DbWeight = RocksDbWeight; - /// Block & extrinsics weights: base values and limits. - type BlockWeights = RuntimeBlockWeights; - /// The maximum length of a block (in bytes). - type BlockLength = RuntimeBlockLength; - /// This is used as an identifier of the chain. 42 is the generic substrate prefix. - type SS58Prefix = SS58Prefix; - /// The action to take on a Runtime Upgrade - type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -impl pallet_timestamp::Config for Runtime { - /// A timestamp: milliseconds since the unix epoch. - type Moment = u64; - type OnTimestampSet = Aura; - type MinimumPeriod = ConstU64<0>; - type WeightInfo = (); -} - -impl pallet_authorship::Config for Runtime { - type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = (CollatorSelection,); -} - -parameter_types! { - pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT; -} - -impl pallet_balances::Config for Runtime { - type MaxLocks = ConstU32<50>; - /// The type for recording an account's balance. - type Balance = Balance; - /// The ubiquitous event type. - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = pallet_balances::weights::SubstrateWeight; - type MaxReserves = ConstU32<50>; - type ReserveIdentifier = [u8; 8]; - type RuntimeHoldReason = RuntimeHoldReason; - type RuntimeFreezeReason = RuntimeFreezeReason; - type FreezeIdentifier = RuntimeFreezeReason; - type MaxFreezes = VariantCountOf; -} - -parameter_types! { - /// Relay Chain `TransactionByteFee` / 10 - pub const TransactionByteFee: Balance = 10 * MICROUNIT; -} - -impl pallet_transaction_payment::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = pallet_transaction_payment::FungibleAdapter; - type WeightToFee = WeightToFee; - type LengthToFee = ConstantMultiplier; - type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; - type OperationalFeeMultiplier = ConstU8<5>; -} - -impl pallet_sudo::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type RuntimeCall = RuntimeCall; - type WeightInfo = (); -} - -parameter_types! { - pub const ReservedXcmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); - pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); - pub const RelayOrigin: AggregateMessageOrigin = AggregateMessageOrigin::Parent; -} - -impl cumulus_pallet_parachain_system::Config for Runtime { - type WeightInfo = (); - type RuntimeEvent = RuntimeEvent; - type OnSystemEvent = (); - type SelfParaId = parachain_info::Pallet; - type OutboundXcmpMessageSource = XcmpQueue; - type DmpQueue = frame_support::traits::EnqueueWithOrigin; - type ReservedDmpWeight = ReservedDmpWeight; - type XcmpMessageHandler = XcmpQueue; - type ReservedXcmpWeight = ReservedXcmpWeight; - type CheckAssociatedRelayNumber = RelayNumberMonotonicallyIncreases; - type ConsensusHook = ConsensusHook; -} - -impl parachain_info::Config for Runtime {} - -parameter_types! { - pub MessageQueueServiceWeight: Weight = Perbill::from_percent(35) * RuntimeBlockWeights::get().max_block; -} - -impl pallet_message_queue::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor< - cumulus_primitives_core::AggregateMessageOrigin, - >; - #[cfg(not(feature = "runtime-benchmarks"))] - type MessageProcessor = xcm_builder::ProcessXcmMessage< - AggregateMessageOrigin, - xcm_executor::XcmExecutor, - RuntimeCall, - >; - type Size = u32; - // The XCMP queue pallet is only ever able to handle the `Sibling(ParaId)` origin: - type QueueChangeHandler = NarrowOriginToSibling; - type QueuePausedQuery = NarrowOriginToSibling; - type HeapSize = sp_core::ConstU32<{ 103 * 1024 }>; - type MaxStale = sp_core::ConstU32<8>; - type ServiceWeight = MessageQueueServiceWeight; - type IdleMaxServiceWeight = (); -} - -impl cumulus_pallet_aura_ext::Config for Runtime {} - -impl cumulus_pallet_xcmp_queue::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type ChannelInfo = ParachainSystem; - type VersionWrapper = (); - // Enqueue XCMP messages from siblings for later processing. - type XcmpQueue = TransformOrigin; - type MaxInboundSuspended = sp_core::ConstU32<1_000>; - type MaxActiveOutboundChannels = ConstU32<128>; - type MaxPageSize = ConstU32<{ 1 << 16 }>; - type ControllerOrigin = EnsureRoot; - type ControllerOriginConverter = XcmOriginToTransactDispatchOrigin; - type WeightInfo = (); - type PriceForSiblingDelivery = NoPriceForMessageDelivery; -} - -parameter_types! { - pub const Period: u32 = 6 * HOURS; - pub const Offset: u32 = 0; -} - -impl pallet_session::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type ValidatorId = ::AccountId; - // we don't have stash and controller, thus we don't need the convert as well. - type ValidatorIdOf = pallet_collator_selection::IdentityCollator; - type ShouldEndSession = pallet_session::PeriodicSessions; - type NextSessionRotation = pallet_session::PeriodicSessions; - type SessionManager = CollatorSelection; - // Essentially just Aura, but let's be pedantic. - type SessionHandler = ::KeyTypeIdProviders; - type Keys = SessionKeys; - type WeightInfo = (); -} - -#[docify::export(aura_config)] -impl pallet_aura::Config for Runtime { - type AuthorityId = AuraId; - type DisabledValidators = (); - type MaxAuthorities = ConstU32<100_000>; - type AllowMultipleBlocksPerSlot = ConstBool; - type SlotDuration = ConstU64; -} - -parameter_types! { - pub const PotId: PalletId = PalletId(*b"PotStake"); - pub const SessionLength: BlockNumber = 6 * HOURS; - // StakingAdmin pluralistic body. - pub const StakingAdminBodyId: BodyId = BodyId::Defense; -} - -/// We allow root and the StakingAdmin to execute privileged collator selection operations. -pub type CollatorSelectionUpdateOrigin = EitherOfDiverse< - EnsureRoot, - EnsureXcm>, ->; - -impl pallet_collator_selection::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type UpdateOrigin = CollatorSelectionUpdateOrigin; - type PotId = PotId; - type MaxCandidates = ConstU32<100>; - type MinEligibleCollators = ConstU32<4>; - type MaxInvulnerables = ConstU32<20>; - // should be a multiple of session or things will get inconsistent - type KickThreshold = Period; - type ValidatorId = ::AccountId; - type ValidatorIdOf = pallet_collator_selection::IdentityCollator; - type ValidatorRegistration = Session; - type WeightInfo = (); -} - -#[derive_impl(pallet_multimap::config_preludes::TestDefaultConfig)] -impl pallet_multimap::Config for Runtime { - type Key = AccountId; - type Value = VarAddress; - type CapacityPerKey = ConstU32<{ VarAddress::variant_count() }>; -} - -impl unify_account::Config for Runtime { - type AddressMap = AddressMap; - type DrainBalance = Balances; -} - -const BLOCK_GAS_LIMIT: u64 = 75_000_000; -const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; -const WEIGHT_MILLISECS_PER_BLOCK: u64 = 2000; - -parameter_types! { - pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); - pub const ChainId: u64 = 1337; - pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); - pub PrecompilesValue: BabelPrecompiles = BabelPrecompiles::<_>::new(); - pub WeightPerGas: Weight = Weight::from_parts(weight_per_gas(BLOCK_GAS_LIMIT, NORMAL_DISPATCH_RATIO, WEIGHT_MILLISECS_PER_BLOCK), 0); - pub SuicideQuickClearLimit: u32 = 0; -} - -impl frame_babel::ethereum::precompile::Config for Runtime { - type DispatchValidator = (); - type DecodeLimit = ConstU32<8>; - type StorageFilter = (); -} - -impl frame_babel::ethereum::Erc20Metadata for Runtime { - fn name() -> &'static str { - "Ziggurat" - } - - fn symbol() -> &'static str { - "ZIG" - } - - fn decimals() -> u8 { - 18 - } - - fn is_native_currency() -> bool { - true - } -} - -impl frame_babel::ethereum::AddressToAssetId for Runtime { - fn address_to_asset_id(address: H160) -> Option { - let mut data = [0u8; 4]; - let address_bytes: [u8; 20] = address.into(); - if ASSET_PRECOMPILE_ADDRESS_PREFIX.eq(&address_bytes[0..16]) { - data.copy_from_slice(&address_bytes[16..20]); - Some(AssetId::from_be_bytes(data)) - } else { - None - } - } - - fn asset_id_to_address(asset_id: AssetId) -> H160 { - let mut data = [0u8; 20]; - data[0..16].copy_from_slice(ASSET_PRECOMPILE_ADDRESS_PREFIX); - data[16..20].copy_from_slice(&asset_id.to_be_bytes()); - H160::from(data) - } -} - -impl pallet_evm::Config for Runtime { - type FeeCalculator = BaseFee; - type GasWeightMapping = pallet_evm::FixedGasWeightMapping; - type WeightPerGas = WeightPerGas; - type BlockHashMapping = pallet_ethereum::EthereumBlockHashMapping; - type CallOrigin = EnsureAddress; - type WithdrawOrigin = EnsureAddress; - type AddressMapping = ethereum::AddressMapping; - type Currency = Balances; - type RuntimeEvent = RuntimeEvent; - type PrecompilesType = BabelPrecompiles; - type PrecompilesValue = PrecompilesValue; - type ChainId = ChainId; - type BlockGasLimit = BlockGasLimit; - type Runner = pallet_evm::runner::stack::Runner; - type OnChargeTransaction = (); - type OnCreate = (); - type FindAuthor = (); - type GasLimitPovSizeRatio = GasLimitPovSizeRatio; - type SuicideQuickClearLimit = SuicideQuickClearLimit; - type Timestamp = Timestamp; - type WeightInfo = pallet_evm::weights::SubstrateWeight; -} - -parameter_types! { - pub const PostBlockAndTxnHashes: PostLogContent = PostLogContent::BlockAndTxnHashes; -} - -impl pallet_ethereum::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type StateRoot = pallet_ethereum::IntermediateStateRoot; - type PostLogContent = PostBlockAndTxnHashes; - type ExtraDataLength = ConstU32<30>; -} - -parameter_types! { - pub DefaultBaseFeePerGas: U256 = U256::from(1_000_000_000); - pub DefaultElasticity: Permill = Permill::from_parts(125_000); -} - -pub struct BaseFeeThreshold; -impl pallet_base_fee::BaseFeeThreshold for BaseFeeThreshold { - fn lower() -> Permill { - Permill::zero() - } - fn ideal() -> Permill { - Permill::from_parts(500_000) - } - fn upper() -> Permill { - Permill::from_parts(1_000_000) - } -} - -impl pallet_base_fee::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Threshold = BaseFeeThreshold; - type DefaultBaseFeePerGas = DefaultBaseFeePerGas; - type DefaultElasticity = DefaultElasticity; -} - -impl pallet_multimap::Config for Runtime { - type Key = AssetIdOf; - type Value = DenomOf; - type CapacityPerKey = ConstU32<1>; - type KeyHasher = Blake2_128Concat; - type ValueHasher = Blake2_128Concat; -} - -impl pallet_assets::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Balance = Balance; - type AssetId = AssetId; - type AssetIdParameter = codec::Compact; - type Currency = Balances; - type CreateOrigin = NeverEnsureOrigin; - type ForceOrigin = EnsureRoot; - type AssetDeposit = ConstU128<500>; - type AssetAccountDeposit = ConstU128<500>; - type MetadataDepositBase = ConstU128<0>; - type MetadataDepositPerByte = ConstU128<0>; - type ApprovalDeposit = ConstU128<0>; - type StringLimit = ConstU32<20>; - type Freezer = (); - type Extra = (); - type CallbackHandle = (); - type WeightInfo = (); - type RemoveItemsLimit = ConstU32<1000>; -} - -pub struct MinGasPrices; -impl context::traits::MinGasPrices for MinGasPrices { - fn min_prices() -> Vec { - vec![DecCoin { denom: NativeDenom::get().to_string(), amount: 1 }] - } -} - -pub struct AssetToDenom; -impl TryConvert for AssetToDenom { - fn try_convert(denom: String) -> Result { - if denom == NativeDenom::get() { - Ok(NativeAssetId::get()) - } else { - let denom_raw: BoundedVec = - denom.as_bytes().to_vec().try_into().map_err(|_| denom.clone())?; - AssetMap::find_key(denom_raw).ok_or(denom.clone()) - } - } -} -impl TryConvert for AssetToDenom { - fn try_convert(asset_id: AssetId) -> Result { - if asset_id == NativeAssetId::get() { - Ok(NativeDenom::get().to_string()) - } else { - let denom = - >>::get(asset_id) - .ok_or(asset_id)?; - String::from_utf8(denom.into()).map_err(|_| asset_id) - } - } -} - -impl pallet_cosmos::Config for Runtime { - type AddressMapping = cosmos::address::AddressMapping; - type Balance = Balance; - type AssetId = AssetId; - type NativeAsset = Balances; - type Assets = Assets; - type NativeDenom = NativeDenom; - type NativeAssetId = NativeAssetId; - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_cosmos::weights::CosmosWeight; - type WeightToGas = WeightToGas; - type MinGasPrices = MinGasPrices; - type AssetToDenom = AssetToDenom; - type Context = context::Context; - type ChainInfo = np_cosmos::traits::CosmosHub; - type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; - type MsgFilter = MsgFilter; - type MsgServiceRouter = cosmos::msg::MsgServiceRouter; - type SigVerifiableTx = SigVerifiableTx; - type SignModeHandler = SignModeHandler; - type MaxMemoCharacters = MaxMemoCharacters; - type TxSigLimit = TxSigLimit; - type MaxDenomLimit = MaxDenomLimit; - type SimulationGasLimit = SimulationGasLimit; -} - -parameter_types! { - pub const CosmwasmPalletId: PalletId = PalletId(*b"cosmwasm"); - pub const MaxContractLabelSize: u32 = 64; - pub const MaxContractTrieIdSize: u32 = Hash::len_bytes() as u32; - pub const MaxInstantiateSaltSize: u32 = 128; - pub const MaxFundsAssets: u32 = 32; - pub const CodeTableSizeLimit: u32 = 4096; - pub const CodeGlobalVariableLimit: u32 = 256; - pub const CodeParameterLimit: u32 = 128; - pub const CodeBranchTableSizeLimit: u32 = 256; - pub const CodeStorageByteDeposit: u32 = 1_000_000; - pub const ContractStorageByteReadPrice: u32 = 1; - pub const ContractStorageByteWritePrice: u32 = 1; - pub WasmCostRules: CostRules = Default::default(); -} - -impl pallet_cosmwasm::Config for Runtime { - const MAX_FRAMES: u8 = 64; - type RuntimeEvent = RuntimeEvent; - type AccountIdExtended = AccountId; - type PalletId = CosmwasmPalletId; - type MaxCodeSize = ConstU32<{ 1024 * 1024 }>; - type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; - type MaxMessageSize = ConstU32<{ 64 * 1024 }>; - type AccountToAddr = cosmos::address::AccountToAddr; - type AssetToDenom = AssetToDenom; - type Balance = Balance; - type AssetId = AssetId; - type Assets = Assets; - type NativeAsset = Balances; - type ChainInfo = np_cosmos::traits::CosmosHub; - type MaxContractLabelSize = MaxContractLabelSize; - type MaxContractTrieIdSize = MaxContractTrieIdSize; - type MaxInstantiateSaltSize = MaxInstantiateSaltSize; - type MaxFundsAssets = MaxFundsAssets; - - type CodeTableSizeLimit = CodeTableSizeLimit; - type CodeGlobalVariableLimit = CodeGlobalVariableLimit; - type CodeStackLimit = ConstU32<{ u32::MAX }>; - - type CodeParameterLimit = CodeParameterLimit; - type CodeBranchTableSizeLimit = CodeBranchTableSizeLimit; - type CodeStorageByteDeposit = CodeStorageByteDeposit; - type ContractStorageByteReadPrice = ContractStorageByteReadPrice; - type ContractStorageByteWritePrice = ContractStorageByteWritePrice; - - type WasmCostRules = WasmCostRules; - type UnixTime = Timestamp; - type WeightInfo = pallet_cosmwasm::weights::SubstrateWeight; - - type PalletHook = Precompiles; - - type UploadWasmOrigin = EnsureSigned; - - type ExecuteWasmOrigin = EnsureSigned; - - type NativeDenom = NativeDenom; - - type NativeAssetId = NativeAssetId; -} - -impl frame_babel::Config for Runtime { - type AddressMap = AddressMap; - type AssetMap = AssetMap; -} diff --git a/ziggurat/runtime/src/configs/xcm_config.rs b/ziggurat/runtime/src/configs/xcm_config.rs deleted file mode 100644 index e162bcbf..00000000 --- a/ziggurat/runtime/src/configs/xcm_config.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::{ - AccountId, AllPalletsWithSystem, Balances, ParachainInfo, ParachainSystem, PolkadotXcm, - Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, -}; -use frame_support::{ - parameter_types, - traits::{ConstU32, Contains, Everything, Nothing}, - weights::Weight, -}; -use frame_system::EnsureRoot; -use pallet_xcm::XcmPassthrough; -use polkadot_parachain_primitives::primitives::Sibling; -use polkadot_runtime_common::impls::ToAuthor; -use xcm::latest::prelude::*; -use xcm_builder::{ - AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowTopLevelPaidExecutionFrom, - DenyReserveTransferToRelayChain, DenyThenTry, EnsureXcmOrigin, FixedWeightBounds, - FrameTransactionalProcessor, FungibleAdapter, IsConcrete, NativeAsset, ParentIsPreset, - RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, -}; -use xcm_executor::XcmExecutor; - -parameter_types! { - pub const RelayLocation: Location = Location::parent(); - pub const RelayNetwork: Option = None; - pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); - // For the real deployment, it is recommended to set `RelayNetwork` according to the relay chain - // and prepend `UniversalLocation` with `GlobalConsensus(RelayNetwork::get())`. - pub UniversalLocation: InteriorLocation = Parachain(ParachainInfo::parachain_id().into()).into(); -} - -/// Type for specifying how a `Location` can be converted into an `AccountId`. This is used -/// when determining ownership of accounts for asset transacting and when attempting to use XCM -/// `Transact` in order to determine the dispatch Origin. -pub type LocationToAccountId = ( - // The parent (Relay-chain) origin converts to the parent `AccountId`. - ParentIsPreset, - // Sibling parachain origins convert to AccountId via the `ParaId::into`. - SiblingParachainConvertsVia, - // Straight up local `AccountId32` origins just alias directly to `AccountId`. - AccountId32Aliases, -); - -/// Means for transacting assets on this chain. -pub type LocalAssetTransactor = FungibleAdapter< - // Use this currency: - Balances, - // Use this currency when it is a fungible asset matching the given location or name: - IsConcrete, - // Do a simple punn to convert an AccountId32 Location into a native chain account ID: - LocationToAccountId, - // Our chain's account ID type (we can't get away without mentioning it explicitly): - AccountId, - // We don't track any teleports. - (), ->; - -/// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, -/// ready for dispatching a transaction with Xcm's `Transact`. There is an `OriginKind` which can -/// biases the kind of local `Origin` it will become. -pub type XcmOriginToTransactDispatchOrigin = ( - // Sovereign account converter; this attempts to derive an `AccountId` from the origin location - // using `LocationToAccountId` and then turn that into the usual `Signed` origin. Useful for - // foreign chains who want to have a local sovereign account on this chain which they control. - SovereignSignedViaLocation, - // Native converter for Relay-chain (Parent) location; will convert to a `Relay` origin when - // recognized. - RelayChainAsNative, - // Native converter for sibling Parachains; will convert to a `SiblingPara` origin when - // recognized. - SiblingParachainAsNative, - // Native signed account converter; this just converts an `AccountId32` origin into a normal - // `RuntimeOrigin::Signed` origin of the same 32-byte value. - SignedAccountId32AsNative, - // Xcm origins can be represented natively under the Xcm pallet's Xcm origin. - XcmPassthrough, -); - -parameter_types! { - // One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate. - pub UnitWeightCost: Weight = Weight::from_parts(1_000_000_000, 64 * 1024); - pub const MaxInstructions: u32 = 100; - pub const MaxAssetsIntoHolding: u32 = 64; -} - -pub struct ParentOrParentsExecutivePlurality; -impl Contains for ParentOrParentsExecutivePlurality { - fn contains(location: &Location) -> bool { - matches!(location.unpack(), (1, []) | (1, [Plurality { id: BodyId::Executive, .. }])) - } -} - -pub type Barrier = TrailingSetTopicAsId< - DenyThenTry< - DenyReserveTransferToRelayChain, - ( - TakeWeightCredit, - WithComputedOrigin< - ( - AllowTopLevelPaidExecutionFrom, - AllowExplicitUnpaidExecutionFrom, - // ^^^ Parent and its exec plurality get free execution - ), - UniversalLocation, - ConstU32<8>, - >, - ), - >, ->; - -pub struct XcmConfig; -impl xcm_executor::Config for XcmConfig { - type RuntimeCall = RuntimeCall; - type XcmSender = XcmRouter; - // How to withdraw and deposit an asset. - type AssetTransactor = LocalAssetTransactor; - type OriginConverter = XcmOriginToTransactDispatchOrigin; - type IsReserve = NativeAsset; - type IsTeleporter = (); // Teleporting is disabled. - type UniversalLocation = UniversalLocation; - type Barrier = Barrier; - type Weigher = FixedWeightBounds; - type Trader = - UsingComponents>; - type ResponseHandler = PolkadotXcm; - type AssetTrap = PolkadotXcm; - type AssetClaims = PolkadotXcm; - type SubscriptionService = PolkadotXcm; - type PalletInstancesInfo = AllPalletsWithSystem; - type MaxAssetsIntoHolding = MaxAssetsIntoHolding; - type AssetLocker = (); - type AssetExchanger = (); - type FeeManager = (); - type MessageExporter = (); - type UniversalAliases = Nothing; - type CallDispatcher = RuntimeCall; - type SafeCallFilter = Everything; - type Aliasers = Nothing; - type TransactionalProcessor = FrameTransactionalProcessor; - type HrmpNewChannelOpenRequestHandler = (); - type HrmpChannelAcceptedHandler = (); - type HrmpChannelClosingHandler = (); - type XcmRecorder = PolkadotXcm; -} - -/// No local origins on this chain are allowed to dispatch XCM sends/executions. -pub type LocalOriginToLocation = SignedToAccountId32; - -/// The means for routing XCM messages which are not for local execution into the right message -/// queues. -pub type XcmRouter = WithUniqueTopic<( - // Two routers - use UMP to communicate with the relay chain: - cumulus_primitives_utility::ParentAsUmp, - // ..and XCMP to communicate with the sibling chains. - XcmpQueue, -)>; - -impl pallet_xcm::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type SendXcmOrigin = EnsureXcmOrigin; - type XcmRouter = XcmRouter; - type ExecuteXcmOrigin = EnsureXcmOrigin; - type XcmExecuteFilter = Nothing; - // ^ Disable dispatchable execute on the XCM pallet. - // Needs to be `Everything` for local testing. - type XcmExecutor = XcmExecutor; - type XcmTeleportFilter = Everything; - type XcmReserveTransferFilter = Nothing; - type Weigher = FixedWeightBounds; - type UniversalLocation = UniversalLocation; - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - - const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; - // ^ Override for AdvertisedXcmVersion default - type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; - type Currency = Balances; - type CurrencyMatcher = (); - type TrustedLockers = (); - type SovereignAccountOf = LocationToAccountId; - type MaxLockers = ConstU32<8>; - type WeightInfo = pallet_xcm::TestWeightInfo; - type AdminOrigin = EnsureRoot; - type MaxRemoteLockConsumers = ConstU32<0>; - type RemoteLockConsumerIdentifier = (); -} - -impl cumulus_pallet_xcm::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type XcmExecutor = XcmExecutor; -} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs deleted file mode 100644 index ee824c8e..00000000 --- a/ziggurat/runtime/src/lib.rs +++ /dev/null @@ -1,333 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] -// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. -#![recursion_limit = "256"] - -// Make the WASM binary available. -#[cfg(feature = "std")] -include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); - -pub mod apis; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarks; -mod call; -mod configs; -mod weights; - -extern crate alloc; -use alloc::vec::Vec; -use np_runtime::{self_contained, MultiSignature}; -use smallvec::smallvec; -use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, - traits::{BlakeTwo256, IdentifyAccount, Verify}, -}; - -#[cfg(feature = "std")] -use sp_version::NativeVersion; -use sp_version::RuntimeVersion; - -use frame_support::{ - instances::Instance1, - weights::{ - constants::WEIGHT_REF_TIME_PER_SECOND, Weight, WeightToFeeCoefficient, - WeightToFeeCoefficients, WeightToFeePolynomial, - }, -}; -pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; -pub use sp_runtime::{MultiAddress, Perbill, Permill}; - -#[cfg(any(feature = "std", test))] -pub use sp_runtime::BuildStorage; - -use weights::ExtrinsicBaseWeight; - -/// Alias to 512-bit hash when used in the context of a transaction signature on the chain. -pub type Signature = MultiSignature; - -/// Some way of identifying an account on the chain. We intentionally make it equivalent -/// to the public key of our transaction signing scheme. -pub type AccountId = <::Signer as IdentifyAccount>::AccountId; - -/// Balance of an account. -pub type Balance = u128; - -/// Index of a transaction in the chain. -pub type Nonce = u32; - -/// A hash of some data used by the chain. -pub type Hash = sp_core::H256; - -/// An index to a block. -pub type BlockNumber = u32; - -/// The address format for describing accounts. -pub type Address = MultiAddress; - -/// Block header type as expected by this runtime. -pub type Header = generic::Header; - -/// Block type as expected by this runtime. -pub type Block = generic::Block; - -/// A Block signed with a Justification -pub type SignedBlock = generic::SignedBlock; - -/// BlockId type as expected by this runtime. -pub type BlockId = generic::BlockId; - -pub type AssetId = u32; - -/// The SignedExtension to the basic transaction logic. -#[docify::export(template_signed_extra)] -pub type SignedExtra = ( - frame_system::CheckNonZeroSender, - frame_system::CheckSpecVersion, - frame_system::CheckTxVersion, - frame_system::CheckGenesis, - frame_system::CheckEra, - frame_system::CheckNonce, - frame_system::CheckWeight, - pallet_transaction_payment::ChargeTransactionPayment, - frame_babel::UnifyAccount, - cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim, - frame_metadata_hash_extension::CheckMetadataHash, -); - -/// Unchecked extrinsic type as expected by this runtime. -pub type UncheckedExtrinsic = - self_contained::UncheckedExtrinsic; - -/// Executive: handles dispatch to the various modules. -pub type Executive = frame_executive::Executive< - Runtime, - Block, - frame_system::ChainContext, - Runtime, - AllPalletsWithSystem, ->; - -/// Handles converting a weight scalar to a fee value, based on the scale and granularity of the -/// node's balance type. -/// -/// This should typically create a mapping between the following ranges: -/// - `[0, MAXIMUM_BLOCK_WEIGHT]` -/// - `[Balance::min, Balance::max]` -/// -/// Yet, it can be used for any other sort of change to weight-fee. Some examples being: -/// - Setting it to `0` will essentially disable the weight fee. -/// - Setting it to `1` will cause the literal `#[weight = x]` values to be charged. -pub struct WeightToFee; -impl WeightToFeePolynomial for WeightToFee { - type Balance = Balance; - fn polynomial() -> WeightToFeeCoefficients { - // in Rococo, extrinsic base weight (smallest non-zero weight) is mapped to 1 MILLIUNIT: - // in our template, we map to 1/10 of that, or 1/10 MILLIUNIT - let p = MILLIUNIT / 10; - let q = 100 * Balance::from(ExtrinsicBaseWeight::get().ref_time()); - smallvec![WeightToFeeCoefficient { - degree: 1, - negative: false, - coeff_frac: Perbill::from_rational(p % q, q), - coeff_integer: p / q, - }] - } -} - -/// Opaque types. These are used by the CLI to instantiate machinery that don't need to know -/// the specifics of the runtime. They can then be made to be agnostic over specific formats -/// of data like extrinsics, allowing for them to continue syncing the network through upgrades -/// to even the core data structures. -pub mod opaque { - use super::*; - use sp_runtime::{ - generic, - traits::{BlakeTwo256, Hash as HashT}, - }; - - pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; - /// Opaque block header type. - pub type Header = generic::Header; - /// Opaque block type. - pub type Block = generic::Block; - /// Opaque block identifier type. - pub type BlockId = generic::BlockId; - /// Opaque block hash type. - pub type Hash = ::Output; -} - -impl_opaque_keys! { - pub struct SessionKeys { - pub aura: Aura, - } -} - -#[sp_version::runtime_version] -pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: create_runtime_str!("ziggurat-runtime"), - impl_name: create_runtime_str!("ziggurat-runtime"), - authoring_version: 1, - spec_version: 1, - impl_version: 0, - apis: apis::RUNTIME_API_VERSIONS, - transaction_version: 1, - state_version: 1, -}; - -#[docify::export] -mod block_times { - /// This determines the average expected block time that we are targeting. Blocks will be - /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by - /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn - /// slot_duration()`. - /// - /// Change this to adjust the block time. - pub const MILLISECS_PER_BLOCK: u64 = 6000; - - // NOTE: Currently it is not possible to change the slot duration after the chain has started. - // Attempting to do so will brick block production. - pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; -} -pub use block_times::*; - -// Time is measured by number of blocks. -pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); -pub const HOURS: BlockNumber = MINUTES * 60; -pub const DAYS: BlockNumber = HOURS * 24; - -// Unit = the base number of indivisible units for balances -pub const UNIT: Balance = 1_000_000_000_000_000_000; -pub const MILLIUNIT: Balance = 1_000_000_000_000_000; -pub const MICROUNIT: Balance = 1_000_000_000_000; - -/// The existential deposit. Set to 1/10 of the Connected Relay Chain. -pub const EXISTENTIAL_DEPOSIT: Balance = MILLIUNIT; - -/// We assume that ~5% of the block weight is consumed by `on_initialize` handlers. This is -/// used to limit the maximal weight of a single extrinsic. -const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(5); - -/// We allow `Normal` extrinsics to fill up the block up to 75%, the rest can be used by -/// `Operational` extrinsics. -const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); - -#[docify::export(max_block_weight)] -/// We allow for 2 seconds of compute with a 6 second average block time. -const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( - WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), - cumulus_primitives_core::relay_chain::MAX_POV_SIZE as u64, -); - -#[docify::export] -mod async_backing_params { - /// Maximum number of blocks simultaneously accepted by the Runtime, not yet included - /// into the relay chain. - pub(crate) const UNINCLUDED_SEGMENT_CAPACITY: u32 = 3; - /// How many parachain blocks are processed by the relay chain per parent. Limits the - /// number of blocks authored per slot. - pub(crate) const BLOCK_PROCESSING_VELOCITY: u32 = 1; - /// Relay chain slot duration, in milliseconds. - pub(crate) const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; -} -pub(crate) use async_backing_params::*; - -#[docify::export] -/// Aura consensus hook -type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< - Runtime, - RELAY_CHAIN_SLOT_DURATION_MILLIS, - BLOCK_PROCESSING_VELOCITY, - UNINCLUDED_SEGMENT_CAPACITY, ->; - -/// The version information used to identify this runtime when compiled natively. -#[cfg(feature = "std")] -pub fn native_version() -> NativeVersion { - NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } -} - -// Create the runtime by composing the FRAME pallets that were previously configured. -#[frame_support::runtime] -mod runtime { - #[runtime::runtime] - #[runtime::derive( - RuntimeCall, - RuntimeEvent, - RuntimeError, - RuntimeOrigin, - RuntimeFreezeReason, - RuntimeHoldReason, - RuntimeSlashReason, - RuntimeLockId, - RuntimeTask - )] - pub struct Runtime; - - #[runtime::pallet_index(0)] - pub type System = frame_system; - #[runtime::pallet_index(1)] - pub type ParachainSystem = cumulus_pallet_parachain_system; - #[runtime::pallet_index(2)] - pub type Timestamp = pallet_timestamp; - #[runtime::pallet_index(3)] - pub type ParachainInfo = parachain_info; - - // Monetary stuff. - #[runtime::pallet_index(10)] - pub type Balances = pallet_balances; - #[runtime::pallet_index(11)] - pub type TransactionPayment = pallet_transaction_payment; - - // Governance - #[runtime::pallet_index(15)] - pub type Sudo = pallet_sudo; - - // Collator support. The order of these 4 are important and shall not change. - #[runtime::pallet_index(20)] - pub type Authorship = pallet_authorship; - #[runtime::pallet_index(21)] - pub type CollatorSelection = pallet_collator_selection; - #[runtime::pallet_index(22)] - pub type Session = pallet_session; - #[runtime::pallet_index(23)] - pub type Aura = pallet_aura; - #[runtime::pallet_index(24)] - pub type AuraExt = cumulus_pallet_aura_ext; - - // XCM helpers. - #[runtime::pallet_index(30)] - pub type XcmpQueue = cumulus_pallet_xcmp_queue; - #[runtime::pallet_index(31)] - pub type PolkadotXcm = pallet_xcm; - #[runtime::pallet_index(32)] - pub type CumulusXcm = cumulus_pallet_xcm; - #[runtime::pallet_index(33)] - pub type MessageQueue = pallet_message_queue; - #[runtime::pallet_index(39)] - pub type Assets = pallet_assets; - - // Ethereum compatibility. - #[runtime::pallet_index(60)] - pub type Ethereum = pallet_ethereum; - #[runtime::pallet_index(61)] - pub type Evm = pallet_evm; - #[runtime::pallet_index(62)] - pub type BaseFee = pallet_base_fee; - - #[runtime::pallet_index(128)] - pub type AddressMap = pallet_multimap; - #[runtime::pallet_index(129)] - pub type AssetMap = pallet_multimap; - #[runtime::pallet_index(179)] - pub type Cosmos = pallet_cosmos; - #[runtime::pallet_index(180)] - pub type Cosmwasm = pallet_cosmwasm; - - #[runtime::pallet_index(255)] - pub type Babel = frame_babel; -} - -#[docify::export(register_validate_block)] -cumulus_pallet_parachain_system::register_validate_block! { - Runtime = Runtime, - BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::, -} diff --git a/ziggurat/runtime/src/weights/block_weights.rs b/ziggurat/runtime/src/weights/block_weights.rs deleted file mode 100644 index e7fdb2aa..00000000 --- a/ziggurat/runtime/src/weights/block_weights.rs +++ /dev/null @@ -1,53 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod constants { - use frame_support::{ - parameter_types, - weights::{constants, Weight}, - }; - - parameter_types! { - /// Importing a block with 0 Extrinsics. - pub const BlockExecutionWeight: Weight = - Weight::from_parts(constants::WEIGHT_REF_TIME_PER_NANOS.saturating_mul(5_000_000), 0); - } - - #[cfg(test)] - mod test_weights { - use frame_support::weights::constants; - - /// Checks that the weight exists and is sane. - // NOTE: If this test fails but you are sure that the generated values are fine, - // you can delete it. - #[test] - fn sane() { - let w = super::constants::BlockExecutionWeight::get(); - - // At least 100 µs. - assert!( - w.ref_time() >= 100u64 * constants::WEIGHT_REF_TIME_PER_MICROS, - "Weight should be at least 100 µs." - ); - // At most 50 ms. - assert!( - w.ref_time() <= 50u64 * constants::WEIGHT_REF_TIME_PER_MILLIS, - "Weight should be at most 50 ms." - ); - } - } -} diff --git a/ziggurat/runtime/src/weights/extrinsic_weights.rs b/ziggurat/runtime/src/weights/extrinsic_weights.rs deleted file mode 100644 index 1a4adb96..00000000 --- a/ziggurat/runtime/src/weights/extrinsic_weights.rs +++ /dev/null @@ -1,53 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod constants { - use frame_support::{ - parameter_types, - weights::{constants, Weight}, - }; - - parameter_types! { - /// Executing a NO-OP `System::remarks` Extrinsic. - pub const ExtrinsicBaseWeight: Weight = - Weight::from_parts(constants::WEIGHT_REF_TIME_PER_NANOS.saturating_mul(125_000), 0); - } - - #[cfg(test)] - mod test_weights { - use frame_support::weights::constants; - - /// Checks that the weight exists and is sane. - // NOTE: If this test fails but you are sure that the generated values are fine, - // you can delete it. - #[test] - fn sane() { - let w = super::constants::ExtrinsicBaseWeight::get(); - - // At least 10 µs. - assert!( - w.ref_time() >= 10u64 * constants::WEIGHT_REF_TIME_PER_MICROS, - "Weight should be at least 10 µs." - ); - // At most 1 ms. - assert!( - w.ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, - "Weight should be at most 1 ms." - ); - } - } -} diff --git a/ziggurat/runtime/src/weights/mod.rs b/ziggurat/runtime/src/weights/mod.rs deleted file mode 100644 index b473d49e..00000000 --- a/ziggurat/runtime/src/weights/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Expose the auto generated weight files. - -pub mod block_weights; -pub mod extrinsic_weights; -pub mod paritydb_weights; -pub mod rocksdb_weights; - -pub use block_weights::constants::BlockExecutionWeight; -pub use extrinsic_weights::constants::ExtrinsicBaseWeight; -pub use rocksdb_weights::constants::RocksDbWeight; diff --git a/ziggurat/runtime/src/weights/paritydb_weights.rs b/ziggurat/runtime/src/weights/paritydb_weights.rs deleted file mode 100644 index 25679703..00000000 --- a/ziggurat/runtime/src/weights/paritydb_weights.rs +++ /dev/null @@ -1,63 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod constants { - use frame_support::{ - parameter_types, - weights::{constants, RuntimeDbWeight}, - }; - - parameter_types! { - /// `ParityDB` can be enabled with a feature flag, but is still experimental. These weights - /// are available for brave runtime engineers who may want to try this out as default. - pub const ParityDbWeight: RuntimeDbWeight = RuntimeDbWeight { - read: 8_000 * constants::WEIGHT_REF_TIME_PER_NANOS, - write: 50_000 * constants::WEIGHT_REF_TIME_PER_NANOS, - }; - } - - #[cfg(test)] - mod test_db_weights { - use super::constants::ParityDbWeight as W; - use frame_support::weights::constants; - - /// Checks that all weights exist and have sane values. - // NOTE: If this test fails but you are sure that the generated values are fine, - // you can delete it. - #[test] - fn sane() { - // At least 1 µs. - assert!( - W::get().reads(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, - "Read weight should be at least 1 µs." - ); - assert!( - W::get().writes(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, - "Write weight should be at least 1 µs." - ); - // At most 1 ms. - assert!( - W::get().reads(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, - "Read weight should be at most 1 ms." - ); - assert!( - W::get().writes(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, - "Write weight should be at most 1 ms." - ); - } - } -} diff --git a/ziggurat/runtime/src/weights/rocksdb_weights.rs b/ziggurat/runtime/src/weights/rocksdb_weights.rs deleted file mode 100644 index 3dd817aa..00000000 --- a/ziggurat/runtime/src/weights/rocksdb_weights.rs +++ /dev/null @@ -1,63 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod constants { - use frame_support::{ - parameter_types, - weights::{constants, RuntimeDbWeight}, - }; - - parameter_types! { - /// By default, Substrate uses `RocksDB`, so this will be the weight used throughout - /// the runtime. - pub const RocksDbWeight: RuntimeDbWeight = RuntimeDbWeight { - read: 25_000 * constants::WEIGHT_REF_TIME_PER_NANOS, - write: 100_000 * constants::WEIGHT_REF_TIME_PER_NANOS, - }; - } - - #[cfg(test)] - mod test_db_weights { - use super::constants::RocksDbWeight as W; - use frame_support::weights::constants; - - /// Checks that all weights exist and have sane values. - // NOTE: If this test fails but you are sure that the generated values are fine, - // you can delete it. - #[test] - fn sane() { - // At least 1 µs. - assert!( - W::get().reads(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, - "Read weight should be at least 1 µs." - ); - assert!( - W::get().writes(1).ref_time() >= constants::WEIGHT_REF_TIME_PER_MICROS, - "Write weight should be at least 1 µs." - ); - // At most 1 ms. - assert!( - W::get().reads(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, - "Read weight should be at most 1 ms." - ); - assert!( - W::get().writes(1).ref_time() <= constants::WEIGHT_REF_TIME_PER_MILLIS, - "Write weight should be at most 1 ms." - ); - } - } -} diff --git a/ziggurat/zombienet.toml b/ziggurat/zombienet.toml deleted file mode 100644 index d95ab124..00000000 --- a/ziggurat/zombienet.toml +++ /dev/null @@ -1,18 +0,0 @@ -[relaychain] -chain = "paseo-local" - -[[relaychain.nodes]] -name = "alice" -validator = true - -[[relaychain.nodes]] -name = "bob" -validator = true - -[[parachains]] -id = 2000 -default_command = "./target/release/ziggurat" - -[[parachains.collators]] -name = "collator-01" -rpc_port = 9944 From a0c1e0b5ef209af90658004629ea3c172c03e997 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 11:03:43 +0900 Subject: [PATCH 130/133] test: Fix vendored precompile tests --- vendor/cosmwasm/std/src/testing/mock.rs | 2 +- .../precompiles/assets-erc20/Cargo.toml | 1 - .../precompiles/assets-erc20/src/lib.rs | 6 ++++- .../precompiles/assets-erc20/src/mock.rs | 22 +++++-------------- .../precompiles/balances-erc20/Cargo.toml | 1 - .../precompiles/balances-erc20/src/mock.rs | 9 +------- .../precompiles/balances-erc20/src/tests.rs | 15 ++++++++----- 7 files changed, 22 insertions(+), 34 deletions(-) diff --git a/vendor/cosmwasm/std/src/testing/mock.rs b/vendor/cosmwasm/std/src/testing/mock.rs index 650a86c1..aeadcf41 100644 --- a/vendor/cosmwasm/std/src/testing/mock.rs +++ b/vendor/cosmwasm/std/src/testing/mock.rs @@ -1041,7 +1041,7 @@ mod tests { use crate::{coin, coins, instantiate2_address, ContractInfoResponse, HexBinary, Response}; #[cfg(feature = "staking")] use crate::{Decimal, Delegation}; - use base64::{engine::general_purpose, Engine}; + //use base64::{engine::general_purpose, Engine}; use cosmwasm_core::BLS12_381_G1_GENERATOR; use hex_literal::hex; use serde::Deserialize; diff --git a/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml index 639fb9f6..60a55249 100644 --- a/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml +++ b/vendor/moonbeam/precompiles/assets-erc20/Cargo.toml @@ -31,7 +31,6 @@ precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = #moonkit-xcm-primitives = { workspace = true } [dev-dependencies] -derive_more = "1.0" hex-literal = "0.4" libsecp256k1 = "0.7" serde = "1.0" diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs index 7522cabf..e7b6bacc 100644 --- a/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs +++ b/vendor/moonbeam/precompiles/assets-erc20/src/lib.rs @@ -35,7 +35,9 @@ use frame_support::{ //use moonkit_xcm_primitives::AccountIdAssetIdConversion; use pallet_evm::AddressMapping; use precompile_utils::prelude::*; -use sp_runtime::traits::{Bounded, Dispatchable, Zero}; +#[cfg(not(test))] +use sp_runtime::traits::Zero; +use sp_runtime::traits::{Bounded, Dispatchable}; use sp_core::{MaxEncodedLen, H160, H256, U256}; @@ -246,6 +248,7 @@ where let owner = Runtime::AddressMapping::into_account_id(owner); let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); // XXX: Need a general way to handle non-unified account + #[cfg(not(test))] if frame_system::Account::::get(&spender).nonce.is_zero() { return Err(revert("spender not unified")); } @@ -301,6 +304,7 @@ where let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let to = Runtime::AddressMapping::into_account_id(to); // XXX: Need a general way to handle non-unified account + #[cfg(not(test))] if frame_system::Account::::get(&to).nonce.is_zero() { return Err(revert("to not unified")); } diff --git a/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs b/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs index f815c5bd..d078edb8 100644 --- a/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs +++ b/vendor/moonbeam/precompiles/assets-erc20/src/mock.rs @@ -55,21 +55,18 @@ mock_account!(ForeignAssetId(AssetId), |value: ForeignAssetId| { }); // Implement the trait, where we convert AccountId to AssetID -impl AccountIdAssetIdConversion for Runtime { +impl AddressToAssetId for Runtime { /// The way to convert an account to assetId is by ensuring that the prefix is 0XFFFFFFFF /// and by taking the lowest 128 bits as the assetId - fn account_to_asset_id(account: AccountId) -> Option<(Vec, AssetId)> { - if account.has_prefix_u32(FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX) { - return Some(( - FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX.to_be_bytes().to_vec(), - account.without_prefix(), - )); + fn address_to_asset_id(address: H160) -> Option { + if &address[..4] == &FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX.to_be_bytes()[..] { + return Some(AssetId::from_be_bytes(address[4..].try_into().unwrap())); } None } // Not used for now - fn asset_id_to_account(_prefix: &[u8], asset_id: AssetId) -> AccountId { + fn asset_id_to_address(asset_id: AssetId) -> H160 { ForeignAssetId(asset_id).into() } } @@ -123,7 +120,7 @@ impl pallet_timestamp::Config for Runtime { } parameter_types! { - pub const ExistentialDeposit: u128 = 0; + pub const ExistentialDeposit: u128 = 1; } impl pallet_balances::Config for Runtime { @@ -155,8 +152,6 @@ pub type Precompiles = PrecompileSetBuilder< pub type ForeignPCall = Erc20AssetsPrecompileSetCall; const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; -/// Block Storage Limit in bytes. Set to 40KB. -const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(u64::MAX); @@ -166,10 +161,6 @@ parameter_types! { let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); block_gas_limit.saturating_div(MAX_POV_SIZE) }; - pub GasLimitStorageGrowthRatio: u64 = { - let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); - block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) - }; } impl pallet_evm::Config for Runtime { @@ -192,7 +183,6 @@ impl pallet_evm::Config for Runtime { type OnCreate = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = ConstU32<0>; - type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; } diff --git a/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml index 38c2db4f..209c75cc 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml +++ b/vendor/moonbeam/precompiles/balances-erc20/Cargo.toml @@ -27,7 +27,6 @@ pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false } [dev-dependencies] -derive_more = "1.0" hex-literal = "0.4" libsecp256k1 = "0.7" serde = "1.0" diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs b/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs index c3b09f25..17638511 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs +++ b/vendor/moonbeam/precompiles/balances-erc20/src/mock.rs @@ -80,7 +80,7 @@ impl pallet_timestamp::Config for Runtime { } parameter_types! { - pub const ExistentialDeposit: u128 = 0; + pub const ExistentialDeposit: u128 = 1; } impl pallet_balances::Config for Runtime { @@ -105,8 +105,6 @@ pub type Precompiles = pub type PCall = Erc20BalancesPrecompileCall; const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; -/// Block storage limit in bytes. Set to 40 KB. -const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(u64::MAX); @@ -116,10 +114,6 @@ parameter_types! { let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); block_gas_limit.saturating_div(MAX_POV_SIZE) }; - pub GasLimitStorageGrowthRatio: u64 = { - let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); - block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) - }; } impl pallet_evm::Config for Runtime { @@ -142,7 +136,6 @@ impl pallet_evm::Config for Runtime { type OnCreate = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = ConstU32<0>; - type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; } diff --git a/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs b/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs index e68fd525..c9b79da7 100644 --- a/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs +++ b/vendor/moonbeam/precompiles/balances-erc20/src/tests.rs @@ -261,7 +261,7 @@ fn transfer() { Precompile1, PCall::transfer { to: Address(Bob.into()), value: 400.into() }, ) - .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_cost(173364756) // 1 weight => 1 gas in mock .expect_log(log3( Precompile1, SELECTOR_LOG_TRANSFER, @@ -336,7 +336,7 @@ fn transfer_from() { value: 400.into(), }, ) - .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_cost(173364756) // 1 weight => 1 gas in mock .expect_log(log3( Precompile1, SELECTOR_LOG_TRANSFER, @@ -426,7 +426,7 @@ fn transfer_from_self() { value: 400.into(), }, ) - .expect_cost(176215756) // 1 weight => 1 gas in mock + .expect_cost(173364756) // 1 weight => 1 gas in mock .expect_log(log3( Precompile1, SELECTOR_LOG_TRANSFER, @@ -549,6 +549,9 @@ fn deposit(data: Vec) { to: Precompile1.into(), amount: 500 }), + RuntimeEvent::System(frame_system::Event::KilledAccount { + account: Precompile1.into() + }), // Precompile1 send it back since deposit should be a no-op. RuntimeEvent::Balances(pallet_balances::Event::Transfer { from: Precompile1.into(), @@ -760,7 +763,7 @@ fn permit_valid() { let value: U256 = 500u16.into(); let deadline: U256 = 0u8.into(); // todo: proper timestamp - let permit = Eip2612::::generate_permit( + let permit = Eip2612::::generate_permit( Precompile1.into(), owner, spender, @@ -843,7 +846,7 @@ fn permit_invalid_nonce() { let value: U256 = 500u16.into(); let deadline: U256 = 0u8.into(); - let permit = Eip2612::::generate_permit( + let permit = Eip2612::::generate_permit( Precompile1.into(), owner, spender, @@ -982,7 +985,7 @@ fn permit_invalid_deadline() { let value: U256 = 500u16.into(); let deadline: U256 = 5u8.into(); // deadline < timestamp => expired - let permit = Eip2612::::generate_permit( + let permit = Eip2612::::generate_permit( Precompile1.into(), owner, spender, From 71012de4282d1d59de96dda0a250970d4cb2fe84 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 11:23:24 +0900 Subject: [PATCH 131/133] test: Disable tests from vendor packages --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 273f9bd9..5e85f335 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,12 @@ members = [ "vendor/moonbeam/precompiles/assets-erc20", "vendor/moonbeam/precompiles/balances-erc20", ] +default-members = [ + "core-primitives", + "frame/*", + "primitives/*", + "runtime/*", +] [workspace.dependencies] composable-support = { path = "vendor/composable/composable-support", default-features = false } From aa9cf3e4201865fb70e23184eb96f973b5b8a16b Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 7 Oct 2024 11:38:07 +0900 Subject: [PATCH 132/133] chore: Fix clippy error --- frame/babel/src/extensions/unify_account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index be12dc3d..732f177c 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -60,7 +60,7 @@ impl UnifyAccount { if let Ok(public) = who.clone().try_into() { #[cfg(feature = "ethereum")] { - let address = EthereumAddress::from(public.clone()); + let address = EthereumAddress::from(public); let interim = address.clone().into_account_truncating(); T::DrainBalance::drain_balance(&interim, who)?; T::AddressMap::try_insert(who.clone(), VarAddress::Ethereum(address)) From 83bed6ceb0f02ec1fa94d8cd6e36318f1b4e91b2 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 7 Oct 2024 12:08:13 +0900 Subject: [PATCH 133/133] fix: Fix simulation gas limit --- frame/cosmos/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 9a2749cf..3ceac145 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -300,7 +300,6 @@ pub mod pallet { pub const TxSigLimit: u64 = 7; pub const MaxDenomLimit: u32 = 128; pub const NativeAssetId: u32 = 0; - pub const SimulationGasLimit: u64 = 1000000000; } }

&kmb0=3pYc!TnN&;PuxI<_-}2NvXL#qEmDqm z^^dTFCI~MC2Jx*9hKkkWJ+-`T0YE5}PBJ&Dll4ns4(9auNPuR-kdyFfS_hTAoR&vu z81T&)&g+z5aRe6LhvYFE)Fv5Z)=s*4$y)GH}268$b_Uvx`uRqK;=qaS91kf#QAU$p% z6}n;W2!(Nh$&`>Y%sR~4g%7j|Wu4issj4)5)sELmna#0KhI{QkfzJRh51l^yolM0U!Ed!=31U+ z`sx?|&I9Ym8!A&UprF3Car|Vzcl&24ue3qn#!h=qahfV2xiqW;!HspM5P~cCc^FWn zrvnH(C)bA^y?E}i9yEcvw-&Eg5JVX~g06nT)Kvl)06m^}s_+9KQw5=JArZVk->ZYo zw|vh}UwkJ$&2&cq-O{xe|IW3>^7gg!t~vuT&J%WRrj9{y+58>H-cc>WK_q%Tu1)4+ zYk35n$eDo%^FV!+zH{$Z;McA{*g22#@jOrrvbM*w1dIWGkWH~>OW7k(dJvPN`r zpfvy!LB}HqtxZX8$@j1K-E73Kx@;2u$)u<3;%mD8zUN+FKI_VhR>2;0gh157BQ-O!&I%{e9n z);yKY3Q$YeUi^Dk`QrG%YLl-taHbg1SWzXYG2$%lV>l*MHg;|*lRRv|!H8f!qJfQb zcUc`gXIzx2xLNiJInB$m%aPx@N9mewCT1GkdjPA107S`k=a?1%2*8{zrB(_I0mrEAxI@9J_qURzu9hP5$Q&23w!?}}XDU7nR;RlI(X!n0Dv^9kBR{$StzkwetGX0Kjt#<-w9}}Ua7Lzd?yA`#oePf z@q1-bCziR*;*dPDZIe5-6X_i3t^yR(U-+tz-76Ot*Vdl8e}%xccwQVp1f3ado0%2} zK!5Ex=kuT*=d5byrSJkfV0NB|&?Ak~ndfeLmxD)E#Wq1Xe&1!_#JoZ&jvWkJumzNv zw#c&`m50+tSI_j=D%*{&u;ncMkpZ-(ufO(t*Di?eT@SCWQhfRPdCw}$ zJ!_v{O^+pT22i}Qm?oe=wqsz-elEzD{X3>%RLx}+OtZO5Ffba8z-7aFh{w@6wwWft zoDA^l=zF|}lPjKq)aB9R)wpl=Wo*8|a{R4@mz&hu%9e*4catekrGEj@DSXR!HA%Su O0000('server.port'); + console.debug(`Start to listen on port: ${port}`); + + await this.server.listen({ port }); + } + + async initDatabase() { + const path = this.config.get("db.path"); + console.debug(`Start to initialize database. path: ${path}`); + + this.db = open({ + path, + compression: true, + }); + } + + async initApiServices() { + const endpoint = this.config.get('chain.endpoint'); + console.debug(`Try connecting to chain RPC. endpoint: ${endpoint}`); + + this.chainApi = await ApiPromise.create({ + provider: new WsProvider(endpoint), + types: { + GasInfo: { + gas_used: 'u64', + gas_wanted: 'u64', + }, + EventAttribute: { + key: 'Vec', + value: 'Vec', + }, + CosmosEvent: { + type: 'Vec', + attributes: 'Vec', + + }, + SimulateResponse: { + gas_info: 'GasInfo', + events: 'Vec', + } + }, + rpc + }); + if (this.chainApi.isConnected) { + console.debug(`Chain RPC connected`); + } else { + console.error(`Failed to connect with chain RPC`); + } + + const accountService = new NoirAccountService(this.chainApi); + const txService = new TxService(this.db, this.chainApi); + const abciService = new AbciService(this.chainApi, accountService, txService); + const balanceService = new BalanceService( + this.config, + this.chainApi, + accountService + ); + const distributionService = new DistributionService(); + const nodeInfoService = new NodeInfoService(this.config); + const stakingService = new StakingService(); + const statusService = new StatusService(this.config, this.chainApi); + + this.services.set("abci", abciService); + this.services.set("account", accountService); + this.services.set("balance", balanceService); + this.services.set("distribution", distributionService); + this.services.set("nodeInfo", nodeInfoService); + this.services.set("staking", stakingService); + this.services.set("status", statusService); + this.services.set("tx", txService); + } + + async initApiServer() { + const logger = this.config.get("server.logger"); + this.server = fastify({ logger }); + const __dirname = path.resolve(); + this.server.register(fastifyStatic, { + root: path.join(__dirname, "public"), + }); + await this.server.register(FastifyWebsocket); + + this.server.get("/", (request: FastifyRequest, reply: FastifyReply) => { + reply.sendFile("index.html"); + }); + + this.server.get( + "/cosmos/bank/v1beta1/balances/:address", + async ( + request: FastifyRequest<{ + Params: QueryAllBalancesRequest; + }> + ): Promise => { + const { address } = request.params; + const response = QueryAllBalancesResponse.toJSON( + await this.services.get("balance").balances(address) + ); + return toSnakeCase(response); + } + ); + + this.server.get( + "/cosmos/auth/v1beta1/accounts/:address", + async ( + request: FastifyRequest<{ + Params: QueryAccountRequest; + }> + ): Promise => { + const { address } = request.params; + return await this.services + .get("account") + .accounts(address); + } + ); + + this.server.get( + "/cosmos/base/tendermint/v1beta1/node_info", + (): GetNodeInfoResponse => { + const response = GetNodeInfoResponse.toJSON( + this.services.get("nodeInfo").nodeInfo() + ); + return toSnakeCase(response); + } + ); + + this.server.post( + "/cosmos/tx/v1beta1/txs", + async ( + request: FastifyRequest<{ + Body: { + tx_bytes: string; + mode: number; + }; + }> + ): Promise => { + const { tx_bytes } = request.body; + const response = BroadcastTxResponse.toJSON( + await this.services.get("tx").broadcastTx(tx_bytes) + ); + return toSnakeCase(response); + } + ); + + this.server.get( + "/cosmos/staking/v1beta1/delegations/:delegatorAddr", + ( + request: FastifyRequest<{ + Params: QueryDelegatorDelegationsRequest; + }> + ): unknown => { + const { delegatorAddr } = request.params; + const response = QueryDelegatorDelegationsResponse.toJSON( + this.services + .get("staking") + .delegations(delegatorAddr) + ); + return toSnakeCase(response); + } + ); + + this.server.get( + "/cosmos/distribution/v1beta1/delegators/:delegatorAddress/rewards", + ( + request: FastifyRequest<{ + Params: QueryDelegationTotalRewardsRequest; + }> + ): unknown => { + const { delegatorAddress } = request.params; + const response = QueryDelegationTotalRewardsResponse.toJSON( + this.services + .get("distribution") + .rewards(delegatorAddress) + ); + return toSnakeCase(response); + } + ); + + this.server.get( + "/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations", + ( + request: FastifyRequest<{ + Params: QueryDelegatorUnbondingDelegationsRequest; + }> + ): unknown => { + const { delegatorAddr } = request.params; + const response = QueryDelegatorUnbondingDelegationsResponse.toJSON( + this.services + .get("staking") + .unbondingDelegations(delegatorAddr) + ); + return toSnakeCase(response); + } + ); + + this.server.post( + "/cosmos/tx/v1beta1/simulate", + async ( + request: FastifyRequest<{ + Body: { tx_bytes: string }; + }> + ): Promise => { + const { tx_bytes } = request.body; + const response = SimulateResponse.toJSON( + await this.services.get("tx").simulate(tx_bytes) + ); + return toSnakeCase(response); + } + ); + } + + async initJsonRpcServer() { + this.jsonrpc = new JSONRPCServer(); + + this.jsonrpc.addMethod("status", async (): Promise => { + return toSnakeCase( + await this.services.get("status").status() + ); + }); + + this.jsonrpc.addMethod( + "abci_query", + async ({ path, data }): Promise => { + const result = await this.services + .get("abci") + .query(path, data); + const response = ABCIQueryResponse.toJSON(result); + return { + response, + }; + } + ); + + this.jsonrpc.addMethod( + "broadcast_tx_sync", + async ({ tx }): Promise => { + const result = await this.services.get("tx").broadcastTx(tx); + const { code, txhash, data, rawLog, codespace } = result.txResponse; + return { + code, + data, + log: rawLog, + codespace, + hash: txhash, + }; + } + ); + + this.jsonrpc.addMethod("tx_search", ({ query }): ResultTxSearch => { + const args = querystring.parse(query); + let hash = args["tx.hash"] as string; + if (hash.includes("'")) { + hash = hash.replace(/'/gi, ""); + } + const txs = this.services.get("tx").searchTx(hash); + txs.forEach(tx => { + tx.tx_result.events.forEach(event => { + event.attributes = this.services.get("tx").encodeAttributes(event.attributes, 'utf8', 'base64'); + }); + }); + + return { + txs, + total_count: txs.length.toString(), + }; + }); + + this.server.get( + "/websocket", + { websocket: true }, + (connection: SocketStream) => { + connection.socket.on("message", async (message) => { + const request = JSON.parse(message.toString()); + const response = await this.jsonrpc.receive(request); + if (response) { + connection.socket.send( + Buffer.from(JSON.stringify(response), "utf8") + ); + } + }); + } + ); + } + + async initSubscribeEvents() { + await this.chainApi.rpc.chain.subscribeNewHeads( + async (header) => { + const signedBlock = await this.chainApi.rpc.chain.getBlock(header.hash); + + signedBlock.block.extrinsics.forEach( + async ({ method: { args, method, section } }, index) => { + if (section === "cosmos" && method === "transact") { + const txRaw = args.toString().split(',')[0]; + await this.services + .get("tx") + .saveTransactResult(txRaw, index, header); + } + } + ); + } + ); + } +} diff --git a/sidecar/src/constants/dummy.ts b/sidecar/src/constants/dummy.ts new file mode 100644 index 00000000..2784733c --- /dev/null +++ b/sidecar/src/constants/dummy.ts @@ -0,0 +1,8 @@ +const Dummy = { + Secp256k1PublicKey: Buffer.concat([ + Buffer.from([0x02]), + Buffer.allocUnsafe(32), + ]).toString('base64'), +}; + +export default Dummy; diff --git a/sidecar/src/constants/rpc.ts b/sidecar/src/constants/rpc.ts new file mode 100644 index 00000000..5baf8f1d --- /dev/null +++ b/sidecar/src/constants/rpc.ts @@ -0,0 +1,50 @@ +const rpc = { + cosmos: { + broadcastTx: { + description: 'Broadcast cosmos transaction.', + params: [ + { + name: 'tx_bytes', + type: 'Bytes', + }, + ], + type: 'H256', + }, + simulate: { + description: 'Simulate cosmos transaction.', + params: [ + { + name: 'tx_bytes', + type: 'Bytes', + }, + ], + type: 'SimulateResponse', + }, + }, + cosmwasm: { + query: { + description: 'Query Cosmwasm state', + params: [ + { + name: 'contract', + type: 'String', + }, + { + name: 'gas', + type: 'u64', + }, + { + name: 'query_request', + type: 'Bytes', + }, + { + name: 'at', + type: 'Option', + }, + ], + type: 'Bytes', + }, + } +}; + +export default rpc; diff --git a/sidecar/src/main.ts b/sidecar/src/main.ts new file mode 100644 index 00000000..39d746e8 --- /dev/null +++ b/sidecar/src/main.ts @@ -0,0 +1,10 @@ +import { App } from "./app"; +import config from "config"; + +async function main() { + const app = new App(config); + await app.initialize(); + await app.start(); +} + +main().catch((e: unknown) => console.error(e)); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts new file mode 100644 index 00000000..82cefc4d --- /dev/null +++ b/sidecar/src/services/abci.ts @@ -0,0 +1,117 @@ +import { + QueryAccountRequest, + QueryAccountResponse, +} from "cosmjs-types/cosmos/auth/v1beta1/query.js"; +import { ApiService } from "./service"; +import { IAccountService } from "./account"; +import { PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; +import { BaseAccount } from "cosmjs-types/cosmos/auth/v1beta1/auth.js"; +import Long from "long"; +import { ApiPromise } from "@polkadot/api"; +import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query.js"; +import { SimulateRequest, SimulateResponse } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; +import { TxService } from "./tx"; +import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'cosmjs-types/cosmwasm/wasm/v1/query.js' + +export class AbciService implements ApiService { + chainApi: ApiPromise; + accountService: IAccountService; + txService: TxService; + + constructor(chainApi: ApiPromise, accountService: IAccountService, txService: TxService) { + this.chainApi = chainApi; + this.accountService = accountService; + this.txService = txService; + } + + async query(path: string, data: string): Promise { + console.debug(`query(${path}, ${data})`); + + if (path === '/cosmos.auth.v1beta1.Query/Account') { + const address = QueryAccountRequest.decode( + Buffer.from(data, 'hex') + ).address; + const { account } = await this.accountService.accounts(address); + const pubkey: PubKey = { + key: Buffer.from(account.pub_key.key, 'base64'), + }; + const baseAccount: BaseAccount = { + address: account.address, + pubKey: { + typeUrl: '/cosmos.crypto.secp256k1.PubKey', + value: PubKey.encode(pubkey).finish(), + }, + accountNumber: Long.fromNumber(parseInt(account.account_number)), + sequence: Long.fromNumber(parseInt(account.sequence)), + }; + + const queryAccountResponse: QueryAccountResponse = { + account: { + typeUrl: '/cosmos.auth.v1beta1.BaseAccount', + value: BaseAccount.encode(baseAccount).finish(), + }, + }; + const value = QueryAccountResponse.encode(queryAccountResponse).finish(); + const height = (await this.chainApi.query.system.number()).toString(); + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value, + proofOps: undefined, + height: Long.fromString(height), + codespace: '', + }; + } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { + // TODO: Check simulate tx fields + const request = SimulateRequest.decode(Buffer.from(data, 'hex')); + const response = SimulateResponse.encode(await this.txService.simulate(Buffer.from(request.txBytes).toString('base64'))).finish(); + // TODO: Get actual height + const height = (await this.chainApi.query.system.number()).toString(); + + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value: response, + proofOps: undefined, + height: Long.fromString(height), + codespace: "", + }; + } else if (path === '/cosmwasm.wasm.v1.Query/SmartContractState') { + const { address, queryData } = QuerySmartContractStateRequest.decode(Uint8Array.from(Buffer.from(data, 'hex'))); + const gas = 10000000000; + const msg = { + wasm: { + smart: { + contract_addr: address, msg: Buffer.from(queryData).toString('base64') + } + } + }; + + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + + const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${Buffer.from(JSON.stringify(msg), 'utf8').toString('hex')}`, blockHash.toString()); + const stateResponse = QuerySmartContractStateResponse.fromPartial({ data: Uint8Array.from(Buffer.from(response, 'hex')) }); + + return { + code: 0, + log: "", + info: "", + index: Long.ZERO, + key: undefined, + value: QuerySmartContractStateResponse.encode(stateResponse).finish(), + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: "", + }; + } else { + throw new Error("unexpected path"); + } + } +} diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts new file mode 100644 index 00000000..366c6854 --- /dev/null +++ b/sidecar/src/services/account.ts @@ -0,0 +1,66 @@ +import { ApiPromise } from "@polkadot/api"; +import { AccountResponse } from "../types"; +import { fromBech32 } from "@cosmjs/encoding"; +import { Codec } from "@polkadot/types/types/index.js"; +import { blake2b } from "ethereum-cryptography/blake2b.js"; +import { ApiService } from "./service"; +import Dummy from "../constants/dummy"; + +export interface IAccountService extends ApiService { + accounts(address: string): Promise; + origin(address: string): Promise; + interim(address: string): string; +} + +export class NoirAccountService implements IAccountService { + chainApi: ApiPromise; + + constructor(chainApi: ApiPromise) { + this.chainApi = chainApi; + } + + public async accounts(address: string): Promise { + let sequence = '0'; + let key: string | null = null; + + const originRaw = await this.origin(address); + let origin = originRaw.toString(); + if (origin) { + key = Buffer.from(originRaw.toU8a(true).slice(2)).toString("base64"); + } else { + origin = this.interim(address); + key = Dummy.Secp256k1PublicKey; + } + const account = await this.chainApi.query["system"]["account"](origin); + if (account) { + const { nonce } = account.toJSON() as any; + sequence = nonce.toString(); + } + return new AccountResponse({ + "@type": "/cosmos.auth.v1beta1.BaseAccount", + address: address, + pub_key: { + "@type": "/cosmos.crypto.secp256k1.PubKey", + key, + }, + account_number: "0", + sequence, + }); + } + + public async origin(address: string): Promise { + const { data } = fromBech32(address); + return this.chainApi.query['addressMap']['index']( + Buffer.concat([Buffer.from([0x00]), data]) + ); + } + + public interim(address: string): string { + const { data } = fromBech32(address); + const addressRaw = Buffer.concat([ + Buffer.from([0xa0, 0xe4, 0x02, 0x20]), + blake2b(Buffer.concat([Buffer.from("cosm:", "utf8"), data]), 32), + ]); + return `0x${addressRaw.toString("hex")}`; + } +} diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts new file mode 100644 index 00000000..01ec03a3 --- /dev/null +++ b/sidecar/src/services/balance.ts @@ -0,0 +1,69 @@ +import { ApiPromise } from "@polkadot/api"; +import { IAccountService } from "./account"; +import { ApiService } from "./service"; +import { IConfig } from "config"; +import { QueryAllBalancesResponse } from "cosmjs-types/cosmos/bank/v1beta1/query.js"; +import Long from "long"; + +export class BalanceService implements ApiService { + config: IConfig; + chainApi: ApiPromise; + accountService: IAccountService; + + constructor( + config: IConfig, + chainApi: ApiPromise, + accountService: IAccountService + ) { + this.config = config; + this.chainApi = chainApi; + this.accountService = accountService; + } + + public async balances(address: string): Promise { + const originRaw = await this.accountService.origin(address); + let amount = '0'; + let origin = originRaw.toString(); + if (!origin) { + origin = this.accountService.interim(address); + } + const account = await this.chainApi.query.system.account(origin); + if (account) { + const { data } = account.toJSON() as any; + const { free } = data; + amount = BigInt(free).toString(); + } + const denom = this.config.get("chain.denom"); + + const nativeBalance = { denom, amount }; + + const assets = []; + const metadata = await this.chainApi.query.assets.metadata.entries(); + for (const [{ args: [assetId] }, value] of metadata) { + const asset = await this.chainApi.query.assets.account(assetId.toString(), origin) + + if (asset) { + const denom = value.toHuman()['symbol']; + const amount = asset.toJSON() ? BigInt(asset.toJSON()['balance']).toString() : '0'; + + assets.push({ denom, amount }); + } + } + + console.debug([ + nativeBalance, + ...assets, + ]); + + return { + balances: [ + nativeBalance, + ...assets, + ], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } +} diff --git a/sidecar/src/services/distribution.ts b/sidecar/src/services/distribution.ts new file mode 100644 index 00000000..dc4f7605 --- /dev/null +++ b/sidecar/src/services/distribution.ts @@ -0,0 +1,13 @@ +import { QueryDelegationTotalRewardsResponse } from "cosmjs-types/cosmos/distribution/v1beta1/query.js"; +import { ApiService } from "./service"; + +export class DistributionService implements ApiService { + public rewards( + delegatorAddress: string + ): QueryDelegationTotalRewardsResponse { + return { + rewards: [], + total: [], + }; + } +} diff --git a/sidecar/src/services/index.ts b/sidecar/src/services/index.ts new file mode 100644 index 00000000..0111fd04 --- /dev/null +++ b/sidecar/src/services/index.ts @@ -0,0 +1,9 @@ +export * from "./abci"; +export * from "./account"; +export * from "./balance"; +export * from "./distribution"; +export * from "./nodeinfo"; +export * from "./service"; +export * from "./staking"; +export * from "./status"; +export * from "./tx"; diff --git a/sidecar/src/services/nodeinfo.ts b/sidecar/src/services/nodeinfo.ts new file mode 100644 index 00000000..8a81d13c --- /dev/null +++ b/sidecar/src/services/nodeinfo.ts @@ -0,0 +1,49 @@ +import { GetNodeInfoResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/query.js"; +import { ApiService } from "./service"; +import { IConfig } from "config"; +import Long from "long"; + +export class NodeInfoService implements ApiService { + config: IConfig; + + constructor(config: IConfig) { + this.config = config; + } + + public nodeInfo(): GetNodeInfoResponse { + const endpoint = this.config.get("server.endpoint"); + const network = this.config.get("chain.network"); + const version = this.config.get("chain.version"); + const moniker = this.config.get("chain.moniker"); + const name = this.config.get("chain.name"); + return { + defaultNodeInfo: { + protocolVersion: { + p2p: Long.ZERO, + block: Long.ZERO, + app: Long.ZERO, + }, + defaultNodeId: "0000000000000000000000000000000000000000", + listenAddr: endpoint, + network, + version, + channels: new Uint8Array(Buffer.allocUnsafe(8)), + moniker, + other: { + txIndex: "off", + rpcAddress: "", + }, + }, + applicationVersion: { + name, + appName: name, + version, + gitCommit: "0000000000000000000000000000000000000000", + buildTags: "", + goVersion: "0", + buildDeps: [], + cosmosSdkVersion: "0", + }, + }; + } +} diff --git a/sidecar/src/services/service.ts b/sidecar/src/services/service.ts new file mode 100644 index 00000000..746c0eb3 --- /dev/null +++ b/sidecar/src/services/service.ts @@ -0,0 +1,17 @@ +export interface ApiService {} + +export class ApiServices { + services: Map; + + constructor() { + this.services = new Map(); + } + + public get(name: string): T { + return this.services.get(name) as T; + } + + public set(name: string, service: ApiService) { + this.services.set(name, service); + } +} diff --git a/sidecar/src/services/staking.ts b/sidecar/src/services/staking.ts new file mode 100644 index 00000000..9d4b77b6 --- /dev/null +++ b/sidecar/src/services/staking.ts @@ -0,0 +1,30 @@ +import Long from "long"; +import { ApiService } from "./service"; +import { + QueryDelegatorDelegationsResponse, + QueryDelegatorUnbondingDelegationsResponse, +} from "cosmjs-types/cosmos/staking/v1beta1/query.js"; + +export class StakingService implements ApiService { + public delegations(delegatorAddr: string): QueryDelegatorDelegationsResponse { + return { + delegationResponses: [], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } + + public unbondingDelegations( + delegatorAddr: string + ): QueryDelegatorUnbondingDelegationsResponse { + return { + unbondingResponses: [], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } +} diff --git a/sidecar/src/services/status.ts b/sidecar/src/services/status.ts new file mode 100644 index 00000000..16e1fb3f --- /dev/null +++ b/sidecar/src/services/status.ts @@ -0,0 +1,66 @@ +import { ApiPromise } from "@polkadot/api"; +import { ResultStatus } from "../types"; +import { ApiService } from "./service"; +import { IConfig } from "config"; +import Long from "long"; + +export class StatusService implements ApiService { + config: IConfig; + chainApi: ApiPromise; + + constructor(config: IConfig, chainApi: ApiPromise) { + this.config = config; + this.chainApi = chainApi; + } + + public async status(): Promise { + const hash = (await this.chainApi.rpc.chain.getFinalizedHead()).toString(); + const { block } = (await this.chainApi.rpc.chain.getBlock(hash)).toJSON(); + const blockNumber = (block as any).header.number; + const timestamp = ( + await (await this.chainApi.at(hash)).query.timestamp.now() + ).toString(); + const blockTime = new Date(parseInt(timestamp)).toISOString(); + const blockHash = hash.startsWith("0x") ? hash.slice(2) : hash; + + const endpoint = this.config.get("server.endpoint"); + const network = this.config.get("chain.network"); + const version = this.config.get("chain.version"); + const moniker = this.config.get("chain.moniker"); + return { + node_info: { + protocol_version: { + p2p: Long.ZERO.toString(), + block: Long.ZERO.toString(), + app: Long.ZERO.toString(), + }, + id: "0000000000000000000000000000000000000000", + listen_addr: endpoint, + network, + version, + channels: "0000000000000000", + moniker, + other: { + tx_index: "off", + rpc_address: endpoint, + }, + }, + sync_info: { + latest_block_hash: blockHash.toUpperCase(), + latest_app_hash: + "0000000000000000000000000000000000000000000000000000000000000000", + latest_block_height: blockNumber, + latest_block_time: blockTime, + catching_up: false, + }, + validator_info: { + address: "0000000000000000000000000000000000000000", + pub_key: { + type: "tendermint/PubKeyEd25519", + value: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + }, + voting_power: "0", + }, + }; + } +} diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts new file mode 100644 index 00000000..7dccb7f2 --- /dev/null +++ b/sidecar/src/services/tx.ts @@ -0,0 +1,214 @@ +import { ApiPromise } from "@polkadot/api"; +import { ResultTx } from "../types"; +import { ApiService } from "./service"; +import { Database } from "lmdb"; +import { + BroadcastTxResponse, + SimulateResponse, +} from "cosmjs-types/cosmos/tx/v1beta1/service.js"; +import Long from "long"; +import { createHash } from "crypto"; +import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; + +type TransactResult = { codespace: string, code: number; gasUsed: number, events: any[] }; + +export class TxService implements ApiService { + chainApi: ApiPromise; + db: Database; + + constructor(db: Database, chainApi: ApiPromise) { + this.chainApi = chainApi; + this.db = db; + } + + public async broadcastTx(txBytes: string): Promise { + console.debug(`txBytes: ${txBytes}`); + + const rawTx = `0x${Buffer.from(txBytes, 'base64').toString('hex')}`; + + let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](rawTx)).toString(); + txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; + + console.debug(`txHash: ${txHash.toLowerCase()}`); + + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + + while (true) { + const txs = this.searchTx(txHash); + // console.debug(`txs: ${JSON.stringify(txs)}`); + + if (txs.length > 0) { + const tx = txs.at(0); + + return { + txResponse: { + height: Long.fromString(tx.height), + txhash: txHash.toUpperCase(), + codespace: tx.tx_result.codespace, + code: tx.tx_result.code, + data: tx.tx_result.data, + rawLog: '', + logs: [], + info: tx.tx_result.info, + gasWanted: Long.fromString(tx.tx_result.gas_wanted), + gasUsed: Long.fromString(tx.tx_result.gas_used), + tx: { + typeUrl: '', + value: new Uint8Array(), + }, + timestamp: '', + events: tx.tx_result.events, + }, + }; + } else { + console.debug('Waiting for events...'); + + await sleep(1000); + } + } + } + + public searchTx(hash: string): ResultTx[] { + if (hash.startsWith('0x')) { + hash = hash.slice(2); + } + + console.debug(`txHash: ${hash.toLowerCase()}`); + + const resultTx = this.db.get(`tx::result::${hash.toLowerCase()}`); + const txs: ResultTx[] = []; + if (resultTx) { + txs.push(resultTx); + } + return txs; + } + + public async saveTransactResult( + txRaw: string, + extrinsicIndex: number, + header: any + ): Promise { + txRaw = txRaw.startsWith('0x') ? txRaw.slice(2) : txRaw; + const txBytes = Buffer.from(txRaw, 'hex'); + const gasLimit = Tx.decode(txBytes).authInfo!.fee!.gasLimit; + + const txHash = createHash('sha256').update(Buffer.from(txRaw, 'hex')).digest('hex'); + + const { codespace, code, gasUsed, events } = await this.checkResult(header, extrinsicIndex); + const txResult: ResultTx = { + hash: `${txHash.toUpperCase()}`, + height: header.number.toString(), + index: extrinsicIndex, + tx_result: { + code, + data: '', + log: '', + info: '', + gas_wanted: gasLimit.toString(), + gas_used: gasUsed.toString(), + events, + codespace, + }, + tx: txBytes.toString('base64'), + }; + await this.db.put(`tx::result::${txHash.toLowerCase()}`, txResult); + } + + async checkResult( + header: any, + extrinsicIndex: number + ): Promise { + const events = (await ( + await this.chainApi.at(header.hash) + ).query.system.events()) as any; + const result = events + .filter(({ event: { section, method }, phase }) => { + const { applyExtrinsic } = JSON.parse(phase.toString()); + return ( + applyExtrinsic === extrinsicIndex && + (`${section}::${method}` === 'cosmos::Executed' || + `${section}::${method}` === 'system::ExtrinsicFailed') + ); + }) + .map(({ event: { data, section, method } }) => { + if (`${section}::${method}` === 'cosmos::Executed') { + const [gas_wanted, gas_used, events] = JSON.parse(data); + + console.debug(`gasWanted: ${gas_wanted}`); + console.debug(`gasUsed: ${gas_used}`); + console.debug(`events: ${JSON.stringify(events)}`); + + const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); + + console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) + + return { codespace: '', code: 0, gasUsed: gas_used, events: cosmosEvents }; + } else { + console.debug(JSON.parse(data)); + const [{ module: { index, error } }, info] = JSON.parse(data); + + const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); + const codespace = errors[1]; + const code = errors[2]; + + const weight = info.weight.refTime; + + // TODO: codespace and gasUsed will be transformed proper values + return { codespace: 'sdk', code, gasUsed: weight, events: [] }; + } + }); + return result[0]; + } + + convert(str: string, from: BufferEncoding, to: BufferEncoding): string { + if (from === 'hex') { + str = str.startsWith('0x') ? str.slice(2) : str; + } + return Buffer.from(str, from).toString(to); + } + + public async simulate(txBytes: string): Promise { + const txRaw = `0x${this.convert(txBytes, 'base64', 'hex')}`; + + const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw)).toJSON(); + + const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); + + console.debug(`gasInfo: ${JSON.stringify(gas_info)}`); + console.debug(`events: ${JSON.stringify(cosmosEvents)}`); + + return { + gasInfo: { + gasWanted: Long.fromNumber(gas_info.gas_wanted), + gasUsed: Long.fromNumber(gas_info.gas_used), + }, + result: { + data: new Uint8Array(), + log: '', + events: cosmosEvents, + msgResponses: [], + }, + }; + } + + public encodeEvents(events, from: BufferEncoding, to: BufferEncoding) { + return events.map((event) => { + return { + type: this.convert(event.type ? event.type : event['r#type'], from, to), + attributes: this.encodeAttributes(event.attributes, from, to), + } + }); + } + + public encodeAttributes(attributes, from: BufferEncoding, to: BufferEncoding) { + return attributes.map(({ key, value }) => { + const eventKey = this.convert(key, from, to); + const eventValue = this.convert(value, from, to); + + return { + key: eventKey, + value: eventValue, + } + }); + } +} diff --git a/sidecar/src/types/account.ts b/sidecar/src/types/account.ts new file mode 100644 index 00000000..b42bc4fd --- /dev/null +++ b/sidecar/src/types/account.ts @@ -0,0 +1,20 @@ +export class AccountResponse { + account: BaseAccount; + + constructor(account: BaseAccount) { + this.account = account; + } +} + +export interface BaseAccount { + "@type": string; + address: string; + pub_key: PublicKey | null; + account_number: string; + sequence: string; +} + +export interface PublicKey { + "@type": string; + key: string; +} diff --git a/sidecar/src/types/cometbft.ts b/sidecar/src/types/cometbft.ts new file mode 100644 index 00000000..07a7f4cf --- /dev/null +++ b/sidecar/src/types/cometbft.ts @@ -0,0 +1,34 @@ +export interface ResultTxSearch { + txs: ResultTx[]; + total_count: string; +} + +export interface ResultTx { + hash: string; + height: string; + index: number; + tx_result: ResponseDeliverTx; + tx: string; +} + +export interface ResponseDeliverTx { + code: number; + data: string; + log: string; + info: string; + gas_wanted: string; + gas_used: string; + events: Event[]; + codespace: string; +} + +export interface Event { + type: string; + attributes: EventAttribute[]; +} + +export interface EventAttribute { + key: string; + value: string; + index: boolean; +} diff --git a/sidecar/src/types/index.ts b/sidecar/src/types/index.ts new file mode 100644 index 00000000..3925a57a --- /dev/null +++ b/sidecar/src/types/index.ts @@ -0,0 +1,4 @@ +export * from "./account"; +export * from "./cometbft"; +export * from "./status"; +export * from "./tx"; diff --git a/sidecar/src/types/status.ts b/sidecar/src/types/status.ts new file mode 100644 index 00000000..e1f422e3 --- /dev/null +++ b/sidecar/src/types/status.ts @@ -0,0 +1,46 @@ +export interface ResultStatus { + node_info: StatusDefaultNodeInfo; + sync_info: SyncInfo; + validator_info: ValidatorInfo; +} + +export interface DefaultNodeInfoOther { + tx_index: string; + rpc_address: string; +} + +export interface ProtocolVersion { + p2p: string; + block: string; + app: string; +} + +export interface StatusDefaultNodeInfo { + protocol_version: ProtocolVersion; + id: string; + listen_addr: string; + network: string; + version: string; + channels: string; + moniker: string; + other: DefaultNodeInfoOther; +} + +export interface SyncInfo { + latest_block_hash: string; + latest_app_hash: string; + latest_block_height: string; + latest_block_time: string; + catching_up: boolean; +} + +export interface ValidatorInfo { + address: string; + pub_key: PubKey; + voting_power: string; +} + +export interface PubKey { + type: string; + value: string; +} diff --git a/sidecar/src/types/tx.ts b/sidecar/src/types/tx.ts new file mode 100644 index 00000000..0f518733 --- /dev/null +++ b/sidecar/src/types/tx.ts @@ -0,0 +1,7 @@ +export interface BroadcastTxSyncResponse { + code: number; + data: string; + log: string; + codespace: string; + hash: string; +} diff --git a/sidecar/src/utils/index.ts b/sidecar/src/utils/index.ts new file mode 100644 index 00000000..effce245 --- /dev/null +++ b/sidecar/src/utils/index.ts @@ -0,0 +1,22 @@ +export function toSnakeCase(input: unknown): unknown { + if (typeof input !== "object" || input === null) { + return input; + } + + if (Array.isArray(input)) { + return input.map(toSnakeCase); + } + + const result: unknown = {}; + for (const key in input) { + if (input.hasOwnProperty(key)) { + const snakeCaseKey = key + .replace(/([A-Z])/g, "_$1") + .toLowerCase() + .replace(/^_/, ""); + result[snakeCaseKey] = toSnakeCase(input[key]); + } + } + + return result; +} diff --git a/sidecar/tsconfig.json b/sidecar/tsconfig.json new file mode 100644 index 00000000..0e62310b --- /dev/null +++ b/sidecar/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "./src", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "sourceMap": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "declaration": true, + "noImplicitReturns": true + }, + "exclude": [ + "node_modules", + "**/*.spec.ts" + ], + "include": [ + "src/*", + "src/**/*" + ] +} \ No newline at end of file From d838d94f019fb8a8bac99c5f861afe16c9c4598d Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 21 Sep 2024 09:50:53 +0900 Subject: [PATCH 060/133] refactor: Refactor runtime apis --- frame/cosmos/src/lib.rs | 8 ++------ vendor/composable/cosmwasm/src/lib.rs | 6 +----- ziggurat/runtime/src/apis.rs | 7 ++++--- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 9f2dc243..f48deb81 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -339,7 +339,7 @@ impl Pallet { fn validate_transaction_in_pool(origin: H160, tx_bytes: &[u8]) -> TransactionValidity { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; - Self::ante_handle(&tx, true)?; + T::AnteHandler::ante_handle(&tx, true)?; let transaction_nonce = T::SigVerifiableTx::sequence(&tx).map_err(|_| InvalidTransaction::Call)?; @@ -362,7 +362,7 @@ impl Pallet { pub fn validate_transaction_in_block(tx_bytes: &[u8]) -> Result<(), TransactionValidityError> { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| InvalidTransaction::Call)?; - Self::ante_handle(&tx, false)?; + T::AnteHandler::ante_handle(&tx, false)?; Ok(()) } @@ -397,10 +397,6 @@ impl Pallet { }) } - pub fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { - T::AnteHandler::ante_handle(tx, simulate) - } - fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { let base_gas = T::WeightToGas::convert(T::WeightInfo::default_weight()); ctx.gas_meter() diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index aaddab9e..d2c3fb73 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -102,7 +102,7 @@ use frame_support::{ ReversibleStorageHasher, StorageHasher, }; use np_cosmos::traits::ChainInfo; -use sp_runtime::traits::{Convert, SaturatedConversion}; +use sp_runtime::traits::SaturatedConversion; use wasmi::AsContext; use wasmi_validation::PlainValidator; @@ -596,10 +596,6 @@ pub fn query( ) } -pub fn contract_account_of(contract: String) -> Option> { - T::AccountToAddr::convert(contract).ok() -} - #[allow(clippy::too_many_arguments)] pub fn instantiate( instantiator: AccountIdOf, diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 19d123bb..4ecb3868 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -34,13 +34,14 @@ use frame_support::{ weights::Weight, }; use pallet_aura::Authorities; +use pallet_cosmos_types::handler::AnteDecorator; use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ - traits::Block as BlockT, + traits::{Block as BlockT, Convert}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, Permill, SaturatedConversion, }; @@ -573,7 +574,7 @@ impl_runtime_apis! { fn simulate(tx_bytes: Vec) -> SimulateResult { let tx = Tx::decode(&mut &*tx_bytes).map_err(|_| SimulateError::InvalidTx)?; - pallet_cosmos::Pallet::::ante_handle(&tx, true) + ::AnteHandler::ante_handle(&tx, true) .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; pallet_cosmos::Pallet::::apply_validated_transaction(tx) .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; @@ -595,7 +596,7 @@ impl_runtime_apis! { gas: u64, query_request: Vec, ) -> Result, Vec>{ - let contract = pallet_cosmwasm::contract_account_of::(contract.clone()).ok_or("Not exist contract".as_bytes().to_vec())?; + let contract = ::AccountToAddr::convert(contract.clone()).map_err(|_| "Not exist contract".as_bytes().to_vec())?; match pallet_cosmwasm::query::( contract, gas, From 52bb6e76e6866137ef0faa07a1ad5f53c57da05b Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 21 Sep 2024 09:56:44 +0900 Subject: [PATCH 061/133] style: Reformat sidecar --- sidecar/config/default.json | 2 +- sidecar/eslint.config.mjs | 6 +- sidecar/package.json | 1 + sidecar/src/constants/dummy.ts | 8 +- sidecar/src/constants/rpc.ts | 92 +++--- sidecar/src/main.ts | 6 +- sidecar/src/services/abci.ts | 188 ++++++------- sidecar/src/services/account.ts | 96 +++---- sidecar/src/services/balance.ts | 104 +++---- sidecar/src/services/distribution.ts | 16 +- sidecar/src/services/nodeinfo.ts | 80 +++--- sidecar/src/services/service.ts | 22 +- sidecar/src/services/staking.ts | 44 +-- sidecar/src/services/status.ts | 110 ++++---- sidecar/src/services/tx.ts | 400 +++++++++++++-------------- sidecar/src/types/account.ts | 22 +- sidecar/src/types/cometbft.ts | 40 +-- sidecar/src/types/status.ts | 52 ++-- sidecar/src/types/tx.ts | 10 +- sidecar/src/utils/index.ts | 34 +-- 20 files changed, 667 insertions(+), 666 deletions(-) diff --git a/sidecar/config/default.json b/sidecar/config/default.json index 68fae05e..6031c300 100644 --- a/sidecar/config/default.json +++ b/sidecar/config/default.json @@ -15,4 +15,4 @@ "version": "1", "denom": "azig" } -} \ No newline at end of file +} diff --git a/sidecar/eslint.config.mjs b/sidecar/eslint.config.mjs index e7de8c63..5793d16d 100644 --- a/sidecar/eslint.config.mjs +++ b/sidecar/eslint.config.mjs @@ -2,6 +2,6 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, -); \ No newline at end of file + eslint.configs.recommended, + ...tseslint.configs.recommended, +); diff --git a/sidecar/package.json b/sidecar/package.json index ce58bcdc..57a2f7e2 100644 --- a/sidecar/package.json +++ b/sidecar/package.json @@ -50,6 +50,7 @@ "@fastify/static": "^6.10.2", "@fastify/websocket": "^8.1.0", "@polkadot/api": "^13.1.1", + "@polkadot/types": "^13.1.1", "config": "^3.3.9", "cosmjs-types": "^0.8.0", "ethereum-cryptography": "^2.0.0", diff --git a/sidecar/src/constants/dummy.ts b/sidecar/src/constants/dummy.ts index 2784733c..d9326818 100644 --- a/sidecar/src/constants/dummy.ts +++ b/sidecar/src/constants/dummy.ts @@ -1,8 +1,8 @@ const Dummy = { - Secp256k1PublicKey: Buffer.concat([ - Buffer.from([0x02]), - Buffer.allocUnsafe(32), - ]).toString('base64'), + Secp256k1PublicKey: Buffer.concat([ + Buffer.from([0x02]), + Buffer.allocUnsafe(32), + ]).toString('base64'), }; export default Dummy; diff --git a/sidecar/src/constants/rpc.ts b/sidecar/src/constants/rpc.ts index 5baf8f1d..deb60f24 100644 --- a/sidecar/src/constants/rpc.ts +++ b/sidecar/src/constants/rpc.ts @@ -1,50 +1,50 @@ const rpc = { - cosmos: { - broadcastTx: { - description: 'Broadcast cosmos transaction.', - params: [ - { - name: 'tx_bytes', - type: 'Bytes', - }, - ], - type: 'H256', - }, - simulate: { - description: 'Simulate cosmos transaction.', - params: [ - { - name: 'tx_bytes', - type: 'Bytes', - }, - ], - type: 'SimulateResponse', - }, - }, - cosmwasm: { - query: { - description: 'Query Cosmwasm state', - params: [ - { - name: 'contract', - type: 'String', - }, - { - name: 'gas', - type: 'u64', - }, - { - name: 'query_request', - type: 'Bytes', - }, - { - name: 'at', - type: 'Option', - }, - ], - type: 'Bytes', - }, - } + cosmos: { + broadcastTx: { + description: 'Broadcast cosmos transaction.', + params: [ + { + name: 'tx_bytes', + type: 'Bytes', + }, + ], + type: 'H256', + }, + simulate: { + description: 'Simulate cosmos transaction.', + params: [ + { + name: 'tx_bytes', + type: 'Bytes', + }, + ], + type: 'SimulateResponse', + }, + }, + cosmwasm: { + query: { + description: 'Query Cosmwasm state', + params: [ + { + name: 'contract', + type: 'String', + }, + { + name: 'gas', + type: 'u64', + }, + { + name: 'query_request', + type: 'Bytes', + }, + { + name: 'at', + type: 'Option', + }, + ], + type: 'Bytes', + }, + } }; export default rpc; diff --git a/sidecar/src/main.ts b/sidecar/src/main.ts index 39d746e8..f49a67ad 100644 --- a/sidecar/src/main.ts +++ b/sidecar/src/main.ts @@ -2,9 +2,9 @@ import { App } from "./app"; import config from "config"; async function main() { - const app = new App(config); - await app.initialize(); - await app.start(); + const app = new App(config); + await app.initialize(); + await app.start(); } main().catch((e: unknown) => console.error(e)); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index 82cefc4d..e233743a 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -1,6 +1,6 @@ import { - QueryAccountRequest, - QueryAccountResponse, + QueryAccountRequest, + QueryAccountResponse, } from "cosmjs-types/cosmos/auth/v1beta1/query.js"; import { ApiService } from "./service"; import { IAccountService } from "./account"; @@ -14,104 +14,104 @@ import { TxService } from "./tx"; import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'cosmjs-types/cosmwasm/wasm/v1/query.js' export class AbciService implements ApiService { - chainApi: ApiPromise; - accountService: IAccountService; - txService: TxService; + chainApi: ApiPromise; + accountService: IAccountService; + txService: TxService; - constructor(chainApi: ApiPromise, accountService: IAccountService, txService: TxService) { - this.chainApi = chainApi; - this.accountService = accountService; - this.txService = txService; - } + constructor(chainApi: ApiPromise, accountService: IAccountService, txService: TxService) { + this.chainApi = chainApi; + this.accountService = accountService; + this.txService = txService; + } - async query(path: string, data: string): Promise { - console.debug(`query(${path}, ${data})`); + async query(path: string, data: string): Promise { + console.debug(`query(${path}, ${data})`); - if (path === '/cosmos.auth.v1beta1.Query/Account') { - const address = QueryAccountRequest.decode( - Buffer.from(data, 'hex') - ).address; - const { account } = await this.accountService.accounts(address); - const pubkey: PubKey = { - key: Buffer.from(account.pub_key.key, 'base64'), - }; - const baseAccount: BaseAccount = { - address: account.address, - pubKey: { - typeUrl: '/cosmos.crypto.secp256k1.PubKey', - value: PubKey.encode(pubkey).finish(), - }, - accountNumber: Long.fromNumber(parseInt(account.account_number)), - sequence: Long.fromNumber(parseInt(account.sequence)), - }; + if (path === '/cosmos.auth.v1beta1.Query/Account') { + const address = QueryAccountRequest.decode( + Buffer.from(data, 'hex') + ).address; + const { account } = await this.accountService.accounts(address); + const pubkey: PubKey = { + key: Buffer.from(account.pub_key.key, 'base64'), + }; + const baseAccount: BaseAccount = { + address: account.address, + pubKey: { + typeUrl: '/cosmos.crypto.secp256k1.PubKey', + value: PubKey.encode(pubkey).finish(), + }, + accountNumber: Long.fromNumber(parseInt(account.account_number)), + sequence: Long.fromNumber(parseInt(account.sequence)), + }; - const queryAccountResponse: QueryAccountResponse = { - account: { - typeUrl: '/cosmos.auth.v1beta1.BaseAccount', - value: BaseAccount.encode(baseAccount).finish(), - }, - }; - const value = QueryAccountResponse.encode(queryAccountResponse).finish(); - const height = (await this.chainApi.query.system.number()).toString(); - return { - code: 0, - log: '', - info: '', - index: Long.ZERO, - key: undefined, - value, - proofOps: undefined, - height: Long.fromString(height), - codespace: '', - }; - } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { - // TODO: Check simulate tx fields - const request = SimulateRequest.decode(Buffer.from(data, 'hex')); - const response = SimulateResponse.encode(await this.txService.simulate(Buffer.from(request.txBytes).toString('base64'))).finish(); - // TODO: Get actual height - const height = (await this.chainApi.query.system.number()).toString(); + const queryAccountResponse: QueryAccountResponse = { + account: { + typeUrl: '/cosmos.auth.v1beta1.BaseAccount', + value: BaseAccount.encode(baseAccount).finish(), + }, + }; + const value = QueryAccountResponse.encode(queryAccountResponse).finish(); + const height = (await this.chainApi.query.system.number()).toString(); + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value, + proofOps: undefined, + height: Long.fromString(height), + codespace: '', + }; + } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { + // TODO: Check simulate tx fields + const request = SimulateRequest.decode(Buffer.from(data, 'hex')); + const response = SimulateResponse.encode(await this.txService.simulate(Buffer.from(request.txBytes).toString('base64'))).finish(); + // TODO: Get actual height + const height = (await this.chainApi.query.system.number()).toString(); - return { - code: 0, - log: '', - info: '', - index: Long.ZERO, - key: undefined, - value: response, - proofOps: undefined, - height: Long.fromString(height), - codespace: "", - }; - } else if (path === '/cosmwasm.wasm.v1.Query/SmartContractState') { - const { address, queryData } = QuerySmartContractStateRequest.decode(Uint8Array.from(Buffer.from(data, 'hex'))); - const gas = 10000000000; - const msg = { - wasm: { - smart: { - contract_addr: address, msg: Buffer.from(queryData).toString('base64') - } - } - }; + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value: response, + proofOps: undefined, + height: Long.fromString(height), + codespace: "", + }; + } else if (path === '/cosmwasm.wasm.v1.Query/SmartContractState') { + const { address, queryData } = QuerySmartContractStateRequest.decode(Uint8Array.from(Buffer.from(data, 'hex'))); + const gas = 10000000000; + const msg = { + wasm: { + smart: { + contract_addr: address, msg: Buffer.from(queryData).toString('base64') + } + } + }; - const height = await this.chainApi.query.system.number(); - const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); - const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${Buffer.from(JSON.stringify(msg), 'utf8').toString('hex')}`, blockHash.toString()); - const stateResponse = QuerySmartContractStateResponse.fromPartial({ data: Uint8Array.from(Buffer.from(response, 'hex')) }); + const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${Buffer.from(JSON.stringify(msg), 'utf8').toString('hex')}`, blockHash.toString()); + const stateResponse = QuerySmartContractStateResponse.fromPartial({ data: Uint8Array.from(Buffer.from(response, 'hex')) }); - return { - code: 0, - log: "", - info: "", - index: Long.ZERO, - key: undefined, - value: QuerySmartContractStateResponse.encode(stateResponse).finish(), - proofOps: undefined, - height: Long.fromString(height.toString()), - codespace: "", - }; - } else { - throw new Error("unexpected path"); - } - } + return { + code: 0, + log: "", + info: "", + index: Long.ZERO, + key: undefined, + value: QuerySmartContractStateResponse.encode(stateResponse).finish(), + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: "", + }; + } else { + throw new Error("unexpected path"); + } + } } diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 366c6854..e3b2d9c4 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -7,60 +7,60 @@ import { ApiService } from "./service"; import Dummy from "../constants/dummy"; export interface IAccountService extends ApiService { - accounts(address: string): Promise; - origin(address: string): Promise; - interim(address: string): string; + accounts(address: string): Promise; + origin(address: string): Promise; + interim(address: string): string; } export class NoirAccountService implements IAccountService { - chainApi: ApiPromise; + chainApi: ApiPromise; - constructor(chainApi: ApiPromise) { - this.chainApi = chainApi; - } + constructor(chainApi: ApiPromise) { + this.chainApi = chainApi; + } - public async accounts(address: string): Promise { - let sequence = '0'; - let key: string | null = null; + public async accounts(address: string): Promise { + let sequence = '0'; + let key: string | null = null; - const originRaw = await this.origin(address); - let origin = originRaw.toString(); - if (origin) { - key = Buffer.from(originRaw.toU8a(true).slice(2)).toString("base64"); - } else { - origin = this.interim(address); - key = Dummy.Secp256k1PublicKey; - } - const account = await this.chainApi.query["system"]["account"](origin); - if (account) { - const { nonce } = account.toJSON() as any; - sequence = nonce.toString(); - } - return new AccountResponse({ - "@type": "/cosmos.auth.v1beta1.BaseAccount", - address: address, - pub_key: { - "@type": "/cosmos.crypto.secp256k1.PubKey", - key, - }, - account_number: "0", - sequence, - }); - } + const originRaw = await this.origin(address); + let origin = originRaw.toString(); + if (origin) { + key = Buffer.from(originRaw.toU8a(true).slice(2)).toString("base64"); + } else { + origin = this.interim(address); + key = Dummy.Secp256k1PublicKey; + } + const account = await this.chainApi.query["system"]["account"](origin); + if (account) { + const { nonce } = account.toJSON() as any; + sequence = nonce.toString(); + } + return new AccountResponse({ + "@type": "/cosmos.auth.v1beta1.BaseAccount", + address: address, + pub_key: { + "@type": "/cosmos.crypto.secp256k1.PubKey", + key, + }, + account_number: "0", + sequence, + }); + } - public async origin(address: string): Promise { - const { data } = fromBech32(address); - return this.chainApi.query['addressMap']['index']( - Buffer.concat([Buffer.from([0x00]), data]) - ); - } + public async origin(address: string): Promise { + const { data } = fromBech32(address); + return this.chainApi.query['addressMap']['index']( + Buffer.concat([Buffer.from([0x00]), data]) + ); + } - public interim(address: string): string { - const { data } = fromBech32(address); - const addressRaw = Buffer.concat([ - Buffer.from([0xa0, 0xe4, 0x02, 0x20]), - blake2b(Buffer.concat([Buffer.from("cosm:", "utf8"), data]), 32), - ]); - return `0x${addressRaw.toString("hex")}`; - } + public interim(address: string): string { + const { data } = fromBech32(address); + const addressRaw = Buffer.concat([ + Buffer.from([0xa0, 0xe4, 0x02, 0x20]), + blake2b(Buffer.concat([Buffer.from("cosm:", "utf8"), data]), 32), + ]); + return `0x${addressRaw.toString("hex")}`; + } } diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 01ec03a3..56c35927 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -6,64 +6,64 @@ import { QueryAllBalancesResponse } from "cosmjs-types/cosmos/bank/v1beta1/query import Long from "long"; export class BalanceService implements ApiService { - config: IConfig; - chainApi: ApiPromise; - accountService: IAccountService; + config: IConfig; + chainApi: ApiPromise; + accountService: IAccountService; - constructor( - config: IConfig, - chainApi: ApiPromise, - accountService: IAccountService - ) { - this.config = config; - this.chainApi = chainApi; - this.accountService = accountService; - } + constructor( + config: IConfig, + chainApi: ApiPromise, + accountService: IAccountService + ) { + this.config = config; + this.chainApi = chainApi; + this.accountService = accountService; + } - public async balances(address: string): Promise { - const originRaw = await this.accountService.origin(address); - let amount = '0'; - let origin = originRaw.toString(); - if (!origin) { - origin = this.accountService.interim(address); - } - const account = await this.chainApi.query.system.account(origin); - if (account) { - const { data } = account.toJSON() as any; - const { free } = data; - amount = BigInt(free).toString(); - } - const denom = this.config.get("chain.denom"); + public async balances(address: string): Promise { + const originRaw = await this.accountService.origin(address); + let amount = '0'; + let origin = originRaw.toString(); + if (!origin) { + origin = this.accountService.interim(address); + } + const account = await this.chainApi.query.system.account(origin); + if (account) { + const { data } = account.toJSON() as any; + const { free } = data; + amount = BigInt(free).toString(); + } + const denom = this.config.get("chain.denom"); - const nativeBalance = { denom, amount }; + const nativeBalance = { denom, amount }; - const assets = []; - const metadata = await this.chainApi.query.assets.metadata.entries(); - for (const [{ args: [assetId] }, value] of metadata) { - const asset = await this.chainApi.query.assets.account(assetId.toString(), origin) + const assets = []; + const metadata = await this.chainApi.query.assets.metadata.entries(); + for (const [{ args: [assetId] }, value] of metadata) { + const asset = await this.chainApi.query.assets.account(assetId.toString(), origin) - if (asset) { - const denom = value.toHuman()['symbol']; - const amount = asset.toJSON() ? BigInt(asset.toJSON()['balance']).toString() : '0'; + if (asset) { + const denom = value.toHuman()['symbol']; + const amount = asset.toJSON() ? BigInt(asset.toJSON()['balance']).toString() : '0'; - assets.push({ denom, amount }); - } - } + assets.push({ denom, amount }); + } + } - console.debug([ - nativeBalance, - ...assets, - ]); + console.debug([ + nativeBalance, + ...assets, + ]); - return { - balances: [ - nativeBalance, - ...assets, - ], - pagination: { - nextKey: new Uint8Array(), - total: Long.ZERO, - }, - }; - } + return { + balances: [ + nativeBalance, + ...assets, + ], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } } diff --git a/sidecar/src/services/distribution.ts b/sidecar/src/services/distribution.ts index dc4f7605..4c992c98 100644 --- a/sidecar/src/services/distribution.ts +++ b/sidecar/src/services/distribution.ts @@ -2,12 +2,12 @@ import { QueryDelegationTotalRewardsResponse } from "cosmjs-types/cosmos/distrib import { ApiService } from "./service"; export class DistributionService implements ApiService { - public rewards( - delegatorAddress: string - ): QueryDelegationTotalRewardsResponse { - return { - rewards: [], - total: [], - }; - } + public rewards( + delegatorAddress: string + ): QueryDelegationTotalRewardsResponse { + return { + rewards: [], + total: [], + }; + } } diff --git a/sidecar/src/services/nodeinfo.ts b/sidecar/src/services/nodeinfo.ts index 8a81d13c..99921438 100644 --- a/sidecar/src/services/nodeinfo.ts +++ b/sidecar/src/services/nodeinfo.ts @@ -4,46 +4,46 @@ import { IConfig } from "config"; import Long from "long"; export class NodeInfoService implements ApiService { - config: IConfig; + config: IConfig; - constructor(config: IConfig) { - this.config = config; - } + constructor(config: IConfig) { + this.config = config; + } - public nodeInfo(): GetNodeInfoResponse { - const endpoint = this.config.get("server.endpoint"); - const network = this.config.get("chain.network"); - const version = this.config.get("chain.version"); - const moniker = this.config.get("chain.moniker"); - const name = this.config.get("chain.name"); - return { - defaultNodeInfo: { - protocolVersion: { - p2p: Long.ZERO, - block: Long.ZERO, - app: Long.ZERO, - }, - defaultNodeId: "0000000000000000000000000000000000000000", - listenAddr: endpoint, - network, - version, - channels: new Uint8Array(Buffer.allocUnsafe(8)), - moniker, - other: { - txIndex: "off", - rpcAddress: "", - }, - }, - applicationVersion: { - name, - appName: name, - version, - gitCommit: "0000000000000000000000000000000000000000", - buildTags: "", - goVersion: "0", - buildDeps: [], - cosmosSdkVersion: "0", - }, - }; - } + public nodeInfo(): GetNodeInfoResponse { + const endpoint = this.config.get("server.endpoint"); + const network = this.config.get("chain.network"); + const version = this.config.get("chain.version"); + const moniker = this.config.get("chain.moniker"); + const name = this.config.get("chain.name"); + return { + defaultNodeInfo: { + protocolVersion: { + p2p: Long.ZERO, + block: Long.ZERO, + app: Long.ZERO, + }, + defaultNodeId: "0000000000000000000000000000000000000000", + listenAddr: endpoint, + network, + version, + channels: new Uint8Array(Buffer.allocUnsafe(8)), + moniker, + other: { + txIndex: "off", + rpcAddress: "", + }, + }, + applicationVersion: { + name, + appName: name, + version, + gitCommit: "0000000000000000000000000000000000000000", + buildTags: "", + goVersion: "0", + buildDeps: [], + cosmosSdkVersion: "0", + }, + }; + } } diff --git a/sidecar/src/services/service.ts b/sidecar/src/services/service.ts index 746c0eb3..7fdf956b 100644 --- a/sidecar/src/services/service.ts +++ b/sidecar/src/services/service.ts @@ -1,17 +1,17 @@ -export interface ApiService {} +export interface ApiService { } export class ApiServices { - services: Map; + services: Map; - constructor() { - this.services = new Map(); - } + constructor() { + this.services = new Map(); + } - public get(name: string): T { - return this.services.get(name) as T; - } + public get(name: string): T { + return this.services.get(name) as T; + } - public set(name: string, service: ApiService) { - this.services.set(name, service); - } + public set(name: string, service: ApiService) { + this.services.set(name, service); + } } diff --git a/sidecar/src/services/staking.ts b/sidecar/src/services/staking.ts index 9d4b77b6..bed35d37 100644 --- a/sidecar/src/services/staking.ts +++ b/sidecar/src/services/staking.ts @@ -1,30 +1,30 @@ import Long from "long"; import { ApiService } from "./service"; import { - QueryDelegatorDelegationsResponse, - QueryDelegatorUnbondingDelegationsResponse, + QueryDelegatorDelegationsResponse, + QueryDelegatorUnbondingDelegationsResponse, } from "cosmjs-types/cosmos/staking/v1beta1/query.js"; export class StakingService implements ApiService { - public delegations(delegatorAddr: string): QueryDelegatorDelegationsResponse { - return { - delegationResponses: [], - pagination: { - nextKey: new Uint8Array(), - total: Long.ZERO, - }, - }; - } + public delegations(delegatorAddr: string): QueryDelegatorDelegationsResponse { + return { + delegationResponses: [], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } - public unbondingDelegations( - delegatorAddr: string - ): QueryDelegatorUnbondingDelegationsResponse { - return { - unbondingResponses: [], - pagination: { - nextKey: new Uint8Array(), - total: Long.ZERO, - }, - }; - } + public unbondingDelegations( + delegatorAddr: string + ): QueryDelegatorUnbondingDelegationsResponse { + return { + unbondingResponses: [], + pagination: { + nextKey: new Uint8Array(), + total: Long.ZERO, + }, + }; + } } diff --git a/sidecar/src/services/status.ts b/sidecar/src/services/status.ts index 16e1fb3f..23837620 100644 --- a/sidecar/src/services/status.ts +++ b/sidecar/src/services/status.ts @@ -5,62 +5,62 @@ import { IConfig } from "config"; import Long from "long"; export class StatusService implements ApiService { - config: IConfig; - chainApi: ApiPromise; + config: IConfig; + chainApi: ApiPromise; - constructor(config: IConfig, chainApi: ApiPromise) { - this.config = config; - this.chainApi = chainApi; - } + constructor(config: IConfig, chainApi: ApiPromise) { + this.config = config; + this.chainApi = chainApi; + } - public async status(): Promise { - const hash = (await this.chainApi.rpc.chain.getFinalizedHead()).toString(); - const { block } = (await this.chainApi.rpc.chain.getBlock(hash)).toJSON(); - const blockNumber = (block as any).header.number; - const timestamp = ( - await (await this.chainApi.at(hash)).query.timestamp.now() - ).toString(); - const blockTime = new Date(parseInt(timestamp)).toISOString(); - const blockHash = hash.startsWith("0x") ? hash.slice(2) : hash; + public async status(): Promise { + const hash = (await this.chainApi.rpc.chain.getFinalizedHead()).toString(); + const { block } = (await this.chainApi.rpc.chain.getBlock(hash)).toJSON(); + const blockNumber = (block as any).header.number; + const timestamp = ( + await (await this.chainApi.at(hash)).query.timestamp.now() + ).toString(); + const blockTime = new Date(parseInt(timestamp)).toISOString(); + const blockHash = hash.startsWith("0x") ? hash.slice(2) : hash; - const endpoint = this.config.get("server.endpoint"); - const network = this.config.get("chain.network"); - const version = this.config.get("chain.version"); - const moniker = this.config.get("chain.moniker"); - return { - node_info: { - protocol_version: { - p2p: Long.ZERO.toString(), - block: Long.ZERO.toString(), - app: Long.ZERO.toString(), - }, - id: "0000000000000000000000000000000000000000", - listen_addr: endpoint, - network, - version, - channels: "0000000000000000", - moniker, - other: { - tx_index: "off", - rpc_address: endpoint, - }, - }, - sync_info: { - latest_block_hash: blockHash.toUpperCase(), - latest_app_hash: - "0000000000000000000000000000000000000000000000000000000000000000", - latest_block_height: blockNumber, - latest_block_time: blockTime, - catching_up: false, - }, - validator_info: { - address: "0000000000000000000000000000000000000000", - pub_key: { - type: "tendermint/PubKeyEd25519", - value: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - }, - voting_power: "0", - }, - }; - } + const endpoint = this.config.get("server.endpoint"); + const network = this.config.get("chain.network"); + const version = this.config.get("chain.version"); + const moniker = this.config.get("chain.moniker"); + return { + node_info: { + protocol_version: { + p2p: Long.ZERO.toString(), + block: Long.ZERO.toString(), + app: Long.ZERO.toString(), + }, + id: "0000000000000000000000000000000000000000", + listen_addr: endpoint, + network, + version, + channels: "0000000000000000", + moniker, + other: { + tx_index: "off", + rpc_address: endpoint, + }, + }, + sync_info: { + latest_block_hash: blockHash.toUpperCase(), + latest_app_hash: + "0000000000000000000000000000000000000000000000000000000000000000", + latest_block_height: blockNumber, + latest_block_time: blockTime, + catching_up: false, + }, + validator_info: { + address: "0000000000000000000000000000000000000000", + pub_key: { + type: "tendermint/PubKeyEd25519", + value: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + }, + voting_power: "0", + }, + }; + } } diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 7dccb7f2..09be7caf 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -3,8 +3,8 @@ import { ResultTx } from "../types"; import { ApiService } from "./service"; import { Database } from "lmdb"; import { - BroadcastTxResponse, - SimulateResponse, + BroadcastTxResponse, + SimulateResponse, } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; import Long from "long"; import { createHash } from "crypto"; @@ -13,202 +13,202 @@ import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; type TransactResult = { codespace: string, code: number; gasUsed: number, events: any[] }; export class TxService implements ApiService { - chainApi: ApiPromise; - db: Database; - - constructor(db: Database, chainApi: ApiPromise) { - this.chainApi = chainApi; - this.db = db; - } - - public async broadcastTx(txBytes: string): Promise { - console.debug(`txBytes: ${txBytes}`); - - const rawTx = `0x${Buffer.from(txBytes, 'base64').toString('hex')}`; - - let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](rawTx)).toString(); - txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; - - console.debug(`txHash: ${txHash.toLowerCase()}`); - - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - - while (true) { - const txs = this.searchTx(txHash); - // console.debug(`txs: ${JSON.stringify(txs)}`); - - if (txs.length > 0) { - const tx = txs.at(0); - - return { - txResponse: { - height: Long.fromString(tx.height), - txhash: txHash.toUpperCase(), - codespace: tx.tx_result.codespace, - code: tx.tx_result.code, - data: tx.tx_result.data, - rawLog: '', - logs: [], - info: tx.tx_result.info, - gasWanted: Long.fromString(tx.tx_result.gas_wanted), - gasUsed: Long.fromString(tx.tx_result.gas_used), - tx: { - typeUrl: '', - value: new Uint8Array(), - }, - timestamp: '', - events: tx.tx_result.events, - }, - }; - } else { - console.debug('Waiting for events...'); - - await sleep(1000); - } - } - } - - public searchTx(hash: string): ResultTx[] { - if (hash.startsWith('0x')) { - hash = hash.slice(2); - } - - console.debug(`txHash: ${hash.toLowerCase()}`); - - const resultTx = this.db.get(`tx::result::${hash.toLowerCase()}`); - const txs: ResultTx[] = []; - if (resultTx) { - txs.push(resultTx); - } - return txs; - } - - public async saveTransactResult( - txRaw: string, - extrinsicIndex: number, - header: any - ): Promise { - txRaw = txRaw.startsWith('0x') ? txRaw.slice(2) : txRaw; - const txBytes = Buffer.from(txRaw, 'hex'); - const gasLimit = Tx.decode(txBytes).authInfo!.fee!.gasLimit; - - const txHash = createHash('sha256').update(Buffer.from(txRaw, 'hex')).digest('hex'); - - const { codespace, code, gasUsed, events } = await this.checkResult(header, extrinsicIndex); - const txResult: ResultTx = { - hash: `${txHash.toUpperCase()}`, - height: header.number.toString(), - index: extrinsicIndex, - tx_result: { - code, - data: '', - log: '', - info: '', - gas_wanted: gasLimit.toString(), - gas_used: gasUsed.toString(), - events, - codespace, - }, - tx: txBytes.toString('base64'), - }; - await this.db.put(`tx::result::${txHash.toLowerCase()}`, txResult); - } - - async checkResult( - header: any, - extrinsicIndex: number - ): Promise { - const events = (await ( - await this.chainApi.at(header.hash) - ).query.system.events()) as any; - const result = events - .filter(({ event: { section, method }, phase }) => { - const { applyExtrinsic } = JSON.parse(phase.toString()); - return ( - applyExtrinsic === extrinsicIndex && - (`${section}::${method}` === 'cosmos::Executed' || - `${section}::${method}` === 'system::ExtrinsicFailed') - ); - }) - .map(({ event: { data, section, method } }) => { - if (`${section}::${method}` === 'cosmos::Executed') { - const [gas_wanted, gas_used, events] = JSON.parse(data); - - console.debug(`gasWanted: ${gas_wanted}`); - console.debug(`gasUsed: ${gas_used}`); - console.debug(`events: ${JSON.stringify(events)}`); - - const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); - - console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) - - return { codespace: '', code: 0, gasUsed: gas_used, events: cosmosEvents }; - } else { - console.debug(JSON.parse(data)); - const [{ module: { index, error } }, info] = JSON.parse(data); - - const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); - const codespace = errors[1]; - const code = errors[2]; - - const weight = info.weight.refTime; - - // TODO: codespace and gasUsed will be transformed proper values - return { codespace: 'sdk', code, gasUsed: weight, events: [] }; - } - }); - return result[0]; - } - - convert(str: string, from: BufferEncoding, to: BufferEncoding): string { - if (from === 'hex') { - str = str.startsWith('0x') ? str.slice(2) : str; - } - return Buffer.from(str, from).toString(to); - } - - public async simulate(txBytes: string): Promise { - const txRaw = `0x${this.convert(txBytes, 'base64', 'hex')}`; - - const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw)).toJSON(); - - const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); - - console.debug(`gasInfo: ${JSON.stringify(gas_info)}`); - console.debug(`events: ${JSON.stringify(cosmosEvents)}`); - - return { - gasInfo: { - gasWanted: Long.fromNumber(gas_info.gas_wanted), - gasUsed: Long.fromNumber(gas_info.gas_used), - }, - result: { - data: new Uint8Array(), - log: '', - events: cosmosEvents, - msgResponses: [], - }, - }; - } - - public encodeEvents(events, from: BufferEncoding, to: BufferEncoding) { - return events.map((event) => { - return { - type: this.convert(event.type ? event.type : event['r#type'], from, to), - attributes: this.encodeAttributes(event.attributes, from, to), - } - }); - } - - public encodeAttributes(attributes, from: BufferEncoding, to: BufferEncoding) { - return attributes.map(({ key, value }) => { - const eventKey = this.convert(key, from, to); - const eventValue = this.convert(value, from, to); - - return { - key: eventKey, - value: eventValue, - } - }); - } + chainApi: ApiPromise; + db: Database; + + constructor(db: Database, chainApi: ApiPromise) { + this.chainApi = chainApi; + this.db = db; + } + + public async broadcastTx(txBytes: string): Promise { + console.debug(`txBytes: ${txBytes}`); + + const rawTx = `0x${Buffer.from(txBytes, 'base64').toString('hex')}`; + + let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](rawTx)).toString(); + txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; + + console.debug(`txHash: ${txHash.toLowerCase()}`); + + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + + while (true) { + const txs = this.searchTx(txHash); + // console.debug(`txs: ${JSON.stringify(txs)}`); + + if (txs.length > 0) { + const tx = txs.at(0); + + return { + txResponse: { + height: Long.fromString(tx.height), + txhash: txHash.toUpperCase(), + codespace: tx.tx_result.codespace, + code: tx.tx_result.code, + data: tx.tx_result.data, + rawLog: '', + logs: [], + info: tx.tx_result.info, + gasWanted: Long.fromString(tx.tx_result.gas_wanted), + gasUsed: Long.fromString(tx.tx_result.gas_used), + tx: { + typeUrl: '', + value: new Uint8Array(), + }, + timestamp: '', + events: tx.tx_result.events, + }, + }; + } else { + console.debug('Waiting for events...'); + + await sleep(1000); + } + } + } + + public searchTx(hash: string): ResultTx[] { + if (hash.startsWith('0x')) { + hash = hash.slice(2); + } + + console.debug(`txHash: ${hash.toLowerCase()}`); + + const resultTx = this.db.get(`tx::result::${hash.toLowerCase()}`); + const txs: ResultTx[] = []; + if (resultTx) { + txs.push(resultTx); + } + return txs; + } + + public async saveTransactResult( + txRaw: string, + extrinsicIndex: number, + header: any + ): Promise { + txRaw = txRaw.startsWith('0x') ? txRaw.slice(2) : txRaw; + const txBytes = Buffer.from(txRaw, 'hex'); + const gasLimit = Tx.decode(txBytes).authInfo!.fee!.gasLimit; + + const txHash = createHash('sha256').update(Buffer.from(txRaw, 'hex')).digest('hex'); + + const { codespace, code, gasUsed, events } = await this.checkResult(header, extrinsicIndex); + const txResult: ResultTx = { + hash: `${txHash.toUpperCase()}`, + height: header.number.toString(), + index: extrinsicIndex, + tx_result: { + code, + data: '', + log: '', + info: '', + gas_wanted: gasLimit.toString(), + gas_used: gasUsed.toString(), + events, + codespace, + }, + tx: txBytes.toString('base64'), + }; + await this.db.put(`tx::result::${txHash.toLowerCase()}`, txResult); + } + + async checkResult( + header: any, + extrinsicIndex: number + ): Promise { + const events = (await ( + await this.chainApi.at(header.hash) + ).query.system.events()) as any; + const result = events + .filter(({ event: { section, method }, phase }) => { + const { applyExtrinsic } = JSON.parse(phase.toString()); + return ( + applyExtrinsic === extrinsicIndex && + (`${section}::${method}` === 'cosmos::Executed' || + `${section}::${method}` === 'system::ExtrinsicFailed') + ); + }) + .map(({ event: { data, section, method } }) => { + if (`${section}::${method}` === 'cosmos::Executed') { + const [gas_wanted, gas_used, events] = JSON.parse(data); + + console.debug(`gasWanted: ${gas_wanted}`); + console.debug(`gasUsed: ${gas_used}`); + console.debug(`events: ${JSON.stringify(events)}`); + + const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); + + console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) + + return { codespace: '', code: 0, gasUsed: gas_used, events: cosmosEvents }; + } else { + console.debug(JSON.parse(data)); + const [{ module: { index, error } }, info] = JSON.parse(data); + + const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); + const codespace = errors[1]; + const code = errors[2]; + + const weight = info.weight.refTime; + + // TODO: codespace and gasUsed will be transformed proper values + return { codespace: 'sdk', code, gasUsed: weight, events: [] }; + } + }); + return result[0]; + } + + convert(str: string, from: BufferEncoding, to: BufferEncoding): string { + if (from === 'hex') { + str = str.startsWith('0x') ? str.slice(2) : str; + } + return Buffer.from(str, from).toString(to); + } + + public async simulate(txBytes: string): Promise { + const txRaw = `0x${this.convert(txBytes, 'base64', 'hex')}`; + + const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw)).toJSON(); + + const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); + + console.debug(`gasInfo: ${JSON.stringify(gas_info)}`); + console.debug(`events: ${JSON.stringify(cosmosEvents)}`); + + return { + gasInfo: { + gasWanted: Long.fromNumber(gas_info.gas_wanted), + gasUsed: Long.fromNumber(gas_info.gas_used), + }, + result: { + data: new Uint8Array(), + log: '', + events: cosmosEvents, + msgResponses: [], + }, + }; + } + + public encodeEvents(events, from: BufferEncoding, to: BufferEncoding) { + return events.map((event) => { + return { + type: this.convert(event.type ? event.type : event['r#type'], from, to), + attributes: this.encodeAttributes(event.attributes, from, to), + } + }); + } + + public encodeAttributes(attributes, from: BufferEncoding, to: BufferEncoding) { + return attributes.map(({ key, value }) => { + const eventKey = this.convert(key, from, to); + const eventValue = this.convert(value, from, to); + + return { + key: eventKey, + value: eventValue, + } + }); + } } diff --git a/sidecar/src/types/account.ts b/sidecar/src/types/account.ts index b42bc4fd..13c0dac3 100644 --- a/sidecar/src/types/account.ts +++ b/sidecar/src/types/account.ts @@ -1,20 +1,20 @@ export class AccountResponse { - account: BaseAccount; + account: BaseAccount; - constructor(account: BaseAccount) { - this.account = account; - } + constructor(account: BaseAccount) { + this.account = account; + } } export interface BaseAccount { - "@type": string; - address: string; - pub_key: PublicKey | null; - account_number: string; - sequence: string; + "@type": string; + address: string; + pub_key: PublicKey | null; + account_number: string; + sequence: string; } export interface PublicKey { - "@type": string; - key: string; + "@type": string; + key: string; } diff --git a/sidecar/src/types/cometbft.ts b/sidecar/src/types/cometbft.ts index 07a7f4cf..3eee20a5 100644 --- a/sidecar/src/types/cometbft.ts +++ b/sidecar/src/types/cometbft.ts @@ -1,34 +1,34 @@ export interface ResultTxSearch { - txs: ResultTx[]; - total_count: string; + txs: ResultTx[]; + total_count: string; } export interface ResultTx { - hash: string; - height: string; - index: number; - tx_result: ResponseDeliverTx; - tx: string; + hash: string; + height: string; + index: number; + tx_result: ResponseDeliverTx; + tx: string; } export interface ResponseDeliverTx { - code: number; - data: string; - log: string; - info: string; - gas_wanted: string; - gas_used: string; - events: Event[]; - codespace: string; + code: number; + data: string; + log: string; + info: string; + gas_wanted: string; + gas_used: string; + events: Event[]; + codespace: string; } export interface Event { - type: string; - attributes: EventAttribute[]; + type: string; + attributes: EventAttribute[]; } export interface EventAttribute { - key: string; - value: string; - index: boolean; + key: string; + value: string; + index: boolean; } diff --git a/sidecar/src/types/status.ts b/sidecar/src/types/status.ts index e1f422e3..35252598 100644 --- a/sidecar/src/types/status.ts +++ b/sidecar/src/types/status.ts @@ -1,46 +1,46 @@ export interface ResultStatus { - node_info: StatusDefaultNodeInfo; - sync_info: SyncInfo; - validator_info: ValidatorInfo; + node_info: StatusDefaultNodeInfo; + sync_info: SyncInfo; + validator_info: ValidatorInfo; } export interface DefaultNodeInfoOther { - tx_index: string; - rpc_address: string; + tx_index: string; + rpc_address: string; } export interface ProtocolVersion { - p2p: string; - block: string; - app: string; + p2p: string; + block: string; + app: string; } export interface StatusDefaultNodeInfo { - protocol_version: ProtocolVersion; - id: string; - listen_addr: string; - network: string; - version: string; - channels: string; - moniker: string; - other: DefaultNodeInfoOther; + protocol_version: ProtocolVersion; + id: string; + listen_addr: string; + network: string; + version: string; + channels: string; + moniker: string; + other: DefaultNodeInfoOther; } export interface SyncInfo { - latest_block_hash: string; - latest_app_hash: string; - latest_block_height: string; - latest_block_time: string; - catching_up: boolean; + latest_block_hash: string; + latest_app_hash: string; + latest_block_height: string; + latest_block_time: string; + catching_up: boolean; } export interface ValidatorInfo { - address: string; - pub_key: PubKey; - voting_power: string; + address: string; + pub_key: PubKey; + voting_power: string; } export interface PubKey { - type: string; - value: string; + type: string; + value: string; } diff --git a/sidecar/src/types/tx.ts b/sidecar/src/types/tx.ts index 0f518733..71328f11 100644 --- a/sidecar/src/types/tx.ts +++ b/sidecar/src/types/tx.ts @@ -1,7 +1,7 @@ export interface BroadcastTxSyncResponse { - code: number; - data: string; - log: string; - codespace: string; - hash: string; + code: number; + data: string; + log: string; + codespace: string; + hash: string; } diff --git a/sidecar/src/utils/index.ts b/sidecar/src/utils/index.ts index effce245..a5963d88 100644 --- a/sidecar/src/utils/index.ts +++ b/sidecar/src/utils/index.ts @@ -1,22 +1,22 @@ export function toSnakeCase(input: unknown): unknown { - if (typeof input !== "object" || input === null) { - return input; - } + if (typeof input !== "object" || input === null) { + return input; + } - if (Array.isArray(input)) { - return input.map(toSnakeCase); - } + if (Array.isArray(input)) { + return input.map(toSnakeCase); + } - const result: unknown = {}; - for (const key in input) { - if (input.hasOwnProperty(key)) { - const snakeCaseKey = key - .replace(/([A-Z])/g, "_$1") - .toLowerCase() - .replace(/^_/, ""); - result[snakeCaseKey] = toSnakeCase(input[key]); - } - } + const result: unknown = {}; + for (const key in input) { + if (input.hasOwnProperty(key)) { + const snakeCaseKey = key + .replace(/([A-Z])/g, "_$1") + .toLowerCase() + .replace(/^_/, ""); + result[snakeCaseKey] = toSnakeCase(input[key]); + } + } - return result; + return result; } From 0da0f035f50a439e9909ffc60a9aab08903ca946 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 21 Sep 2024 10:37:07 +0900 Subject: [PATCH 062/133] fix: Fix derive interim account in sidecar --- sidecar/src/services/account.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index e3b2d9c4..29652192 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -57,10 +57,9 @@ export class NoirAccountService implements IAccountService { public interim(address: string): string { const { data } = fromBech32(address); - const addressRaw = Buffer.concat([ - Buffer.from([0xa0, 0xe4, 0x02, 0x20]), + const addressRaw = Buffer.from( blake2b(Buffer.concat([Buffer.from("cosm:", "utf8"), data]), 32), - ]); + ); return `0x${addressRaw.toString("hex")}`; } } From e1d4dfb33ef423adbd5574885ba9b112b17b456d Mon Sep 17 00:00:00 2001 From: code0xff Date: Sat, 21 Sep 2024 17:05:49 +0900 Subject: [PATCH 063/133] fix: Replace ante handler error type from TransactionValidityError to CosmosError --- frame/cosmos/types/Cargo.toml | 4 +- frame/cosmos/types/src/errors.rs | 21 +++- frame/cosmos/types/src/handler.rs | 15 ++- frame/cosmos/x/auth/Cargo.toml | 24 ++--- frame/cosmos/x/auth/src/basic.rs | 39 ++++--- frame/cosmos/x/auth/src/fee.rs | 44 ++++---- frame/cosmos/x/auth/src/msg.rs | 18 ++-- frame/cosmos/x/auth/src/sigverify.rs | 145 +++++++++++++-------------- sidecar/config/default.json | 4 +- 9 files changed, 163 insertions(+), 151 deletions(-) diff --git a/frame/cosmos/types/Cargo.toml b/frame/cosmos/types/Cargo.toml index dcb4c7ff..06b22cc5 100644 --- a/frame/cosmos/types/Cargo.toml +++ b/frame/cosmos/types/Cargo.toml @@ -9,7 +9,9 @@ publish = false [dependencies] bech32 = { version = "0.11", default-features = false, features = ["alloc"] } -cosmos-sdk-proto = { version = "0.24", default-features = false, features = ["cosmwasm"] } +cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ + "cosmwasm", +] } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } impl-trait-for-tuples = { version = "0.2.2" } parity-scale-codec = { version = "3.6", default-features = false, features = [ diff --git a/frame/cosmos/types/src/errors.rs b/frame/cosmos/types/src/errors.rs index d6de7b6d..07b35d0c 100644 --- a/frame/cosmos/types/src/errors.rs +++ b/frame/cosmos/types/src/errors.rs @@ -15,9 +15,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use frame_support::PalletError; +use frame_support::{pallet_prelude::TransactionValidityError, PalletError}; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; +use sp_runtime::transaction_validity::InvalidTransaction; #[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo, PalletError)] pub struct CosmosError { @@ -25,17 +26,35 @@ pub struct CosmosError { pub code: u8, } +impl From for TransactionValidityError { + fn from(e: CosmosError) -> Self { + TransactionValidityError::Invalid(InvalidTransaction::Custom(e.code)) + } +} + pub const ROOT_CODESPACE: u8 = 0; #[derive(Clone, PartialEq, Eq, Debug)] pub enum RootError { TxDecodeError = 2, + Unauthorized = 4, InsufficientFunds = 5, UnknownRequest = 6, InvalidAddress = 7, + InvalidPubKey = 8, InvalidCoins = 10, OutOfGas = 11, + MemoTooLarge = 12, + InsufficientFee = 13, + TooManySignatures = 14, + NoSignatures = 15, + InvalidRequest = 18, + InvalidSigner = 24, + TxTimeoutHeightError = 30, + WrongSequence = 32, UnpackAnyError = 34, + NotSupported = 37, + InvalidGasLimit = 41, } impl From for CosmosError { diff --git a/frame/cosmos/types/src/handler.rs b/frame/cosmos/types/src/handler.rs index db10e6b5..e8a5bfb7 100644 --- a/frame/cosmos/types/src/handler.rs +++ b/frame/cosmos/types/src/handler.rs @@ -15,24 +15,23 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::errors::CosmosError; use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; -use sp_runtime::transaction_validity::{TransactionValidity, ValidTransaction}; pub trait AnteDecorator { - fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity; + fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError>; } impl AnteDecorator for () { - fn ante_handle(_tx: &Tx, _simulate: bool) -> TransactionValidity { - Ok(ValidTransaction::default()) + fn ante_handle(_tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + Ok(()) } } #[impl_trait_for_tuples::impl_for_tuples(1, 12)] impl AnteDecorator for Tuple { - fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { - let valid = ValidTransaction::default(); - for_tuples!( #( let valid = valid.combine_with(Tuple::ante_handle(tx, simulate)?); )* ); - Ok(valid) + fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError> { + for_tuples!( #( Tuple::ante_handle(tx, simulate)?; )* ); + Ok(()) } } diff --git a/frame/cosmos/x/auth/Cargo.toml b/frame/cosmos/x/auth/Cargo.toml index 84642fbf..49501715 100644 --- a/frame/cosmos/x/auth/Cargo.toml +++ b/frame/cosmos/x/auth/Cargo.toml @@ -26,16 +26,16 @@ hex = { version = "0.4.3", default-features = false } [features] default = ["std"] std = [ - "cosmos-sdk-proto/std", - "frame-support/std", - "frame-system/std", - "np-cosmos/std", - "pallet-cosmos/std", - "pallet-cosmos-types/std", - "pallet-cosmos-x-auth-signing/std", - "ripemd/std", - "sp-core/std", - "sp-io/std", - "sp-runtime/std", - "hex/std", + "cosmos-sdk-proto/std", + "frame-support/std", + "frame-system/std", + "np-cosmos/std", + "pallet-cosmos/std", + "pallet-cosmos-types/std", + "pallet-cosmos-x-auth-signing/std", + "ripemd/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "hex/std", ] diff --git a/frame/cosmos/x/auth/src/basic.rs b/frame/cosmos/x/auth/src/basic.rs index ab7c2290..aff6e8c9 100644 --- a/frame/cosmos/x/auth/src/basic.rs +++ b/frame/cosmos/x/auth/src/basic.rs @@ -17,56 +17,53 @@ use core::marker::PhantomData; use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; -use frame_support::{ensure, pallet_prelude::ValidTransaction, traits::Get}; -use pallet_cosmos_types::handler::AnteDecorator; -use sp_runtime::{ - transaction_validity::{InvalidTransaction, TransactionValidity}, - SaturatedConversion, +use frame_support::{ensure, traits::Get}; +use pallet_cosmos_types::{ + errors::{CosmosError, RootError}, + handler::AnteDecorator, }; +use sp_runtime::SaturatedConversion; pub struct ValidateBasicDecorator(PhantomData); - impl AnteDecorator for ValidateBasicDecorator where T: frame_system::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { - ensure!(!tx.signatures.is_empty(), InvalidTransaction::Call); - let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; - ensure!(auth_info.signer_infos.len() == tx.signatures.len(), InvalidTransaction::Call); + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + ensure!(!tx.signatures.is_empty(), RootError::NoSignatures); + let auth_info = tx.auth_info.as_ref().ok_or(RootError::TxDecodeError)?; + ensure!(auth_info.signer_infos.len() == tx.signatures.len(), RootError::Unauthorized); - Ok(ValidTransaction::default()) + Ok(()) } } pub struct TxTimeoutHeightDecorator(PhantomData); - impl AnteDecorator for TxTimeoutHeightDecorator where T: frame_system::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { - let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; let block_number: u64 = frame_system::Pallet::::block_number().saturated_into(); if body.timeout_height > 0 && block_number > body.timeout_height { - return Err(InvalidTransaction::Stale.into()); + return Err(RootError::TxTimeoutHeightError.into()); } - Ok(ValidTransaction::default()) + Ok(()) } } pub struct ValidateMemoDecorator(PhantomData); - impl AnteDecorator for ValidateMemoDecorator where T: pallet_cosmos::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { - let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; - ensure!(body.memo.len() <= T::MaxMemoCharacters::get() as usize, InvalidTransaction::Call); + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; + ensure!(body.memo.len() <= T::MaxMemoCharacters::get() as usize, RootError::MemoTooLarge); - Ok(ValidTransaction::default()) + Ok(()) } } diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs index f9ec70f2..8ed21499 100644 --- a/frame/cosmos/x/auth/src/fee.rs +++ b/frame/cosmos/x/auth/src/fee.rs @@ -20,7 +20,6 @@ use core::marker::PhantomData; use cosmos_sdk_proto::cosmos::tx::v1beta1::{Fee, Tx}; use frame_support::{ ensure, - pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, traits::{ fungibles::Balanced, tokens::{Fortitude, Precision, Preservation}, @@ -31,6 +30,7 @@ use pallet_cosmos::AddressMapping; use pallet_cosmos_types::{ address::acc_address_from_bech32, coin::amount_to_string, + errors::{CosmosError, RootError}, events::{ CosmosEvent, EventAttribute, ATTRIBUTE_KEY_FEE, ATTRIBUTE_KEY_FEE_PAYER, EVENT_TYPE_TX, }, @@ -44,27 +44,28 @@ use sp_runtime::{ }; pub struct DeductFeeDecorator(PhantomData); - impl AnteDecorator for DeductFeeDecorator where T: frame_system::Config + pallet_cosmos::Config, { - fn ante_handle(tx: &Tx, simulate: bool) -> TransactionValidity { + fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError> { let fee = tx .auth_info .as_ref() .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(InvalidTransaction::Call)?; + .ok_or(RootError::TxDecodeError)?; if !simulate && !frame_system::Pallet::::block_number().is_zero() && fee.gas_limit == 0 { - return Err(InvalidTransaction::Call.into()); + return Err(RootError::InvalidGasLimit.into()); } - // TODO: Implements txFeeChecker + if !simulate { + // TODO: Implements txFeeChecker + } Self::check_deduct_fee(tx)?; - Ok(ValidTransaction::default()) + Ok(()) } } @@ -72,24 +73,23 @@ impl DeductFeeDecorator where T: pallet_cosmos::Config, { - fn check_deduct_fee(tx: &Tx) -> TransactionValidity { - let fee_payer = T::SigVerifiableTx::fee_payer(tx).map_err(|_| InvalidTransaction::Call)?; + fn check_deduct_fee(tx: &Tx) -> Result<(), CosmosError> { + let fee_payer = T::SigVerifiableTx::fee_payer(tx).map_err(|_| RootError::TxDecodeError)?; let fee = tx .auth_info .as_ref() .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(InvalidTransaction::Call)?; + .ok_or(RootError::TxDecodeError)?; - // TODO: Fee granter support - ensure!(fee.granter.is_empty(), InvalidTransaction::Call); + // Fee granter not supported + ensure!(fee.granter.is_empty(), RootError::InvalidRequest); let (_hrp, address_raw) = - acc_address_from_bech32(&fee_payer).map_err(|_| InvalidTransaction::BadSigner)?; - ensure!(address_raw.len() == 20, InvalidTransaction::BadSigner); + acc_address_from_bech32(&fee_payer).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); let deduct_fees_from = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); - // TODO: Check fee is zero if !fee.amount.is_empty() { Self::deduct_fees(&deduct_fees_from, fee)?; } @@ -107,12 +107,12 @@ where }, ])); - Ok(ValidTransaction::default()) + Ok(()) } - fn deduct_fees(acc: &T::AccountId, fee: &Fee) -> TransactionValidity { + fn deduct_fees(acc: &T::AccountId, fee: &Fee) -> Result<(), CosmosError> { for amt in fee.amount.iter() { - let amount = amt.amount.parse::().map_err(|_| InvalidTransaction::Call)?; + let amount = amt.amount.parse::().map_err(|_| RootError::InsufficientFee)?; if amt.denom == T::NativeDenom::get() { let _imbalance = T::NativeAsset::withdraw( @@ -121,12 +121,12 @@ where WithdrawReasons::TRANSACTION_PAYMENT, ExistenceRequirement::KeepAlive, ) - .map_err(|_| InvalidTransaction::Payment)?; + .map_err(|_| RootError::InsufficientFunds)?; // TODO: Resolve imbalance } else { let asset_id = T::AssetToDenom::convert(amt.denom.clone()) - .map_err(|_| InvalidTransaction::Call)?; + .map_err(|_| RootError::InsufficientFunds)?; let _imbalance = T::Assets::withdraw( asset_id, acc, @@ -135,12 +135,12 @@ where Preservation::Preserve, Fortitude::Polite, ) - .map_err(|_| InvalidTransaction::Payment)?; + .map_err(|_| RootError::InsufficientFunds)?; // TODO: Resolve imbalance } } - Ok(ValidTransaction::default()) + Ok(()) } } diff --git a/frame/cosmos/x/auth/src/msg.rs b/frame/cosmos/x/auth/src/msg.rs index 85e57d6f..2515d40c 100644 --- a/frame/cosmos/x/auth/src/msg.rs +++ b/frame/cosmos/x/auth/src/msg.rs @@ -17,24 +17,22 @@ use core::marker::PhantomData; use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; -use frame_support::{ - ensure, - pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, - traits::Contains, +use frame_support::{ensure, traits::Contains}; +use pallet_cosmos_types::{ + errors::{CosmosError, RootError}, + handler::AnteDecorator, }; -use pallet_cosmos_types::handler::AnteDecorator; pub struct KnownMsgDecorator(PhantomData); - impl AnteDecorator for KnownMsgDecorator where T: pallet_cosmos::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { - let body = tx.body.as_ref().ok_or(InvalidTransaction::Call)?; + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + let body = tx.body.as_ref().ok_or(RootError::TxDecodeError)?; - ensure!(body.messages.iter().all(T::MsgFilter::contains), InvalidTransaction::Call); + ensure!(body.messages.iter().all(T::MsgFilter::contains), RootError::TxDecodeError); - Ok(ValidTransaction::default()) + Ok(()) } } diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index 3270c0b7..69458832 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -24,67 +24,68 @@ use cosmos_sdk_proto::{ prost::Message, Any, }; -use frame_support::{ - ensure, - pallet_prelude::{InvalidTransaction, TransactionValidity, ValidTransaction}, -}; +use frame_support::ensure; use np_cosmos::traits::ChainInfo; use pallet_cosmos::AddressMapping; -use pallet_cosmos_types::{address::acc_address_from_bech32, any_match, handler::AnteDecorator}; +use pallet_cosmos_types::{ + address::acc_address_from_bech32, + any_match, + errors::{CosmosError, RootError}, + handler::AnteDecorator, +}; use pallet_cosmos_x_auth_signing::{ sign_mode_handler::{traits::SignModeHandler, SignerData}, sign_verifiable_tx::traits::SigVerifiableTx, }; use ripemd::Digest; use sp_core::{ecdsa, sha2_256, ByteArray, Get, H160}; -use sp_runtime::{transaction_validity::TransactionValidityError, SaturatedConversion}; +use sp_runtime::SaturatedConversion; pub struct SigVerificationDecorator(PhantomData); - impl AnteDecorator for SigVerificationDecorator where T: pallet_cosmos::Config + frame_system::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError> { let signatures = &tx.signatures; - let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| InvalidTransaction::Call)?; - let signer_infos = &tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?.signer_infos; + let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| RootError::TxDecodeError)?; + let signer_infos = &tx.auth_info.as_ref().ok_or(RootError::TxDecodeError)?.signer_infos; - ensure!(signatures.len() == signers.len(), InvalidTransaction::Call); - ensure!(signatures.len() == signer_infos.len(), InvalidTransaction::Call); + ensure!(signatures.len() == signers.len(), RootError::Unauthorized); + ensure!(signatures.len() == signer_infos.len(), RootError::Unauthorized); for (i, sig) in signatures.iter().enumerate() { - let signer = signers.get(i).ok_or(InvalidTransaction::Call)?; - let signer_info = signer_infos.get(i).ok_or(InvalidTransaction::Call)?; + let signer = signers.get(i).ok_or(RootError::TxDecodeError)?; + let signer_info = signer_infos.get(i).ok_or(RootError::TxDecodeError)?; let (_hrp, signer_addr_raw) = - acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; - ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); + acc_address_from_bech32(signer).map_err(|_| RootError::InvalidAddress)?; + ensure!(signer_addr_raw.len() == 20, RootError::InvalidAddress); let who = T::AddressMapping::into_account_id(H160::from_slice(&signer_addr_raw)); let sequence = frame_system::Pallet::::account_nonce(&who).saturated_into(); - match signer_info.sequence.cmp(&sequence) { - Ordering::Greater => Err(InvalidTransaction::Future), - Ordering::Less => Err(InvalidTransaction::Stale), - Ordering::Equal => Ok(()), + Ordering::Less => Err(RootError::WrongSequence), + _ => Ok(()), }?; - let public_key = signer_info.public_key.as_ref().ok_or(InvalidTransaction::Call)?; - let chain_id = T::ChainInfo::chain_id().into(); - let signer_data = SignerData { - address: signer.clone(), - chain_id, - account_number: 0, - sequence: signer_info.sequence, - pub_key: public_key.clone(), - }; - let sign_mode = signer_info.mode_info.as_ref().ok_or(InvalidTransaction::Call)?; - - Self::verify_signature(public_key, &signer_data, sign_mode, sig, tx)?; + if !simulate { + let public_key = signer_info.public_key.as_ref().ok_or(RootError::InvalidPubKey)?; + let chain_id = T::ChainInfo::chain_id().into(); + let signer_data = SignerData { + address: signer.clone(), + chain_id, + account_number: 0, + sequence: signer_info.sequence, + pub_key: public_key.clone(), + }; + let sign_mode = signer_info.mode_info.as_ref().ok_or(RootError::TxDecodeError)?; + + Self::verify_signature(public_key, &signer_data, sign_mode, sig, tx)?; + } } - Ok(ValidTransaction::default()) + Ok(()) } } @@ -98,37 +99,34 @@ where sign_mode: &ModeInfo, signature: &[u8], tx: &Tx, - ) -> Result<(), TransactionValidityError> { + ) -> Result<(), CosmosError> { any_match!( public_key, { secp256k1::PubKey => { let public_key = - secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| { - InvalidTransaction::BadSigner - })?; + secp256k1::PubKey::decode(&mut &*public_key.value).map_err(|_| RootError::UnpackAnyError)?; let mut hasher = ripemd::Ripemd160::new(); hasher.update(sha2_256(&public_key.key)); let address = H160::from_slice(&hasher.finalize()); let (_hrp, signer_addr_raw) = - acc_address_from_bech32(&signer_data.address).map_err(|_| { - InvalidTransaction::BadSigner - })?; - ensure!(signer_addr_raw.len() == 20, InvalidTransaction::BadSigner); - ensure!(H160::from_slice(&signer_addr_raw) == address, InvalidTransaction::BadSigner); + acc_address_from_bech32(&signer_data.address).map_err(|_| RootError::InvalidAddress)?; + ensure!(signer_addr_raw.len() == 20, RootError::InvalidAddress); + + ensure!(H160::from_slice(&signer_addr_raw) == address, RootError::Unauthorized); let sign_bytes = T::SignModeHandler::get_sign_bytes(sign_mode, signer_data, tx) - .map_err(|_| InvalidTransaction::Call)?; + .map_err(|_| RootError::Unauthorized)?; if !ecdsa_verify_prehashed(signature, &sign_bytes, &public_key.key) { - return Err(InvalidTransaction::BadProof.into()); + return Err(RootError::Unauthorized.into()); } Ok(()) } }, - Err(InvalidTransaction::BadSigner.into()) + Err(RootError::InvalidPubKey.into()) ) } } @@ -154,48 +152,31 @@ pub fn ecdsa_verify_prehashed(signature: &[u8], message: &[u8], public_key: &[u8 } } -#[cfg(test)] -pub mod tests { - use super::*; - - #[test] - fn ecdsa_verify_test() { - let sig = hex::decode("f7e0d198c62821cc5817c8e935f523308301e29819f5d882f3249b9e173a614f38000ddbff446c0abfa7c7d019dbb17072b28933fc8187c973fbf03d0459f76e").unwrap(); - let message = hex::decode("0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331716436396e75776a393567746134616b6a677978746a39756a6d7a34773865646d7179737177122d636f736d6f7331676d6a32657861673033747467616670726b6463337438383067726d61396e776566636432771a100a057561746f6d12073130303030303012710a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a112040a020801121f0a150a057561746f6d120c3838363838303030303030301080c0f1c59495141a1174686574612d746573746e65742d30303120ad8a2e").unwrap(); - let public_key = - hex::decode("020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1") - .unwrap(); - - assert!(ecdsa_verify_prehashed(&sig, &message, &public_key)); - } -} - pub struct ValidateSigCountDecorator(PhantomData); - impl AnteDecorator for ValidateSigCountDecorator where T: pallet_cosmos::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { let mut sig_count = 0u64; - let auth_info = tx.auth_info.as_ref().ok_or(InvalidTransaction::Call)?; + let auth_info = tx.auth_info.as_ref().ok_or(RootError::TxDecodeError)?; for SignerInfo { public_key, .. } in auth_info.signer_infos.iter() { - let public_key = public_key.as_ref().ok_or(InvalidTransaction::BadSigner)?; + let public_key = public_key.as_ref().ok_or(RootError::TxDecodeError)?; sig_count = sig_count.saturating_add(Self::count_sub_keys(public_key)?); - ensure!(sig_count <= T::TxSigLimit::get(), InvalidTransaction::Call); + ensure!(sig_count <= T::TxSigLimit::get(), RootError::TooManySignatures); } - Ok(ValidTransaction::default()) + Ok(()) } } impl ValidateSigCountDecorator { - fn count_sub_keys(pubkey: &Any) -> Result { + fn count_sub_keys(pubkey: &Any) -> Result { // TODO: Support legacy multi signatures. if LegacyAminoPubKey::decode(&mut &*pubkey.value).is_ok() { - Err(InvalidTransaction::BadSigner.into()) + Err(RootError::TxDecodeError.into()) } else { Ok(1) } @@ -208,17 +189,33 @@ impl AnteDecorator for IncrementSequenceDecorator where T: frame_system::Config + pallet_cosmos::Config, { - fn ante_handle(tx: &Tx, _simulate: bool) -> TransactionValidity { - let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| InvalidTransaction::Call)?; + fn ante_handle(tx: &Tx, _simulate: bool) -> Result<(), CosmosError> { + let signers = T::SigVerifiableTx::get_signers(tx).map_err(|_| RootError::TxDecodeError)?; for signer in signers.iter() { let (_hrp, address_raw) = - acc_address_from_bech32(signer).map_err(|_| InvalidTransaction::BadSigner)?; - ensure!(address_raw.len() == 20, InvalidTransaction::BadSigner); + acc_address_from_bech32(signer).map_err(|_| RootError::InvalidAddress)?; + ensure!(address_raw.len() == 20, RootError::InvalidAddress); let account = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); frame_system::pallet::Pallet::::inc_account_nonce(account); } - Ok(ValidTransaction::default()) + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn ecdsa_verify_test() { + let sig = hex::decode("f7e0d198c62821cc5817c8e935f523308301e29819f5d882f3249b9e173a614f38000ddbff446c0abfa7c7d019dbb17072b28933fc8187c973fbf03d0459f76e").unwrap(); + let message = hex::decode("0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331716436396e75776a393567746134616b6a677978746a39756a6d7a34773865646d7179737177122d636f736d6f7331676d6a32657861673033747467616670726b6463337438383067726d61396e776566636432771a100a057561746f6d12073130303030303012710a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a112040a020801121f0a150a057561746f6d120c3838363838303030303030301080c0f1c59495141a1174686574612d746573746e65742d30303120ad8a2e").unwrap(); + let public_key = + hex::decode("020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1") + .unwrap(); + + assert!(ecdsa_verify_prehashed(&sig, &message, &public_key)); } } diff --git a/sidecar/config/default.json b/sidecar/config/default.json index 6031c300..9046de14 100644 --- a/sidecar/config/default.json +++ b/sidecar/config/default.json @@ -5,10 +5,10 @@ "server": { "endpoint": "http://localhost:1317", "port": 1317, - "logger": true + "logger": false }, "chain": { - "endpoint": "ws://127.0.0.1:59954", + "endpoint": "ws://127.0.0.1:49544", "network": "dev", "moniker": "cosmos", "name": "noir", From 008dcf10cf9f4aebb5031b29161457cb775a45e9 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 22 Sep 2024 00:40:56 +0900 Subject: [PATCH 064/133] fix: Use run_tx for tx simulation in runtime api --- frame/cosmos/src/lib.rs | 17 ++++++----------- frame/cosmos/types/src/lib.rs | 1 + frame/cosmos/types/src/tx.rs | 26 ++++++++++++++++++++++++++ frame/cosmos/x/auth/src/sigverify.rs | 11 ++++++++--- sidecar/public/config.js | 2 +- sidecar/src/services/account.ts | 13 +++---------- ziggurat/runtime/src/apis.rs | 7 +++++-- 7 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 frame/cosmos/types/src/tx.rs diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index f48deb81..7dc301f3 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -38,6 +38,7 @@ use pallet_cosmos_types::{ gas::traits::GasMeter, handler::AnteDecorator, msgservice::traits::MsgServiceRouter, + tx::get_gas_limit, }; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -368,18 +369,12 @@ impl Pallet { } pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { - let gas_limit = tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or( - Error::::CosmosError(RootError::TxDecodeError.into()) - .with_weight(T::WeightInfo::default_weight()), - )? - .gas_limit; + let gas_limit = get_gas_limit(&tx).ok_or( + Error::::CosmosError(RootError::TxDecodeError.into()) + .with_weight(T::WeightInfo::default_weight()), + )?; let mut ctx = T::Context::new(gas_limit); - Self::run_tx(&mut ctx, &tx).map_err(|e| { Error::::CosmosError(e) .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())) @@ -397,7 +392,7 @@ impl Pallet { }) } - fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { + pub fn run_tx(ctx: &mut T::Context, tx: &Tx) -> Result<(), CosmosError> { let base_gas = T::WeightToGas::convert(T::WeightInfo::default_weight()); ctx.gas_meter() .consume_gas(base_gas, "base gas") diff --git a/frame/cosmos/types/src/lib.rs b/frame/cosmos/types/src/lib.rs index a4b6aa4a..bb812a99 100644 --- a/frame/cosmos/types/src/lib.rs +++ b/frame/cosmos/types/src/lib.rs @@ -28,4 +28,5 @@ pub mod gas; pub mod handler; pub mod macros; pub mod msgservice; +pub mod tx; pub mod tx_msgs; diff --git a/frame/cosmos/types/src/tx.rs b/frame/cosmos/types/src/tx.rs new file mode 100644 index 00000000..599acc25 --- /dev/null +++ b/frame/cosmos/types/src/tx.rs @@ -0,0 +1,26 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::gas::Gas; +use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; + +pub fn get_gas_limit(tx: &Tx) -> Option { + tx.auth_info + .as_ref() + .and_then(|auth_info| auth_info.fee.as_ref()) + .map(|fee| fee.gas_limit) +} diff --git a/frame/cosmos/x/auth/src/sigverify.rs b/frame/cosmos/x/auth/src/sigverify.rs index 69458832..b40215df 100644 --- a/frame/cosmos/x/auth/src/sigverify.rs +++ b/frame/cosmos/x/auth/src/sigverify.rs @@ -15,11 +15,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::vec::Vec; use core::{cmp::Ordering, marker::PhantomData}; use cosmos_sdk_proto::{ cosmos::{ crypto::{multisig::LegacyAminoPubKey, secp256k1}, - tx::v1beta1::{ModeInfo, SignerInfo, Tx}, + tx::v1beta1::{ModeInfo, Tx}, }, prost::Message, Any, @@ -161,8 +162,12 @@ where let mut sig_count = 0u64; let auth_info = tx.auth_info.as_ref().ok_or(RootError::TxDecodeError)?; - for SignerInfo { public_key, .. } in auth_info.signer_infos.iter() { - let public_key = public_key.as_ref().ok_or(RootError::TxDecodeError)?; + let public_keys = auth_info + .signer_infos + .iter() + .filter_map(|signer_info| signer_info.public_key.clone()) + .collect::>(); + for public_key in public_keys.iter() { sig_count = sig_count.saturating_add(Self::count_sub_keys(public_key)?); ensure!(sig_count <= T::TxSigLimit::get(), RootError::TooManySignatures); diff --git a/sidecar/public/config.js b/sidecar/public/config.js index f9e40247..241ffb58 100644 --- a/sidecar/public/config.js +++ b/sidecar/public/config.js @@ -13,5 +13,5 @@ const config = { low: "0.8", tiny: "0.6", }, - sendGas: "400000000" + sendGas: "130000000" }; diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 29652192..0c2d383a 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -21,16 +21,9 @@ export class NoirAccountService implements IAccountService { public async accounts(address: string): Promise { let sequence = '0'; - let key: string | null = null; const originRaw = await this.origin(address); - let origin = originRaw.toString(); - if (origin) { - key = Buffer.from(originRaw.toU8a(true).slice(2)).toString("base64"); - } else { - origin = this.interim(address); - key = Dummy.Secp256k1PublicKey; - } + const origin = originRaw.toString(); const account = await this.chainApi.query["system"]["account"](origin); if (account) { const { nonce } = account.toJSON() as any; @@ -38,10 +31,10 @@ export class NoirAccountService implements IAccountService { } return new AccountResponse({ "@type": "/cosmos.auth.v1beta1.BaseAccount", - address: address, + address, pub_key: { "@type": "/cosmos.crypto.secp256k1.PubKey", - key, + key: Dummy.Secp256k1PublicKey, }, account_number: "0", sequence, diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 4ecb3868..3614d0d4 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -34,7 +34,7 @@ use frame_support::{ weights::Weight, }; use pallet_aura::Authorities; -use pallet_cosmos_types::handler::AnteDecorator; +use pallet_cosmos_types::{context::traits::Context, handler::AnteDecorator, tx::get_gas_limit}; use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use sp_api::impl_runtime_apis; @@ -576,7 +576,10 @@ impl_runtime_apis! { ::AnteHandler::ante_handle(&tx, true) .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; - pallet_cosmos::Pallet::::apply_validated_transaction(tx) + + let gas_limit = get_gas_limit(&tx).ok_or(SimulateError::InternalError("Empty gas limit".into()))?; + let mut context = ::Context::new(gas_limit); + pallet_cosmos::Pallet::::run_tx(&mut context, &tx) .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; System::read_events_no_consensus() From 86751ed349cc07a84f2a424063659d90d2c7cdb1 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 22 Sep 2024 16:56:41 +0900 Subject: [PATCH 065/133] feat: Add FeeTx trait --- frame/cosmos/src/lib.rs | 4 +-- frame/cosmos/types/src/lib.rs | 1 - frame/cosmos/types/src/tx.rs | 26 ------------------- frame/cosmos/types/src/tx_msgs.rs | 16 ++++++++++++ .../x/auth/migrations/src/legacytx/stdsign.rs | 4 +-- .../auth/signing/src/sign_mode_handler/mod.rs | 8 ++---- .../signing/src/sign_verifiable_tx/mod.rs | 11 ++++---- frame/cosmos/x/auth/src/fee.rs | 15 +++-------- sidecar/config/default.json | 2 +- ziggurat/runtime/src/apis.rs | 4 +-- 10 files changed, 34 insertions(+), 57 deletions(-) delete mode 100644 frame/cosmos/types/src/tx.rs diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 7dc301f3..0b9061e5 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -38,7 +38,7 @@ use pallet_cosmos_types::{ gas::traits::GasMeter, handler::AnteDecorator, msgservice::traits::MsgServiceRouter, - tx::get_gas_limit, + tx_msgs::FeeTx, }; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -369,7 +369,7 @@ impl Pallet { } pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { - let gas_limit = get_gas_limit(&tx).ok_or( + let gas_limit = tx.gas_limit().ok_or( Error::::CosmosError(RootError::TxDecodeError.into()) .with_weight(T::WeightInfo::default_weight()), )?; diff --git a/frame/cosmos/types/src/lib.rs b/frame/cosmos/types/src/lib.rs index bb812a99..a4b6aa4a 100644 --- a/frame/cosmos/types/src/lib.rs +++ b/frame/cosmos/types/src/lib.rs @@ -28,5 +28,4 @@ pub mod gas; pub mod handler; pub mod macros; pub mod msgservice; -pub mod tx; pub mod tx_msgs; diff --git a/frame/cosmos/types/src/tx.rs b/frame/cosmos/types/src/tx.rs deleted file mode 100644 index 599acc25..00000000 --- a/frame/cosmos/types/src/tx.rs +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of Noir. - -// Copyright (c) Haderech Pte. Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::gas::Gas; -use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx; - -pub fn get_gas_limit(tx: &Tx) -> Option { - tx.auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .map(|fee| fee.gas_limit) -} diff --git a/frame/cosmos/types/src/tx_msgs.rs b/frame/cosmos/types/src/tx_msgs.rs index 4a8e3430..ed054568 100644 --- a/frame/cosmos/types/src/tx_msgs.rs +++ b/frame/cosmos/types/src/tx_msgs.rs @@ -15,9 +15,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::gas::Gas; use alloc::{string::String, vec::Vec}; +use cosmos_sdk_proto::cosmos::tx::v1beta1::{Fee, Tx}; pub trait Msg { // get_signers returns the addresses of signers that must sign. fn get_signers(self) -> Vec; } + +pub trait FeeTx { + fn fee(&self) -> Option; + fn gas_limit(&self) -> Option; +} + +impl FeeTx for Tx { + fn fee(&self) -> Option { + self.auth_info.as_ref().and_then(|auth_info| auth_info.fee.clone()) + } + fn gas_limit(&self) -> Option { + self.fee().map(|fee| fee.gas_limit) + } +} diff --git a/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs b/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs index cdaad810..281b4d8e 100644 --- a/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs +++ b/frame/cosmos/x/auth/migrations/src/legacytx/stdsign.rs @@ -40,8 +40,8 @@ pub struct StdFee { pub gas: String, } -impl From<&Fee> for StdFee { - fn from(fee: &Fee) -> Self { +impl From for StdFee { + fn from(fee: Fee) -> Self { Self { amount: fee.amount.iter().map(Into::into).collect(), gas: fee.gas_limit.to_string() } } } diff --git a/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs index 9a1c7a96..820fcfb7 100644 --- a/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs +++ b/frame/cosmos/x/auth/signing/src/sign_mode_handler/mod.rs @@ -36,7 +36,7 @@ use cosmos_sdk_proto::{ traits::Message, Any, }; -use pallet_cosmos_types::any_match; +use pallet_cosmos_types::{any_match, tx_msgs::FeeTx}; use pallet_cosmos_x_auth_migrations::legacytx::stdsign::{LegacyMsg, StdSignDoc}; use pallet_cosmos_x_bank_types::msgs::msg_send::MsgSend; use pallet_cosmos_x_wasm_types::tx::{ @@ -107,11 +107,7 @@ impl traits::SignModeHandler for SignModeHandler { msgs.push(legacy_msg); } - let fee = tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(SignModeHandlerError::EmptyFee)?; + let fee = tx.fee().ok_or(SignModeHandlerError::EmptyFee)?; let sign_doc = StdSignDoc { account_number: data.account_number.to_string(), chain_id: data.chain_id.clone(), diff --git a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs index 9aad89d2..c468e758 100644 --- a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs +++ b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs @@ -22,7 +22,10 @@ use cosmos_sdk_proto::{ cosmos::{bank, tx::v1beta1::Tx}, cosmwasm::wasm, }; -use pallet_cosmos_types::{any_match, tx_msgs::Msg}; +use pallet_cosmos_types::{ + any_match, + tx_msgs::{FeeTx, Msg}, +}; use pallet_cosmos_x_bank_types::msgs::msg_send::MsgSend; use pallet_cosmos_x_wasm_types::tx::{ msg_execute_contract::MsgExecuteContract, msg_instantiate_contract2::MsgInstantiateContract2, @@ -80,11 +83,7 @@ impl traits::SigVerifiableTx for SigVerifiableTx { } fn fee_payer(tx: &Tx) -> Result { - let fee = tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(SigVerifiableTxError::EmptyFee)?; + let fee = tx.fee().ok_or(SigVerifiableTxError::EmptyFee)?; let fee_payer = if !fee.payer.is_empty() { fee.payer.clone() diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs index 8ed21499..e9388ffd 100644 --- a/frame/cosmos/x/auth/src/fee.rs +++ b/frame/cosmos/x/auth/src/fee.rs @@ -35,6 +35,7 @@ use pallet_cosmos_types::{ CosmosEvent, EventAttribute, ATTRIBUTE_KEY_FEE, ATTRIBUTE_KEY_FEE_PAYER, EVENT_TYPE_TX, }, handler::AnteDecorator, + tx_msgs::FeeTx, }; use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use sp_core::{Get, H160}; @@ -49,11 +50,7 @@ where T: frame_system::Config + pallet_cosmos::Config, { fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError> { - let fee = tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(RootError::TxDecodeError)?; + let fee = tx.fee().ok_or(RootError::TxDecodeError)?; if !simulate && !frame_system::Pallet::::block_number().is_zero() && fee.gas_limit == 0 { return Err(RootError::InvalidGasLimit.into()); @@ -76,11 +73,7 @@ where fn check_deduct_fee(tx: &Tx) -> Result<(), CosmosError> { let fee_payer = T::SigVerifiableTx::fee_payer(tx).map_err(|_| RootError::TxDecodeError)?; - let fee = tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(RootError::TxDecodeError)?; + let fee = tx.fee().ok_or(RootError::TxDecodeError)?; // Fee granter not supported ensure!(fee.granter.is_empty(), RootError::InvalidRequest); @@ -91,7 +84,7 @@ where let deduct_fees_from = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); if !fee.amount.is_empty() { - Self::deduct_fees(&deduct_fees_from, fee)?; + Self::deduct_fees(&deduct_fees_from, &fee)?; } pallet_cosmos::Pallet::::deposit_event(pallet_cosmos::Event::AnteHandled(vec![ diff --git a/sidecar/config/default.json b/sidecar/config/default.json index 9046de14..a014754b 100644 --- a/sidecar/config/default.json +++ b/sidecar/config/default.json @@ -8,7 +8,7 @@ "logger": false }, "chain": { - "endpoint": "ws://127.0.0.1:49544", + "endpoint": "ws://127.0.0.1:57875", "network": "dev", "moniker": "cosmos", "name": "noir", diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 3614d0d4..1e5dcd8c 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -34,7 +34,7 @@ use frame_support::{ weights::Weight, }; use pallet_aura::Authorities; -use pallet_cosmos_types::{context::traits::Context, handler::AnteDecorator, tx::get_gas_limit}; +use pallet_cosmos_types::{context::traits::Context, handler::AnteDecorator, tx_msgs::FeeTx}; use pallet_ethereum::{Transaction as EthereumTransaction, TransactionStatus}; use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner}; use sp_api::impl_runtime_apis; @@ -577,7 +577,7 @@ impl_runtime_apis! { ::AnteHandler::ante_handle(&tx, true) .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; - let gas_limit = get_gas_limit(&tx).ok_or(SimulateError::InternalError("Empty gas limit".into()))?; + let gas_limit = tx.gas_limit().ok_or(SimulateError::InternalError("Empty gas limit".into()))?; let mut context = ::Context::new(gas_limit); pallet_cosmos::Pallet::::run_tx(&mut context, &tx) .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; From 85468b3b9707d910fd68a8fa3d26b42473f1d3d0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Sun, 22 Sep 2024 17:06:39 +0900 Subject: [PATCH 066/133] refactor: Rename method from gas_limit to gas in FeeTx --- frame/cosmos/src/lib.rs | 2 +- frame/cosmos/types/src/tx_msgs.rs | 4 ++-- ziggurat/runtime/src/apis.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 0b9061e5..b4235ea8 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -369,7 +369,7 @@ impl Pallet { } pub fn apply_validated_transaction(tx: Tx) -> DispatchResultWithPostInfo { - let gas_limit = tx.gas_limit().ok_or( + let gas_limit = tx.gas().ok_or( Error::::CosmosError(RootError::TxDecodeError.into()) .with_weight(T::WeightInfo::default_weight()), )?; diff --git a/frame/cosmos/types/src/tx_msgs.rs b/frame/cosmos/types/src/tx_msgs.rs index ed054568..0ac51b18 100644 --- a/frame/cosmos/types/src/tx_msgs.rs +++ b/frame/cosmos/types/src/tx_msgs.rs @@ -26,14 +26,14 @@ pub trait Msg { pub trait FeeTx { fn fee(&self) -> Option; - fn gas_limit(&self) -> Option; + fn gas(&self) -> Option; } impl FeeTx for Tx { fn fee(&self) -> Option { self.auth_info.as_ref().and_then(|auth_info| auth_info.fee.clone()) } - fn gas_limit(&self) -> Option { + fn gas(&self) -> Option { self.fee().map(|fee| fee.gas_limit) } } diff --git a/ziggurat/runtime/src/apis.rs b/ziggurat/runtime/src/apis.rs index 1e5dcd8c..c4658515 100644 --- a/ziggurat/runtime/src/apis.rs +++ b/ziggurat/runtime/src/apis.rs @@ -577,7 +577,7 @@ impl_runtime_apis! { ::AnteHandler::ante_handle(&tx, true) .map_err(|e| SimulateError::InternalError(format!("Failed to ante handle cosmos tx. error: {:?}", e).into()))?; - let gas_limit = tx.gas_limit().ok_or(SimulateError::InternalError("Empty gas limit".into()))?; + let gas_limit = tx.gas().ok_or(SimulateError::InternalError("Empty gas limit".into()))?; let mut context = ::Context::new(gas_limit); pallet_cosmos::Pallet::::run_tx(&mut context, &tx) .map_err(|e| SimulateError::InternalError(format!("Failed to simulate cosmos tx. error: {:?}", e).into()))?; From ebc52a9de68bc65b5d378913d5458df382f3bfc5 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Sat, 21 Sep 2024 15:46:59 +0900 Subject: [PATCH 067/133] refactor: Replace ethereum-cryptography with polkadot-js --- sidecar/package.json | 7 ++++--- sidecar/src/services/account.ts | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sidecar/package.json b/sidecar/package.json index 57a2f7e2..8f745ce2 100644 --- a/sidecar/package.json +++ b/sidecar/package.json @@ -42,7 +42,7 @@ "ts-jest": "^29.0.4", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", - "typescript": "^4.9.5", + "typescript": "^5.6.2", "typescript-eslint": "^8.6.0" }, "dependencies": { @@ -51,10 +51,11 @@ "@fastify/websocket": "^8.1.0", "@polkadot/api": "^13.1.1", "@polkadot/types": "^13.1.1", + "@polkadot/util": "^13.1.1", + "@polkadot/util-crypto": "^13.1.1", "config": "^3.3.9", "cosmjs-types": "^0.8.0", - "ethereum-cryptography": "^2.0.0", - "fastify": "4.17.0", + "fastify": "^4.28.1", "json-rpc-2.0": "^1.6.0", "lmdb": "^2.8.3", "protobufjs": "^7.2.4" diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 0c2d383a..93490f75 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -2,7 +2,8 @@ import { ApiPromise } from "@polkadot/api"; import { AccountResponse } from "../types"; import { fromBech32 } from "@cosmjs/encoding"; import { Codec } from "@polkadot/types/types/index.js"; -import { blake2b } from "ethereum-cryptography/blake2b.js"; +import { stringToU8a, u8aConcat, u8aToHex } from "@polkadot/util"; +import { blake2AsU8a } from "@polkadot/util-crypto"; import { ApiService } from "./service"; import Dummy from "../constants/dummy"; @@ -50,9 +51,7 @@ export class NoirAccountService implements IAccountService { public interim(address: string): string { const { data } = fromBech32(address); - const addressRaw = Buffer.from( - blake2b(Buffer.concat([Buffer.from("cosm:", "utf8"), data]), 32), - ); - return `0x${addressRaw.toString("hex")}`; + const addressRaw = blake2AsU8a(u8aConcat(stringToU8a("cosm:"), data)); + return u8aToHex(addressRaw); } } From 819fdbcab3a8bb92419fa3334622cd761bd70112 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 11:05:49 +0900 Subject: [PATCH 068/133] chore: Remove eslint fix --- sidecar/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sidecar/package.json b/sidecar/package.json index 8f745ce2..32ab4b71 100644 --- a/sidecar/package.json +++ b/sidecar/package.json @@ -8,8 +8,7 @@ "lint": "npx eslint ./src", "test": "jest", "dev": "nodemon --exec ts-node ./src/main.ts", - "start": "node --es-module-specifier-resolution=node ./dist/main.js", - "fmt": "eslint ./src --ext .ts --fix" + "start": "node --es-module-specifier-resolution=node ./dist/main.js" }, "keywords": [], "author": "code0xff ", @@ -60,4 +59,4 @@ "lmdb": "^2.8.3", "protobufjs": "^7.2.4" } -} +} \ No newline at end of file From 6b57a66f778c98f23218833f57d72fd15489e94f Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 13:52:22 +0900 Subject: [PATCH 069/133] fix: Add codespace convert --- sidecar/src/services/tx.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 09be7caf..6d3a59b1 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -148,13 +148,10 @@ export class TxService implements ApiService { const [{ module: { index, error } }, info] = JSON.parse(data); const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); - const codespace = errors[1]; - const code = errors[2]; - const weight = info.weight.refTime; // TODO: codespace and gasUsed will be transformed proper values - return { codespace: 'sdk', code, gasUsed: weight, events: [] }; + return { codespace: codespace(errors[1]), code: errors[2], gasUsed: weight, events: [] }; } }); return result[0]; @@ -212,3 +209,11 @@ export class TxService implements ApiService { }); } } + +const codespace = (codespace: number): string => { + switch (codespace) { + case 0: return 'sdk'; + case 1: return 'wasm'; + default: return 'unknown'; + } +} \ No newline at end of file From d50e576e245d28aed05dbb38260d913f191d412f Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 15:16:55 +0900 Subject: [PATCH 070/133] fix: Add blockhash parameter to simulate rpc --- frame/cosmos/rpc/src/cosmos.rs | 17 ++++++---- sidecar/src/constants/codespace.ts | 7 ++++ sidecar/src/constants/rpc.ts | 4 +++ sidecar/src/services/abci.ts | 51 ++++++++++++++++++++---------- sidecar/src/services/tx.ts | 18 +++-------- 5 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 sidecar/src/constants/codespace.ts diff --git a/frame/cosmos/rpc/src/cosmos.rs b/frame/cosmos/rpc/src/cosmos.rs index e176cb93..31537e8c 100644 --- a/frame/cosmos/rpc/src/cosmos.rs +++ b/frame/cosmos/rpc/src/cosmos.rs @@ -31,12 +31,12 @@ use std::sync::Arc; #[rpc(client, server)] #[async_trait] -pub trait CosmosApi { +pub trait CosmosApi { #[method(name = "cosmos_broadcastTx")] async fn broadcast_tx(&self, tx_bytes: Bytes) -> RpcResult; #[method(name = "cosmos_simulate")] - async fn simulate(&self, tx_bytes: Bytes) -> RpcResult; + async fn simulate(&self, tx_bytes: Bytes, at: Option) -> RpcResult; } pub struct Cosmos { @@ -51,7 +51,7 @@ impl Cosmos { } #[async_trait] -impl CosmosApiServer for Cosmos +impl CosmosApiServer<::Hash> for Cosmos where Block: BlockT, C: Send + Sync + 'static, @@ -75,12 +75,15 @@ where .await } - async fn simulate(&self, tx_bytes: Bytes) -> RpcResult { - let best_hash = self.client.info().best_hash; - + async fn simulate( + &self, + tx_bytes: Bytes, + at: Option<::Hash>, + ) -> RpcResult { + let at = at.unwrap_or(self.client.info().best_hash); self.client .runtime_api() - .simulate(best_hash, tx_bytes.to_vec()) + .simulate(at, tx_bytes.to_vec()) .map_err(internal_error)? .map_err(|e| match e { SimulateError::InvalidTx => request_error("Invalid tx"), diff --git a/sidecar/src/constants/codespace.ts b/sidecar/src/constants/codespace.ts new file mode 100644 index 00000000..85befa64 --- /dev/null +++ b/sidecar/src/constants/codespace.ts @@ -0,0 +1,7 @@ +export const convertToCodespace = (codespace: number): string => { + switch (codespace) { + case 0: return 'sdk'; + case 1: return 'wasm'; + default: return 'unknown'; + } +} \ No newline at end of file diff --git a/sidecar/src/constants/rpc.ts b/sidecar/src/constants/rpc.ts index deb60f24..727972e2 100644 --- a/sidecar/src/constants/rpc.ts +++ b/sidecar/src/constants/rpc.ts @@ -17,6 +17,10 @@ const rpc = { name: 'tx_bytes', type: 'Bytes', }, + { + name: 'at', + type: 'Option', + }, ], type: 'SimulateResponse', }, diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index e233743a..a7f8f486 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -12,6 +12,7 @@ import { ABCIQueryResponse } from "cosmjs-types/cosmos/base/tendermint/v1beta1/q import { SimulateRequest, SimulateResponse } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; import { TxService } from "./tx"; import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'cosmjs-types/cosmwasm/wasm/v1/query.js' +import { convertToCodespace } from "../constants/codespace"; export class AbciService implements ApiService { chainApi: ApiPromise; @@ -65,23 +66,39 @@ export class AbciService implements ApiService { codespace: '', }; } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { - // TODO: Check simulate tx fields - const request = SimulateRequest.decode(Buffer.from(data, 'hex')); - const response = SimulateResponse.encode(await this.txService.simulate(Buffer.from(request.txBytes).toString('base64'))).finish(); - // TODO: Get actual height - const height = (await this.chainApi.query.system.number()).toString(); - - return { - code: 0, - log: '', - info: '', - index: Long.ZERO, - key: undefined, - value: response, - proofOps: undefined, - height: Long.fromString(height), - codespace: "", - }; + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + try { + const req = SimulateRequest.decode(Buffer.from(data, 'hex')); + const res = await this.txService.simulate(Buffer.from(req.txBytes).toString('base64'), blockHash.toString()); + const resBytes = SimulateResponse.encode(res).finish(); + return { + code: 0, + log: '', + info: '', + index: Long.ZERO, + key: undefined, + value: resBytes, + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: '', + }; + } catch (e: any) { + const message = e.toString(); + const codespace = message.slice(message.indexOf('codespace:') + 'codespace:'.length, message.indexOf('code:')).trim(); + const code = message.slice(message.indexOf('code:') + 'code:'.length, message.indexOf('}')).trim(); + return { + code: parseInt(code), + log: message, + info: '', + index: Long.ZERO, + key: undefined, + value: new Uint8Array(), + proofOps: undefined, + height: Long.fromString(height.toString()), + codespace: convertToCodespace(parseInt(codespace)), + }; + } } else if (path === '/cosmwasm.wasm.v1.Query/SmartContractState') { const { address, queryData } = QuerySmartContractStateRequest.decode(Uint8Array.from(Buffer.from(data, 'hex'))); const gas = 10000000000; diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 6d3a59b1..beb0209e 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -9,6 +9,7 @@ import { import Long from "long"; import { createHash } from "crypto"; import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; +import { convertToCodespace } from "../constants/codespace"; type TransactResult = { codespace: string, code: number; gasUsed: number, events: any[] }; @@ -150,8 +151,7 @@ export class TxService implements ApiService { const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); const weight = info.weight.refTime; - // TODO: codespace and gasUsed will be transformed proper values - return { codespace: codespace(errors[1]), code: errors[2], gasUsed: weight, events: [] }; + return { codespace: convertToCodespace(errors[1]), code: errors[2], gasUsed: weight, events: [] }; } }); return result[0]; @@ -164,11 +164,9 @@ export class TxService implements ApiService { return Buffer.from(str, from).toString(to); } - public async simulate(txBytes: string): Promise { + public async simulate(txBytes: string, blockHash?: string): Promise { const txRaw = `0x${this.convert(txBytes, 'base64', 'hex')}`; - - const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw)).toJSON(); - + const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); console.debug(`gasInfo: ${JSON.stringify(gas_info)}`); @@ -209,11 +207,3 @@ export class TxService implements ApiService { }); } } - -const codespace = (codespace: number): string => { - switch (codespace) { - case 0: return 'sdk'; - case 1: return 'wasm'; - default: return 'unknown'; - } -} \ No newline at end of file From 34ede29eb98310aa690b3ff5b2d49b1433877b72 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 15:52:44 +0900 Subject: [PATCH 071/133] fix: Add blockHash parameter to account query --- sidecar/src/services/abci.ts | 9 ++++++--- sidecar/src/services/account.ts | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index a7f8f486..2c7815ad 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -32,7 +32,11 @@ export class AbciService implements ApiService { const address = QueryAccountRequest.decode( Buffer.from(data, 'hex') ).address; - const { account } = await this.accountService.accounts(address); + + const height = await this.chainApi.query.system.number(); + const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); + + const { account } = await this.accountService.accounts(address, blockHash.toString()); const pubkey: PubKey = { key: Buffer.from(account.pub_key.key, 'base64'), }; @@ -53,7 +57,6 @@ export class AbciService implements ApiService { }, }; const value = QueryAccountResponse.encode(queryAccountResponse).finish(); - const height = (await this.chainApi.query.system.number()).toString(); return { code: 0, log: '', @@ -62,7 +65,7 @@ export class AbciService implements ApiService { key: undefined, value, proofOps: undefined, - height: Long.fromString(height), + height: Long.fromString(height.toString()), codespace: '', }; } else if (path === '/cosmos.tx.v1beta1.Service/Simulate') { diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 93490f75..ada5abca 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -8,7 +8,7 @@ import { ApiService } from "./service"; import Dummy from "../constants/dummy"; export interface IAccountService extends ApiService { - accounts(address: string): Promise; + accounts(address: string, blockHash: string): Promise; origin(address: string): Promise; interim(address: string): string; } @@ -20,12 +20,12 @@ export class NoirAccountService implements IAccountService { this.chainApi = chainApi; } - public async accounts(address: string): Promise { + public async accounts(address: string, blockHash?: string): Promise { let sequence = '0'; const originRaw = await this.origin(address); const origin = originRaw.toString(); - const account = await this.chainApi.query["system"]["account"](origin); + const account = await (await (blockHash ? this.chainApi : this.chainApi.at(blockHash))).query["system"]["account"](origin); if (account) { const { nonce } = account.toJSON() as any; sequence = nonce.toString(); From 12e11043dffc3b46015b37207d974b8d284bd27d Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 16:19:24 +0900 Subject: [PATCH 072/133] chore: Set RPC port for collator to 9944 --- sidecar/config/default.json | 2 +- ziggurat/zombienet.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sidecar/config/default.json b/sidecar/config/default.json index a014754b..6c147880 100644 --- a/sidecar/config/default.json +++ b/sidecar/config/default.json @@ -8,7 +8,7 @@ "logger": false }, "chain": { - "endpoint": "ws://127.0.0.1:57875", + "endpoint": "ws://127.0.0.1:9944", "network": "dev", "moniker": "cosmos", "name": "noir", diff --git a/ziggurat/zombienet.toml b/ziggurat/zombienet.toml index 016624a7..fb002bdc 100644 --- a/ziggurat/zombienet.toml +++ b/ziggurat/zombienet.toml @@ -15,3 +15,4 @@ default_command = "./target/release/ziggurat" [[parachains.collators]] name = "collator-01" +rpc_port=9944 From 4b3f21edf12b5c800e2905c3460c50b158c8f9c6 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 16:38:48 +0900 Subject: [PATCH 073/133] fix: Allow blockHash parameter to be nullable in accounts method --- sidecar/src/services/account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index ada5abca..edd95bae 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -8,7 +8,7 @@ import { ApiService } from "./service"; import Dummy from "../constants/dummy"; export interface IAccountService extends ApiService { - accounts(address: string, blockHash: string): Promise; + accounts(address: string, blockHash?: string): Promise; origin(address: string): Promise; interim(address: string): string; } From ac54aa5953f28fd9dd9f6a55fd5f53a5bec565c5 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 19:03:53 +0900 Subject: [PATCH 074/133] refactor: Resolve eslint warnings --- sidecar/.eslintrc | 4 +- sidecar/eslint.config.mjs | 7 -- sidecar/package.json | 7 +- sidecar/src/app.ts | 104 ++++++++++++++------------- sidecar/src/services/abci.ts | 19 ++--- sidecar/src/services/account.ts | 31 ++++---- sidecar/src/services/balance.ts | 17 ++--- sidecar/src/services/distribution.ts | 1 + sidecar/src/services/nodeinfo.ts | 24 +++---- sidecar/src/services/service.ts | 6 +- sidecar/src/services/staking.ts | 2 + sidecar/src/services/status.ts | 29 ++++---- sidecar/src/services/tx.ts | 81 +++++++++------------ sidecar/src/types/account.ts | 4 +- sidecar/src/utils/index.ts | 12 ++-- sidecar/tsconfig.json | 6 +- 16 files changed, 174 insertions(+), 180 deletions(-) delete mode 100644 sidecar/eslint.config.mjs diff --git a/sidecar/.eslintrc b/sidecar/.eslintrc index 60f52edf..98c2fbc1 100644 --- a/sidecar/.eslintrc +++ b/sidecar/.eslintrc @@ -8,8 +8,6 @@ "sourceType": "module" }, "rules": { - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off" + "@typescript-eslint/no-empty-interface": "off" } } \ No newline at end of file diff --git a/sidecar/eslint.config.mjs b/sidecar/eslint.config.mjs deleted file mode 100644 index 5793d16d..00000000 --- a/sidecar/eslint.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, -); diff --git a/sidecar/package.json b/sidecar/package.json index 32ab4b71..80f6c75b 100644 --- a/sidecar/package.json +++ b/sidecar/package.json @@ -28,12 +28,11 @@ "devDependencies": { "@eslint/js": "^9.10.0", "@types/config": "^3.3.0", - "@types/eslint__js": "^8.42.3", "@types/jest": "^29.2.5", "@types/node": "^18.11.11", "@types/ws": "^8.5.5", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", + "@typescript-eslint/eslint-plugin": "^8.6.0", + "@typescript-eslint/parser": "^8.6.0", "eslint": "^8.57.1", "jest": "^29.3.1", "nodemon": "^3.1.6", @@ -59,4 +58,4 @@ "lmdb": "^2.8.3", "protobufjs": "^7.2.4" } -} \ No newline at end of file +} diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index b136bd5d..62ec02ee 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -13,8 +13,7 @@ import { StakingService, StatusService, TxService, - IAccountService, - NoirAccountService, + AccountService, } from "./services"; import { AccountResponse, @@ -53,6 +52,7 @@ import { BroadcastTxResponse, SimulateResponse, } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; +import { Header } from "@polkadot/types/interfaces"; export class App { config: IConfig; @@ -85,7 +85,7 @@ export class App { } async initDatabase() { - const path = this.config.get("db.path"); + const path = this.config.get('db.path'); console.debug(`Start to initialize database. path: ${path}`); this.db = open({ @@ -127,7 +127,7 @@ export class App { console.error(`Failed to connect with chain RPC`); } - const accountService = new NoirAccountService(this.chainApi); + const accountService = new AccountService(this.chainApi); const txService = new TxService(this.db, this.chainApi); const abciService = new AbciService(this.chainApi, accountService, txService); const balanceService = new BalanceService( @@ -140,31 +140,31 @@ export class App { const stakingService = new StakingService(); const statusService = new StatusService(this.config, this.chainApi); - this.services.set("abci", abciService); - this.services.set("account", accountService); - this.services.set("balance", balanceService); - this.services.set("distribution", distributionService); - this.services.set("nodeInfo", nodeInfoService); - this.services.set("staking", stakingService); - this.services.set("status", statusService); - this.services.set("tx", txService); + this.services.set('abci', abciService); + this.services.set('account', accountService); + this.services.set('balance', balanceService); + this.services.set('distribution', distributionService); + this.services.set('nodeInfo', nodeInfoService); + this.services.set('staking', stakingService); + this.services.set('status', statusService); + this.services.set('tx', txService); } async initApiServer() { - const logger = this.config.get("server.logger"); + const logger = this.config.get('server.logger'); this.server = fastify({ logger }); const __dirname = path.resolve(); this.server.register(fastifyStatic, { - root: path.join(__dirname, "public"), + root: path.join(__dirname, 'public'), }); await this.server.register(FastifyWebsocket); - this.server.get("/", (request: FastifyRequest, reply: FastifyReply) => { - reply.sendFile("index.html"); + this.server.get('/', (_request: FastifyRequest, reply: FastifyReply) => { + reply.sendFile('index.html'); }); this.server.get( - "/cosmos/bank/v1beta1/balances/:address", + '/cosmos/bank/v1beta1/balances/:address', async ( request: FastifyRequest<{ Params: QueryAllBalancesRequest; @@ -172,14 +172,14 @@ export class App { ): Promise => { const { address } = request.params; const response = QueryAllBalancesResponse.toJSON( - await this.services.get("balance").balances(address) + await this.services.get('balance').balances(address) ); return toSnakeCase(response); } ); this.server.get( - "/cosmos/auth/v1beta1/accounts/:address", + '/cosmos/auth/v1beta1/accounts/:address', async ( request: FastifyRequest<{ Params: QueryAccountRequest; @@ -187,23 +187,23 @@ export class App { ): Promise => { const { address } = request.params; return await this.services - .get("account") + .get('account') .accounts(address); } ); this.server.get( - "/cosmos/base/tendermint/v1beta1/node_info", + '/cosmos/base/tendermint/v1beta1/node_info', (): GetNodeInfoResponse => { const response = GetNodeInfoResponse.toJSON( - this.services.get("nodeInfo").nodeInfo() + this.services.get('nodeInfo').nodeInfo() ); return toSnakeCase(response); } ); this.server.post( - "/cosmos/tx/v1beta1/txs", + '/cosmos/tx/v1beta1/txs', async ( request: FastifyRequest<{ Body: { @@ -214,14 +214,14 @@ export class App { ): Promise => { const { tx_bytes } = request.body; const response = BroadcastTxResponse.toJSON( - await this.services.get("tx").broadcastTx(tx_bytes) + await this.services.get('tx').broadcastTx(tx_bytes) ); return toSnakeCase(response); } ); this.server.get( - "/cosmos/staking/v1beta1/delegations/:delegatorAddr", + '/cosmos/staking/v1beta1/delegations/:delegatorAddr', ( request: FastifyRequest<{ Params: QueryDelegatorDelegationsRequest; @@ -230,7 +230,7 @@ export class App { const { delegatorAddr } = request.params; const response = QueryDelegatorDelegationsResponse.toJSON( this.services - .get("staking") + .get('staking') .delegations(delegatorAddr) ); return toSnakeCase(response); @@ -238,7 +238,7 @@ export class App { ); this.server.get( - "/cosmos/distribution/v1beta1/delegators/:delegatorAddress/rewards", + '/cosmos/distribution/v1beta1/delegators/:delegatorAddress/rewards', ( request: FastifyRequest<{ Params: QueryDelegationTotalRewardsRequest; @@ -247,7 +247,7 @@ export class App { const { delegatorAddress } = request.params; const response = QueryDelegationTotalRewardsResponse.toJSON( this.services - .get("distribution") + .get('distribution') .rewards(delegatorAddress) ); return toSnakeCase(response); @@ -255,7 +255,7 @@ export class App { ); this.server.get( - "/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations", + '/cosmos/staking/v1beta1/delegators/:delegatorAddr/unbonding_delegations', ( request: FastifyRequest<{ Params: QueryDelegatorUnbondingDelegationsRequest; @@ -264,7 +264,7 @@ export class App { const { delegatorAddr } = request.params; const response = QueryDelegatorUnbondingDelegationsResponse.toJSON( this.services - .get("staking") + .get('staking') .unbondingDelegations(delegatorAddr) ); return toSnakeCase(response); @@ -272,7 +272,7 @@ export class App { ); this.server.post( - "/cosmos/tx/v1beta1/simulate", + '/cosmos/tx/v1beta1/simulate', async ( request: FastifyRequest<{ Body: { tx_bytes: string }; @@ -280,7 +280,7 @@ export class App { ): Promise => { const { tx_bytes } = request.body; const response = SimulateResponse.toJSON( - await this.services.get("tx").simulate(tx_bytes) + await this.services.get('tx').simulate(tx_bytes) ); return toSnakeCase(response); } @@ -290,17 +290,17 @@ export class App { async initJsonRpcServer() { this.jsonrpc = new JSONRPCServer(); - this.jsonrpc.addMethod("status", async (): Promise => { + this.jsonrpc.addMethod('status', async (): Promise => { return toSnakeCase( - await this.services.get("status").status() + await this.services.get('status').status() ); }); this.jsonrpc.addMethod( - "abci_query", + 'abci_query', async ({ path, data }): Promise => { const result = await this.services - .get("abci") + .get('abci') .query(path, data); const response = ABCIQueryResponse.toJSON(result); return { @@ -310,9 +310,9 @@ export class App { ); this.jsonrpc.addMethod( - "broadcast_tx_sync", + 'broadcast_tx_sync', async ({ tx }): Promise => { - const result = await this.services.get("tx").broadcastTx(tx); + const result = await this.services.get('tx').broadcastTx(tx); const { code, txhash, data, rawLog, codespace } = result.txResponse; return { code, @@ -324,16 +324,16 @@ export class App { } ); - this.jsonrpc.addMethod("tx_search", ({ query }): ResultTxSearch => { + this.jsonrpc.addMethod('tx_search', ({ query }): ResultTxSearch => { const args = querystring.parse(query); - let hash = args["tx.hash"] as string; + let hash = args['tx.hash'] as string; if (hash.includes("'")) { - hash = hash.replace(/'/gi, ""); + hash = hash.replace(/'/gi, ''); } - const txs = this.services.get("tx").searchTx(hash); + const txs = this.services.get('tx').searchTx(hash); txs.forEach(tx => { tx.tx_result.events.forEach(event => { - event.attributes = this.services.get("tx").encodeAttributes(event.attributes, 'utf8', 'base64'); + event.attributes = this.services.get('tx').encodeAttributes(event.attributes, 'utf8', 'base64'); }); }); @@ -344,15 +344,15 @@ export class App { }); this.server.get( - "/websocket", + '/websocket', { websocket: true }, (connection: SocketStream) => { - connection.socket.on("message", async (message) => { + connection.socket.on('message', async (message) => { const request = JSON.parse(message.toString()); const response = await this.jsonrpc.receive(request); if (response) { connection.socket.send( - Buffer.from(JSON.stringify(response), "utf8") + Buffer.from(JSON.stringify(response), 'utf8') ); } }); @@ -362,16 +362,18 @@ export class App { async initSubscribeEvents() { await this.chainApi.rpc.chain.subscribeNewHeads( - async (header) => { + async (header: Header) => { const signedBlock = await this.chainApi.rpc.chain.getBlock(header.hash); signedBlock.block.extrinsics.forEach( async ({ method: { args, method, section } }, index) => { - if (section === "cosmos" && method === "transact") { - const txRaw = args.toString().split(',')[0]; + if (section === 'cosmos' && method === 'transact') { + const txBytes = args.toString().split(',')[0]; + console.debug(`txBytes: ${txBytes}`); + await this.services - .get("tx") - .saveTransactResult(txRaw, index, header); + .get('tx') + .saveTransactResult(txBytes, index, header); } } ); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index 2c7815ad..fc9bd46a 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -3,7 +3,7 @@ import { QueryAccountResponse, } from "cosmjs-types/cosmos/auth/v1beta1/query.js"; import { ApiService } from "./service"; -import { IAccountService } from "./account"; +import { AccountService } from "./account"; import { PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { BaseAccount } from "cosmjs-types/cosmos/auth/v1beta1/auth.js"; import Long from "long"; @@ -13,13 +13,14 @@ import { SimulateRequest, SimulateResponse } from "cosmjs-types/cosmos/tx/v1beta import { TxService } from "./tx"; import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'cosmjs-types/cosmwasm/wasm/v1/query.js' import { convertToCodespace } from "../constants/codespace"; +import { encodeTo } from "../utils"; export class AbciService implements ApiService { chainApi: ApiPromise; - accountService: IAccountService; + accountService: AccountService; txService: TxService; - constructor(chainApi: ApiPromise, accountService: IAccountService, txService: TxService) { + constructor(chainApi: ApiPromise, accountService: AccountService, txService: TxService) { this.chainApi = chainApi; this.accountService = accountService; this.txService = txService; @@ -86,7 +87,7 @@ export class AbciService implements ApiService { height: Long.fromString(height.toString()), codespace: '', }; - } catch (e: any) { + } catch (e: unknown) { const message = e.toString(); const codespace = message.slice(message.indexOf('codespace:') + 'codespace:'.length, message.indexOf('code:')).trim(); const code = message.slice(message.indexOf('code:') + 'code:'.length, message.indexOf('}')).trim(); @@ -116,22 +117,22 @@ export class AbciService implements ApiService { const height = await this.chainApi.query.system.number(); const blockHash = await this.chainApi.rpc.chain.getBlockHash(height.toString()); - const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${Buffer.from(JSON.stringify(msg), 'utf8').toString('hex')}`, blockHash.toString()); + const response = await this.chainApi.rpc['cosmwasm']['query'](address, gas, `0x${encodeTo(JSON.stringify(msg), 'utf8', 'hex')}`, blockHash.toString()); const stateResponse = QuerySmartContractStateResponse.fromPartial({ data: Uint8Array.from(Buffer.from(response, 'hex')) }); return { code: 0, - log: "", - info: "", + log: '', + info: '', index: Long.ZERO, key: undefined, value: QuerySmartContractStateResponse.encode(stateResponse).finish(), proofOps: undefined, height: Long.fromString(height.toString()), - codespace: "", + codespace: '', }; } else { - throw new Error("unexpected path"); + throw new Error('Unexpected path'); } } } diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index edd95bae..d54617b1 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -6,14 +6,9 @@ import { stringToU8a, u8aConcat, u8aToHex } from "@polkadot/util"; import { blake2AsU8a } from "@polkadot/util-crypto"; import { ApiService } from "./service"; import Dummy from "../constants/dummy"; +import { AccountInfo } from "@polkadot/types/interfaces"; -export interface IAccountService extends ApiService { - accounts(address: string, blockHash?: string): Promise; - origin(address: string): Promise; - interim(address: string): string; -} - -export class NoirAccountService implements IAccountService { +export class AccountService implements ApiService { chainApi: ApiPromise; constructor(chainApi: ApiPromise) { @@ -21,28 +16,32 @@ export class NoirAccountService implements IAccountService { } public async accounts(address: string, blockHash?: string): Promise { + console.debug(`accounts(${address}, ${blockHash})`); + let sequence = '0'; + let origin = (await this.origin(address)).toString(); + if (!origin) { + origin = this.interim(address); + } + const account = await (await (blockHash ? this.chainApi.at(blockHash) : this.chainApi)).query['system']['account'](origin); - const originRaw = await this.origin(address); - const origin = originRaw.toString(); - const account = await (await (blockHash ? this.chainApi : this.chainApi.at(blockHash))).query["system"]["account"](origin); if (account) { - const { nonce } = account.toJSON() as any; + const { nonce } = account.toJSON() as unknown as AccountInfo; sequence = nonce.toString(); } return new AccountResponse({ - "@type": "/cosmos.auth.v1beta1.BaseAccount", + '@type': '/cosmos.auth.v1beta1.BaseAccount', address, pub_key: { - "@type": "/cosmos.crypto.secp256k1.PubKey", + '@type': '/cosmos.crypto.secp256k1.PubKey', key: Dummy.Secp256k1PublicKey, }, - account_number: "0", + account_number: '0', sequence, }); } - public async origin(address: string): Promise { + public async origin(address: string): Promise { const { data } = fromBech32(address); return this.chainApi.query['addressMap']['index']( Buffer.concat([Buffer.from([0x00]), data]) @@ -51,7 +50,7 @@ export class NoirAccountService implements IAccountService { public interim(address: string): string { const { data } = fromBech32(address); - const addressRaw = blake2AsU8a(u8aConcat(stringToU8a("cosm:"), data)); + const addressRaw = blake2AsU8a(u8aConcat(stringToU8a('cosm:'), data)); return u8aToHex(addressRaw); } } diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 56c35927..c71e6c89 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -1,19 +1,20 @@ import { ApiPromise } from "@polkadot/api"; -import { IAccountService } from "./account"; +import { AccountService } from "./account"; import { ApiService } from "./service"; import { IConfig } from "config"; import { QueryAllBalancesResponse } from "cosmjs-types/cosmos/bank/v1beta1/query.js"; import Long from "long"; +import { AccountInfo } from "@polkadot/types/interfaces"; export class BalanceService implements ApiService { config: IConfig; chainApi: ApiPromise; - accountService: IAccountService; + accountService: AccountService; constructor( config: IConfig, chainApi: ApiPromise, - accountService: IAccountService + accountService: AccountService ) { this.config = config; this.chainApi = chainApi; @@ -21,6 +22,8 @@ export class BalanceService implements ApiService { } public async balances(address: string): Promise { + console.debug(`balance(${address})`); + const originRaw = await this.accountService.origin(address); let amount = '0'; let origin = originRaw.toString(); @@ -29,12 +32,10 @@ export class BalanceService implements ApiService { } const account = await this.chainApi.query.system.account(origin); if (account) { - const { data } = account.toJSON() as any; - const { free } = data; - amount = BigInt(free).toString(); + const { data } = account.toJSON() as unknown as AccountInfo; + amount = data.free.toString(); } - const denom = this.config.get("chain.denom"); - + const denom = this.config.get('chain.denom'); const nativeBalance = { denom, amount }; const assets = []; diff --git a/sidecar/src/services/distribution.ts b/sidecar/src/services/distribution.ts index 4c992c98..370efb45 100644 --- a/sidecar/src/services/distribution.ts +++ b/sidecar/src/services/distribution.ts @@ -2,6 +2,7 @@ import { QueryDelegationTotalRewardsResponse } from "cosmjs-types/cosmos/distrib import { ApiService } from "./service"; export class DistributionService implements ApiService { + /* eslint-disable @typescript-eslint/no-unused-vars */ public rewards( delegatorAddress: string ): QueryDelegationTotalRewardsResponse { diff --git a/sidecar/src/services/nodeinfo.ts b/sidecar/src/services/nodeinfo.ts index 99921438..cea9030b 100644 --- a/sidecar/src/services/nodeinfo.ts +++ b/sidecar/src/services/nodeinfo.ts @@ -11,11 +11,11 @@ export class NodeInfoService implements ApiService { } public nodeInfo(): GetNodeInfoResponse { - const endpoint = this.config.get("server.endpoint"); - const network = this.config.get("chain.network"); - const version = this.config.get("chain.version"); - const moniker = this.config.get("chain.moniker"); - const name = this.config.get("chain.name"); + const endpoint = this.config.get('server.endpoint'); + const network = this.config.get('chain.network'); + const version = this.config.get('chain.version'); + const moniker = this.config.get('chain.moniker'); + const name = this.config.get('chain.name'); return { defaultNodeInfo: { protocolVersion: { @@ -23,26 +23,26 @@ export class NodeInfoService implements ApiService { block: Long.ZERO, app: Long.ZERO, }, - defaultNodeId: "0000000000000000000000000000000000000000", + defaultNodeId: '0000000000000000000000000000000000000000', listenAddr: endpoint, network, version, channels: new Uint8Array(Buffer.allocUnsafe(8)), moniker, other: { - txIndex: "off", - rpcAddress: "", + txIndex: 'off', + rpcAddress: '', }, }, applicationVersion: { name, appName: name, version, - gitCommit: "0000000000000000000000000000000000000000", - buildTags: "", - goVersion: "0", + gitCommit: '0000000000000000000000000000000000000000', + buildTags: '', + goVersion: '0', buildDeps: [], - cosmosSdkVersion: "0", + cosmosSdkVersion: '0', }, }; } diff --git a/sidecar/src/services/service.ts b/sidecar/src/services/service.ts index 7fdf956b..f1bd6673 100644 --- a/sidecar/src/services/service.ts +++ b/sidecar/src/services/service.ts @@ -1,4 +1,5 @@ -export interface ApiService { } +export interface ApiService { +} export class ApiServices { services: Map; @@ -12,6 +13,9 @@ export class ApiServices { } public set(name: string, service: ApiService) { + if (this.get(name)) { + throw new Error(`Already reserved service name: ${name}`); + } this.services.set(name, service); } } diff --git a/sidecar/src/services/staking.ts b/sidecar/src/services/staking.ts index bed35d37..7022457f 100644 --- a/sidecar/src/services/staking.ts +++ b/sidecar/src/services/staking.ts @@ -6,6 +6,7 @@ import { } from "cosmjs-types/cosmos/staking/v1beta1/query.js"; export class StakingService implements ApiService { + /* eslint-disable @typescript-eslint/no-unused-vars */ public delegations(delegatorAddr: string): QueryDelegatorDelegationsResponse { return { delegationResponses: [], @@ -16,6 +17,7 @@ export class StakingService implements ApiService { }; } + /* eslint-disable @typescript-eslint/no-unused-vars */ public unbondingDelegations( delegatorAddr: string ): QueryDelegatorUnbondingDelegationsResponse { diff --git a/sidecar/src/services/status.ts b/sidecar/src/services/status.ts index 23837620..01d81ae6 100644 --- a/sidecar/src/services/status.ts +++ b/sidecar/src/services/status.ts @@ -3,6 +3,7 @@ import { ResultStatus } from "../types"; import { ApiService } from "./service"; import { IConfig } from "config"; import Long from "long"; +import { Block } from "@polkadot/types/interfaces"; export class StatusService implements ApiService { config: IConfig; @@ -16,17 +17,17 @@ export class StatusService implements ApiService { public async status(): Promise { const hash = (await this.chainApi.rpc.chain.getFinalizedHead()).toString(); const { block } = (await this.chainApi.rpc.chain.getBlock(hash)).toJSON(); - const blockNumber = (block as any).header.number; + const blockNumber = (block as unknown as Block).header.number.toString(); const timestamp = ( await (await this.chainApi.at(hash)).query.timestamp.now() ).toString(); const blockTime = new Date(parseInt(timestamp)).toISOString(); - const blockHash = hash.startsWith("0x") ? hash.slice(2) : hash; + const blockHash = hash.startsWith('0x') ? hash.slice(2) : hash; - const endpoint = this.config.get("server.endpoint"); - const network = this.config.get("chain.network"); - const version = this.config.get("chain.version"); - const moniker = this.config.get("chain.moniker"); + const endpoint = this.config.get('server.endpoint'); + const network = this.config.get('chain.network'); + const version = this.config.get('chain.version'); + const moniker = this.config.get('chain.moniker'); return { node_info: { protocol_version: { @@ -34,32 +35,32 @@ export class StatusService implements ApiService { block: Long.ZERO.toString(), app: Long.ZERO.toString(), }, - id: "0000000000000000000000000000000000000000", + id: '0000000000000000000000000000000000000000', listen_addr: endpoint, network, version, - channels: "0000000000000000", + channels: '0000000000000000', moniker, other: { - tx_index: "off", + tx_index: 'off', rpc_address: endpoint, }, }, sync_info: { latest_block_hash: blockHash.toUpperCase(), latest_app_hash: - "0000000000000000000000000000000000000000000000000000000000000000", + '0000000000000000000000000000000000000000000000000000000000000000', latest_block_height: blockNumber, latest_block_time: blockTime, catching_up: false, }, validator_info: { - address: "0000000000000000000000000000000000000000", + address: '0000000000000000000000000000000000000000', pub_key: { - type: "tendermint/PubKeyEd25519", - value: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + type: 'tendermint/PubKeyEd25519', + value: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', }, - voting_power: "0", + voting_power: '0', }, }; } diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index beb0209e..5c06f6f0 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -8,10 +8,12 @@ import { } from "cosmjs-types/cosmos/tx/v1beta1/service.js"; import Long from "long"; import { createHash } from "crypto"; -import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; import { convertToCodespace } from "../constants/codespace"; +import { encodeTo } from "../utils"; +import { Event as CosmosEvent } from "cosmjs-types/tendermint/abci/types"; +import { EventRecord, Header } from "@polkadot/types/interfaces"; -type TransactResult = { codespace: string, code: number; gasUsed: number, events: any[] }; +type TransactResult = { codespace: string, code: number, gasWanted: number, gasUsed: number, events: CosmosEvent[] }; export class TxService implements ApiService { chainApi: ApiPromise; @@ -23,24 +25,18 @@ export class TxService implements ApiService { } public async broadcastTx(txBytes: string): Promise { - console.debug(`txBytes: ${txBytes}`); + console.debug(`broadcastTx(${txBytes})`); - const rawTx = `0x${Buffer.from(txBytes, 'base64').toString('hex')}`; - - let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](rawTx)).toString(); + let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](`0x${encodeTo(txBytes, 'base64', 'hex')}`)).toString(); txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; - - console.debug(`txHash: ${txHash.toLowerCase()}`); + console.debug(`txHash: ${txHash}`); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); while (true) { const txs = this.searchTx(txHash); - // console.debug(`txs: ${JSON.stringify(txs)}`); - if (txs.length > 0) { const tx = txs.at(0); - return { txResponse: { height: Long.fromString(tx.height), @@ -63,18 +59,16 @@ export class TxService implements ApiService { }; } else { console.debug('Waiting for events...'); - await sleep(1000); } } } public searchTx(hash: string): ResultTx[] { - if (hash.startsWith('0x')) { - hash = hash.slice(2); - } + console.debug('searchTx'); - console.debug(`txHash: ${hash.toLowerCase()}`); + hash = hash.startsWith('0x') ? hash.slice(2) : hash; + console.debug(`txHash: ${hash}`); const resultTx = this.db.get(`tx::result::${hash.toLowerCase()}`); const txs: ResultTx[] = []; @@ -85,18 +79,19 @@ export class TxService implements ApiService { } public async saveTransactResult( - txRaw: string, + txBytes: string, extrinsicIndex: number, - header: any + header: Header ): Promise { - txRaw = txRaw.startsWith('0x') ? txRaw.slice(2) : txRaw; - const txBytes = Buffer.from(txRaw, 'hex'); - const gasLimit = Tx.decode(txBytes).authInfo!.fee!.gasLimit; + console.debug(`saveTransactResult(${txBytes}, ${extrinsicIndex}, ${header})`); + + txBytes = txBytes.startsWith('0x') ? txBytes.slice(2) : txBytes; - const txHash = createHash('sha256').update(Buffer.from(txRaw, 'hex')).digest('hex'); + const txHash = createHash('sha256').update(Buffer.from(txBytes, 'hex')).digest('hex'); + console.debug(`txHash: ${txHash}`) - const { codespace, code, gasUsed, events } = await this.checkResult(header, extrinsicIndex); - const txResult: ResultTx = { + const { codespace, code, gasWanted, gasUsed, events } = await this.checkResult(header, extrinsicIndex); + const result: ResultTx = { hash: `${txHash.toUpperCase()}`, height: header.number.toString(), index: extrinsicIndex, @@ -105,23 +100,24 @@ export class TxService implements ApiService { data: '', log: '', info: '', - gas_wanted: gasLimit.toString(), + gas_wanted: gasWanted.toString(), gas_used: gasUsed.toString(), events, codespace, }, - tx: txBytes.toString('base64'), + tx: encodeTo(txBytes, 'hex', 'base64'), }; - await this.db.put(`tx::result::${txHash.toLowerCase()}`, txResult); + await this.db.put(`tx::result::${txHash.toLowerCase()}`, result); } async checkResult( - header: any, + header: Header, extrinsicIndex: number ): Promise { const events = (await ( await this.chainApi.at(header.hash) - ).query.system.events()) as any; + ).query.system.events()).toJSON() as unknown as EventRecord[]; + const result = events .filter(({ event: { section, method }, phase }) => { const { applyExtrinsic } = JSON.parse(phase.toString()); @@ -133,7 +129,7 @@ export class TxService implements ApiService { }) .map(({ event: { data, section, method } }) => { if (`${section}::${method}` === 'cosmos::Executed') { - const [gas_wanted, gas_used, events] = JSON.parse(data); + const [gas_wanted, gas_used, events] = JSON.parse(data.toString()); console.debug(`gasWanted: ${gas_wanted}`); console.debug(`gasUsed: ${gas_used}`); @@ -143,29 +139,22 @@ export class TxService implements ApiService { console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) - return { codespace: '', code: 0, gasUsed: gas_used, events: cosmosEvents }; + return { codespace: '', code: 0, gasWanted: gas_wanted, gasUsed: gas_used, events: cosmosEvents }; } else { - console.debug(JSON.parse(data)); - const [{ module: { index, error } }, info] = JSON.parse(data); - + const [{ module: { error } }, info] = JSON.parse(data.toString()); const errors = Uint8Array.from(Buffer.from(error.startsWith('0x') ? error.slice(2) : error, 'hex')); const weight = info.weight.refTime; - return { codespace: convertToCodespace(errors[1]), code: errors[2], gasUsed: weight, events: [] }; + return { codespace: convertToCodespace(errors[1]), code: errors[2], gasWanted: 0, gasUsed: weight, events: [] }; } }); return result[0]; } - convert(str: string, from: BufferEncoding, to: BufferEncoding): string { - if (from === 'hex') { - str = str.startsWith('0x') ? str.slice(2) : str; - } - return Buffer.from(str, from).toString(to); - } - public async simulate(txBytes: string, blockHash?: string): Promise { - const txRaw = `0x${this.convert(txBytes, 'base64', 'hex')}`; + console.debug(`simulate(${txBytes}, ${blockHash})`); + + const txRaw = `0x${encodeTo(txBytes, 'base64', 'hex')}`; const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); @@ -189,7 +178,7 @@ export class TxService implements ApiService { public encodeEvents(events, from: BufferEncoding, to: BufferEncoding) { return events.map((event) => { return { - type: this.convert(event.type ? event.type : event['r#type'], from, to), + type: encodeTo(event.type ? event.type : event['r#type'], from, to), attributes: this.encodeAttributes(event.attributes, from, to), } }); @@ -197,8 +186,8 @@ export class TxService implements ApiService { public encodeAttributes(attributes, from: BufferEncoding, to: BufferEncoding) { return attributes.map(({ key, value }) => { - const eventKey = this.convert(key, from, to); - const eventValue = this.convert(value, from, to); + const eventKey = encodeTo(key, from, to); + const eventValue = encodeTo(value, from, to); return { key: eventKey, diff --git a/sidecar/src/types/account.ts b/sidecar/src/types/account.ts index 13c0dac3..cd1541a8 100644 --- a/sidecar/src/types/account.ts +++ b/sidecar/src/types/account.ts @@ -7,7 +7,7 @@ export class AccountResponse { } export interface BaseAccount { - "@type": string; + '@type': string; address: string; pub_key: PublicKey | null; account_number: string; @@ -15,6 +15,6 @@ export interface BaseAccount { } export interface PublicKey { - "@type": string; + '@type': string; key: string; } diff --git a/sidecar/src/utils/index.ts b/sidecar/src/utils/index.ts index a5963d88..dc27b635 100644 --- a/sidecar/src/utils/index.ts +++ b/sidecar/src/utils/index.ts @@ -1,5 +1,5 @@ export function toSnakeCase(input: unknown): unknown { - if (typeof input !== "object" || input === null) { + if (typeof input !== 'object' || input === null) { return input; } @@ -9,14 +9,18 @@ export function toSnakeCase(input: unknown): unknown { const result: unknown = {}; for (const key in input) { - if (input.hasOwnProperty(key)) { + if (Object.hasOwn(input, key)) { const snakeCaseKey = key - .replace(/([A-Z])/g, "_$1") + .replace(/([A-Z])/g, '_$1') .toLowerCase() - .replace(/^_/, ""); + .replace(/^_/, ''); result[snakeCaseKey] = toSnakeCase(input[key]); } } return result; } + +export const encodeTo = (value: string, from: BufferEncoding, to: BufferEncoding): string => { + return Buffer.from(from === 'hex' && value.startsWith('0x') ? value.slice(2) : value, from).toString(to); +} \ No newline at end of file diff --git a/sidecar/tsconfig.json b/sidecar/tsconfig.json index 0e62310b..b0df5395 100644 --- a/sidecar/tsconfig.json +++ b/sidecar/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "module": "CommonJS", "rootDir": "./src", "moduleResolution": "node", @@ -12,7 +12,7 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "declaration": true, - "noImplicitReturns": true + "noImplicitReturns": true, }, "exclude": [ "node_modules", @@ -20,6 +20,6 @@ ], "include": [ "src/*", - "src/**/*" + "src/**/*" ] } \ No newline at end of file From acbed6f1dc6d84678ce58d5bc4d68d0b5adbb760 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 20:09:09 +0900 Subject: [PATCH 075/133] fix: Fix phase json parse error --- sidecar/src/services/tx.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 5c06f6f0..0baa6214 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -11,9 +11,9 @@ import { createHash } from "crypto"; import { convertToCodespace } from "../constants/codespace"; import { encodeTo } from "../utils"; import { Event as CosmosEvent } from "cosmjs-types/tendermint/abci/types"; -import { EventRecord, Header } from "@polkadot/types/interfaces"; +import { Header } from "@polkadot/types/interfaces"; -type TransactResult = { codespace: string, code: number, gasWanted: number, gasUsed: number, events: CosmosEvent[] }; +export type TransactResult = { codespace: string, code: number, gasWanted: number, gasUsed: number, events: CosmosEvent[] }; export class TxService implements ApiService { chainApi: ApiPromise; @@ -114,9 +114,10 @@ export class TxService implements ApiService { header: Header, extrinsicIndex: number ): Promise { + /* eslint-disable @typescript-eslint/no-explicit-any */ const events = (await ( await this.chainApi.at(header.hash) - ).query.system.events()).toJSON() as unknown as EventRecord[]; + ).query.system.events()) as any; const result = events .filter(({ event: { section, method }, phase }) => { @@ -138,7 +139,6 @@ export class TxService implements ApiService { const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) - return { codespace: '', code: 0, gasWanted: gas_wanted, gasUsed: gas_used, events: cosmosEvents }; } else { const [{ module: { error } }, info] = JSON.parse(data.toString()); @@ -148,6 +148,7 @@ export class TxService implements ApiService { return { codespace: convertToCodespace(errors[1]), code: errors[2], gasWanted: 0, gasUsed: weight, events: [] }; } }); + return result[0]; } From 1e2951b165ddb13469a48896f55fd243a4f85398 Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 22:19:03 +0900 Subject: [PATCH 076/133] chore: Modify sidecar log simple --- sidecar/src/app.ts | 2 -- sidecar/src/services/abci.ts | 2 +- sidecar/src/services/account.ts | 2 +- sidecar/src/services/balance.ts | 2 +- sidecar/src/services/tx.ts | 10 ++++------ 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 62ec02ee..91750e4c 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -369,8 +369,6 @@ export class App { async ({ method: { args, method, section } }, index) => { if (section === 'cosmos' && method === 'transact') { const txBytes = args.toString().split(',')[0]; - console.debug(`txBytes: ${txBytes}`); - await this.services .get('tx') .saveTransactResult(txBytes, index, header); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index fc9bd46a..f03efcb6 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -27,7 +27,7 @@ export class AbciService implements ApiService { } async query(path: string, data: string): Promise { - console.debug(`query(${path}, ${data})`); + console.debug(`query`); if (path === '/cosmos.auth.v1beta1.Query/Account') { const address = QueryAccountRequest.decode( diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index d54617b1..4a8895df 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -16,7 +16,7 @@ export class AccountService implements ApiService { } public async accounts(address: string, blockHash?: string): Promise { - console.debug(`accounts(${address}, ${blockHash})`); + console.debug(`accounts`); let sequence = '0'; let origin = (await this.origin(address)).toString(); diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index c71e6c89..0b7beee5 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -22,7 +22,7 @@ export class BalanceService implements ApiService { } public async balances(address: string): Promise { - console.debug(`balance(${address})`); + console.debug(`balance`); const originRaw = await this.accountService.origin(address); let amount = '0'; diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index 0baa6214..c2d7f7d8 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -25,7 +25,7 @@ export class TxService implements ApiService { } public async broadcastTx(txBytes: string): Promise { - console.debug(`broadcastTx(${txBytes})`); + console.debug(`broadcastTx`); let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](`0x${encodeTo(txBytes, 'base64', 'hex')}`)).toString(); txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; @@ -83,7 +83,7 @@ export class TxService implements ApiService { extrinsicIndex: number, header: Header ): Promise { - console.debug(`saveTransactResult(${txBytes}, ${extrinsicIndex}, ${header})`); + console.debug(`saveTransactResult`); txBytes = txBytes.startsWith('0x') ? txBytes.slice(2) : txBytes; @@ -131,14 +131,12 @@ export class TxService implements ApiService { .map(({ event: { data, section, method } }) => { if (`${section}::${method}` === 'cosmos::Executed') { const [gas_wanted, gas_used, events] = JSON.parse(data.toString()); - console.debug(`gasWanted: ${gas_wanted}`); console.debug(`gasUsed: ${gas_used}`); - console.debug(`events: ${JSON.stringify(events)}`); const cosmosEvents = this.encodeEvents(events, 'hex', 'utf8'); - console.debug(`cosmosEvents: ${JSON.stringify(cosmosEvents)}`) + return { codespace: '', code: 0, gasWanted: gas_wanted, gasUsed: gas_used, events: cosmosEvents }; } else { const [{ module: { error } }, info] = JSON.parse(data.toString()); @@ -153,7 +151,7 @@ export class TxService implements ApiService { } public async simulate(txBytes: string, blockHash?: string): Promise { - console.debug(`simulate(${txBytes}, ${blockHash})`); + console.debug(`simulate`); const txRaw = `0x${encodeTo(txBytes, 'base64', 'hex')}`; const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); From 1fffe16327b991ea6d53c3d843295cd302acf55c Mon Sep 17 00:00:00 2001 From: code0xff Date: Mon, 23 Sep 2024 23:24:30 +0900 Subject: [PATCH 077/133] feat: Add gas consumption to wasm message handlers --- frame/cosmos/rpc/src/cosmos.rs | 3 ++- frame/cosmos/x/wasm/Cargo.toml | 2 ++ frame/cosmos/x/wasm/src/msgs.rs | 37 ++++++++++++++++++++++++++------- ziggurat/zombienet.toml | 2 +- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/frame/cosmos/rpc/src/cosmos.rs b/frame/cosmos/rpc/src/cosmos.rs index 31537e8c..18f144eb 100644 --- a/frame/cosmos/rpc/src/cosmos.rs +++ b/frame/cosmos/rpc/src/cosmos.rs @@ -36,7 +36,8 @@ pub trait CosmosApi { async fn broadcast_tx(&self, tx_bytes: Bytes) -> RpcResult; #[method(name = "cosmos_simulate")] - async fn simulate(&self, tx_bytes: Bytes, at: Option) -> RpcResult; + async fn simulate(&self, tx_bytes: Bytes, at: Option) + -> RpcResult; } pub struct Cosmos { diff --git a/frame/cosmos/x/wasm/Cargo.toml b/frame/cosmos/x/wasm/Cargo.toml index 33851d8f..4df16b4d 100644 --- a/frame/cosmos/x/wasm/Cargo.toml +++ b/frame/cosmos/x/wasm/Cargo.toml @@ -15,6 +15,7 @@ cosmos-sdk-proto = { version = "0.24", default-features = false, features = [ frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } hex = { version = "0.4.3", default-features = false } libflate = { version = "2.1.0", default-features = false } +log = { version = "0.4.21", default-features = false } pallet-cosmos = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } pallet-cosmos-x-wasm-types = { workspace = true, default-features = false } @@ -30,6 +31,7 @@ std = [ "frame-support/std", "hex/std", "libflate/std", + "log/std", "pallet-cosmos/std", "pallet-cosmos-types/std", "pallet-cosmos-x-wasm-types/std", diff --git a/frame/cosmos/x/wasm/src/msgs.rs b/frame/cosmos/x/wasm/src/msgs.rs index 42580fe0..6d58d46b 100644 --- a/frame/cosmos/x/wasm/src/msgs.rs +++ b/frame/cosmos/x/wasm/src/msgs.rs @@ -122,7 +122,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgInstantiateContract2 { sender, admin, code_id, label, msg, funds, salt, fix_msg: _ } = MsgInstantiateContract2::decode(&mut &*msg.value) .map_err(|_| RootError::TxDecodeError)?; @@ -164,7 +163,14 @@ where funds, message, ) - .map_err(|_| WasmError::InstantiateFailed)?; + .map_err(|e| { + log::debug!("{:?}", e); + WasmError::InstantiateFailed + })?; + ctx.gas_meter() + .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") + .map_err(|_| RootError::OutOfGas)?; + let contract = T::AccountToAddr::convert(contract); // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm @@ -198,7 +204,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgExecuteContract { sender, contract, msg, funds } = MsgExecuteContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -226,7 +231,13 @@ where funds, message, ) - .map_err(|_| WasmError::ExecuteFailed)?; + .map_err(|e| { + log::debug!("{:?}", e); + WasmError::ExecuteFailed + })?; + ctx.gas_meter() + .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") + .map_err(|_| RootError::OutOfGas)?; // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm let msg_event = CosmosEvent { @@ -271,7 +282,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgMigrateContract { sender, contract, code_id, msg } = MsgMigrateContract::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -299,7 +309,13 @@ where new_code_identifier, message, ) - .map_err(|_| WasmError::MigrationFailed)?; + .map_err(|e| { + log::debug!("{:?}", e); + WasmError::MigrationFailed + })?; + ctx.gas_meter() + .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") + .map_err(|_| RootError::OutOfGas)?; // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm let msg_event = CosmosEvent { @@ -332,7 +348,6 @@ where Context: context::traits::Context, { fn handle(&self, ctx: &mut Context, msg: &Any) -> Result<(), CosmosError> { - // TODO: Add gas metering let MsgUpdateAdmin { sender, new_admin, contract } = MsgUpdateAdmin::decode(&mut &*msg.value).map_err(|_| RootError::TxDecodeError)?; @@ -364,7 +379,13 @@ where contract_account, new_admin_account, ) - .map_err(|_| WasmError::MigrationFailed)?; + .map_err(|e| { + log::debug!("{:?}", e); + RootError::Unauthorized + })?; + ctx.gas_meter() + .consume_gas(gas.saturating_sub(shared.gas.remaining()), "") + .map_err(|_| RootError::OutOfGas)?; // TODO: Same events emitted pallet_cosmos and pallet_cosmwasm let msg_event = CosmosEvent { diff --git a/ziggurat/zombienet.toml b/ziggurat/zombienet.toml index fb002bdc..d95ab124 100644 --- a/ziggurat/zombienet.toml +++ b/ziggurat/zombienet.toml @@ -15,4 +15,4 @@ default_command = "./target/release/ziggurat" [[parachains.collators]] name = "collator-01" -rpc_port=9944 +rpc_port = 9944 From 9dcd9ee80059297d3b2e70fbef5f977e7744b1a4 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 24 Sep 2024 10:49:06 +0900 Subject: [PATCH 078/133] style: Replace backticks with single quotes in sidecar log --- sidecar/src/app.ts | 4 ++-- sidecar/src/services/abci.ts | 2 +- sidecar/src/services/account.ts | 2 +- sidecar/src/services/balance.ts | 2 +- sidecar/src/services/tx.ts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sidecar/src/app.ts b/sidecar/src/app.ts index 91750e4c..cfc216f8 100644 --- a/sidecar/src/app.ts +++ b/sidecar/src/app.ts @@ -122,9 +122,9 @@ export class App { rpc }); if (this.chainApi.isConnected) { - console.debug(`Chain RPC connected`); + console.debug('Chain RPC connected'); } else { - console.error(`Failed to connect with chain RPC`); + console.error('Failed to connect with chain RPC'); } const accountService = new AccountService(this.chainApi); diff --git a/sidecar/src/services/abci.ts b/sidecar/src/services/abci.ts index f03efcb6..3f5de677 100644 --- a/sidecar/src/services/abci.ts +++ b/sidecar/src/services/abci.ts @@ -27,7 +27,7 @@ export class AbciService implements ApiService { } async query(path: string, data: string): Promise { - console.debug(`query`); + console.debug('query'); if (path === '/cosmos.auth.v1beta1.Query/Account') { const address = QueryAccountRequest.decode( diff --git a/sidecar/src/services/account.ts b/sidecar/src/services/account.ts index 4a8895df..b4119dfe 100644 --- a/sidecar/src/services/account.ts +++ b/sidecar/src/services/account.ts @@ -16,7 +16,7 @@ export class AccountService implements ApiService { } public async accounts(address: string, blockHash?: string): Promise { - console.debug(`accounts`); + console.debug('accounts'); let sequence = '0'; let origin = (await this.origin(address)).toString(); diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 0b7beee5..78f6961f 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -22,7 +22,7 @@ export class BalanceService implements ApiService { } public async balances(address: string): Promise { - console.debug(`balance`); + console.debug('balances'); const originRaw = await this.accountService.origin(address); let amount = '0'; diff --git a/sidecar/src/services/tx.ts b/sidecar/src/services/tx.ts index c2d7f7d8..f37b13fd 100644 --- a/sidecar/src/services/tx.ts +++ b/sidecar/src/services/tx.ts @@ -25,7 +25,7 @@ export class TxService implements ApiService { } public async broadcastTx(txBytes: string): Promise { - console.debug(`broadcastTx`); + console.debug('broadcastTx'); let txHash = (await this.chainApi.rpc['cosmos']['broadcastTx'](`0x${encodeTo(txBytes, 'base64', 'hex')}`)).toString(); txHash = txHash.startsWith('0x') ? txHash.slice(2) : txHash; @@ -83,7 +83,7 @@ export class TxService implements ApiService { extrinsicIndex: number, header: Header ): Promise { - console.debug(`saveTransactResult`); + console.debug('saveTransactResult'); txBytes = txBytes.startsWith('0x') ? txBytes.slice(2) : txBytes; @@ -151,7 +151,7 @@ export class TxService implements ApiService { } public async simulate(txBytes: string, blockHash?: string): Promise { - console.debug(`simulate`); + console.debug('simulate'); const txRaw = `0x${encodeTo(txBytes, 'base64', 'hex')}`; const { gas_info, events } = (await this.chainApi.rpc['cosmos']['simulate'](txRaw, blockHash)).toJSON(); From 889d15769b102088e43066152f3ea8b5a14da747 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 24 Sep 2024 11:28:17 +0900 Subject: [PATCH 079/133] fix: Fix balances bug --- sidecar/src/services/balance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidecar/src/services/balance.ts b/sidecar/src/services/balance.ts index 78f6961f..d556e933 100644 --- a/sidecar/src/services/balance.ts +++ b/sidecar/src/services/balance.ts @@ -33,7 +33,7 @@ export class BalanceService implements ApiService { const account = await this.chainApi.query.system.account(origin); if (account) { const { data } = account.toJSON() as unknown as AccountInfo; - amount = data.free.toString(); + amount = BigInt(data.free.toString()).toString(); } const denom = this.config.get('chain.denom'); const nativeBalance = { denom, amount }; From ace037176c07b62f84f26258973041a246b49d19 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 23 Sep 2024 11:43:51 +0900 Subject: [PATCH 080/133] build(deps): Add missing dependency --- ziggurat/runtime/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index cef8ec2f..3c3f840a 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -93,6 +93,7 @@ pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } pallet-cosmwasm = { workspace = true, default-features = false } [build-dependencies] +docify = "0.2" substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", optional = true } [features] From 71bc4ee7ccb917fc4b40d9e3acfc2cb31d69c463 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 23 Sep 2024 20:43:32 +0900 Subject: [PATCH 081/133] chore: Allocate initial balance to dev account --- ziggurat/node/src/chain_spec.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs index 8396b0c6..77df3ae5 100644 --- a/ziggurat/node/src/chain_spec.rs +++ b/ziggurat/node/src/chain_spec.rs @@ -3,7 +3,7 @@ use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT, UNIT}; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; use serde::{Deserialize, Serialize}; -use sp_core::{ecdsa, sr25519, Pair, Public}; +use sp_core::{crypto::Ss58Codec, ecdsa, sr25519, Pair, Public}; use sp_runtime::traits::{IdentifyAccount, Verify}; use ziggurat_runtime as runtime; @@ -106,6 +106,7 @@ pub fn development_config() -> ChainSpec { get_account_id_from_seed::("Eve//stash"), get_account_id_from_seed::("Ferdie//stash"), get_account_id_from_seed::("Alice"), + AccountId::from_string("5HSVzaktLBoAFZ89RQHEWrJizmSuXrbpGm7favc99p5qt6UM").unwrap(), ], get_account_id_from_seed::("Alice"), 2000.into(), @@ -158,6 +159,7 @@ pub fn local_testnet_config() -> ChainSpec { get_account_id_from_seed::("Eve//stash"), get_account_id_from_seed::("Ferdie//stash"), get_account_id_from_seed::("Alice"), + AccountId::from_string("5HSVzaktLBoAFZ89RQHEWrJizmSuXrbpGm7favc99p5qt6UM").unwrap(), ], get_account_id_from_seed::("Alice"), 2000.into(), From a4171b40d1ea4cb1f27e75c9a9b290531433789d Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 24 Sep 2024 16:47:57 +0900 Subject: [PATCH 082/133] feat: Add UniqueMap --- frame/cosmos/src/lib.rs | 1 + frame/multimap/src/traits.rs | 49 ++++++++++++++++++++++++++++++++++++ ziggurat/runtime/src/lib.rs | 2 ++ 3 files changed, 52 insertions(+) diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index b4235ea8..3d069b48 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -16,6 +16,7 @@ // limitations under the License. #![cfg_attr(not(feature = "std"), no_std)] +#![allow(unreachable_patterns)] extern crate alloc; diff --git a/frame/multimap/src/traits.rs b/frame/multimap/src/traits.rs index a115c1ea..89ac9be2 100644 --- a/frame/multimap/src/traits.rs +++ b/frame/multimap/src/traits.rs @@ -82,3 +82,52 @@ impl, I: 'static> UniqueMultimap for Pallet }) } } + +/// Unique map whose the value is unique across all keys. +pub trait UniqueMap { + type Error; + + /// Tries to insert a value into the map. + fn try_insert(key: K, value: V) -> Result; + + /// Gets the value for a key. + fn get(key: K) -> Option; + + /// Finds the key for a value. + fn find_key(value: V) -> Option; + + /// Removes a value from the map. + fn remove(key: K); +} + +impl, I: 'static> UniqueMap for Pallet { + type Error = Error; + + fn try_insert(key: T::Key, value: T::Value) -> Result> { + Map::::try_mutate(&key, |values| { + ensure!( + Index::::get(&value).filter(|k| *k != key).is_none(), + Error::::DuplicateValue + ); + + *values = BTreeSet::from([value.clone()]) + .try_into() + .map_err(|_| Error::::CapacityOverflow)?; + Index::::insert(&value, &key); + + Ok(true) + }) + } + + fn get(key: T::Key) -> Option { + Map::::get(&key).first().cloned() + } + + fn find_key(value: T::Value) -> Option { + Index::::get(&value) + } + + fn remove(key: T::Key) { + Map::::remove(&key); + } +} diff --git a/ziggurat/runtime/src/lib.rs b/ziggurat/runtime/src/lib.rs index f6c782bf..02b5a7a6 100644 --- a/ziggurat/runtime/src/lib.rs +++ b/ziggurat/runtime/src/lib.rs @@ -315,6 +315,8 @@ mod runtime { #[runtime::pallet_index(128)] pub type AddressMap = pallet_multimap; + #[runtime::pallet_index(129)] + pub type AssetMap = pallet_multimap; #[runtime::pallet_index(179)] pub type Cosmos = pallet_cosmos; #[runtime::pallet_index(180)] From 444b3464b0583519fd6e6b9fec4f7484946b149f Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 24 Sep 2024 22:29:46 +0900 Subject: [PATCH 083/133] feat: Implement AssetToDenom with AssetMap --- frame/babel/Cargo.toml | 2 +- .../src/{cosmos.rs => cosmos/address.rs} | 79 ++----------------- frame/babel/src/cosmos/asset.rs | 58 ++++++++++++++ frame/babel/src/cosmos/mod.rs | 21 +++++ frame/babel/src/cosmos/msg.rs | 55 +++++++++++++ frame/cosmos/Cargo.toml | 2 + frame/cosmos/src/lib.rs | 11 --- frame/cosmos/src/types.rs | 1 + ziggurat/runtime/src/configs/mod.rs | 28 +++++-- 9 files changed, 164 insertions(+), 93 deletions(-) rename frame/babel/src/{cosmos.rs => cosmos/address.rs} (54%) create mode 100644 frame/babel/src/cosmos/asset.rs create mode 100644 frame/babel/src/cosmos/mod.rs create mode 100644 frame/babel/src/cosmos/msg.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 08325042..5b0de28b 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -28,7 +28,7 @@ pallet-evm-precompile-bn128 = { git = "https://github.com/polkadot-evm/frontier" pallet-evm-precompile-modexp = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-sha3fips = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-simple = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } -pallet-multimap = { workspace = true } +pallet-multimap = { workspace = true, default-features = false } parity-scale-codec = { version = "3.6", default-features = false, features = [ "derive", ] } diff --git a/frame/babel/src/cosmos.rs b/frame/babel/src/cosmos/address.rs similarity index 54% rename from frame/babel/src/cosmos.rs rename to frame/babel/src/cosmos/address.rs index dc355971..16317760 100644 --- a/frame/babel/src/cosmos.rs +++ b/frame/babel/src/cosmos/address.rs @@ -17,32 +17,13 @@ // along with this program. If not, see . use crate::{extensions::unify_account, Address}; -use alloc::{ - boxed::Box, - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use bech32::{Bech32, Hrp}; use core::marker::PhantomData; -use cosmos_sdk_proto::{ - cosmos::bank::v1beta1::MsgSend, - cosmwasm::wasm::v1::{ - MsgExecuteContract, MsgInstantiateContract2, MsgMigrateContract, MsgStoreCode, - MsgUpdateAdmin, - }, - Any, -}; -use np_cosmos::traits::ChainInfo; -pub use np_cosmos::Address as CosmosAddress; -use pallet_cosmos::types::DenomOf; -use pallet_cosmos_types::{address::acc_address_from_bech32, any_match, context, msgservice}; -use pallet_cosmos_x_bank::MsgSendHandler; -use pallet_cosmos_x_wasm::msgs::{ - MsgExecuteContractHandler, MsgInstantiateContract2Handler, MsgMigrateContractHandler, - MsgStoreCodeHandler, MsgUpdateAdminHandler, -}; +use np_cosmos::{traits::ChainInfo, Address as CosmosAddress}; +use pallet_cosmos_types::address::acc_address_from_bech32; use pallet_multimap::traits::UniqueMultimap; -use sp_core::{Get, H160, H256}; +use sp_core::{H160, H256}; use sp_runtime::traits::{AccountIdConversion, Convert}; pub struct AddressMapping(PhantomData); @@ -57,56 +38,6 @@ where } } -pub struct AssetToDenom(PhantomData); -impl Convert> for AssetToDenom -where - T: pallet_cosmos::Config, -{ - fn convert(denom: String) -> Result { - if denom == T::NativeDenom::get() { - Ok(T::NativeAssetId::get()) - } else { - let denom: DenomOf = denom.as_bytes().to_vec().try_into().map_err(|_| ())?; - pallet_cosmos::DenomAssetRouter::::get(denom).ok_or(()) - } - } -} -impl Convert for AssetToDenom -where - T: pallet_cosmos::Config, -{ - fn convert(asset_id: T::AssetId) -> String { - if asset_id == T::NativeAssetId::get() { - T::NativeDenom::get().to_string() - } else { - // TODO: Handle option - let denom = pallet_cosmos::AssetDenomRouter::::get(asset_id).unwrap(); - String::from_utf8(denom.into()).unwrap() - } - } -} - -pub struct MsgServiceRouter(PhantomData); -impl msgservice::traits::MsgServiceRouter for MsgServiceRouter -where - T: frame_system::Config + pallet_cosmos::Config + pallet_cosmwasm::Config, - Context: context::traits::Context, -{ - fn route(msg: &Any) -> Option>> { - any_match!( - msg, { - MsgSend => Some(Box::>::default()), - MsgStoreCode => Some(Box::>::default()), - MsgInstantiateContract2 => Some(Box::>::default()), - MsgExecuteContract => Some(Box::>::default()), - MsgMigrateContract => Some(Box::>::default()), - MsgUpdateAdmin => Some(Box::>::default()), - }, - None - ) - } -} - pub struct AccountToAddr(PhantomData); impl Convert for AccountToAddr where @@ -126,6 +57,7 @@ where bech32::encode::(hrp, address_raw).unwrap() } } + impl Convert> for AccountToAddr where T: pallet_cosmwasm::Config + unify_account::Config, @@ -135,6 +67,7 @@ where Self::convert(address_raw) } } + impl Convert, Result> for AccountToAddr where T: pallet_cosmwasm::Config + unify_account::Config, diff --git a/frame/babel/src/cosmos/asset.rs b/frame/babel/src/cosmos/asset.rs new file mode 100644 index 00000000..1b840540 --- /dev/null +++ b/frame/babel/src/cosmos/asset.rs @@ -0,0 +1,58 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::string::{String, ToString}; +use core::marker::PhantomData; +use pallet_cosmos::types::{AssetIdOf, DenomOf}; +use pallet_multimap::traits::UniqueMap; +use sp_core::Get; +use sp_runtime::traits::Convert; + +pub struct AssetToDenom(PhantomData<(T, I)>); +impl Convert, ()>> for AssetToDenom +where + T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, +{ + fn convert(denom: String) -> Result, ()> { + if denom == T::NativeDenom::get() { + Ok(T::NativeAssetId::get()) + } else { + let denom: DenomOf = denom.as_bytes().to_vec().try_into().map_err(|_| ())?; + pallet_multimap::Pallet::::find_key(denom).ok_or(()) + } + } +} + +impl Convert, String> for AssetToDenom +where + T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, +{ + fn convert(asset_id: AssetIdOf) -> String { + if asset_id == T::NativeAssetId::get() { + T::NativeDenom::get().to_string() + } else { + // TODO: Handle option + let denom = + as UniqueMap, DenomOf>>::get( + asset_id, + ) + .unwrap(); + String::from_utf8(denom.into()).unwrap() + } + } +} diff --git a/frame/babel/src/cosmos/mod.rs b/frame/babel/src/cosmos/mod.rs new file mode 100644 index 00000000..9916eba6 --- /dev/null +++ b/frame/babel/src/cosmos/mod.rs @@ -0,0 +1,21 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub mod address; +pub mod asset; +pub mod msg; diff --git a/frame/babel/src/cosmos/msg.rs b/frame/babel/src/cosmos/msg.rs new file mode 100644 index 00000000..cfef2826 --- /dev/null +++ b/frame/babel/src/cosmos/msg.rs @@ -0,0 +1,55 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::boxed::Box; +use core::marker::PhantomData; +use cosmos_sdk_proto::{ + cosmos::bank::v1beta1::MsgSend, + cosmwasm::wasm::v1::{ + MsgExecuteContract, MsgInstantiateContract2, MsgMigrateContract, MsgStoreCode, + MsgUpdateAdmin, + }, + Any, +}; +use pallet_cosmos_types::{any_match, context, msgservice}; +use pallet_cosmos_x_bank::MsgSendHandler; +use pallet_cosmos_x_wasm::msgs::{ + MsgExecuteContractHandler, MsgInstantiateContract2Handler, MsgMigrateContractHandler, + MsgStoreCodeHandler, MsgUpdateAdminHandler, +}; + +pub struct MsgServiceRouter(PhantomData); +impl msgservice::traits::MsgServiceRouter for MsgServiceRouter +where + T: frame_system::Config + pallet_cosmos::Config + pallet_cosmwasm::Config, + Context: context::traits::Context, +{ + fn route(msg: &Any) -> Option>> { + any_match!( + msg, { + MsgSend => Some(Box::>::default()), + MsgStoreCode => Some(Box::>::default()), + MsgInstantiateContract2 => Some(Box::>::default()), + MsgExecuteContract => Some(Box::>::default()), + MsgMigrateContract => Some(Box::>::default()), + MsgUpdateAdmin => Some(Box::>::default()), + }, + None + ) + } +} diff --git a/frame/cosmos/Cargo.toml b/frame/cosmos/Cargo.toml index 738f8b97..b726eaaf 100644 --- a/frame/cosmos/Cargo.toml +++ b/frame/cosmos/Cargo.toml @@ -13,6 +13,7 @@ frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = " frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos-x-auth-signing = { workspace = true, default-features = false } pallet-cosmos-types = { workspace = true, default-features = false } +pallet-multimap = { workspace = true, default-features = false } parity-scale-codec = { version = "3.6", default-features = false, features = [ "derive", ] } @@ -31,6 +32,7 @@ std = [ "frame-system/std", "pallet-cosmos-x-auth-signing/std", "pallet-cosmos-types/std", + "pallet-multimap/std", "parity-scale-codec/std", "np-cosmos/std", "scale-info/std", diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index 3d069b48..b14887d4 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -137,7 +137,6 @@ pub trait AddressMapping { #[frame_support::pallet] pub mod pallet { use super::*; - use crate::types::DenomOf; use alloc::{string::String, vec::Vec}; use cosmos_sdk_proto::Any; use frame_support::{ @@ -296,16 +295,6 @@ pub mod pallet { } } - #[pallet::storage] - #[pallet::getter(fn denom_to_asset)] - pub type DenomAssetRouter = - StorageMap<_, Twox64Concat, DenomOf, T::AssetId, OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn asset_to_denom)] - pub type AssetDenomRouter = - StorageMap<_, Twox64Concat, T::AssetId, DenomOf, OptionQuery>; - #[pallet::call] impl Pallet where diff --git a/frame/cosmos/src/types.rs b/frame/cosmos/src/types.rs index c78e6481..d6a5b498 100644 --- a/frame/cosmos/src/types.rs +++ b/frame/cosmos/src/types.rs @@ -18,4 +18,5 @@ use crate::Config; use sp_runtime::BoundedVec; +pub type AssetIdOf = ::AssetId; pub type DenomOf = BoundedVec::MaxDenomLimit>; diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 660d8bf6..e123cf6d 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -30,7 +30,7 @@ use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ - cosmos::{self, AccountToAddr, AssetToDenom, MsgServiceRouter}, + cosmos::{self, address::AccountToAddr, asset::AssetToDenom, msg::MsgServiceRouter}, ethereum::{self, EnsureAddress, FrontierPrecompiles}, extensions::unify_account, }; @@ -43,15 +43,19 @@ use frame_support::{ TransformOrigin, VariantCountOf, }, weights::{ConstantMultiplier, Weight}, - PalletId, + Blake2_128Concat, PalletId, }; use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; -use pallet_cosmos::config_preludes::{ - MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, TxSigLimit, - WeightToGas, +use pallet_assets::pallet::Instance2; +use pallet_cosmos::{ + config_preludes::{ + MaxDenomLimit, MaxMemoCharacters, MsgFilter, NativeAssetId, NativeDenom, TxSigLimit, + WeightToGas, + }, + types::{AssetIdOf, DenomOf}, }; use pallet_cosmos_types::context::Context; use pallet_cosmos_x_auth_signing::{ @@ -430,8 +434,16 @@ impl pallet_assets::Config for Runtime { type RemoveItemsLimit = ConstU32<1000>; } +impl pallet_multimap::Config for Runtime { + type Key = AssetIdOf; + type Value = DenomOf; + type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; + type KeyHasher = Blake2_128Concat; + type ValueHasher = Blake2_128Concat; +} + impl pallet_cosmos::Config for Runtime { - type AddressMapping = cosmos::AddressMapping; + type AddressMapping = cosmos::address::AddressMapping; type Balance = Balance; type AssetId = AssetId; type NativeAsset = Balances; @@ -441,7 +453,7 @@ impl pallet_cosmos::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_cosmos::weights::CosmosWeight; type WeightToGas = WeightToGas; - type AssetToDenom = AssetToDenom; + type AssetToDenom = AssetToDenom; type Context = Context; type ChainInfo = np_cosmos::traits::CosmosHub; type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; @@ -479,7 +491,7 @@ impl pallet_cosmwasm::Config for Runtime { type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; type MaxMessageSize = ConstU32<{ 64 * 1024 }>; type AccountToAddr = AccountToAddr; - type AssetToDenom = AssetToDenom; + type AssetToDenom = AssetToDenom; type Balance = Balance; type AssetId = AssetId; type Assets = Assets; From c99fa66b2189b9130b58198bee9b4a80d41831f0 Mon Sep 17 00:00:00 2001 From: code0xff Date: Tue, 24 Sep 2024 23:39:33 +0900 Subject: [PATCH 084/133] feat: Implement pallet-asset callback to map asset id to denom --- frame/babel/Cargo.toml | 6 +++++- frame/babel/src/cosmos/asset.rs | 23 +++++++++++++++++++++++ ziggurat/runtime/src/configs/mod.rs | 28 ++++++++++++++-------------- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 5b0de28b..79b9b0b2 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -8,13 +8,16 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -bech32 = { version = "0.11", default-features = false, features = ["alloc"], optional = true } +bech32 = { version = "0.11", default-features = false, features = [ + "alloc", +], optional = true } cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true } ethereum = { version = "0.15.0", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } np-cosmos = { workspace = true, optional = true } np-ethereum = { workspace = true, optional = true } +pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } pallet-cosmos-types = { workspace = true, default-features = false, optional = true } @@ -49,6 +52,7 @@ std = [ "frame-system/std", "np-cosmos?/std", "np-ethereum?/std", + "pallet-assets/std", "pallet-balances/std", "pallet-cosmos?/std", "pallet-cosmos-types?/std", diff --git a/frame/babel/src/cosmos/asset.rs b/frame/babel/src/cosmos/asset.rs index 1b840540..02397c41 100644 --- a/frame/babel/src/cosmos/asset.rs +++ b/frame/babel/src/cosmos/asset.rs @@ -18,6 +18,7 @@ use alloc::string::{String, ToString}; use core::marker::PhantomData; +use frame_support::{ensure, traits::fungibles::metadata::Inspect}; use pallet_cosmos::types::{AssetIdOf, DenomOf}; use pallet_multimap::traits::UniqueMap; use sp_core::Get; @@ -56,3 +57,25 @@ where } } } + +pub struct AssetsCallback(PhantomData<(T, I)>); +impl pallet_assets::AssetsCallback, T::AccountId> + for AssetsCallback +where + T: pallet_cosmos::Config + pallet_multimap::Config, Value = DenomOf>, +{ + fn created(id: &AssetIdOf, _owner: &T::AccountId) -> Result<(), ()> { + let symbol = T::Assets::symbol(id.clone()); + ensure!(!symbol.is_empty(), ()); + + let denom: DenomOf = symbol.try_into().map_err(|_| ())?; + pallet_multimap::Pallet::::try_insert(id.clone(), denom).map_err(|_| ())?; + + Ok(()) + } + + fn destroyed(id: &AssetIdOf) -> Result<(), ()> { + pallet_multimap::Pallet::::remove(id.clone()); + Ok(()) + } +} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index e123cf6d..22718a30 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -30,7 +30,7 @@ use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ - cosmos::{self, address::AccountToAddr, asset::AssetToDenom, msg::MsgServiceRouter}, + cosmos, ethereum::{self, EnsureAddress, FrontierPrecompiles}, extensions::unify_account, }; @@ -413,6 +413,14 @@ impl pallet_base_fee::Config for Runtime { type DefaultElasticity = DefaultElasticity; } +impl pallet_multimap::Config for Runtime { + type Key = AssetIdOf; + type Value = DenomOf; + type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; + type KeyHasher = Blake2_128Concat; + type ValueHasher = Blake2_128Concat; +} + impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -429,19 +437,11 @@ impl pallet_assets::Config for Runtime { type StringLimit = ConstU32<20>; type Freezer = (); type Extra = (); - type CallbackHandle = (); + type CallbackHandle = cosmos::asset::AssetsCallback; type WeightInfo = (); type RemoveItemsLimit = ConstU32<1000>; } -impl pallet_multimap::Config for Runtime { - type Key = AssetIdOf; - type Value = DenomOf; - type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; - type KeyHasher = Blake2_128Concat; - type ValueHasher = Blake2_128Concat; -} - impl pallet_cosmos::Config for Runtime { type AddressMapping = cosmos::address::AddressMapping; type Balance = Balance; @@ -453,12 +453,12 @@ impl pallet_cosmos::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_cosmos::weights::CosmosWeight; type WeightToGas = WeightToGas; - type AssetToDenom = AssetToDenom; + type AssetToDenom = cosmos::asset::AssetToDenom; type Context = Context; type ChainInfo = np_cosmos::traits::CosmosHub; type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; type MsgFilter = MsgFilter; - type MsgServiceRouter = MsgServiceRouter; + type MsgServiceRouter = cosmos::msg::MsgServiceRouter; type SigVerifiableTx = SigVerifiableTx; type SignModeHandler = SignModeHandler; type MaxMemoCharacters = MaxMemoCharacters; @@ -490,8 +490,8 @@ impl pallet_cosmwasm::Config for Runtime { type MaxCodeSize = ConstU32<{ 1024 * 1024 }>; type MaxInstrumentedCodeSize = ConstU32<{ 2 * 1024 * 1024 }>; type MaxMessageSize = ConstU32<{ 64 * 1024 }>; - type AccountToAddr = AccountToAddr; - type AssetToDenom = AssetToDenom; + type AccountToAddr = cosmos::address::AccountToAddr; + type AssetToDenom = cosmos::asset::AssetToDenom; type Balance = Balance; type AssetId = AssetId; type Assets = Assets; From 092070594413e9e6fe004eb3b6b21242c59215b4 Mon Sep 17 00:00:00 2001 From: code0xff Date: Wed, 25 Sep 2024 15:56:35 +0900 Subject: [PATCH 085/133] feat: Add cosmos tx fee validation --- frame/cosmos/src/lib.rs | 8 ++- .../cosmos/types/src/{coin.rs => coin/mod.rs} | 49 ++++++++++++++----- frame/cosmos/types/src/coin/traits.rs | 25 ++++++++++ frame/cosmos/types/src/context/traits.rs | 6 +++ frame/cosmos/types/src/tx_msgs.rs | 8 +++ .../signing/src/sign_verifiable_tx/mod.rs | 24 +++------ frame/cosmos/x/auth/src/fee.rs | 22 ++++++--- frame/cosmos/x/bank/src/lib.rs | 4 +- ziggurat/runtime/src/configs/mod.rs | 13 ++++- 9 files changed, 118 insertions(+), 41 deletions(-) rename frame/cosmos/types/src/{coin.rs => coin/mod.rs} (62%) create mode 100644 frame/cosmos/types/src/coin/traits.rs diff --git a/frame/cosmos/src/lib.rs b/frame/cosmos/src/lib.rs index b14887d4..4b743ba1 100644 --- a/frame/cosmos/src/lib.rs +++ b/frame/cosmos/src/lib.rs @@ -148,7 +148,9 @@ pub mod pallet { }, }; use np_cosmos::traits::ChainInfo; - use pallet_cosmos_types::{errors::CosmosError, events::CosmosEvent, gas::Gas}; + use pallet_cosmos_types::{ + context::traits::MinGasPrices, errors::CosmosError, events::CosmosEvent, gas::Gas, + }; use pallet_cosmos_x_auth_signing::sign_mode_handler::traits::SignModeHandler; #[pallet::pallet] @@ -205,6 +207,8 @@ pub mod pallet { /// Converter between Weight and Gas. type WeightToGas: Convert + Convert; + type MinGasPrices: MinGasPrices; + /// Converter between AssetId and Denom. #[pallet::no_default] type AssetToDenom: Convert> @@ -370,6 +374,8 @@ impl Pallet { .with_weight(T::WeightToGas::convert(ctx.gas_meter().consumed_gas())) })?; + // TODO: Implement PostHandlers for refund functionality + Self::deposit_event(Event::Executed { gas_wanted: gas_limit, gas_used: ctx.gas_meter().consumed_gas(), diff --git a/frame/cosmos/types/src/coin.rs b/frame/cosmos/types/src/coin/mod.rs similarity index 62% rename from frame/cosmos/types/src/coin.rs rename to frame/cosmos/types/src/coin/mod.rs index a045d18c..dc9c94f7 100644 --- a/frame/cosmos/types/src/coin.rs +++ b/frame/cosmos/types/src/coin/mod.rs @@ -15,7 +15,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::string::{String, ToString}; +pub mod traits; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -30,33 +35,51 @@ impl From<&cosmos_sdk_proto::cosmos::base::v1beta1::Coin> for Coin { } } -pub fn amount_to_string(amount: &[cosmos_sdk_proto::cosmos::base::v1beta1::Coin]) -> String { - let mut ret = "".to_string(); - for (i, coin) in amount.iter().enumerate() { - ret.push_str(&coin.amount); - ret.push_str(&coin.denom); - if i < amount.len() - 1 { - ret.push(','); +impl traits::Coins for Vec { + type Error = (); + + fn to_string(&self) -> String { + let mut ret = "".to_string(); + for (i, coin) in self.iter().enumerate() { + ret.push_str(&coin.amount); + ret.push_str(&coin.denom); + if i < self.len() - 1 { + ret.push(','); + } } + ret + } + + fn amount_of(&self, denom: &str) -> Result { + self.iter() + .find(|coin| coin.denom == denom) + .ok_or(())? + .amount + .parse::() + .map_err(|_| ()) } - ret +} + +pub struct DecCoin { + pub denom: String, + pub amount: u128, } #[cfg(test)] mod tests { - use crate::coin::amount_to_string; + use super::traits::Coins; use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; #[test] fn amount_to_string_test() { let mut amounts = Vec::::new(); - assert_eq!(amount_to_string(&amounts), ""); + assert_eq!(amounts.to_string(), ""); amounts.push(Coin { denom: "uatom".to_string(), amount: "1000".to_string() }); - assert_eq!(amount_to_string(&amounts), "1000uatom"); + assert_eq!(amounts.to_string(), "1000uatom"); amounts.push(Coin { denom: "stake".to_string(), amount: "2000".to_string() }); - assert_eq!(amount_to_string(&amounts), "1000uatom,2000stake"); + assert_eq!(amounts.to_string(), "1000uatom,2000stake"); } } diff --git a/frame/cosmos/types/src/coin/traits.rs b/frame/cosmos/types/src/coin/traits.rs new file mode 100644 index 00000000..2a8494c5 --- /dev/null +++ b/frame/cosmos/types/src/coin/traits.rs @@ -0,0 +1,25 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::string::String; + +pub trait Coins { + type Error; + + fn to_string(&self) -> String; + fn amount_of(&self, denom: &str) -> Result; +} diff --git a/frame/cosmos/types/src/context/traits.rs b/frame/cosmos/types/src/context/traits.rs index bad4b1f3..4c2707fb 100644 --- a/frame/cosmos/types/src/context/traits.rs +++ b/frame/cosmos/types/src/context/traits.rs @@ -16,9 +16,11 @@ // limitations under the License. use crate::{ + coin::DecCoin, events::traits::EventManager, gas::{traits::GasMeter, Gas}, }; +use alloc::vec::Vec; pub trait Context { type GasMeter: GasMeter; @@ -28,3 +30,7 @@ pub trait Context { fn gas_meter(&mut self) -> &mut Self::GasMeter; fn event_manager(&mut self) -> &mut Self::EventManager; } + +pub trait MinGasPrices { + fn min_prices() -> Vec; +} diff --git a/frame/cosmos/types/src/tx_msgs.rs b/frame/cosmos/types/src/tx_msgs.rs index 0ac51b18..00c42e3b 100644 --- a/frame/cosmos/types/src/tx_msgs.rs +++ b/frame/cosmos/types/src/tx_msgs.rs @@ -27,6 +27,8 @@ pub trait Msg { pub trait FeeTx { fn fee(&self) -> Option; fn gas(&self) -> Option; + fn fee_payer(&self) -> Option; + fn fee_granter(&self) -> Option; } impl FeeTx for Tx { @@ -36,4 +38,10 @@ impl FeeTx for Tx { fn gas(&self) -> Option { self.fee().map(|fee| fee.gas_limit) } + fn fee_payer(&self) -> Option { + self.fee().map(|fee| fee.payer.clone()) + } + fn fee_granter(&self) -> Option { + self.fee().map(|fee| fee.granter.clone()) + } } diff --git a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs index c468e758..05cd3fe8 100644 --- a/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs +++ b/frame/cosmos/x/auth/signing/src/sign_verifiable_tx/mod.rs @@ -68,14 +68,8 @@ impl traits::SigVerifiableTx for SigVerifiableTx { } } - let fee_payer = &tx - .auth_info - .as_ref() - .and_then(|auth_info| auth_info.fee.as_ref()) - .ok_or(SigVerifiableTxError::EmptyFee)? - .payer; - - if !fee_payer.is_empty() && !signers.contains(fee_payer) { + let fee_payer = tx.fee_payer().ok_or(SigVerifiableTxError::EmptyFee)?; + if !fee_payer.is_empty() && !signers.contains(&fee_payer) { signers.push(fee_payer.clone()); } @@ -83,18 +77,16 @@ impl traits::SigVerifiableTx for SigVerifiableTx { } fn fee_payer(tx: &Tx) -> Result { - let fee = tx.fee().ok_or(SigVerifiableTxError::EmptyFee)?; + let fee_payer = tx.fee_payer().ok_or(SigVerifiableTxError::EmptyFee)?; - let fee_payer = if !fee.payer.is_empty() { - fee.payer.clone() + if !fee_payer.is_empty() { + Ok(fee_payer) } else { Self::get_signers(tx)? .first() - .ok_or(SigVerifiableTxError::EmptySigners)? - .clone() - }; - - Ok(fee_payer) + .ok_or(SigVerifiableTxError::EmptySigners) + .cloned() + } } fn sequence(tx: &Tx) -> Result { diff --git a/frame/cosmos/x/auth/src/fee.rs b/frame/cosmos/x/auth/src/fee.rs index e9388ffd..cfcefa6e 100644 --- a/frame/cosmos/x/auth/src/fee.rs +++ b/frame/cosmos/x/auth/src/fee.rs @@ -29,7 +29,8 @@ use frame_support::{ use pallet_cosmos::AddressMapping; use pallet_cosmos_types::{ address::acc_address_from_bech32, - coin::amount_to_string, + coin::traits::Coins, + context::traits::MinGasPrices, errors::{CosmosError, RootError}, events::{ CosmosEvent, EventAttribute, ATTRIBUTE_KEY_FEE, ATTRIBUTE_KEY_FEE_PAYER, EVENT_TYPE_TX, @@ -50,14 +51,22 @@ where T: frame_system::Config + pallet_cosmos::Config, { fn ante_handle(tx: &Tx, simulate: bool) -> Result<(), CosmosError> { - let fee = tx.fee().ok_or(RootError::TxDecodeError)?; - - if !simulate && !frame_system::Pallet::::block_number().is_zero() && fee.gas_limit == 0 { + let gas_limit = tx.gas().ok_or(RootError::TxDecodeError)?; + if !simulate && !frame_system::Pallet::::block_number().is_zero() && gas_limit == 0 { return Err(RootError::InvalidGasLimit.into()); } if !simulate { - // TODO: Implements txFeeChecker + let fee_coins = tx.fee().ok_or(RootError::TxDecodeError)?.amount; + let min_prices = T::MinGasPrices::min_prices(); + for gp in min_prices.iter() { + let fee = + gp.amount.checked_mul(gas_limit.into()).ok_or(RootError::InsufficientFee)?; + let amount = + fee_coins.amount_of(&gp.denom).map_err(|_| RootError::InsufficientFee)?; + + ensure!(amount >= fee && fee != 0, RootError::InsufficientFee); + } } Self::check_deduct_fee(tx)?; @@ -72,7 +81,6 @@ where { fn check_deduct_fee(tx: &Tx) -> Result<(), CosmosError> { let fee_payer = T::SigVerifiableTx::fee_payer(tx).map_err(|_| RootError::TxDecodeError)?; - let fee = tx.fee().ok_or(RootError::TxDecodeError)?; // Fee granter not supported @@ -93,7 +101,7 @@ where attributes: vec![ EventAttribute { key: ATTRIBUTE_KEY_FEE.into(), - value: amount_to_string(&fee.amount).into(), + value: fee.amount.to_string().into(), }, EventAttribute { key: ATTRIBUTE_KEY_FEE_PAYER.into(), value: fee_payer.into() }, ], diff --git a/frame/cosmos/x/bank/src/lib.rs b/frame/cosmos/x/bank/src/lib.rs index 28170d56..f9220233 100644 --- a/frame/cosmos/x/bank/src/lib.rs +++ b/frame/cosmos/x/bank/src/lib.rs @@ -33,7 +33,7 @@ use frame_support::{ use pallet_cosmos::AddressMapping; use pallet_cosmos_types::{ address::acc_address_from_bech32, - coin::amount_to_string, + coin::traits::Coins, context, errors::{CosmosError, RootError}, events::{ @@ -106,7 +106,7 @@ where EventAttribute { key: ATTRIBUTE_KEY_RECIPIENT.into(), value: to_address.into() }, EventAttribute { key: ATTRIBUTE_KEY_AMOUNT.into(), - value: amount_to_string(&amount).into(), + value: amount.to_string().into(), }, ], }; diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 22718a30..817b9d77 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -26,6 +26,7 @@ mod xcm_config; // Substrate and Polkadot dependencies +use alloc::{string::ToString, vec, vec::Vec}; use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; @@ -57,7 +58,7 @@ use pallet_cosmos::{ }, types::{AssetIdOf, DenomOf}, }; -use pallet_cosmos_types::context::Context; +use pallet_cosmos_types::{coin::DecCoin, context}; use pallet_cosmos_x_auth_signing::{ sign_mode_handler::SignModeHandler, sign_verifiable_tx::SigVerifiableTx, }; @@ -442,6 +443,13 @@ impl pallet_assets::Config for Runtime { type RemoveItemsLimit = ConstU32<1000>; } +pub struct MinGasPrices; +impl context::traits::MinGasPrices for MinGasPrices { + fn min_prices() -> Vec { + vec![DecCoin { denom: NativeDenom::get().to_string(), amount: 1 }] + } +} + impl pallet_cosmos::Config for Runtime { type AddressMapping = cosmos::address::AddressMapping; type Balance = Balance; @@ -453,8 +461,9 @@ impl pallet_cosmos::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_cosmos::weights::CosmosWeight; type WeightToGas = WeightToGas; + type MinGasPrices = MinGasPrices; type AssetToDenom = cosmos::asset::AssetToDenom; - type Context = Context; + type Context = context::Context; type ChainInfo = np_cosmos::traits::CosmosHub; type AnteHandler = pallet_cosmos_x_auth::AnteDecorators; type MsgFilter = MsgFilter; From 5d2ee24c254c732155eb960f271570e569bd4d02 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 25 Sep 2024 11:20:27 +0900 Subject: [PATCH 086/133] refactor: Add np-babel --- Cargo.toml | 2 + frame/babel/Cargo.toml | 10 ++-- frame/babel/src/cosmos/address.rs | 2 +- frame/babel/src/ethereum.rs | 3 +- frame/babel/src/extensions/unify_account.rs | 6 +- frame/babel/src/lib.rs | 41 +------------ primitives/babel/Cargo.toml | 28 +++++++++ primitives/babel/src/lib.rs | 66 +++++++++++++++++++++ ziggurat/runtime/src/configs/mod.rs | 2 +- 9 files changed, 108 insertions(+), 52 deletions(-) create mode 100644 primitives/babel/Cargo.toml create mode 100644 primitives/babel/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index a01c4daf..0f94c1a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "frame/cosmos/x/wasm", "frame/cosmos/x/wasm/types", "frame/multimap", + "primitives/babel", "primitives/cosmos", "primitives/ethereum", "primitives/runtime", @@ -42,6 +43,7 @@ cosmwasm-vm-wasmi = { path = "vendor/composable/vm-wasmi", default-features = fa frame-babel = { path = "frame/babel", default-features = false } noir-core-primitives = { path = "core-primitives", default-features = false } noir-runtime-common = { path = "runtime/common", default-features = false } +np-babel = { path = "primitives/babel", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 79b9b0b2..e5110161 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -15,8 +15,7 @@ cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true ethereum = { version = "0.15.0", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -np-cosmos = { workspace = true, optional = true } -np-ethereum = { workspace = true, optional = true } +np-babel = { workspace = true, default-features = false } pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } @@ -50,8 +49,7 @@ std = [ "ethereum?/std", "frame-support/std", "frame-system/std", - "np-cosmos?/std", - "np-ethereum?/std", + "np-babel/std", "pallet-assets/std", "pallet-balances/std", "pallet-cosmos?/std", @@ -75,7 +73,7 @@ std = [ cosmos = [ "bech32", "cosmos-sdk-proto", - "np-cosmos", + "np-babel/cosmos", "pallet-cosmos", "pallet-cosmos-types", "pallet-cosmos-x-bank", @@ -84,7 +82,7 @@ cosmos = [ ] ethereum = [ "dep:ethereum", - "np-ethereum", + "np-babel/ethereum", "pallet-ethereum", "pallet-evm", "pallet-evm-precompile-blake2", diff --git a/frame/babel/src/cosmos/address.rs b/frame/babel/src/cosmos/address.rs index 16317760..a0dc7c7e 100644 --- a/frame/babel/src/cosmos/address.rs +++ b/frame/babel/src/cosmos/address.rs @@ -20,7 +20,7 @@ use crate::{extensions::unify_account, Address}; use alloc::{string::String, vec::Vec}; use bech32::{Bech32, Hrp}; use core::marker::PhantomData; -use np_cosmos::{traits::ChainInfo, Address as CosmosAddress}; +use np_babel::cosmos::{traits::ChainInfo, Address as CosmosAddress}; use pallet_cosmos_types::address::acc_address_from_bech32; use pallet_multimap::traits::UniqueMultimap; use sp_core::{H160, H256}; diff --git a/frame/babel/src/ethereum.rs b/frame/babel/src/ethereum.rs index 40d2ad7d..84c06353 100644 --- a/frame/babel/src/ethereum.rs +++ b/frame/babel/src/ethereum.rs @@ -21,7 +21,7 @@ use crate::*; use crate::extensions::unify_account; use core::marker::PhantomData; use frame_support::dispatch::RawOrigin; -use np_ethereum::Address as EthereumAddress; +use np_babel::EthereumAddress; use pallet_ethereum::Transaction; use pallet_evm::{ EnsureAddressOrigin, IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, @@ -32,6 +32,7 @@ use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; +use pallet_multimap::traits::UniqueMultimap; use sp_core::{ecdsa, H160}; use sp_runtime::traits::AccountIdConversion; diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index d3a4ef77..119757fe 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -16,13 +16,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::*; - use core::marker::PhantomData; use frame_support::traits::tokens::{fungible, Fortitude, Preservation}; +use np_babel::{Address, CosmosAddress, EthereumAddress}; +use pallet_multimap::traits::UniqueMultimap; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_core::H256; +use sp_core::{ecdsa, H256}; use sp_runtime::{ traits::{AccountIdConversion, DispatchInfoOf, One, SignedExtension, Zero}, transaction_validity::{TransactionValidityError, ValidTransaction}, diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index ad744e91..cbb2e855 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -27,43 +27,4 @@ pub mod ethereum; pub mod extensions; pub use extensions::unify_account::UnifyAccount; - -#[cfg(feature = "cosmos")] -pub use np_cosmos::Address as CosmosAddress; -#[cfg(feature = "ethereum")] -pub use np_ethereum::Address as EthereumAddress; -use pallet_multimap::traits::UniqueMultimap; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; -use sp_core::ecdsa; - -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub enum Address { - #[cfg(feature = "cosmos")] - Cosmos(CosmosAddress), - #[cfg(feature = "ethereum")] - Ethereum(EthereumAddress), -} - -impl Address { - pub const fn variant_count() -> u32 { - let mut n = 0; - if cfg!(feature = "cosmos") { - n += 1; - } - if cfg!(feature = "ethereum") { - n += 1; - } - n - } - - #[cfg(feature = "cosmos")] - pub fn cosmos(public: ecdsa::Public) -> Self { - Self::Cosmos(CosmosAddress::from(public)) - } - - #[cfg(feature = "ethereum")] - pub fn ethereum(public: ecdsa::Public) -> Self { - Self::Ethereum(EthereumAddress::from(public)) - } -} +pub use np_babel::Address; diff --git a/primitives/babel/Cargo.toml b/primitives/babel/Cargo.toml new file mode 100644 index 00000000..04fed038 --- /dev/null +++ b/primitives/babel/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "np-babel" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +np-cosmos = { workspace = true, default-features = false, optional = true } +np-ethereum = { workspace = true, default-features = false, optional = true } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } + +[features] +default = ["std", "cosmos", "ethereum"] +std = [ + "np-cosmos?/std", + "np-ethereum?/std", +] +cosmos = [ + "np-cosmos", +] +ethereum = [ + "np-ethereum", +] diff --git a/primitives/babel/src/lib.rs b/primitives/babel/src/lib.rs new file mode 100644 index 00000000..c2ce43f8 --- /dev/null +++ b/primitives/babel/src/lib.rs @@ -0,0 +1,66 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::ecdsa; + +#[cfg(feature = "cosmos")] +pub use np_cosmos as cosmos; +#[cfg(feature = "cosmos")] +pub use np_cosmos::Address as CosmosAddress; +#[cfg(feature = "ethereum")] +pub use np_ethereum as ethereum; +#[cfg(feature = "ethereum")] +pub use np_ethereum::Address as EthereumAddress; +pub use sp_core::crypto::AccountId32; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub enum Address { + Polkadot(AccountId32), + #[cfg(feature = "cosmos")] + Cosmos(CosmosAddress), + #[cfg(feature = "ethereum")] + Ethereum(EthereumAddress), +} + +impl Address { + pub const fn variant_count() -> u32 { + let mut n = 1; + if cfg!(feature = "cosmos") { + n += 1; + } + if cfg!(feature = "ethereum") { + n += 1; + } + n + } + + #[cfg(feature = "cosmos")] + pub fn cosmos(public: ecdsa::Public) -> Self { + Self::Cosmos(CosmosAddress::from(public)) + } + + #[cfg(feature = "ethereum")] + pub fn ethereum(public: ecdsa::Public) -> Self { + Self::Ethereum(EthereumAddress::from(public)) + } +} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 817b9d77..a2bce5d1 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -417,7 +417,7 @@ impl pallet_base_fee::Config for Runtime { impl pallet_multimap::Config for Runtime { type Key = AssetIdOf; type Value = DenomOf; - type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() }>; + type CapacityPerKey = ConstU32<{ frame_babel::Address::variant_count() - 1 }>; type KeyHasher = Blake2_128Concat; type ValueHasher = Blake2_128Concat; } From 578cc237a212be89e14e275ab3990785a86b232b Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 25 Sep 2024 12:48:10 +0900 Subject: [PATCH 087/133] feat: Add frame-babel pallet --- frame/babel/Cargo.toml | 6 +++- frame/babel/src/lib.rs | 69 +++++++++++++++++++++++++++++++++++++ ziggurat/runtime/Cargo.toml | 2 +- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index e5110161..83cf12a7 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -42,7 +42,7 @@ sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable24 sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [features] -default = ["std", "cosmos", "ethereum"] +default = ["std", "pallet"] std = [ "bech32?/std", "cosmos-sdk-proto?/std", @@ -91,6 +91,10 @@ ethereum = [ "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", ] +pallet = [ + "cosmos", + "ethereum", +] runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index cbb2e855..d736ab46 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -28,3 +28,72 @@ pub mod extensions; pub use extensions::unify_account::UnifyAccount; pub use np_babel::Address; + +#[cfg(feature = "pallet")] +pub use pallet::*; + +#[cfg(feature = "pallet")] +#[frame_support::pallet] +pub mod pallet { + use alloc::vec::Vec; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_core::ecdsa; + use sp_runtime::traits::UniqueSaturatedInto; + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_cosmos::Config + pallet_ethereum::Config + { + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::error] + pub enum Error { + InvalidOrigin, + InvalidTransaction, + } + + #[pallet::call] + impl Pallet + where + OriginFor: Into>>, + T::AccountId: TryInto, + T::RuntimeOrigin: From, + { + #[pallet::call_index(0)] + #[pallet::weight({ + use ethereum::EnvelopedDecodable; + use pallet_ethereum::{Transaction, TransactionData}; + use pallet_evm::GasWeightMapping; + let without_base_extrinsic_weight = true; + match ::decode(&transaction) { + Ok(transaction) => { + ::GasWeightMapping::gas_to_weight({ + let transaction_data = TransactionData::from(&transaction); + transaction_data.gas_limit.unique_saturated_into() + }, without_base_extrinsic_weight) + }, + Err(_) => Weight::MAX, + } + })] + pub fn ethereum_transact( + origin: OriginFor, + transaction: Vec, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let public: ecdsa::Public = who.try_into().map_err(|_| Error::::InvalidOrigin)?; + let address: np_babel::EthereumAddress = public.into(); + + let origin = T::RuntimeOrigin::from(pallet_ethereum::RawOrigin::EthereumTransaction( + address.into(), + )); + let transaction = ethereum::EnvelopedDecodable::decode(&transaction) + .map_err(|_| Error::::InvalidTransaction)?; + + pallet_ethereum::Pallet::::transact(origin, transaction) + } + } +} diff --git a/ziggurat/runtime/Cargo.toml b/ziggurat/runtime/Cargo.toml index 3c3f840a..e2315a32 100644 --- a/ziggurat/runtime/Cargo.toml +++ b/ziggurat/runtime/Cargo.toml @@ -20,7 +20,7 @@ scale-info = { version = "2.11", default-features = false, features = [ ] } smallvec = "1.13" -frame-babel = { workspace = true, features = ["cosmos", "ethereum"] } +frame-babel = { workspace = true, features = ["pallet"] } np-ethereum = { workspace = true } np-runtime = { workspace = true, features = ["serde"] } pallet-multimap = { workspace = true } From 2c6b1e34cf423b1a9b7ce808ad4790532b667782 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Tue, 24 Sep 2024 19:12:34 +0900 Subject: [PATCH 088/133] feat: Add babel precompile --- frame/babel/Cargo.toml | 9 +- .../src/{ethereum.rs => ethereum/mod.rs} | 43 ++--- frame/babel/src/ethereum/precompile.rs | 169 ++++++++++++++++++ ziggurat/runtime/src/configs/mod.rs | 12 +- 4 files changed, 206 insertions(+), 27 deletions(-) rename frame/babel/src/{ethereum.rs => ethereum/mod.rs} (85%) create mode 100644 frame/babel/src/ethereum/precompile.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 83cf12a7..11dbc6cc 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -13,6 +13,7 @@ bech32 = { version = "0.11", default-features = false, features = [ ], optional = true } cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true } ethereum = { version = "0.15.0", default-features = false, optional = true } +fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } np-babel = { workspace = true, default-features = false } @@ -28,12 +29,12 @@ pallet-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl pallet-evm-precompile-blake2 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-bn128 = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-modexp = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } -pallet-evm-precompile-sha3fips = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-evm-precompile-simple = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } pallet-multimap = { workspace = true, default-features = false } parity-scale-codec = { version = "3.6", default-features = false, features = [ "derive", ] } +precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } scale-info = { version = "2.11", default-features = false, features = [ "derive", ] } @@ -47,6 +48,7 @@ std = [ "bech32?/std", "cosmos-sdk-proto?/std", "ethereum?/std", + "fp-evm?/std", "frame-support/std", "frame-system/std", "np-babel/std", @@ -62,10 +64,10 @@ std = [ "pallet-evm-precompile-blake2?/std", "pallet-evm-precompile-bn128?/std", "pallet-evm-precompile-modexp?/std", - "pallet-evm-precompile-sha3fips?/std", "pallet-evm-precompile-simple?/std", "pallet-multimap/std", "parity-scale-codec/std", + "precompile-utils?/std", "scale-info/std", "sp-core/std", "sp-runtime/std", @@ -82,14 +84,15 @@ cosmos = [ ] ethereum = [ "dep:ethereum", + "fp-evm", "np-babel/ethereum", "pallet-ethereum", "pallet-evm", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-modexp", - "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "precompile-utils", ] pallet = [ "cosmos", diff --git a/frame/babel/src/ethereum.rs b/frame/babel/src/ethereum/mod.rs similarity index 85% rename from frame/babel/src/ethereum.rs rename to frame/babel/src/ethereum/mod.rs index 84c06353..493835d2 100644 --- a/frame/babel/src/ethereum.rs +++ b/frame/babel/src/ethereum/mod.rs @@ -16,11 +16,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod precompile; + use crate::*; use crate::extensions::unify_account; use core::marker::PhantomData; -use frame_support::dispatch::RawOrigin; +use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo, RawOrigin}; use np_babel::EthereumAddress; use pallet_ethereum::Transaction; use pallet_evm::{ @@ -30,11 +32,12 @@ use pallet_evm::{ use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_modexp::Modexp; -use pallet_evm_precompile_sha3fips::Sha3FIPS256; -use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; +use pallet_evm_precompile_simple::{ECRecover, Identity, Ripemd160, Sha256}; use pallet_multimap::traits::UniqueMultimap; +use parity_scale_codec::Decode; +use precompile::Babel; use sp_core::{ecdsa, H160}; -use sp_runtime::traits::AccountIdConversion; +use sp_runtime::traits::{AccountIdConversion, Dispatchable}; pub struct EnsureAddress(PhantomData); @@ -76,25 +79,23 @@ where } } -pub struct FrontierPrecompiles(PhantomData); +pub struct BabelPrecompiles(PhantomData); -impl Default for FrontierPrecompiles -where - R: pallet_evm::Config, -{ +impl Default for BabelPrecompiles { fn default() -> Self { - Self::new() + Self(Default::default()) } } -impl FrontierPrecompiles +impl BabelPrecompiles where - R: pallet_evm::Config, + T: precompile::Config, { pub fn new() -> Self { - Self(Default::default()) + Self::default() } - pub fn used_addresses() -> [H160; 11] { + + pub fn used_addresses() -> [H160; 10] { [ hash(1), hash(2), @@ -105,14 +106,16 @@ where hash(7), hash(8), hash(9), - hash(1024), - hash(1025), + hash(0x400 /* 1024 */), ] } } -impl PrecompileSet for FrontierPrecompiles + +impl PrecompileSet for BabelPrecompiles where - R: pallet_evm::Config, + T: precompile::Config, + T::RuntimeCall: Dispatchable + GetDispatchInfo + Decode, + ::RuntimeOrigin: From>, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { match handle.code_address() { @@ -126,9 +129,7 @@ where a if a == hash(7) => Some(Bn128Mul::execute(handle)), a if a == hash(8) => Some(Bn128Pairing::execute(handle)), a if a == hash(9) => Some(Blake2F::execute(handle)), - // Non-Frontier specific nor Ethereum precompiles : - a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)), - a if a == hash(1025) => Some(ECRecoverPublicKey::execute(handle)), + a if a == hash(0x400) => Some(Babel::::execute(handle)), _ => None, } } diff --git a/frame/babel/src/ethereum/precompile.rs b/frame/babel/src/ethereum/precompile.rs new file mode 100644 index 00000000..708a96ca --- /dev/null +++ b/frame/babel/src/ethereum/precompile.rs @@ -0,0 +1,169 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::{format, vec::Vec}; +use core::marker::PhantomData; +use fp_evm::{ExitError, PrecompileFailure, PrecompileHandle}; +use frame_support::{ + dispatch::{DispatchClass, GetDispatchInfo, Pays, PostDispatchInfo}, + StorageHasher, Twox128, +}; +use pallet_evm::{AddressMapping, GasWeightMapping}; +use parity_scale_codec::{Decode, DecodeLimit, Encode}; +use precompile_utils::{prelude::*, EvmResult}; +use sp_runtime::traits::{Dispatchable, Get}; + +pub trait Config: pallet_evm::Config { + type DispatchValidator: DispatchValidate; + type DecodeLimit: Get; + type StorageFilter: StorageFilter; +} + +pub trait DispatchValidate { + fn validate_before_dispatch( + origin: &AccountId, + call: &RuntimeCall, + ) -> Option; +} + +impl DispatchValidate for () +where + RuntimeCall: GetDispatchInfo, +{ + fn validate_before_dispatch( + _origin: &AccountId, + call: &RuntimeCall, + ) -> Option { + let info = call.get_dispatch_info(); + if !(info.pays_fee == Pays::Yes && info.class == DispatchClass::Normal) { + return Some(PrecompileFailure::Error { + exit_status: ExitError::Other("invalid call".into()), + }); + } + None + } +} + +pub trait StorageFilter { + fn allow(prefix: &[u8]) -> bool; +} + +impl StorageFilter for () { + fn allow(prefix: &[u8]) -> bool { + prefix != Twox128::hash(b"Evm") + } +} + +pub struct Babel(PhantomData); + +#[precompile_utils::precompile] +impl Babel +where + T: Config, + T::RuntimeCall: Dispatchable + GetDispatchInfo + Decode, + ::RuntimeOrigin: From>, +{ + #[precompile::public("accountId(address)")] + #[precompile::view] + fn account_id(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + let output = T::AddressMapping::into_account_id(owner.into()).encode(); + + handle.record_db_read::(output.len())?; + + Ok(output.into()) + } + + // darwinia-precompile-state-storage + #[precompile::public("getStorage(bytes)")] + #[precompile::view] + fn get_storage( + handle: &mut impl PrecompileHandle, + key: UnboundedBytes, + ) -> EvmResult { + const PALLET_PREFIX_LENGTH: usize = 16; + + let key = key.as_bytes(); + if key.len() < PALLET_PREFIX_LENGTH || + !T::StorageFilter::allow(&key[0..PALLET_PREFIX_LENGTH]) + { + return Err(revert("Read restriction")); + } + + let output = frame_support::storage::unhashed::get_raw(key).unwrap_or_default(); + // Record proof_size cost for the db content + handle.record_db_read::(output.len())?; + + Ok(output.as_slice().into()) + } + + // pallet-evm-precompile-dispatch + #[precompile::public("dispatch(bytes)")] + fn dispatch(handle: &mut impl PrecompileHandle, input: UnboundedBytes) -> EvmResult { + let input: Vec = input.into(); + let target_gas = handle.gas_limit(); + let context = handle.context(); + + let call = T::RuntimeCall::decode_with_depth_limit(T::DecodeLimit::get(), &mut &*input) + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("decode failed".into()), + })?; + let info = call.get_dispatch_info(); + + if let Some(gas) = target_gas { + let valid_weight = + info.weight.ref_time() <= T::GasWeightMapping::gas_to_weight(gas, false).ref_time(); + if !valid_weight { + return Err(PrecompileFailure::Error { exit_status: ExitError::OutOfGas }); + } + } + + let origin = T::AddressMapping::into_account_id(context.caller); + + if let Some(err) = T::DispatchValidator::validate_before_dispatch(&origin, &call) { + return Err(err); + } + + handle.record_external_cost( + Some(info.weight.ref_time()), + Some(info.weight.proof_size()), + None, + )?; + + match call.dispatch(Some(origin).into()) { + Ok(post_info) => { + if post_info.pays_fee(&info) == Pays::Yes { + let actual_weight = post_info.actual_weight.unwrap_or(info.weight); + let cost = T::GasWeightMapping::weight_to_gas(actual_weight); + handle.record_cost(cost)?; + + handle.refund_external_cost( + Some(info.weight.ref_time().saturating_sub(actual_weight.ref_time())), + Some(info.weight.proof_size().saturating_sub(actual_weight.proof_size())), + ); + } + + Ok(()) + }, + Err(e) => Err(PrecompileFailure::Error { + exit_status: ExitError::Other( + format!("dispatch execution failed: {}", <&'static str>::from(e)).into(), + ), + }), + } + } +} diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index a2bce5d1..59806b3c 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -32,7 +32,7 @@ use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ cosmos, - ethereum::{self, EnsureAddress, FrontierPrecompiles}, + ethereum::{self, BabelPrecompiles, EnsureAddress}, extensions::unify_account, }; use frame_support::{ @@ -349,11 +349,17 @@ parameter_types! { pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); pub const ChainId: u64 = 1337; pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); - pub PrecompilesValue: FrontierPrecompiles = FrontierPrecompiles::<_>::new(); + pub PrecompilesValue: BabelPrecompiles = BabelPrecompiles::<_>::new(); pub WeightPerGas: Weight = Weight::from_parts(weight_per_gas(BLOCK_GAS_LIMIT, NORMAL_DISPATCH_RATIO, WEIGHT_MILLISECS_PER_BLOCK), 0); pub SuicideQuickClearLimit: u32 = 0; } +impl frame_babel::ethereum::precompile::Config for Runtime { + type DispatchValidator = (); + type DecodeLimit = ConstU32<8>; + type StorageFilter = (); +} + impl pallet_evm::Config for Runtime { type FeeCalculator = BaseFee; type GasWeightMapping = pallet_evm::FixedGasWeightMapping; @@ -364,7 +370,7 @@ impl pallet_evm::Config for Runtime { type AddressMapping = ethereum::AddressMapping; type Currency = Balances; type RuntimeEvent = RuntimeEvent; - type PrecompilesType = FrontierPrecompiles; + type PrecompilesType = BabelPrecompiles; type PrecompilesValue = PrecompilesValue; type ChainId = ChainId; type BlockGasLimit = BlockGasLimit; From a81e2ed10b9d69ef7968e385260b15090e37b95a Mon Sep 17 00:00:00 2001 From: code0xff Date: Thu, 26 Sep 2024 20:02:30 +0900 Subject: [PATCH 089/133] feat: Add precompile for dispatch call --- frame/babel/Cargo.toml | 27 +++++- frame/babel/src/cosmos/address.rs | 14 +-- frame/babel/src/cosmos/mod.rs | 1 + frame/babel/src/cosmos/precompile.rs | 127 ++++++++++++++++++++++++++ vendor/composable/cosmwasm/Cargo.toml | 4 +- vendor/composable/cosmwasm/src/lib.rs | 2 +- ziggurat/runtime/src/configs/mod.rs | 5 +- 7 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 frame/babel/src/cosmos/precompile.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 11dbc6cc..35addd2a 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -12,12 +12,17 @@ bech32 = { version = "0.11", default-features = false, features = [ "alloc", ], optional = true } cosmos-sdk-proto = { version = "0.24", default-features = false, optional = true } +cosmwasm-std = { workspace = true, default-features = false, optional = true } +cosmwasm-vm = { workspace = true, default-features = false, optional = true } +cosmwasm-vm-wasmi = { workspace = true, default-features = false, optional = true } ethereum = { version = "0.15.0", default-features = false, optional = true } fp-evm = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } np-babel = { workspace = true, default-features = false } -pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +np-cosmos = { workspace = true, optional = true } +np-ethereum = { workspace = true, optional = true } +pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false, optional = true } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } pallet-cosmos = { workspace = true, default-features = false, optional = true } pallet-cosmos-types = { workspace = true, default-features = false, optional = true } @@ -38,6 +43,10 @@ precompile-utils = { git = "https://github.com/polkadot-evm/frontier", branch = scale-info = { version = "2.11", default-features = false, features = [ "derive", ] } +serde = { version = "1.0.210", default-features = false, features = [ + "derive", +], optional = true } +serde-json-wasm = { version = "1.0.1", default-features = false, optional = true } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -47,12 +56,17 @@ default = ["std", "pallet"] std = [ "bech32?/std", "cosmos-sdk-proto?/std", + "cosmwasm-std?/std", + "cosmwasm-vm?/std", + "cosmwasm-vm-wasmi?/std", "ethereum?/std", "fp-evm?/std", "frame-support/std", "frame-system/std", "np-babel/std", - "pallet-assets/std", + "np-cosmos?/std", + "np-ethereum?/std", + "pallet-assets?/std", "pallet-balances/std", "pallet-cosmos?/std", "pallet-cosmos-types?/std", @@ -69,18 +83,27 @@ std = [ "parity-scale-codec/std", "precompile-utils?/std", "scale-info/std", + "serde?/std", + "serde-json-wasm?/std", "sp-core/std", "sp-runtime/std", ] cosmos = [ "bech32", "cosmos-sdk-proto", + "cosmwasm-std", + "cosmwasm-vm", + "cosmwasm-vm-wasmi", "np-babel/cosmos", + "np-cosmos", + "pallet-assets", "pallet-cosmos", "pallet-cosmos-types", "pallet-cosmos-x-bank", "pallet-cosmos-x-wasm", "pallet-cosmwasm", + "serde", + "serde-json-wasm", ] ethereum = [ "dep:ethereum", diff --git a/frame/babel/src/cosmos/address.rs b/frame/babel/src/cosmos/address.rs index a0dc7c7e..15ea43d1 100644 --- a/frame/babel/src/cosmos/address.rs +++ b/frame/babel/src/cosmos/address.rs @@ -22,6 +22,7 @@ use bech32::{Bech32, Hrp}; use core::marker::PhantomData; use np_babel::cosmos::{traits::ChainInfo, Address as CosmosAddress}; use pallet_cosmos_types::address::acc_address_from_bech32; +use pallet_cosmwasm::types::AccountIdOf; use pallet_multimap::traits::UniqueMultimap; use sp_core::{H160, H256}; use sp_runtime::traits::{AccountIdConversion, Convert}; @@ -39,11 +40,11 @@ where } pub struct AccountToAddr(PhantomData); -impl Convert for AccountToAddr +impl Convert, String> for AccountToAddr where T: pallet_cosmwasm::Config + unify_account::Config, { - fn convert(account: T::AccountIdExtended) -> String { + fn convert(account: AccountIdOf) -> String { let addresses = T::AddressMap::get(account.clone()); let address: Option<&CosmosAddress> = addresses.iter().find_map(|address| match address { Address::Cosmos(address) => Some(address), @@ -58,22 +59,21 @@ where } } -impl Convert> for AccountToAddr +impl Convert, ()>> for AccountToAddr where T: pallet_cosmwasm::Config + unify_account::Config, { - fn convert(address: String) -> Result { + fn convert(address: String) -> Result, ()> { let (_hrp, address_raw) = acc_address_from_bech32(&address).map_err(|_| ())?; Self::convert(address_raw) } } -impl Convert, Result> for AccountToAddr +impl Convert, Result, ()>> for AccountToAddr where T: pallet_cosmwasm::Config + unify_account::Config, - T::AccountIdExtended: From, { - fn convert(address: Vec) -> Result { + fn convert(address: Vec) -> Result, ()> { match address.len() { 20 => { let address = CosmosAddress::from(H160::from_slice(&address)); diff --git a/frame/babel/src/cosmos/mod.rs b/frame/babel/src/cosmos/mod.rs index 9916eba6..9143a891 100644 --- a/frame/babel/src/cosmos/mod.rs +++ b/frame/babel/src/cosmos/mod.rs @@ -19,3 +19,4 @@ pub mod address; pub mod asset; pub mod msg; +pub mod precompile; diff --git a/frame/babel/src/cosmos/precompile.rs b/frame/babel/src/cosmos/precompile.rs new file mode 100644 index 00000000..1c0c60a5 --- /dev/null +++ b/frame/babel/src/cosmos/precompile.rs @@ -0,0 +1,127 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::vec::Vec; +use core::marker::PhantomData; +use cosmwasm_std::{Binary, ContractResult, Response}; +use cosmwasm_vm::{ + executor::QueryResponse, + vm::{VMBase, VmErrorOf}, +}; +use cosmwasm_vm_wasmi::OwnedWasmiVM; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + PalletId, +}; +use pallet_cosmos::AddressMapping; +use pallet_cosmos_types::address::acc_address_from_bech32; +use pallet_cosmwasm::{ + pallet_hook::PalletHook, + runtimes::vm::{CosmwasmVM, CosmwasmVMError}, + types::{AccountIdOf, ContractLabelOf, ContractTrieIdOf, EntryPoint, PalletContractCodeInfo}, +}; +use parity_scale_codec::{Decode, DecodeLimit}; +use serde::{Deserialize, Serialize}; +use sp_core::H160; +use sp_runtime::traits::{AccountIdConversion, Dispatchable}; + +const ID: PalletId = PalletId(*b"dispatch"); +const DECODE_LIMIT: u32 = 8; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum ExecuteMsg { + Dispatch { input: Binary }, +} + +pub struct Precompiles(PhantomData); +impl PalletHook for Precompiles +where + T: pallet_cosmwasm::Config, + T: pallet_cosmos::Config, + T::RuntimeCall: Dispatchable + GetDispatchInfo + Decode, + ::RuntimeOrigin: From>, +{ + fn info( + contract_address: &AccountIdOf, + ) -> Option, ContractLabelOf, ContractTrieIdOf>> { + let dispatch = AccountIdConversion::::into_account_truncating(&ID); + + match contract_address { + address if address == &dispatch => Some(PalletContractCodeInfo::new( + dispatch, + false, + ID.0.to_vec().try_into().unwrap_or_default(), + )), + _ => None, + } + } + + fn execute<'a>( + vm: &mut OwnedWasmiVM>, + _entrypoint: EntryPoint, + message: &[u8], + ) -> Result< + ContractResult> as VMBase>::MessageCustom>>, + VmErrorOf>>, + > { + let contract_address = vm.0.data().contract_address.clone().into_inner(); + let dispatch = AccountIdConversion::::into_account_truncating(&ID); + match contract_address { + address if address == dispatch => { + if let Ok(ExecuteMsg::Dispatch { input }) = serde_json_wasm::from_slice(message) { + let call = T::RuntimeCall::decode_with_depth_limit(DECODE_LIMIT, &mut &*input) + .unwrap(); + + let sender = vm.0.data().cosmwasm_message_info.sender.clone().into_string(); + let (_hrp, address_raw) = acc_address_from_bech32(&sender) + .map_err(|_| CosmwasmVMError::AccountConvert)?; + if address_raw.len() != 20 { + return Err(CosmwasmVMError::AccountConvert); + } + let origin = T::AddressMapping::into_account_id(H160::from_slice(&address_raw)); + + call.dispatch(Some(origin).into()) + .map_err(|e| CosmwasmVMError::SubstrateDispatch(e.error))?; + + Ok(ContractResult::Ok(Response::new())) + } else { + Err(CosmwasmVMError::ExecuteDeserialize) + } + }, + _ => Err(CosmwasmVMError::ContractNotFound), + } + } + + fn run<'a>( + _vm: &mut OwnedWasmiVM>, + _entrypoint: EntryPoint, + _message: &[u8], + ) -> Result, VmErrorOf>>> { + Err(CosmwasmVMError::ContractNotFound) + } + + fn query<'a>( + _vm: &mut OwnedWasmiVM>, + _message: &[u8], + ) -> Result< + ContractResult, + VmErrorOf>>, + > { + Err(CosmwasmVMError::ContractNotFound) + } +} diff --git a/vendor/composable/cosmwasm/Cargo.toml b/vendor/composable/cosmwasm/Cargo.toml index 165b3a5a..4afcf404 100644 --- a/vendor/composable/cosmwasm/Cargo.toml +++ b/vendor/composable/cosmwasm/Cargo.toml @@ -37,7 +37,7 @@ pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = " pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-arithmetic = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } -sp-io ={ git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } composable-support = { workspace = true, default-features = false } @@ -56,6 +56,7 @@ cosmwasm-vm-wasmi = { workspace = true, default-features = false, features = [ ] } np-cosmos = { workspace = true, default-features = false } +pallet-cosmos-types = { workspace = true, default-features = false } [features] default = ["std"] @@ -88,5 +89,6 @@ std = [ "cosmwasm-vm/std", "cosmwasm-vm-wasmi/std", "np-cosmos/std", + "pallet-cosmos-types/std", ] try-runtime = [] diff --git a/vendor/composable/cosmwasm/src/lib.rs b/vendor/composable/cosmwasm/src/lib.rs index d2c3fb73..b26cc153 100644 --- a/vendor/composable/cosmwasm/src/lib.rs +++ b/vendor/composable/cosmwasm/src/lib.rs @@ -378,7 +378,7 @@ pub mod pallet { #[pallet::genesis_config] pub struct GenesisConfig { - pub contracts: Vec<(T::AccountIdExtended, ContractCodeOf)>, + pub contracts: Vec<(AccountIdOf, ContractCodeOf)>, } impl Default for GenesisConfig { diff --git a/ziggurat/runtime/src/configs/mod.rs b/ziggurat/runtime/src/configs/mod.rs index 59806b3c..fdb9b711 100644 --- a/ziggurat/runtime/src/configs/mod.rs +++ b/ziggurat/runtime/src/configs/mod.rs @@ -31,7 +31,7 @@ use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use fp_evm::weight_per_gas; use frame_babel::{ - cosmos, + cosmos::{self, precompile::Precompiles}, ethereum::{self, BabelPrecompiles, EnsureAddress}, extensions::unify_account, }; @@ -531,8 +531,7 @@ impl pallet_cosmwasm::Config for Runtime { type UnixTime = Timestamp; type WeightInfo = pallet_cosmwasm::weights::SubstrateWeight; - // TODO: Precompile to use execute or query pallet - type PalletHook = (); + type PalletHook = Precompiles; type UploadWasmOrigin = frame_system::EnsureSigned; From f1a6ca811c9507e712b1e30408a6f35d87e6729e Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Thu, 26 Sep 2024 21:59:14 +0900 Subject: [PATCH 090/133] chore: Add address mapping to genesis runtime --- frame/babel/Cargo.toml | 1 + frame/multimap/src/lib.rs | 40 ++++++++++++++++++++++++-- primitives/babel/Cargo.toml | 7 +++++ primitives/babel/src/lib.rs | 3 ++ primitives/cosmos/Cargo.toml | 8 +++++- primitives/cosmos/src/lib.rs | 50 +++++++++++++++++++++++++++++---- primitives/ethereum/Cargo.toml | 6 ++++ primitives/ethereum/src/lib.rs | 45 +++++++++++++++++++++++++++-- ziggurat/node/Cargo.toml | 2 ++ ziggurat/node/src/chain_spec.rs | 14 +++++++++ 10 files changed, 164 insertions(+), 12 deletions(-) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 35addd2a..18f1dfcb 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -120,6 +120,7 @@ ethereum = [ pallet = [ "cosmos", "ethereum", + "np-babel/serde", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", diff --git a/frame/multimap/src/lib.rs b/frame/multimap/src/lib.rs index 8abff9b6..836564d1 100644 --- a/frame/multimap/src/lib.rs +++ b/frame/multimap/src/lib.rs @@ -33,8 +33,11 @@ mod mock; mod tests; pub mod traits; +use alloc::vec::Vec; use core::{fmt::Debug, marker::PhantomData}; -use frame_support::{BoundedBTreeSet, StorageHasher}; +use frame_support::{ + sp_runtime::traits::MaybeSerializeDeserialize, BoundedBTreeSet, StorageHasher, +}; use parity_scale_codec::{FullCodec, MaxEncodedLen}; use scale_info::TypeInfo; @@ -50,14 +53,20 @@ pub mod pallet { pub trait Config: frame_system::Config { /// Type of the keys. #[pallet::no_default] - type Key: Clone + Debug + PartialEq + FullCodec + MaxEncodedLen + TypeInfo; + type Key: Clone + + Debug + + PartialEq + + FullCodec + + MaxEncodedLen + + TypeInfo + + MaybeSerializeDeserialize; /// Storage hasher for the keys. type KeyHasher: StorageHasher; /// Type of the values. #[pallet::no_default] - type Value: Clone + Ord + FullCodec + MaxEncodedLen + TypeInfo; + type Value: Clone + Ord + FullCodec + MaxEncodedLen + TypeInfo + MaybeSerializeDeserialize; /// Storage hasher for the values. type ValueHasher: StorageHasher; @@ -104,4 +113,29 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn find_key)] pub type Index, I: 'static = ()> = StorageMap<_, T::ValueHasher, T::Value, T::Key>; + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + multimap: Vec<(T::Key, Vec)>, + } + + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { multimap: Vec::new() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> BuildGenesisConfig for GenesisConfig { + fn build(&self) { + self.multimap.iter().for_each(|(key, values)| { + let mut set = BoundedBTreeSet::::new(); + values.iter().for_each(|value| { + Index::::insert(value, key); + let _ = set.try_insert(value.clone()); + }); + Map::::insert(key.clone(), set); + }); + } + } } diff --git a/primitives/babel/Cargo.toml b/primitives/babel/Cargo.toml index 04fed038..716bf5a2 100644 --- a/primitives/babel/Cargo.toml +++ b/primitives/babel/Cargo.toml @@ -12,6 +12,7 @@ np-cosmos = { workspace = true, default-features = false, optional = true } np-ethereum = { workspace = true, default-features = false, optional = true } parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } scale-info = { version = "2.11", default-features = false, features = ["derive"] } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } [features] @@ -19,6 +20,12 @@ default = ["std", "cosmos", "ethereum"] std = [ "np-cosmos?/std", "np-ethereum?/std", + "serde/std", +] +serde = [ + "dep:serde", + "np-cosmos?/serde", + "np-ethereum?/serde", ] cosmos = [ "np-cosmos", diff --git a/primitives/babel/src/lib.rs b/primitives/babel/src/lib.rs index c2ce43f8..f1c2d528 100644 --- a/primitives/babel/src/lib.rs +++ b/primitives/babel/src/lib.rs @@ -21,6 +21,8 @@ extern crate alloc; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use sp_core::ecdsa; #[cfg(feature = "cosmos")] @@ -34,6 +36,7 @@ pub use np_ethereum::Address as EthereumAddress; pub use sp_core::crypto::AccountId32; #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Address { Polkadot(AccountId32), #[cfg(feature = "cosmos")] diff --git a/primitives/cosmos/Cargo.toml b/primitives/cosmos/Cargo.toml index f3d46f43..e1799b7f 100644 --- a/primitives/cosmos/Cargo.toml +++ b/primitives/cosmos/Cargo.toml @@ -8,11 +8,12 @@ repository = "https://github.com/noirhq/noir.git" publish = false [dependencies] -bech32 = { version = "0.11", default-features = false } +bech32 = { version = "0.11", default-features = false, optional = true } buidl = { version = "0.1.1", default-features = false, features = ["derive"] } parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } ripemd = { version = "0.1", default-features = false } scale-info = { version = "2.11", default-features = false, features = ["derive"] } +serde = { version = "1.0", default-features = false, optional = true } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -28,7 +29,12 @@ std = [ "parity-scale-codec/std", "ripemd/std", "scale-info/std", + "serde/std", "sp-core/std", "sp-io/std", "sp-runtime/std", ] +serde = [ + "dep:serde", + "bech32", +] diff --git a/primitives/cosmos/src/lib.rs b/primitives/cosmos/src/lib.rs index 79750a65..ac278725 100644 --- a/primitives/cosmos/src/lib.rs +++ b/primitives/cosmos/src/lib.rs @@ -23,15 +23,18 @@ extern crate alloc; pub mod traits; -#[cfg(feature = "std")] +#[cfg(feature = "serde")] use crate::traits::ChainInfo; use crate::traits::CosmosHub; -#[cfg(feature = "std")] +use alloc::string::String; +#[cfg(feature = "serde")] use bech32::{Bech32, Hrp}; use buidl::FixedBytes; use core::marker::PhantomData; use parity_scale_codec::{Decode, Encode}; use ripemd::{Digest, Ripemd160}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use sp_core::{ecdsa, H160, H256}; use sp_io::hashing::{blake2_256, sha2_256}; use sp_runtime::traits::AccountIdConversion; @@ -61,9 +64,9 @@ impl From for Address { } } -#[cfg(feature = "std")] -impl std::fmt::Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +#[cfg(feature = "serde")] +impl core::fmt::Display for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { let hrp = Hrp::parse_unchecked(T::bech32_prefix()); write!(f, "{}", bech32::encode::(hrp, &self.0).expect("bech32 encode")) } @@ -75,6 +78,43 @@ impl core::fmt::Debug for Address { } } +#[cfg(feature = "serde")] +impl core::str::FromStr for Address { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let (hrp, data) = bech32::decode(s).map_err(|_| "bech32 decode")?; + if hrp.as_str() != T::bech32_prefix() { + return Err("invalid bech32 prefix"); + } + let data: [u8; 20] = data.try_into().map_err(|_| "invalid data length")?; + Ok(Self(data, PhantomData)) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use alloc::string::ToString; + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de, T: ChainInfo> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + use core::str::FromStr; + let s = String::deserialize(deserializer)?; + Address::from_str(&s).map_err(serde::de::Error::custom) + } +} + impl> AccountIdConversion for Address { fn into_account_truncating(&self) -> AccountId { let mut data = [0u8; 25]; diff --git a/primitives/ethereum/Cargo.toml b/primitives/ethereum/Cargo.toml index 6bbf1f46..22397db9 100644 --- a/primitives/ethereum/Cargo.toml +++ b/primitives/ethereum/Cargo.toml @@ -13,6 +13,7 @@ const-hex = { version = "1.12", default-features = false, optional = true } k256 = { version = "0.13", default-features = false } parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } scale-info = { version = "2.11", default-features = false, features = ["derive"] } +serde = { version = "1.0", default-features = false, optional = true } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } @@ -25,7 +26,12 @@ std = [ "k256/std", "parity-scale-codec/std", "scale-info/std", + "serde/std", "sp-core/std", "sp-io/std", "sp-runtime/std", ] +serde = [ + "dep:serde", + "const-hex/alloc", +] diff --git a/primitives/ethereum/src/lib.rs b/primitives/ethereum/src/lib.rs index eb9366b8..2d95d033 100644 --- a/primitives/ethereum/src/lib.rs +++ b/primitives/ethereum/src/lib.rs @@ -21,9 +21,13 @@ extern crate alloc; +#[cfg(feature = "serde")] +use alloc::string::String; use buidl::FixedBytes; use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; use parity_scale_codec::{Decode, Encode}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use sp_core::{ecdsa, H160, H256}; use sp_io::hashing::{blake2_256, keccak_256}; use sp_runtime::traits::AccountIdConversion; @@ -58,9 +62,10 @@ impl From for Address { } } -#[cfg(feature = "std")] -impl std::fmt::Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +#[cfg(feature = "serde")] +impl core::fmt::Display for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use alloc::string::ToString; let address = const_hex::encode(self.0); let address_hash = const_hex::encode(keccak_256(address.as_bytes())); @@ -85,12 +90,46 @@ impl std::fmt::Display for Address { } } +#[cfg(feature = "serde")] +impl core::str::FromStr for Address { + type Err = &'static str; + + // NOTE: For strict conversion, we should check the checksum. + fn from_str(s: &str) -> Result { + let data: [u8; 20] = const_hex::decode_to_array(s).map_err(|_| "invalid address")?; + Ok(Address(data)) + } +} + impl core::fmt::Debug for Address { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "{}", sp_core::hexdisplay::HexDisplay::from(&self.0)) } } +#[cfg(feature = "serde")] +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use alloc::string::ToString; + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use core::str::FromStr; + let s = String::deserialize(deserializer)?; + Address::from_str(&s).map_err(serde::de::Error::custom) + } +} + impl> AccountIdConversion for Address { fn into_account_truncating(&self) -> AccountId { let mut data = [0u8; 24]; diff --git a/ziggurat/node/Cargo.toml b/ziggurat/node/Cargo.toml index 902271e7..66cf3019 100644 --- a/ziggurat/node/Cargo.toml +++ b/ziggurat/node/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" clap = { version = "4.5", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.6" } color-print = "0.3" +const-hex = "1.12" docify = "0.2" futures = "0.3" jsonrpsee = { version = "0.23", features = ["server"] } @@ -23,6 +24,7 @@ log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +frame-babel = { workspace = true } ziggurat-runtime = { path = "../runtime", default-features = true } fc-api = { git = "https://github.com/polkadot-evm/frontier", branch = "stable2407" } diff --git a/ziggurat/node/src/chain_spec.rs b/ziggurat/node/src/chain_spec.rs index 77df3ae5..80a914ea 100644 --- a/ziggurat/node/src/chain_spec.rs +++ b/ziggurat/node/src/chain_spec.rs @@ -1,4 +1,5 @@ use cumulus_primitives_core::ParaId; +use frame_babel::Address; use runtime::{AccountId, AuraId, Signature, EXISTENTIAL_DEPOSIT, UNIT}; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; @@ -176,6 +177,12 @@ fn testnet_genesis( id: ParaId, ) -> serde_json::Value { serde_json::json!({ + "addressMap": { + "multimap": vec![ + address_map(get_from_seed::("Alice")), + address_map(ecdsa::Public::from(const_hex::const_decode_to_array(b"0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f").unwrap())), + ], + }, "balances": { "balances": endowed_accounts.iter().cloned().map(|k| (k, 1_000_000 * UNIT)).collect::>(), }, @@ -204,3 +211,10 @@ fn testnet_genesis( "sudo": { "key": Some(root) } }) } + +fn address_map(account: ecdsa::Public) -> (AccountId, Vec