diff --git a/Cargo.lock b/Cargo.lock index 6323f544d23..90222b5b4ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2709,6 +2709,7 @@ dependencies = [ "clap", "devault", "dialoguer", + "either", "forc", "forc-pkg", "forc-tracing 0.66.7", @@ -2731,18 +2732,22 @@ dependencies = [ "pretty_assertions", "rand", "regex", + "reqwest", "rexpect 0.5.0", "rpassword", "serde", "serde_json", + "sway-ast", "sway-core", "sway-features", + "sway-parse", "sway-types", "sway-utils", "tempfile", "tokio", "toml_edit", "tracing", + "url", ] [[package]] @@ -2967,6 +2972,7 @@ dependencies = [ "forc-util", "fuel-tx", "fuel-types", + "fuels-core", "serde", "serde_json", "thiserror 1.0.69", diff --git a/docs/book/spell-check-custom-words.txt b/docs/book/spell-check-custom-words.txt index 833d7ed01eb..ad51e822cb3 100644 --- a/docs/book/spell-check-custom-words.txt +++ b/docs/book/spell-check-custom-words.txt @@ -237,4 +237,8 @@ FuelLabs github toml hardcoded -subdirectories \ No newline at end of file +subdirectories +RawSlice +StringArray +StringSlice +calldata \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index afef1b6d6e0..f4ce92798fb 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -99,6 +99,7 @@ - [forc deploy](./forc/plugins/forc_client/forc_deploy.md) - [forc run](./forc/plugins/forc_client/forc_run.md) - [forc submit](./forc/plugins/forc_client/forc_submit.md) + - [forc call](./forc/plugins/forc_client/forc_call.md) - [forc crypto](./forc/plugins/forc_crypto.md) - [forc debug](./forc/plugins/forc_debug.md) - [forc doc](./forc/plugins/forc_doc.md) diff --git a/docs/book/src/forc/plugins/forc_client/forc_call.md b/docs/book/src/forc/plugins/forc_client/forc_call.md new file mode 100644 index 00000000000..77704c346f0 --- /dev/null +++ b/docs/book/src/forc/plugins/forc_client/forc_call.md @@ -0,0 +1,307 @@ +# Forc Call + +`forc-call` is a command-line tool for interacting with deployed Fuel contracts. It allows you to make contract calls, query contract state, and interact with any deployed contract on the Fuel network - all from your command line! + +The `forc call` command is part of the Forc toolchain and is installed alongside other Forc tools. + +## Getting Started + +Here are a few examples of what you can do with `forc call`: + +Call a simple addition function on a deployed contract (in dry-run mode): + +```bash +forc call 0xe18de7c7c8c61a1c706dccb3533caa00ba5c11b5230da4428582abf1b6831b4d \ + --abi ./out/debug/counter-contract-abi.json \ + add 1 2 +``` + +Query the owner of a deployed DEX contract on testnet: + +```bash +forc call \ + --testnet \ + --abi https://raw.githubusercontent.com/mira-amm/mira-v1-periphery/refs/heads/main/fixtures/mira-amm/mira_amm_contract-abi.json \ + 0xd5a716d967a9137222219657d7877bd8c79c64e1edb5de9f2901c98ebe74da80 \ + owner +``` + +## Usage + +The basic syntax for `forc call` is: + +```bash +forc call [OPTIONS] --abi [ARGS]... +``` + +Where the following arguments are required: + +- `CONTRACT_ID` is the ID of the deployed contract you want to interact with +- `ABI-PATH/URL` is the path or URL to the contract's JSON ABI file +- `SELECTOR` is the function name (selector) you want to call +- `ARGS` are the arguments to pass to the function + +## CLI Reference + +
+ Forc Call CLI reference + +```sh +forc call --help +``` + +```output +Perform Fuel RPC calls from the comfort of your command line + +Usage: forc call [OPTIONS] --abi [FUNCTION_ARGS]... + +Arguments: + + The contract ID to call + + + The function signature to call. When ABI is provided, this should be a selector (e.g. "transfer") When no ABI is provided, this should be the full function signature (e.g. "transfer(address,u64)") + + [FUNCTION_ARGS]... + Arguments to pass into the function to be called + +Options: + --abi + Path or URI to a JSON ABI file + + --node-url + The URL of the Fuel node to which we're submitting the transaction. If unspecified, checks the manifest's `network` table, then falls back to `http://127.0.0.1:4000` + + You can also use `--target`, `--testnet`, or `--mainnet` to specify the Fuel node. + + [env: FUEL_NODE_URL=] + + --target + Preset configurations for using a specific target. + + You can also use `--node-url`, `--testnet`, or `--mainnet` to specify the Fuel node. + + Possible values are: [local, testnet, mainnet] + + --mainnet + Use preset configuration for mainnet. + + You can also use `--node-url`, `--target`, or `--testnet` to specify the Fuel node. + + --testnet + Use preset configuration for testnet. + + You can also use `--node-url`, `--target`, or `--mainnet` to specify the Fuel node. + + --devnet + Use preset configuration for devnet. + + You can also use `--node-url`, `--target`, or `--testnet` to specify the Fuel node. + + --signing-key + Derive an account from a secret key to make the call + + [env: SIGNING_KEY=] + + --wallet + Use forc-wallet to make the call + + --amount + Amount of native assets to forward with the call + + [default: 0] + + --asset-id + Asset ID to forward with the call + + --gas-forwarded + Amount of gas to forward with the call + + --mode + The execution mode to use for the call; defaults to dry-run; possible values: dry-run, simulate, live + + [default: dry-run] + + --gas-price + Gas price for the transaction + + --script-gas-limit + Gas limit for the transaction + + --max-fee + Max fee for the transaction + + --tip + The tip for the transaction + + --external-contracts + The external contract addresses to use for the call If none are provided, the call will automatically populate external contracts by making a dry-run calls to the node, and extract the contract addresses based on the revert reason + + --output + The output format to use; possible values: default, raw + + [default: default] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version +``` + +
+ +## Type Encoding + +When passing arguments to contract functions, values are encoded according to their Sway types. +Here's how to format different types: + +| Types | Example input | Notes | +|-----------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------| +| bool | `true` or `false` | | +| u8, u16, u32, u64, u128, u256 | `42` | | +| b256 | `0x0000000000000000000000000000000000000000000000000000000000000042` or `0000000000000000000000000000000000000000000000000000000000000042` | `0x` prefix is optional | +| bytes, RawSlice | `0x42` or `42` | `0x` prefix is optional | +| String, StringSlice, StringArray (Fixed-size) | `"abc"` | | +| Tuple | `(42, true)` | The types in tuple can be different | +| Array (Fixed-size), Vector (Dynamic) | `[42, 128]` | The types in array or vector must be the same; i.e. you cannot have `[42, true]` | +| Struct | `{42, 128}` | Since structs are packed encoded, the attribute names are not encoded; i.e. `{42, 128}`; this could represent the following `struct Polygon { x: u64, y: u64 }` | +| Enum | `(Active: true)` or `(1: true)` | Enums are key-val pairs with keys as being variant name (case-sensitive) or variant index (starting from 0) and values as being the variant value; this could represent the following `enum MyEnum { Inactive, Active(bool) }` | + +## ABI Support + +The ABI (Application Binary Interface) can be provided in two ways. + +### Local file + +```bash +forc call --abi ./path/to/abi.json [ARGS...] +``` + +### Remote ABI file/URL + +```bash +forc call --abi https://example.com/abi.json [ARGS...] +``` + +## Network Configuration + +```bash +forc call --node-url http://127.0.0.1:4000 ... +# or +forc call --target local ... +``` + +## Advanced Usage + +### Using Wallets + +```sh +# utilising the forc-wallet +forc call --abi --wallet +``` + +```sh +# with an explicit signing key +forc call --abi --signing-key +``` + +### Asset Transfers + +```sh +# Native asset transfer +forc call --abi --amount 100 --live +``` + +```sh +# Custom asset transfer +forc call --abi \ + --amount 100 \ + --asset-id 0x1234... \ + --live +``` + +### Gas Configuration + +```sh +# Set gas price +forc call --abi --gas-price 1 + +# Forward gas to contract +forc call --abi --gas-forwarded 1000 + +# Set maximum fee +forc call --abi --max-fee 5000 +``` + +### Common Use Cases + +- 1. Contract State Queries + +```sh +# Read contract state +forc call --abi get_balance + +# Query with parameters +forc call --abi get_user_info 0x1234... +``` + +- 2. Token Operations + +```sh +# Check token balance +forc call --abi balance_of 0x1234... + +# Transfer tokens +forc call --abi transfer 0x1234... 100 --live +``` + +- 3. Contract Administration + +```sh +# Check contract owner +forc call --abi owner + +# Update contract parameters +forc call --abi update_params 42 --live +``` + +## Tips and Tricks + +- Use `--mode simulate` to estimate gas costs before making live transactions +- External contracts are automatically detected (via internal simulations), but can be manually specified with `--external-contracts` +- For complex parameter types (tuples, structs, enums), refer to the parameter types table above +- Always verify contract addresses and ABIs before making live calls +- Use environment variables for sensitive data like signing keys: `SIGNING_KEY=` + +## Troubleshooting + +### Common issues and solutions + +- **ABI Mismatch**: + - Ensure the ABI matches the deployed contract + - Verify function selectors match exactly + +- **Parameter Type Errors**: + - Check parameter formats in the types table + - Ensure correct number of parameters + +- **Network Issues**: + - Verify node connection + - Check network selection (testnet/mainnet) + +- **Transaction Failures**: + - Use simulation mode to debug + - Check gas settings + - Verify wallet has sufficient balance + +## Future Features + +The following features are planned for future releases: + +- Decode and display logs for contract calls +- Support direct transfer of asset(s) to addresses +- Function signature based calls without ABI +- Raw calldata input support +- Function selector completion +- Enhanced error messages, debugging, and logging (additional verbosity modes) diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index c3c5d5d57da..dde742c6da8 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -17,6 +17,7 @@ chrono = { workspace = true, features = ["std"] } clap = { workspace = true, features = ["derive", "env"] } devault.workspace = true dialoguer.workspace = true +either.workspace = true forc.workspace = true forc-pkg.workspace = true forc-tracing.workspace = true @@ -36,13 +37,18 @@ futures.workspace = true hex.workspace = true k256.workspace = true rand.workspace = true +regex.workspace = true +reqwest = { workspace = true } rpassword.workspace = true serde.workspace = true serde_json.workspace = true +sway-ast.workspace = true sway-core.workspace = true sway-features.workspace = true +sway-parse.workspace = true sway-types.workspace = true sway-utils.workspace = true +tempfile.workspace = true tokio = { workspace = true, features = [ "macros", "process", @@ -50,12 +56,12 @@ tokio = { workspace = true, features = [ ] } toml_edit.workspace = true tracing.workspace = true +url.workspace = true [dev-dependencies] portpicker = "0.1.1" pretty_assertions = "1.4.1" rexpect = "0.5" -tempfile = "3" [build-dependencies] regex = "1.5.4" @@ -72,5 +78,9 @@ path = "src/bin/run.rs" name = "forc-submit" path = "src/bin/submit.rs" +[[bin]] +name = "forc-call" +path = "src/bin/call.rs" + [lib] path = "src/lib.rs" diff --git a/forc-plugins/forc-client/src/bin/call.rs b/forc-plugins/forc-client/src/bin/call.rs new file mode 100644 index 00000000000..29d93ecb421 --- /dev/null +++ b/forc-plugins/forc-client/src/bin/call.rs @@ -0,0 +1,12 @@ +use clap::Parser; +use forc_tracing::{init_tracing_subscriber, println_error}; + +#[tokio::main] +async fn main() { + init_tracing_subscriber(Default::default()); + let command = forc_client::cmd::Call::parse(); + if let Err(err) = forc_client::op::call(command).await { + println_error(&format!("{}", err)); + std::process::exit(1); + } +} diff --git a/forc-plugins/forc-client/src/cmd/call.rs b/forc-plugins/forc-client/src/cmd/call.rs new file mode 100644 index 00000000000..4737b357684 --- /dev/null +++ b/forc-plugins/forc-client/src/cmd/call.rs @@ -0,0 +1,237 @@ +use crate::NodeTarget; +use clap::Parser; +use either::Either; +use fuel_crypto::SecretKey; +use fuels::programs::calls::CallParameters; +use fuels_core::types::{AssetId, ContractId}; +use std::{path::PathBuf, str::FromStr}; +use url::Url; + +pub use forc::cli::shared::{BuildOutput, BuildProfile, Minify, Pkg, Print}; +pub use forc_tx::{Gas, Maturity}; + +#[derive(Debug, Clone)] +pub enum FuncType { + Selector(String), + // TODO: add support for function signatures - without ABI files + // ↳ gh issue: https://github.com/FuelLabs/sway/issues/6886 + // Signature(String), +} + +impl Default for FuncType { + fn default() -> Self { + FuncType::Selector(String::new()) + } +} + +impl FromStr for FuncType { + type Err = String; + fn from_str(s: &str) -> Result { + let s = s.trim().replace(' ', ""); + if s.is_empty() { + return Err("Function signature cannot be empty".to_string()); + } + Ok(FuncType::Selector(s.to_string())) + } +} + +/// Flags for specifying the caller. +#[derive(Debug, Default, Clone, Parser, serde::Deserialize, serde::Serialize)] +pub struct Caller { + /// Derive an account from a secret key to make the call + #[clap(long, env = "SIGNING_KEY")] + pub signing_key: Option, + + /// Use forc-wallet to make the call + #[clap(long, default_value = "false")] + pub wallet: bool, +} + +#[derive(Debug, Default, Clone, Parser)] +pub struct CallParametersOpts { + /// Amount of native assets to forward with the call + #[clap(long, default_value = "0", alias = "value")] + pub amount: u64, + + /// Asset ID to forward with the call + #[clap(long)] + pub asset_id: Option, + + /// Amount of gas to forward with the call + #[clap(long)] + pub gas_forwarded: Option, +} + +impl From for CallParameters { + fn from(opts: CallParametersOpts) -> Self { + let mut params = CallParameters::default(); + if opts.amount != 0 { + params = params.with_amount(opts.amount); + } + if let Some(asset_id) = opts.asset_id { + params = params.with_asset_id(asset_id); + } + if let Some(gas) = opts.gas_forwarded { + params = params.with_gas_forwarded(gas); + } + params + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum ExecutionMode { + /// Execute a dry run - no state changes, no gas fees, wallet is not used or validated + #[default] + DryRun, + /// Execute in simulation mode - no state changes, estimates gas, wallet is used but not validated + /// State changes are not applied + Simulate, + /// Execute live on chain - state changes, gas fees apply, wallet is used and validated + /// State changes are applied + Live, +} + +impl FromStr for ExecutionMode { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dry-run" => Ok(ExecutionMode::DryRun), + "simulate" => Ok(ExecutionMode::Simulate), + "live" => Ok(ExecutionMode::Live), + _ => Err(format!("Invalid execution mode: {}", s)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum OutputFormat { + #[default] + Default, + Raw, +} + +impl FromStr for OutputFormat { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "default" => Ok(OutputFormat::Default), + "raw" => Ok(OutputFormat::Raw), + _ => Err(format!("Invalid output format: {}", s)), + } + } +} + +/// Perform Fuel RPC calls from the comfort of your command line. +#[derive(Debug, Parser, Clone)] +#[clap(bin_name = "forc call", version)] +#[clap(after_help = r#"EXAMPLES: + +# Call a contract with function parameters +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + get_balance 0x0087675439e10a8351b1d5e4cf9d0ea6da77675623ff6b16470b5e3c58998423 + +# Call a contract without function parameters +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + get_name + +# Call a contract that makes external contract calls +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + transfer 0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 \ + --contracts 0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 + +# Call a contract in simulation mode +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + add 1 2 \ + --mode simulate + +# Call a contract in dry-run mode on custom node URL using explicit signing-key +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --node-url "http://127.0.0.1:4000/v1/graphql" \ + --signing-key 0x... \ + --abi ./contract-abi.json \ + add 1 2 \ + --mode dry-run + +# Call a contract in live mode which performs state changes on testnet using forc-wallet +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --testnet \ + --wallet \ + --abi ./contract-abi.json \ + add 1 2 \ + --mode live + +# Call a contract payable function which transfers value of native asset on mainnet +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + transfer 0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 \ + --mode live \ + --amount 100 + +# Call a contract payable function which transfers value of custom asset +» forc call 0x0dcba78d7b09a1f77353f51367afd8b8ab94b5b2bb6c9437d9ba9eea47dede97 \ + --abi ./contract-abi.json \ + transfer 0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 \ + --amount 100 \ + --asset-id 0x0087675439e10a8351b1d5e4cf9d0ea6da77675623ff6b16470b5e3c58998423 \ + --live +"#)] +pub struct Command { + /// The contract ID to call. + pub contract_id: ContractId, + + /// Path or URI to a JSON ABI file. + #[clap(long, value_parser = parse_abi_path)] + pub abi: Either, + + /// The function selector to call. + /// The function selector is the name of the function to call (e.g. "transfer"). + /// It must be a valid selector present in the ABI file. + pub function: FuncType, + + /// Arguments to pass into the function to be called. + pub function_args: Vec, + + #[clap(flatten)] + pub node: NodeTarget, + + /// Select the caller to use for the call + #[clap(flatten)] + pub caller: Caller, + + /// Call parameters to use for the call + #[clap(flatten)] + pub call_parameters: CallParametersOpts, + + /// The execution mode to use for the call; defaults to dry-run; possible values: dry-run, simulate, live + #[clap(long, default_value = "dry-run")] + pub mode: ExecutionMode, + + /// The gas price to use for the call; defaults to 0 + #[clap(flatten)] + pub gas: Option, + + /// The external contract addresses to use for the call + /// If none are provided, the call will automatically populate external contracts by making a dry-run calls + /// to the node, and extract the contract addresses based on the revert reason + #[clap(long, alias = "contracts")] + pub external_contracts: Option>, + + /// The output format to use; possible values: default, raw + #[clap(long, default_value = "default")] + pub output: OutputFormat, +} + +fn parse_abi_path(s: &str) -> Result, String> { + if let Ok(url) = Url::parse(s) { + match url.scheme() { + "http" | "https" | "ipfs" => Ok(Either::Right(url)), + _ => Err(format!("Unsupported URL scheme: {}", url.scheme())), + } + } else { + Ok(Either::Left(PathBuf::from(s))) + } +} diff --git a/forc-plugins/forc-client/src/cmd/mod.rs b/forc-plugins/forc-client/src/cmd/mod.rs index 8fd96dbc05c..76c4667f970 100644 --- a/forc-plugins/forc-client/src/cmd/mod.rs +++ b/forc-plugins/forc-client/src/cmd/mod.rs @@ -1,7 +1,9 @@ +pub mod call; pub mod deploy; pub mod run; pub mod submit; +pub use call::Command as Call; pub use deploy::Command as Deploy; pub use run::Command as Run; pub use submit::Command as Submit; diff --git a/forc-plugins/forc-client/src/constants.rs b/forc-plugins/forc-client/src/constants.rs index 1b195f733e6..8470c9f4362 100644 --- a/forc-plugins/forc-client/src/constants.rs +++ b/forc-plugins/forc-client/src/constants.rs @@ -1,12 +1,15 @@ /// Default to localhost to favour the common case of testing. pub const NODE_URL: &str = sway_utils::constants::DEFAULT_NODE_URL; -pub const TESTNET_ENDPOINT_URL: &str = "https://testnet.fuel.network"; + pub const MAINNET_ENDPOINT_URL: &str = "https://mainnet.fuel.network"; +pub const TESTNET_ENDPOINT_URL: &str = "https://testnet.fuel.network"; +pub const DEVNET_ENDPOINT_URL: &str = "https://devnet.fuel.network"; pub const TESTNET_FAUCET_URL: &str = "https://faucet-testnet.fuel.network"; +pub const DEVNET_FAUCET_URL: &str = "https://faucet-devnet.fuel.network"; -pub const TESTNET_EXPLORER_URL: &str = "https://app-testnet.fuel.network"; pub const MAINNET_EXPLORER_URL: &str = "https://app.fuel.network"; +pub const TESTNET_EXPLORER_URL: &str = "https://app-testnet.fuel.network"; /// Default PrivateKey to sign transactions submitted to local node. pub const DEFAULT_PRIVATE_KEY: &str = diff --git a/forc-plugins/forc-client/src/lib.rs b/forc-plugins/forc-client/src/lib.rs index a4db6b262a1..83b0f6940c8 100644 --- a/forc-plugins/forc-client/src/lib.rs +++ b/forc-plugins/forc-client/src/lib.rs @@ -8,33 +8,114 @@ use serde::{Deserialize, Serialize}; use util::target::Target; /// Flags for specifying the node to target. -#[derive(Debug, Default, Parser, Deserialize, Serialize)] +#[derive(Debug, Default, Clone, Parser, Deserialize, Serialize)] pub struct NodeTarget { /// The URL of the Fuel node to which we're submitting the transaction. /// If unspecified, checks the manifest's `network` table, then falls back /// to `http://127.0.0.1:4000` /// - /// You can also use `--target`, `--testnet`, or `--mainnet` to specify the Fuel node. + /// You can also use `--target`, `--devnet`, `--testnet`, or `--mainnet` to specify the Fuel node. #[clap(long, env = "FUEL_NODE_URL")] pub node_url: Option, - /// Use preset configurations for deploying to a specific target. + /// Preset configurations for using a specific target. /// - /// You can also use `--node-url`, `--testnet`, or `--mainnet` to specify the Fuel node. + /// You can also use `--node-url`, `--devnet`, `--testnet`, or `--mainnet` to specify the Fuel node. /// /// Possible values are: [local, testnet, mainnet] #[clap(long)] pub target: Option, + /// Use preset configuration for mainnet. + /// + /// You can also use `--node-url`, `--target`, or `--testnet` to specify the Fuel node. + #[clap(long)] + pub mainnet: bool, + /// Use preset configuration for testnet. /// /// You can also use `--node-url`, `--target`, or `--mainnet` to specify the Fuel node. #[clap(long)] pub testnet: bool, - /// Use preset configuration for mainnet. + /// Use preset configuration for devnet. /// /// You can also use `--node-url`, `--target`, or `--testnet` to specify the Fuel node. #[clap(long)] - pub mainnet: bool, + pub devnet: bool, +} + +impl NodeTarget { + /// Returns the URL for explorer + pub fn get_explorer_url(&self) -> Option { + match ( + self.testnet, + self.mainnet, + self.devnet, + self.target.clone(), + self.node_url.clone(), + ) { + (true, false, _, None, None) => Target::testnet().explorer_url(), + (false, true, _, None, None) => Target::mainnet().explorer_url(), + (false, false, _, Some(target), None) => target.explorer_url(), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_explorer_url_mainnet() { + let node = NodeTarget { + target: Some(Target::Mainnet), + node_url: None, + mainnet: false, + testnet: false, + devnet: false, + }; + let actual = node.get_explorer_url().unwrap(); + assert_eq!("https://app.fuel.network", actual); + } + + #[test] + fn test_get_explorer_url_testnet() { + let node = NodeTarget { + target: Some(Target::Testnet), + node_url: None, + mainnet: false, + testnet: false, + devnet: false, + }; + let actual = node.get_explorer_url().unwrap(); + assert_eq!("https://app-testnet.fuel.network", actual); + } + + #[test] + fn test_get_explorer_url_devnet() { + let node = NodeTarget { + target: Some(Target::Devnet), + node_url: None, + mainnet: false, + testnet: false, + devnet: true, + }; + let actual = node.get_explorer_url(); + assert_eq!(None, actual); + } + + #[test] + fn test_get_explorer_url_local() { + let node = NodeTarget { + target: Some(Target::Local), + node_url: None, + mainnet: false, + testnet: false, + devnet: false, + }; + let actual = node.get_explorer_url(); + assert_eq!(None, actual); + } } diff --git a/forc-plugins/forc-client/src/op/call/missing_contracts.rs b/forc-plugins/forc-client/src/op/call/missing_contracts.rs new file mode 100644 index 00000000000..c9e6b14e893 --- /dev/null +++ b/forc-plugins/forc-client/src/op/call/missing_contracts.rs @@ -0,0 +1,70 @@ +use anyhow::{bail, Result}; +use fuel_tx::{PanicReason, Receipt}; +use fuels::programs::calls::{traits::TransactionTuner, ContractCall}; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked}; +use fuels_core::types::{ + bech32::Bech32ContractId, transaction::TxPolicies, transaction_builders::VariableOutputPolicy, +}; + +/// Get the missing contracts from a contract call by dry-running the transaction +/// to find contracts that are not explicitly listed in the call's `external_contracts` field. +pub async fn get_missing_contracts( + mut call: ContractCall, + provider: &Provider, + tx_policies: &TxPolicies, + variable_output_policy: &VariableOutputPolicy, + log_decoder: &fuels_core::codec::LogDecoder, + account: &WalletUnlocked, + max_attempts: Option, +) -> Result> { + let max_attempts = max_attempts.unwrap_or(10); + + for attempt in 1..=max_attempts { + forc_tracing::println_warning(&format!( + "Executing dry-run attempt {} to find missing contracts", + attempt + )); + + let tx = call + .build_tx(*tx_policies, *variable_output_policy, account) + .await?; + + match provider + .dry_run(tx) + .await? + .take_receipts_checked(Some(log_decoder)) + { + Ok(_) => return Ok(call.external_contracts), + Err(fuels_core::types::errors::Error::Transaction( + fuels::types::errors::transaction::Reason::Reverted { receipts, .. }, + )) => { + let contract_id = find_id_of_missing_contract(&receipts)?; + call.external_contracts.push(contract_id); + } + Err(err) => bail!(err), + } + } + bail!("Max attempts reached while finding missing contracts") +} + +fn find_id_of_missing_contract(receipts: &[Receipt]) -> Result { + for receipt in receipts { + match receipt { + Receipt::Panic { + reason, + contract_id, + .. + } if *reason.reason() == PanicReason::ContractNotInInputs => { + let contract_id = contract_id + .expect("panic caused by a contract not in inputs must have a contract id"); + return Ok(Bech32ContractId::from(contract_id)); + } + Receipt::Panic { reason, .. } => { + // If it's a panic but not ContractNotInInputs, include the reason + bail!("Contract execution panicked with reason: {:?}", reason); + } + _ => continue, + } + } + bail!("No contract found in receipts: {:?}", receipts) +} diff --git a/forc-plugins/forc-client/src/op/call/mod.rs b/forc-plugins/forc-client/src/op/call/mod.rs new file mode 100644 index 00000000000..cec76a7adfb --- /dev/null +++ b/forc-plugins/forc-client/src/op/call/mod.rs @@ -0,0 +1,721 @@ +mod missing_contracts; +mod parser; + +use crate::{ + cmd, + constants::DEFAULT_PRIVATE_KEY, + op::call::{ + missing_contracts::get_missing_contracts, + parser::{param_type_val_to_token, token_to_string}, + }, + util::{ + node_url::get_node_url, + tx::{prompt_forc_wallet_password, select_local_wallet_account}, + }, +}; +use anyhow::{anyhow, bail, Result}; +use either::Either; +use fuel_abi_types::abi::unified_program::UnifiedProgramABI; +use fuels::{ + accounts::{provider::Provider, wallet::WalletUnlocked}, + crypto::SecretKey, + programs::calls::{ + receipt_parser::ReceiptParser, + traits::{ContractDependencyConfigurator, TransactionTuner}, + ContractCall, + }, +}; +use fuels_core::{ + codec::{ + encode_fn_selector, log_formatters_lookup, ABIDecoder, ABIEncoder, DecoderConfig, + EncoderConfig, LogDecoder, + }, + types::{ + bech32::Bech32ContractId, + param_types::ParamType, + transaction::{Transaction, TxPolicies}, + transaction_builders::{BuildableTransaction, ScriptBuildStrategy, VariableOutputPolicy}, + ContractId, + }, +}; +use std::{collections::HashMap, str::FromStr}; + +/// A command for calling a contract function. +pub async fn call(cmd: cmd::Call) -> anyhow::Result { + let cmd::Call { + contract_id, + abi, + function, + function_args, + node, + caller, + call_parameters, + mode, + gas, + external_contracts, + output, + } = cmd; + let node_url = get_node_url(&node, &None)?; + let provider: Provider = Provider::connect(node_url).await?; + + let wallet = get_wallet(caller.signing_key, caller.wallet, provider).await?; + + let cmd::call::FuncType::Selector(selector) = function; + let abi_str = match abi { + // TODO: add support for fetching verified ABI from registry (forc.pub) + // - This should be the default behaviour if no ABI is provided + // ↳ gh issue: https://github.com/FuelLabs/sway/issues/6893 + Either::Left(path) => std::fs::read_to_string(&path)?, + Either::Right(url) => { + let response = reqwest::get(url).await?.bytes().await?; + String::from_utf8(response.to_vec())? + } + }; + let parsed_abi = UnifiedProgramABI::from_json_abi(&abi_str)?; + + let type_lookup = parsed_abi + .types + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // get the function selector from the abi + let abi_func = parsed_abi + .functions + .iter() + .find(|abi_func| abi_func.name == selector) + .ok_or_else(|| anyhow!("Function '{}' not found in ABI", selector))?; + + if abi_func.inputs.len() != function_args.len() { + bail!("Number of arguments does not match number of parameters in function signature; expected {}, got {}", abi_func.inputs.len(), function_args.len()); + } + + let tokens = abi_func + .inputs + .iter() + .zip(&function_args) + .map(|(type_application, arg)| { + let param_type = ParamType::try_from_type_application(type_application, &type_lookup) + .expect("Failed to convert input type application"); + param_type_val_to_token(¶m_type, arg) + }) + .collect::>>()?; + + let output_param = ParamType::try_from_type_application(&abi_func.output, &type_lookup) + .expect("Failed to convert output type"); + + let abi_encoder = ABIEncoder::new(EncoderConfig::default()); + let encoded_data = abi_encoder.encode(&tokens)?; + + // Create and execute call + let call = ContractCall { + contract_id: contract_id.into(), + encoded_selector: encode_fn_selector(&selector), + encoded_args: Ok(encoded_data), + call_parameters: call_parameters.clone().into(), + external_contracts: vec![], // set below + output_param: output_param.clone(), + is_payable: call_parameters.amount > 0, + custom_assets: Default::default(), + }; + + let provider = wallet.provider().unwrap(); + // TODO: add support for decoding logs and viewing them in output (verbose mode) + // ↳ gh issue: https://github.com/FuelLabs/sway/issues/6887 + let log_decoder = LogDecoder::new(log_formatters_lookup(vec![], contract_id)); + + let tx_policies = gas + .as_ref() + .map(Into::into) + .unwrap_or(TxPolicies::default()); + let variable_output_policy = VariableOutputPolicy::Exactly(call_parameters.amount as usize); + let external_contracts = match external_contracts { + Some(external_contracts) => external_contracts + .iter() + .map(|addr| Bech32ContractId::from(*addr)) + .collect(), + None => { + // Automatically retrieve missing contract addresses from the call - by simulating the call + // and checking for missing contracts in the receipts + // This makes the CLI more ergonomic + let external_contracts = get_missing_contracts( + call.clone(), + provider, + &tx_policies, + &variable_output_policy, + &log_decoder, + &wallet, + None, + ) + .await?; + if !external_contracts.is_empty() { + forc_tracing::println_warning( + "Automatically provided external contract addresses with call (max 10):", + ); + external_contracts.iter().for_each(|addr| { + forc_tracing::println_warning(&format!("- 0x{}", ContractId::from(addr))); + }); + } + external_contracts + } + }; + + let chain_id = provider.consensus_parameters().await?.chain_id(); + let (tx_status, tx_hash) = match mode { + cmd::call::ExecutionMode::DryRun => { + let tx = call + .with_external_contracts(external_contracts) + .build_tx(tx_policies, variable_output_policy, &wallet) + .await + .expect("Failed to build transaction"); + let tx_hash = tx.id(chain_id); + let tx_status = provider + .dry_run(tx) + .await + .expect("Failed to dry run transaction"); + (tx_status, tx_hash) + } + cmd::call::ExecutionMode::Simulate => { + forc_tracing::println_warning(&format!( + "Simulating transaction with wallet... {}", + wallet.address().hash() + )); + let tx = call + .with_external_contracts(external_contracts) + .transaction_builder(tx_policies, variable_output_policy, &wallet) + .await + .expect("Failed to build transaction") + .with_build_strategy(ScriptBuildStrategy::StateReadOnly) + .build(provider) + .await?; + let tx_hash = tx.id(chain_id); + let gas_price = gas.map(|g| g.price).unwrap_or(Some(0)); + let tx_status = provider + .dry_run_opt(tx, false, gas_price) + .await + .expect("Failed to simulate transaction"); + (tx_status, tx_hash) + } + cmd::call::ExecutionMode::Live => { + forc_tracing::println_action_green( + "Sending transaction with wallet", + &format!("0x{}", wallet.address().hash()), + ); + let tx = call + .with_external_contracts(external_contracts) + .build_tx(tx_policies, variable_output_policy, &wallet) + .await + .expect("Failed to build transaction"); + let tx_hash = tx.id(chain_id); + let tx_status = provider + .send_transaction_and_await_commit(tx) + .await + .expect("Failed to send transaction"); + (tx_status, tx_hash) + } + }; + + let receipts = tx_status + .take_receipts_checked(Some(&log_decoder)) + .expect("Failed to take receipts"); + + let mut receipt_parser = ReceiptParser::new(&receipts, DecoderConfig::default()); + let result = match output { + cmd::call::OutputFormat::Default => { + let data = receipt_parser + .extract_contract_call_data(contract_id) + .expect("Failed to extract contract call data"); + ABIDecoder::default() + .decode_as_debug_str(&output_param, data.as_slice()) + .expect("Failed to decode as debug string") + } + cmd::call::OutputFormat::Raw => { + let token = receipt_parser + .parse_call(&Bech32ContractId::from(contract_id), &output_param) + .expect("Failed to extract contract call data"); + token_to_string(&token).expect("Failed to convert token to string") + } + }; + + forc_tracing::println_action_green("receipts:", &format!("{:#?}", receipts)); + forc_tracing::println_action_green("tx hash:", &tx_hash.to_string()); + forc_tracing::println_action_green("result:", &result); + + // display transaction url if live mode + if cmd::call::ExecutionMode::Live == mode { + if let Some(explorer_url) = node.get_explorer_url() { + forc_tracing::println_action_green( + "\nView transaction:", + &format!("{}/tx/0x{}", explorer_url, tx_hash), + ); + } + } + + Ok(result) +} + +/// Get the wallet to use for the call - based on optionally provided signing key and wallet flag. +async fn get_wallet( + signing_key: Option, + use_wallet: bool, + provider: Provider, +) -> Result { + match (signing_key, use_wallet) { + (None, false) => { + let secret_key = SecretKey::from_str(DEFAULT_PRIVATE_KEY).unwrap(); + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + forc_tracing::println_warning(&format!( + "No signing key or wallet flag provided. Using default signer: 0x{}", + wallet.address().hash() + )); + Ok(wallet) + } + (Some(secret_key), false) => { + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + forc_tracing::println_warning(&format!( + "Using account {} derived from signing key...", + wallet.address().hash() + )); + Ok(wallet) + } + (None, true) => { + let password = prompt_forc_wallet_password()?; + let wallet = select_local_wallet_account(&password, &provider).await?; + Ok(wallet) + } + (Some(secret_key), true) => { + forc_tracing::println_warning( + "Signing key is provided while requesting to use forc-wallet. Using signing key...", + ); + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + Ok(wallet) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fuels::accounts::wallet::Wallet; + use fuels::prelude::*; + + abigen!(Contract( + name = "TestContract", + abi = "forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json" + )); + + async fn get_contract_instance() -> (TestContract, ContractId, WalletUnlocked) { + // Launch a local network and deploy the contract + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new( + Some(1), /* Single wallet */ + Some(1), /* Single coin (UTXO) */ + Some(1_000_000_000), /* Amount per coin */ + ), + None, + None, + ) + .await + .unwrap(); + let wallet = wallets.pop().unwrap(); + + let id = Contract::load_from( + "../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types.bin", + LoadConfiguration::default(), + ) + .unwrap() + .deploy(&wallet, TxPolicies::default()) + .await + .unwrap(); + + let instance = TestContract::new(id.clone(), wallet.clone()); + + (instance, id.into(), wallet) + } + + fn get_contract_call_cmd( + id: ContractId, + wallet: &WalletUnlocked, + selector: &str, + args: Vec<&str>, + ) -> cmd::Call { + // get secret key from wallet - use unsafe because secret_key is private + // 0000000000000000000000000000000000000000000000000000000000000001 + let secret_key = + unsafe { std::mem::transmute::<&WalletUnlocked, &(Wallet, SecretKey)>(wallet).1 }; + cmd::Call { + contract_id: id, + abi: Either::Left(std::path::PathBuf::from( + "../../forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json", + )), + function: cmd::call::FuncType::Selector(selector.into()), + function_args: args.into_iter().map(String::from).collect(), + node: crate::NodeTarget { + node_url: Some(wallet.provider().unwrap().url().to_owned()), + ..Default::default() + }, + caller: cmd::call::Caller { + signing_key: Some(secret_key), + wallet: false, + }, + call_parameters: Default::default(), + mode: cmd::call::ExecutionMode::DryRun, + gas: None, + external_contracts: None, + output: cmd::call::OutputFormat::Raw, + } + } + + #[tokio::test] + async fn contract_call_with_abi() { + let (_, id, wallet) = get_contract_instance().await; + + // test_empty_no_return + let cmd = get_contract_call_cmd(id, &wallet, "test_empty_no_return", vec![]); + assert_eq!(call(cmd).await.unwrap(), "()"); + + // test_empty + let cmd = get_contract_call_cmd(id, &wallet, "test_empty", vec![]); + assert_eq!(call(cmd).await.unwrap(), "()"); + + // test_unit + let cmd = get_contract_call_cmd(id, &wallet, "test_unit", vec!["()"]); + assert_eq!(call(cmd).await.unwrap(), "()"); + + // test_u8 + let cmd = get_contract_call_cmd(id, &wallet, "test_u8", vec!["255"]); + assert_eq!(call(cmd).await.unwrap(), "255"); + + // test_u16 + let cmd = get_contract_call_cmd(id, &wallet, "test_u16", vec!["65535"]); + assert_eq!(call(cmd).await.unwrap(), "65535"); + + // test_u32 + let cmd = get_contract_call_cmd(id, &wallet, "test_u32", vec!["4294967295"]); + assert_eq!(call(cmd).await.unwrap(), "4294967295"); + + // test_u64 + let cmd = get_contract_call_cmd(id, &wallet, "test_u64", vec!["18446744073709551615"]); + assert_eq!(call(cmd).await.unwrap(), "18446744073709551615"); + + // test_u128 + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_u128", + vec!["340282366920938463463374607431768211455"], + ); + assert_eq!( + call(cmd).await.unwrap(), + "340282366920938463463374607431768211455" + ); + + // test_u256 + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_u256", + vec!["115792089237316195423570985008687907853269984665640564039457584007913129639935"], + ); + assert_eq!( + call(cmd).await.unwrap(), + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + + // test b256 + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_b256", + vec!["0000000000000000000000000000000000000000000000000000000000000042"], + ); + assert_eq!( + call(cmd).await.unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + ); + + // test_b256 - fails if 0x prefix provided since it extracts input as an external contract; we don't want to do this so explicitly provide the external contract as empty + let mut cmd = get_contract_call_cmd( + id, + &wallet, + "test_b256", + vec!["0x0000000000000000000000000000000000000000000000000000000000000042"], + ); + cmd.external_contracts = Some(vec![]); + assert_eq!( + call(cmd).await.unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + ); + + // test_bytes + let cmd = get_contract_call_cmd(id, &wallet, "test_bytes", vec!["0x42"]); + assert_eq!(call(cmd).await.unwrap(), "0x42"); + + // test bytes without 0x prefix + let cmd = get_contract_call_cmd(id, &wallet, "test_bytes", vec!["42"]); + assert_eq!(call(cmd).await.unwrap(), "0x42"); + + // test_str + let cmd = get_contract_call_cmd(id, &wallet, "test_str", vec!["fuel"]); + assert_eq!(call(cmd).await.unwrap(), "fuel"); + + // test str array + let cmd = get_contract_call_cmd(id, &wallet, "test_str_array", vec!["fuel rocks"]); + assert_eq!(call(cmd).await.unwrap(), "fuel rocks"); + + // test str array - fails if length mismatch + let cmd = get_contract_call_cmd(id, &wallet, "test_str_array", vec!["fuel"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "string array length mismatch: expected 10, got 4" + ); + + // test str slice + let cmd = get_contract_call_cmd(id, &wallet, "test_str_slice", vec!["fuel rocks 42"]); + assert_eq!(call(cmd).await.unwrap(), "fuel rocks 42"); + + // test tuple + let cmd = get_contract_call_cmd(id, &wallet, "test_tuple", vec!["(42, true)"]); + assert_eq!(call(cmd).await.unwrap(), "(42, true)"); + + // test array + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_array", + vec!["[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]"], + ); + assert_eq!( + call(cmd).await.unwrap(), + "[42, 42, 42, 42, 42, 42, 42, 42, 42, 42]" + ); + + // test_array - fails if different types + let cmd = get_contract_call_cmd(id, &wallet, "test_array", vec!["[42, true]"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "failed to parse u64 value: true" + ); + + // test_array - succeeds if length not matched!? + let cmd = get_contract_call_cmd(id, &wallet, "test_array", vec!["[42, 42]"]); + assert_eq!( + call(cmd).await.unwrap(), + "[42, 42, 0, 4718592, 65536, 65536, 0, 0, 0, 0]" + ); + + // test_vector + let cmd = get_contract_call_cmd(id, &wallet, "test_vector", vec!["[42, 42]"]); + assert_eq!(call(cmd).await.unwrap(), "[42, 42]"); + + // test_vector - fails if different types + let cmd = get_contract_call_cmd(id, &wallet, "test_vector", vec!["[42, true]"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "failed to parse u64 value: true" + ); + + // test_struct - Identity { name: str[2], id: u64 } + let cmd = get_contract_call_cmd(id, &wallet, "test_struct", vec!["{fu, 42}"]); + assert_eq!(call(cmd).await.unwrap(), "{fu, 42}"); + + // test_struct - fails if incorrect inner attribute length + let cmd = get_contract_call_cmd(id, &wallet, "test_struct", vec!["{fuel, 42}"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "string array length mismatch: expected 2, got 4" + ); + + // test_struct - succeeds if missing inner final attribute; default value is used + let cmd = get_contract_call_cmd(id, &wallet, "test_struct", vec!["{fu}"]); + assert_eq!(call(cmd).await.unwrap(), "{fu, 0}"); + + // test_struct - succeeds to use default values for all attributes if missing + let cmd = get_contract_call_cmd(id, &wallet, "test_struct", vec!["{}"]); + assert_eq!(call(cmd).await.unwrap(), "{\0\0, 0}"); + + // test_enum + let cmd = get_contract_call_cmd(id, &wallet, "test_enum", vec!["(Active:true)"]); + assert_eq!(call(cmd).await.unwrap(), "(Active:true)"); + + // test_enum - succeeds if using index + let cmd = get_contract_call_cmd(id, &wallet, "test_enum", vec!["(1:56)"]); + assert_eq!(call(cmd).await.unwrap(), "(Pending:56)"); + + // test_enum - fails if variant not found + let cmd = get_contract_call_cmd(id, &wallet, "test_enum", vec!["(A:true)"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "failed to find index of variant: A" + ); + + // test_enum - fails if variant value incorrect + let cmd = get_contract_call_cmd(id, &wallet, "test_enum", vec!["(Active:3)"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "failed to parse `Active` variant enum value: 3" + ); + + // test_enum - fails if variant value is missing + let cmd = get_contract_call_cmd(id, &wallet, "test_enum", vec!["(Active:)"]); + assert_eq!( + call(cmd).await.unwrap_err().to_string(), + "enum must have exactly two parts `(variant:value)`: (Active:)" + ); + + // test_option - encoded like an enum + let cmd = get_contract_call_cmd(id, &wallet, "test_option", vec!["(0:())"]); + assert_eq!(call(cmd).await.unwrap(), "(None:())"); + + // test_option - encoded like an enum; none value ignored + let cmd = get_contract_call_cmd(id, &wallet, "test_option", vec!["(0:42)"]); + assert_eq!(call(cmd).await.unwrap(), "(None:())"); + + // test_option - encoded like an enum; some value + let cmd = get_contract_call_cmd(id, &wallet, "test_option", vec!["(1:42)"]); + assert_eq!(call(cmd).await.unwrap(), "(Some:42)"); + } + + #[tokio::test] + async fn contract_call_with_abi_complex() { + let (_, id, wallet) = get_contract_instance().await; + + // test_complex_struct + let cmd = + get_contract_call_cmd(id, &wallet, "test_struct_with_generic", vec!["{42, fuel}"]); + assert_eq!(call(cmd).await.unwrap(), "{42, fuel}"); + + // test_enum_with_generic + let cmd = get_contract_call_cmd(id, &wallet, "test_enum_with_generic", vec!["(value:32)"]); + assert_eq!(call(cmd).await.unwrap(), "(value:32)"); + + // test_enum_with_complex_generic + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_enum_with_complex_generic", + vec!["(value:{42, fuel})"], + ); + assert_eq!(call(cmd).await.unwrap(), "(value:{42, fuel})"); + + let cmd = get_contract_call_cmd( + id, + &wallet, + "test_enum_with_complex_generic", + vec!["(container:{{42, fuel}, fuel})"], + ); + assert_eq!(call(cmd).await.unwrap(), "(container:{{42, fuel}, fuel})"); + } + + #[tokio::test] + async fn contract_value_forwarding() { + let (_, id, wallet) = get_contract_instance().await; + + let provider = wallet.provider().unwrap(); + let consensus_parameters = provider.consensus_parameters().await.unwrap(); + let base_asset_id = consensus_parameters.base_asset_id(); + let get_recipient_balance = |addr: Bech32Address| async move { + provider + .get_asset_balance(&addr, *base_asset_id) + .await + .unwrap() + }; + let get_contract_balance = |id: ContractId| async move { + provider + .get_contract_asset_balance(&Bech32ContractId::from(id), *base_asset_id) + .await + .unwrap() + }; + + // contract call transfer funds to another contract + let (_, id_2, _) = get_contract_instance().await; + let (amount, asset_id, recipient) = ( + "1", + &format!("{{0x{}}}", base_asset_id), + &format!("(ContractId:{{0x{}}})", id_2), + ); + let mut cmd = + get_contract_call_cmd(id, &wallet, "transfer", vec![amount, asset_id, recipient]); + cmd.call_parameters = cmd::call::CallParametersOpts { + amount: amount.parse::().unwrap(), + asset_id: Some(*base_asset_id), + gas_forwarded: None, + }; + // validate balance is unchanged (dry-run) + assert_eq!(call(cmd.clone()).await.unwrap(), "()"); + assert_eq!(get_contract_balance(id_2).await, 0); + cmd.mode = cmd::call::ExecutionMode::Live; + assert_eq!(call(cmd).await.unwrap(), "()"); + assert_eq!(get_contract_balance(id_2).await, 1); + assert_eq!(get_contract_balance(id).await, 1); + + // contract call transfer funds to another address + let random_wallet = WalletUnlocked::new_random(None); + let (amount, asset_id, recipient) = ( + "2", + &format!("{{0x{}}}", base_asset_id), + &format!("(Address:{{0x{}}})", random_wallet.address().hash()), + ); + let mut cmd = + get_contract_call_cmd(id, &wallet, "transfer", vec![amount, asset_id, recipient]); + cmd.call_parameters = cmd::call::CallParametersOpts { + amount: amount.parse::().unwrap(), + asset_id: Some(*base_asset_id), + gas_forwarded: None, + }; + cmd.mode = cmd::call::ExecutionMode::Live; + assert_eq!(call(cmd).await.unwrap(), "()"); + assert_eq!( + get_recipient_balance(random_wallet.address().clone()).await, + 2 + ); + assert_eq!(get_contract_balance(id).await, 1); + + // contract call transfer funds to another address + // specify amount x, provide amount x - 1 + // fails with panic reason 'NotEnoughBalance' + let random_wallet = WalletUnlocked::new_random(None); + let (amount, asset_id, recipient) = ( + "5", + &format!("{{0x{}}}", base_asset_id), + &format!("(Address:{{0x{}}})", random_wallet.address().hash()), + ); + let mut cmd = + get_contract_call_cmd(id, &wallet, "transfer", vec![amount, asset_id, recipient]); + cmd.call_parameters = cmd::call::CallParametersOpts { + amount: amount.parse::().unwrap() - 3, + asset_id: Some(*base_asset_id), + gas_forwarded: None, + }; + cmd.mode = cmd::call::ExecutionMode::Live; + assert!(call(cmd) + .await + .unwrap_err() + .to_string() + .contains("PanicInstruction { reason: NotEnoughBalance")); + assert_eq!(get_contract_balance(id).await, 1); + + // contract call transfer funds to another address + // specify amount x, provide amount x + 5; should succeed + let random_wallet = WalletUnlocked::new_random(None); + let (amount, asset_id, recipient) = ( + "3", + &format!("{{0x{}}}", base_asset_id), + &format!("(Address:{{0x{}}})", random_wallet.address().hash()), + ); + let mut cmd = + get_contract_call_cmd(id, &wallet, "transfer", vec![amount, asset_id, recipient]); + cmd.call_parameters = cmd::call::CallParametersOpts { + amount: amount.parse::().unwrap() + 5, + asset_id: Some(*base_asset_id), + gas_forwarded: None, + }; + cmd.mode = cmd::call::ExecutionMode::Live; + assert_eq!(call(cmd).await.unwrap(), "()"); + assert_eq!( + get_recipient_balance(random_wallet.address().clone()).await, + 3 + ); + assert_eq!(get_contract_balance(id).await, 6); // extra amount (5) is forwarded to the contract + } +} diff --git a/forc-plugins/forc-client/src/op/call/parser.rs b/forc-plugins/forc-client/src/op/call/parser.rs new file mode 100644 index 00000000000..471d6465291 --- /dev/null +++ b/forc-plugins/forc-client/src/op/call/parser.rs @@ -0,0 +1,1557 @@ +use anyhow::{anyhow, bail, Result}; +use fuels_core::types::{param_types::ParamType, EnumSelector, StaticStringToken, Token, U256}; +use std::{fmt::Write, str::FromStr}; + +/// Converts a ParamType and associated value into a Token +pub fn param_type_val_to_token(param_type: &ParamType, input: &str) -> Result { + // Parses a string value while preserving quotes and escaped characters + let parse_string_value = |input: &str| { + if input.starts_with('"') && input.ends_with('"') { + // Remove outer quotes and unescape internal quotes + let without_outer_quotes = &input[1..input.len() - 1]; + without_outer_quotes.replace("\\\"", "\"") + } else { + // If no quotes, just trim whitespace + input.trim().to_string() + } + }; + + match param_type { + ParamType::Unit => Ok(Token::Unit), + ParamType::Bool => bool::from_str(input) + .map(Token::Bool) + .map_err(|_| anyhow!("failed to parse bool value: {}", input)), + ParamType::U8 => u8::from_str(input) + .map(Token::U8) + .map_err(|_| anyhow!("failed to parse u8 value: {}", input)), + ParamType::U16 => u16::from_str(input) + .map(Token::U16) + .map_err(|_| anyhow!("failed to parse u16 value: {}", input)), + ParamType::U32 => u32::from_str(input) + .map(Token::U32) + .map_err(|_| anyhow!("failed to parse u32 value: {}", input)), + ParamType::U64 => u64::from_str(input) + .map(Token::U64) + .map_err(|_| anyhow!("failed to parse u64 value: {}", input)), + ParamType::U128 => u128::from_str(input) + .map(Token::U128) + .map_err(|_| anyhow!("failed to parse u128 value: {}", input)), + ParamType::U256 => { + // if prefix is 0x, it's a hex string + if input.starts_with("0x") { + U256::from_str(input) + .map(Token::U256) + .map_err(|_| anyhow!("failed to parse U256 value: {}", input)) + } else { + U256::from_dec_str(input) + .map(Token::U256) + .map_err(|_| anyhow!("failed to parse U256 value: {}", input)) + } + } + ParamType::B256 => { + // remove 0x prefix if provided + let input = input.trim_start_matches("0x"); + if input.len() != 64 { + return Err(anyhow!("B256 value must be 64 hex characters: {}", input)); + } + hex::decode(input) + .map(|bytes| Token::B256(bytes.try_into().unwrap())) + .map_err(|_| anyhow!("failed to parse B256 value: {}", input)) + } + ParamType::String => Ok(Token::String(parse_string_value(input))), + ParamType::Bytes => { + // remove 0x prefix if provided + let input = input.trim_start_matches("0x"); + if input.len() % 2 != 0 { + return Err(anyhow!("bytes value must be even length: {}", input)); + } + hex::decode(input) + .map(Token::Bytes) + .map_err(|_| anyhow!("failed to parse bytes value: {}", input)) + } + ParamType::RawSlice => { + // remove 0x prefix if provided + let input = input.trim_start_matches("0x"); + if input.len() % 2 != 0 { + return Err(anyhow!("raw slice value must be even length: {}", input)); + } + hex::decode(input) + .map(Token::RawSlice) + .map_err(|_| anyhow!("failed to parse raw slice value: {}", input)) + } + ParamType::StringArray(size) => { + let parsed_str = parse_string_value(input); + if parsed_str.len() != *size { + return Err(anyhow!( + "string array length mismatch: expected {}, got {}", + size, + parsed_str.len() + )); + } + Ok(Token::StringArray(StaticStringToken::new( + parsed_str, + Some(*size), + ))) + } + ParamType::StringSlice => Ok(Token::StringSlice(StaticStringToken::new( + parse_string_value(input), + None, + ))), + ParamType::Tuple(types) => { + // ensure input starts with '(' and ends with ')' + let parsed_tuple = parse_delimited_string(param_type, input)?; + Ok(Token::Tuple( + types + .iter() + .zip(parsed_tuple.iter()) + .map(|(ty, s)| param_type_val_to_token(ty, s)) + .collect::>>()?, + )) + } + ParamType::Array(ty, _size) => { + // ensure input starts with '[' and ends with ']' + let parsed_array = parse_delimited_string(param_type, input)?; + Ok(Token::Array( + parsed_array + .iter() + .map(|s| param_type_val_to_token(ty, s)) + .collect::>>()?, + )) + } + ParamType::Vector(ty) => { + // ensure input starts with '[' and ends with ']' + let parsed_vector = parse_delimited_string(param_type, input)?; + Ok(Token::Vector( + parsed_vector + .iter() + .map(|s| param_type_val_to_token(ty, s)) + .collect::>>()?, + )) + } + ParamType::Struct { fields, .. } => { + // ensure input starts with '{' and ends with '}' + let parsed_vals = parse_delimited_string(param_type, input)?; + let parsed_struct = fields + .iter() + .zip(parsed_vals.iter()) + .map(|((_, ty), val)| param_type_val_to_token(ty, val)) + .collect::>>()?; + Ok(Token::Struct(parsed_struct)) + } + ParamType::Enum { enum_variants, .. } => { + // enums must start with '(' and end with ')' + // enums must be in format of (variant_index:variant_value) or (variant_name:variant_value) + let parsed_enum = parse_delimited_string(param_type, input)?; + if parsed_enum.len() != 2 { + bail!( + "enum must have exactly two parts `(variant:value)`: {}", + input + ); + } + + let (variant_name_or_index, variant_value) = (&parsed_enum[0], &parsed_enum[1]); + // if variant can be parsed as u64 it is index; else it is name + let discriminant = match variant_name_or_index.parse::() { + Ok(index) => index, + Err(_) => { + // must be name; find index of variant_name_or_index in enum_variants given + let index = enum_variants + .variants() + .iter() + .position(|(name, _)| *name == *variant_name_or_index) + .ok_or(anyhow!( + "failed to find index of variant: {}", + variant_name_or_index + ))?; + index as u64 + } + }; + let (_, ty) = enum_variants.select_variant(discriminant).map_err(|_| { + anyhow!("failed to select enum variant: `{}`", variant_name_or_index) + })?; + let token = param_type_val_to_token(ty, variant_value).map_err(|_| { + anyhow!( + "failed to parse `{}` variant enum value: {}", + variant_name_or_index, + variant_value + ) + })?; + let enum_selector: EnumSelector = (discriminant, token, enum_variants.clone()); + Ok(Token::Enum(enum_selector.into())) + } + } +} + +/// Converts a Token to ParamType - unused unless we want to support input-param validation for enums +#[allow(dead_code)] +pub fn token_to_param_type(token: &Token) -> Result { + match token { + Token::Unit => Ok(ParamType::Unit), + Token::Bool(_) => Ok(ParamType::Bool), + Token::U8(_) => Ok(ParamType::U8), + Token::U16(_) => Ok(ParamType::U16), + Token::U32(_) => Ok(ParamType::U32), + Token::U64(_) => Ok(ParamType::U64), + Token::U128(_) => Ok(ParamType::U128), + Token::U256(_) => Ok(ParamType::U256), + Token::B256(_) => Ok(ParamType::B256), + Token::Bytes(_) => Ok(ParamType::Bytes), + Token::String(_) => Ok(ParamType::String), + Token::RawSlice(_) => Ok(ParamType::RawSlice), + Token::StringArray(str) => Ok(ParamType::StringArray(str.get_encodable_str()?.len())), + Token::StringSlice(_) => Ok(ParamType::StringSlice), + Token::Tuple(tokens) => Ok(ParamType::Tuple( + tokens + .iter() + .map(token_to_param_type) + .collect::>>()?, + )), + Token::Array(tokens) => Ok(ParamType::Array( + Box::new(token_to_param_type( + &tokens.iter().next().unwrap_or(&Token::default()).clone(), + )?), + tokens.len(), + )), + Token::Vector(tokens) => Ok(ParamType::Vector(Box::new(token_to_param_type( + &tokens.iter().next().unwrap_or(&Token::default()).clone(), + )?))), + Token::Struct(tokens) => Ok(ParamType::Struct { + name: "".to_string(), + fields: tokens + .iter() + .map(|t| { + ( + "".to_string(), + token_to_param_type(t).expect("failed to convert token to param type"), + ) + }) + .collect::>(), + generics: vec![], + }), + Token::Enum(boxed_enum) => { + let (discriminant, _, enum_variants) = &**boxed_enum; + let (_name, _ty) = enum_variants + .select_variant(*discriminant) + .expect("failed to select variant"); + Ok(ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: Default::default(), + }) + } + } +} + +/// Converts a Token to a string +pub fn token_to_string(token: &Token) -> Result { + match token { + Token::Unit => Ok("()".to_string()), + Token::Bool(b) => Ok(b.to_string()), + Token::U8(n) => Ok(n.to_string()), + Token::U16(n) => Ok(n.to_string()), + Token::U32(n) => Ok(n.to_string()), + Token::U64(n) => Ok(n.to_string()), + Token::U128(n) => Ok(n.to_string()), + Token::U256(n) => Ok(n.to_string()), + Token::B256(bytes) => { + let mut hex = String::with_capacity(bytes.len() * 2); + for byte in bytes { + write!(hex, "{:02x}", byte).unwrap(); + } + Ok(format!("0x{}", hex)) + } + Token::Bytes(bytes) => { + let mut hex = String::with_capacity(bytes.len() * 2); + for byte in bytes { + write!(hex, "{:02x}", byte).unwrap(); + } + Ok(format!("0x{}", hex)) + } + Token::String(s) => Ok(s.clone()), + Token::RawSlice(bytes) => { + let mut hex = String::with_capacity(bytes.len() * 2); + for byte in bytes { + write!(hex, "{:02x}", byte).unwrap(); + } + Ok(format!("0x{}", hex)) + } + Token::StringArray(token) => Ok(token.get_encodable_str().map(|s| s.to_string())?), + Token::StringSlice(token) => token + .get_encodable_str() + .map(|s| s.to_string()) + .map_err(|_| anyhow!("failed to get encodable string from StringSlice token")), + Token::Tuple(tokens) => { + let inner = tokens + .iter() + .map(token_to_string) + .collect::>>()? + .join(", "); + Ok(format!("({inner})")) + } + Token::Array(tokens) => { + let inner = tokens + .iter() + .map(token_to_string) + .collect::>>()? + .join(", "); + Ok(format!("[{inner}]")) + } + Token::Vector(tokens) => { + let inner = tokens + .iter() + .map(token_to_string) + .collect::>>()? + .join(", "); + Ok(format!("[{inner}]")) + } + Token::Struct(tokens) => { + let inner = tokens + .iter() + .map(token_to_string) + .collect::>>()? + .join(", "); + Ok(format!("{{{inner}}}")) + } + Token::Enum(selector) => { + let (discriminant, value, enum_variants) = &**selector; + let (name, _ty) = enum_variants + .select_variant(*discriminant) + .expect("failed to select variant"); + // TODO: variant validation - currently causing issues since we need deep recursive comparisons.. + // // ensure variant matches expected type + // let ty_got = token_to_param_type(value).map_err(|_| anyhow!("failed to convert token to param type"))?; + // if ty_got != *ty { + // // ensure all fields match of expected type if struct or enum + // match (ty, ty_got.clone()) { + // // (ParamType::Struct { fields: ty_fields, .. }, ParamType::Struct { fields: ty_got_fields, .. }) => { + // // for ((_, ty_param), (_, ty_got_param)) in ty_fields.iter().zip(ty_got_fields.iter()) { + // // if ty_param != ty_got_param { + // // return Err(anyhow!("expected type {:?} but got {:?}; mismatch in field: expected {:?}, got {:?}", ty, ty_got, ty_param, ty_got_param)); + // // } + // // } + // // }, + // (ParamType::Enum { enum_variants: ty_enum_variants, .. }, ParamType::Enum { enum_variants: ty_got_enum_variants, .. }) => { + // for ((_, ty_param), (_, ty_got_param)) in ty_enum_variants.variants().iter().zip(ty_got_enum_variants.variants().iter()) { + // if ty_param != ty_got_param { + // return Err(anyhow!("expected type {:?} but got {:?}; mismatch in variant: expected {:?}, got {:?}", ty, ty_got, ty_param, ty_got_param)); + // } + // } + // }, + // _ => return Err(anyhow!("expected type {:?} but got {:?}", ty, ty_got)), + // } + // } + Ok(format!("({}:{})", name, token_to_string(value)?)) + } + } +} + +/// Parses a delimited string into a vector of strings, preserving quoted content and nested structures +fn parse_delimited_string(param_type: &ParamType, input: &str) -> Result> { + let input = input.trim(); + let (start_delim, end_delim, separator) = match param_type { + ParamType::Tuple(_) => ('(', ')', ','), + ParamType::Array(_, _) | ParamType::Vector(_) => ('[', ']', ','), + ParamType::Struct { .. } => ('{', '}', ','), + ParamType::Enum { .. } => ('(', ')', ':'), + _ => bail!("Unsupported param type: {:?}", param_type), + }; + + if !input.starts_with(start_delim) || !input.ends_with(end_delim) { + bail!( + "input must start with '{}' and end with '{}': {}", + start_delim, + end_delim, + input + ); + } + + let inner = &input[1..input.len() - 1]; + let mut parts = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut escaped = false; + let mut nesting_level = 0u8; + + for c in inner.chars() { + match (c, in_quotes, escaped) { + ('\\', _, false) => { + escaped = true; + current.push(c); + } + ('"', _, true) => { + escaped = false; + current.push(c); + } + ('"', false, false) => { + in_quotes = true; + current.push(c); + } + ('"', true, false) => { + in_quotes = false; + current.push(c); + } + ('{', false, false) => { + nesting_level += 1; + current.push(c); + } + ('}', false, false) => { + nesting_level = nesting_level.saturating_sub(1); + current.push(c); + } + ('(', false, false) => { + nesting_level += 1; + current.push(c); + } + (')', false, false) => { + nesting_level = nesting_level.saturating_sub(1); + current.push(c); + } + ('[', false, false) => { + nesting_level += 1; + current.push(c); + } + (']', false, false) => { + nesting_level = nesting_level.saturating_sub(1); + current.push(c); + } + (c, false, false) if c == separator && nesting_level == 0 => { + if !current.trim().is_empty() { + parts.push(current.trim().to_string()); + current = String::new(); + } + } + (_, _, _) => { + escaped = false; + current.push(c); + } + } + } + + if !current.trim().is_empty() { + parts.push(current.trim().to_string()); + } + + Ok(parts) +} + +#[cfg(test)] +mod tests { + use super::*; + use fuels_core::types::param_types::EnumVariants; + + #[test] + fn test_parse_delimited_string() { + // Test with comma separator + let result = parse_delimited_string(&ParamType::Tuple(vec![]), "(a, b, c)").unwrap(); + assert_eq!(result, vec!["a", "b", "c"]); + + // Test with colon separator + let result = parse_delimited_string( + &ParamType::Enum { + name: "TestEnum".to_string(), + enum_variants: EnumVariants::new(vec![("".to_string(), ParamType::String)]) + .unwrap(), + generics: vec![], + }, + "(key:value)", + ) + .unwrap(); + assert_eq!(result, vec!["key", "value"]); + + // Test with spaces around separator + let result = parse_delimited_string( + &ParamType::Struct { + name: "TestStruct".to_string(), + fields: vec![ + ("a".to_string(), ParamType::String), + ("b".to_string(), ParamType::String), + ("c".to_string(), ParamType::String), + ], + generics: vec![], + }, + "{a , b , c}", + ) + .unwrap(); + assert_eq!(result, vec!["a", "b", "c"]); + + // Test with quoted strings + let result = parse_delimited_string( + &ParamType::Vector(Box::new(ParamType::String)), + "[\"a,b\", c]", + ) + .unwrap(); + assert_eq!(result, vec!["\"a,b\"", "c"]); + + // Test with escaped quotes + let result = + parse_delimited_string(&ParamType::Tuple(vec![]), "(\"\\\"a:b\\\"\", c)").unwrap(); + assert_eq!(result, vec!["\"\\\"a:b\\\"\"", "c"]); + + // Test with separator in quotes + let result = parse_delimited_string(&ParamType::Tuple(vec![]), "(\"a:b\",c)").unwrap(); + assert_eq!(result, vec!["\"a:b\"", "c"]); + } + + #[test] + fn param_type_val_to_token_conversion() { + // unit + let token = param_type_val_to_token(&ParamType::Unit, "").unwrap(); + assert_eq!(token, Token::Unit); + + // bool + let token = param_type_val_to_token(&ParamType::Bool, "true").unwrap(); + assert_eq!(token, Token::Bool(true)); + + // u8 + let token = param_type_val_to_token(&ParamType::U8, "42").unwrap(); + assert_eq!(token, Token::U8(42)); + + // u16 + let token = param_type_val_to_token(&ParamType::U16, "42").unwrap(); + assert_eq!(token, Token::U16(42)); + + // u32 + let token = param_type_val_to_token(&ParamType::U32, "42").unwrap(); + assert_eq!(token, Token::U32(42)); + + // u64 + let token = param_type_val_to_token(&ParamType::U64, "42").unwrap(); + assert_eq!(token, Token::U64(42)); + + // u128 + let token = param_type_val_to_token(&ParamType::U128, "42").unwrap(); + assert_eq!(token, Token::U128(42)); + + // u256 - hex string + let token = param_type_val_to_token(&ParamType::U256, "0x42").unwrap(); + assert_eq!(token, Token::U256(66.into())); + + // u256 - decimal string + let token = param_type_val_to_token(&ParamType::U256, "42").unwrap(); + assert_eq!(token, Token::U256(42.into())); + + // u256 - decimal string with leading 0 + let token = param_type_val_to_token( + &ParamType::U256, + "0000000000000000000000000000000000000000000000000000000000000042", + ) + .unwrap(); + assert_eq!(token, Token::U256(42.into())); + + // b256 - hex string, incorrect length + let token_result = param_type_val_to_token(&ParamType::B256, "0x42"); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "B256 value must be 64 hex characters: 42" + ); + + // b256 - hex string, correct length + let token = param_type_val_to_token( + &ParamType::B256, + "0x0000000000000000000000000000000000000000000000000000000000000042", + ) + .unwrap(); + assert_eq!( + token, + Token::B256([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 66 + ]) + ); + + // b256 - no 0x prefix + let token = param_type_val_to_token( + &ParamType::B256, + "0000000000000000000000000000000000000000000000000000000000000042", + ) + .unwrap(); + assert_eq!( + token, + Token::B256([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 66 + ]) + ); + + // bytes + let token = param_type_val_to_token(&ParamType::Bytes, "0x42").unwrap(); + assert_eq!(token, Token::Bytes(vec![66])); + + // bytes - no 0x prefix + let token = param_type_val_to_token(&ParamType::Bytes, "42").unwrap(); + assert_eq!(token, Token::Bytes(vec![66])); + + // string + let token = param_type_val_to_token(&ParamType::String, "fuel").unwrap(); + assert_eq!(token, Token::String("fuel".to_string())); + + // raw slice + let token = param_type_val_to_token(&ParamType::RawSlice, "0x42").unwrap(); + assert_eq!(token, Token::RawSlice(vec![66])); + + // raw slice - no 0x prefix + let token = param_type_val_to_token(&ParamType::RawSlice, "42").unwrap(); + assert_eq!(token, Token::RawSlice(vec![66])); + + // string array - single val + let token = param_type_val_to_token(&ParamType::StringArray(4), "fuel").unwrap(); + assert_eq!( + token, + Token::StringArray(StaticStringToken::new("fuel".to_string(), Some(4))) + ); + + // string array - incorrect length fails + let token_result = param_type_val_to_token(&ParamType::StringArray(2), "fuel"); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "string array length mismatch: expected 2, got 4" + ); + + // string slice + let token = param_type_val_to_token(&ParamType::StringSlice, "fuel").unwrap(); + assert_eq!( + token, + Token::StringSlice(StaticStringToken::new("fuel".to_string(), None)) + ); + + // tuple - incorrect format + let token_result = param_type_val_to_token( + &ParamType::Tuple(vec![ParamType::String, ParamType::String]), + "fuel, 42", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "input must start with '(' and end with ')': fuel, 42" + ); + + // tuple + let token = param_type_val_to_token( + &ParamType::Tuple(vec![ParamType::String, ParamType::String]), + "(fuel, 42)", + ) + .unwrap(); + assert_eq!( + token, + Token::Tuple(vec![ + Token::String("fuel".to_string()), + Token::String("42".to_string()) + ]) + ); + + // tuple - different param types + let token = param_type_val_to_token( + &ParamType::Tuple(vec![ParamType::String, ParamType::U8]), + "(fuel, 42)", + ) + .unwrap(); + assert_eq!( + token, + Token::Tuple(vec![Token::String("fuel".to_string()), Token::U8(42)]) + ); + + // array + let token = + param_type_val_to_token(&ParamType::Array(ParamType::String.into(), 3), "[fuel, 42]") + .unwrap(); + assert_eq!( + token, + Token::Array(vec![ + Token::String("fuel".to_string()), + Token::String("42".to_string()) + ]) + ); + + // array - incorrect format + let token_result = + param_type_val_to_token(&ParamType::Array(ParamType::String.into(), 3), "fuel 42"); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "input must start with '[' and end with ']': fuel 42" + ); + + // vector - correct format + let token = + param_type_val_to_token(&ParamType::Vector(ParamType::String.into()), "[fuel, 42]") + .unwrap(); + assert_eq!( + token, + Token::Vector(vec![ + Token::String("fuel".to_string()), + Token::String("42".to_string()) + ]) + ); + + // vector - incorrect format + let token_result = + param_type_val_to_token(&ParamType::Vector(ParamType::String.into()), "fuel 42"); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "input must start with '[' and end with ']': fuel 42" + ); + + // struct - correct format; single value + let token = param_type_val_to_token( + &ParamType::Struct { + name: "".to_string(), + fields: vec![("".to_string(), ParamType::String)], + generics: vec![], + }, + "{fuel, 42}", + ) + .unwrap(); + assert_eq!( + token, + Token::Struct(vec![Token::String("fuel".to_string())]) + ); + + // struct - correct format; multiple values + let token = param_type_val_to_token( + &ParamType::Struct { + name: "".to_string(), + fields: vec![ + ("".to_string(), ParamType::String), + ("".to_string(), ParamType::String), + ], + generics: vec![], + }, + "{fuel, 42}", + ) + .unwrap(); + assert_eq!( + token, + Token::Struct(vec![ + Token::String("fuel".to_string()), + Token::String("42".to_string()) + ]) + ); + + // struct - correct format; multiple values; different param types + let token = param_type_val_to_token( + &ParamType::Struct { + name: "".to_string(), + fields: vec![ + ("".to_string(), ParamType::String), + ("".to_string(), ParamType::U8), + ], + generics: vec![], + }, + "{fuel, 42}", + ) + .unwrap(); + assert_eq!( + token, + Token::Struct(vec![Token::String("fuel".to_string()), Token::U8(42)]) + ); + + // struct - incorrect format (same as tuple) + let token_result = param_type_val_to_token( + &ParamType::Struct { + name: "".to_string(), + fields: vec![("a".to_string(), ParamType::String)], + generics: vec![], + }, + "fuel, 42", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "input must start with '{' and end with '}': fuel, 42" + ); + + // enum - incorrect format + let token_result = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: EnumVariants::new(vec![ + ("".to_string(), ParamType::String), + ("".to_string(), ParamType::U8), + ]) + .unwrap(), + generics: vec![], + }, + "Active: true", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "input must start with '(' and end with ')': Active: true" + ); + + // enum - variant not found + let enum_variants = EnumVariants::new(vec![ + ("".to_string(), ParamType::String), + ("".to_string(), ParamType::U8), + ]) + .unwrap(); + let token_result = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(Active: true)", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "failed to find index of variant: Active" + ); + + // enum - variant found, incorrect variant value (expect cannot parse u8 as bool) + let enum_variants = EnumVariants::new(vec![ + ("Input".to_string(), ParamType::String), + ("Active".to_string(), ParamType::U8), + ]) + .unwrap(); + let token_result = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(Active: true)", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "failed to parse `Active` variant enum value: true" + ); + + // enum - variant found, correct variant value + let enum_variants = EnumVariants::new(vec![ + ("Input".to_string(), ParamType::String), + ("Active".to_string(), ParamType::Bool), + ]) + .unwrap(); + let token = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(Active: true)", + ) + .unwrap(); + assert_eq!( + token, + Token::Enum((1u64, Token::Bool(true), enum_variants).into()) + ); + + // enum - variant found by index, incorrect index type (should be bool) + let enum_variants = EnumVariants::new(vec![ + ("Input".to_string(), ParamType::String), + ("Active".to_string(), ParamType::Bool), + ]) + .unwrap(); + let token_result = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(1: 1)", + ); + assert!(token_result.is_err()); + assert_eq!( + token_result.unwrap_err().to_string(), + "failed to parse `1` variant enum value: 1" + ); + + // enum - variant found by index, correct variant value + let enum_variants = EnumVariants::new(vec![ + ("Input".to_string(), ParamType::String), + ("Active".to_string(), ParamType::Bool), + ]) + .unwrap(); + let token = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(1: true)", + ) + .unwrap(); + assert_eq!( + token, + Token::Enum((1u64, Token::Bool(true), enum_variants).into()) + ); + + // enum (complex example) - variants with a struct that contains an enum and a vec that contains another enum with 2 variants + let enum_variants = EnumVariants::new(vec![ + ( + "Input".to_string(), + ParamType::Struct { + generics: vec![], + name: "".to_string(), + fields: vec![ + ( + "".to_string(), + ParamType::Enum { + name: "".to_string(), + enum_variants: EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(), + generics: vec![], + }, + ), + ( + "".to_string(), + ParamType::Vector(Box::new(ParamType::Enum { + name: "".to_string(), + enum_variants: EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(), + generics: vec![], + })), + ), + ], + }, + ), + ("Active".to_string(), ParamType::Bool), + ]) + .unwrap(); + let token = param_type_val_to_token( + &ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![], + }, + "(Input: {(Active: true), [(Pending: 42)]})", + ) + .unwrap(); + assert_eq!( + token, + Token::Enum( + ( + 0u64, + Token::Struct(vec![ + Token::Enum( + ( + 0u64, + Token::Bool(true), + EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64) + ]) + .unwrap() + ) + .into() + ), + Token::Vector(vec![Token::Enum( + ( + 1u64, + Token::U64(42), + EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64) + ]) + .unwrap() + ) + .into() + )]) + ]), + enum_variants + ) + .into() + ) + ); + } + + #[test] + fn token_to_param_type_conversion() { + // unit + let token = Token::Unit; + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::Unit); + + // bool + let token = Token::Bool(true); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::Bool); + + // u8 + let token = Token::U8(42); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U8); + + // u16 + let token = Token::U16(42); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U16); + + // u32 + let token = Token::U32(42); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U32); + + // u64 + let token = Token::U64(42); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U64); + + // u128 + let token = Token::U128(42); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U128); + + // u256 + let token = Token::U256(42.into()); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::U256); + + // b256 + let token = Token::B256([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 66, + ]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::B256); + + // bytes + let token = Token::Bytes(vec![66]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::Bytes); + + // string + let token = Token::String("fuel".to_string()); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::String); + + // raw slice + let token = Token::RawSlice(vec![66]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::RawSlice); + + // string array + let token = Token::StringArray(StaticStringToken::new("fuel".to_string(), Some(4))); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::StringArray(4)); + + // string slice + let token = Token::StringSlice(StaticStringToken::new("fuel".to_string(), None)); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::StringSlice); + + // tuple + let token = Token::Tuple(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!( + param_type, + ParamType::Tuple(vec![ParamType::String, ParamType::U8]) + ); + + // array + let token = Token::Array(vec![ + Token::String("fuel".to_string()), + Token::String("rocks".to_string()), + ]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::Array(Box::new(ParamType::String), 2)); + + // vector + let token = Token::Vector(vec![ + Token::String("fuel".to_string()), + Token::String("rocks".to_string()), + ]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!(param_type, ParamType::Vector(Box::new(ParamType::String))); + + // struct + let token = Token::Struct(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!( + param_type, + ParamType::Struct { + name: "".to_string(), + fields: vec![ + ("".to_string(), ParamType::String), + ("".to_string(), ParamType::U8) + ], + generics: vec![] + } + ); + + // struct (complex example) - struct with 2 fields that contains another struct with 2 fields + let token = Token::Struct(vec![ + Token::Struct(vec![Token::U32(42), Token::U32(42)]), + Token::U32(42), + ]); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!( + param_type, + ParamType::Struct { + name: "".to_string(), + fields: vec![ + ( + "".to_string(), + ParamType::Struct { + name: "".to_string(), + fields: vec![ + ("".to_string(), ParamType::U32), + ("".to_string(), ParamType::U32) + ], + generics: vec![] + } + ), + ("".to_string(), ParamType::U32) + ], + generics: vec![] + } + ); + + // enum + let token = Token::Enum( + ( + 0u64, + Token::U32(42), + EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(), + ) + .into(), + ); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!( + param_type, + ParamType::Enum { + name: "".to_string(), + enum_variants: EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64) + ]) + .unwrap(), + generics: vec![] + } + ); + + // enum (complex example) - variants with a struct that contains an enum and a vec that contains another enum with 2 variants + let inner_enum_variants = EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(); + let enum_variants = EnumVariants::new(vec![ + ( + "Input".to_string(), + ParamType::Struct { + generics: vec![], + name: "".to_string(), + fields: vec![ + ( + "".to_string(), + ParamType::Enum { + name: "".to_string(), + enum_variants: inner_enum_variants.clone(), + generics: vec![], + }, + ), + ( + "".to_string(), + ParamType::Vector(Box::new(ParamType::Enum { + name: "".to_string(), + enum_variants: inner_enum_variants.clone(), + generics: vec![], + })), + ), + ], + }, + ), + ("Active".to_string(), ParamType::Bool), + ]) + .unwrap(); + let token = Token::Enum( + ( + 0u64, + Token::Struct(vec![ + Token::Enum((0u64, Token::Bool(true), inner_enum_variants.clone()).into()), + Token::Vector(vec![Token::Enum( + (1u64, Token::U64(42), inner_enum_variants.clone()).into(), + )]), + ]), + enum_variants.clone(), + ) + .into(), + ); + let param_type = token_to_param_type(&token).unwrap(); + assert_eq!( + param_type, + ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![] + } + ); + + // enum (complex example 2) - 2 variants, one with a struct that contains another struct with 2 fields, another with a struct + // enum GenericEnum { + // container: GenericStruct { + // value: GenericStruct { + // value: u32, + // description: str[4], + // }, + // description: str[4], + // }, + // value: GenericStruct { + // value: u32, + // description: str[4], + // }, + // } + let inner_struct = ParamType::Struct { + generics: vec![ParamType::U32], + name: "GenericStruct".to_string(), + fields: vec![ + ("value".to_string(), ParamType::U32), + ("description".to_string(), ParamType::StringArray(4)), + ], + }; + let enum_variants = EnumVariants::new(vec![ + ( + "container".to_string(), + ParamType::Struct { + name: "GenericStruct".to_string(), + generics: vec![ParamType::Struct { + name: "GenericStruct".to_string(), + fields: vec![ + ("value".to_string(), ParamType::U32), + ("description".to_string(), ParamType::StringArray(4)), + ], + generics: vec![ParamType::U32], + }], + fields: vec![ + ("value".to_string(), inner_struct.clone()), + ("description".to_string(), ParamType::StringArray(4)), + ], + }, + ), + ("value".to_string(), inner_struct.clone()), + ]) + .unwrap(); + let token = Token::Enum( + ( + 0u64, + Token::Struct(vec![ + Token::Struct(vec![ + Token::U32(42), + Token::StringArray(StaticStringToken::new("fuel".into(), Some(4))), + ]), + Token::StringArray(StaticStringToken::new("fuel".into(), Some(4))), + ]), + enum_variants.clone(), + ) + .into(), + ); + let output = token_to_param_type(&token).unwrap(); + assert_eq!( + output, + ParamType::Enum { + name: "".to_string(), + enum_variants: enum_variants.clone(), + generics: vec![] + } + ); + } + + #[test] + fn token_to_string_conversion() { + // unit + let token = Token::Unit; + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "()"); + + // bool + let token = Token::Bool(true); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "true"); + + // u8 + let token = Token::U8(42); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // u16 + let token = Token::U16(42); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // u32 + let token = Token::U32(42); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // u64 + let token = Token::U64(42); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // u128 + let token = Token::U128(42); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // u256 + let token = Token::U256(42.into()); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "42"); + + // b256 + let token = Token::B256([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 66, + ]); + let output = token_to_string(&token).unwrap(); + assert_eq!( + output, + "0x0000000000000000000000000000000000000000000000000000000000000042" + ); + + // bytes + let token = Token::Bytes(vec![66]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "0x42"); + + // string + let token = Token::String("fuel".to_string()); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "fuel"); + + // raw slice + let token = Token::RawSlice(vec![66]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "0x42"); + + // string array - fails if length is incorrect + let token = Token::StringArray(StaticStringToken::new("fuel".to_string(), Some(1))); + let output_res = token_to_string(&token); + assert!(output_res.is_err()); + assert_eq!( + output_res.unwrap_err().to_string(), + "codec: string data has len 4, but the expected len is 1" + ); + + // string array - fails if length overflows + let token = Token::StringArray(StaticStringToken::new("fuel".to_string(), Some(10))); + let output_res = token_to_string(&token); + assert!(output_res.is_err()); + assert_eq!( + output_res.unwrap_err().to_string(), + "codec: string data has len 4, but the expected len is 10" + ); + + // string array - succeeds if length not provided + // TODO: probably an issue in the SDK; should fail validation + let token = Token::StringArray(StaticStringToken::new("fuel".to_string(), None)); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "fuel"); + + // string array - succeeds if length is correct + let token = Token::StringArray(StaticStringToken::new("fuel".to_string(), Some(4))); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "fuel"); + + // string slice + let token = Token::StringSlice(StaticStringToken::new("fuel".to_string(), None)); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "fuel"); + + // tuple + let token = Token::Tuple(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "(fuel, 42)"); + + // array - same param types + let token = Token::Array(vec![ + Token::String("fuel".to_string()), + Token::String("rocks".to_string()), + ]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "[fuel, rocks]"); + + // array - different param types + // TODO: probably an issue in the SDK; should fail validation + let token = Token::Array(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "[fuel, 42]"); + + // vector - same param types + let token = Token::Vector(vec![ + Token::String("fuel".to_string()), + Token::String("rocks".to_string()), + ]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "[fuel, rocks]"); + + // vector - different param types + // TODO: probably an issue in the SDK; should fail validation + let token = Token::Vector(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "[fuel, 42]"); + + // struct - single value + let token = Token::Struct(vec![Token::String("fuel".to_string())]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "{fuel}"); + + // struct - multiple values + let token = Token::Struct(vec![Token::String("fuel".to_string()), Token::U8(42)]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "{fuel, 42}"); + + // struct (complex example) - struct with 2 fields that contains another struct with 2 fields + let token = Token::Struct(vec![ + Token::Struct(vec![Token::U32(42), Token::U32(42)]), + Token::U32(42), + ]); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "{{42, 42}, 42}"); + + // TODO: potentially re-enable this if we want to support input-param validation + // // enum - fails if variant incorrect + // let enum_variants = EnumVariants::new(vec![("Active".to_string(), ParamType::Bool), ("Pending".to_string(), ParamType::U64)]).unwrap(); + // let token = Token::Enum((1u64, Token::Bool(true), enum_variants).into()); + // let output_res = token_to_string(&token); + // assert!(output_res.is_err()); + // assert_eq!(output_res.unwrap_err().to_string(), "expected type U64 but got Bool"); + + // enum - correct variant + let enum_variants = EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(); + let token = Token::Enum((1u64, Token::U64(42), enum_variants).into()); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "(Pending:42)"); + + // enum (complex example) - variants with a struct that contains an enum and a vec that contains another enum with 2 variants + let inner_enum_variants = EnumVariants::new(vec![ + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(); + let enum_variants = EnumVariants::new(vec![ + ( + "Input".to_string(), + ParamType::Struct { + generics: vec![], + name: "".to_string(), + fields: vec![ + ( + "".to_string(), + ParamType::Enum { + name: "".to_string(), + enum_variants: inner_enum_variants.clone(), + generics: vec![], + }, + ), + ( + "".to_string(), + ParamType::Vector(Box::new(ParamType::Enum { + name: "".to_string(), + enum_variants: inner_enum_variants.clone(), + generics: vec![], + })), + ), + ], + }, + ), + ("Active".to_string(), ParamType::Bool), + ("Pending".to_string(), ParamType::U64), + ]) + .unwrap(); + + // test active variant + let token = Token::Enum((1u64, Token::Bool(true), enum_variants.clone()).into()); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "(Active:true)"); + + // test Input variant + let token = Token::Enum( + ( + 0u64, + Token::Struct(vec![ + Token::Enum((0u64, Token::Bool(true), inner_enum_variants.clone()).into()), + Token::Vector(vec![Token::Enum( + (1u64, Token::U64(42), inner_enum_variants.clone()).into(), + )]), + ]), + enum_variants, + ) + .into(), + ); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "(Input:{(Active:true), [(Pending:42)]})"); + + // enum (complex example 2) - 2 variants, one with a struct that contains another struct with 2 fields, another with a struct + // enum GenericEnum> { + // container: GenericStruct { + // value: GenericStruct { + // value: u32, + // description: str[4], + // }, + // description: str[4], + // }, + // value: GenericStruct { + // value: u32, + // description: str[4], + // }, + // } + let inner_struct = ParamType::Struct { + generics: vec![ParamType::U32], + name: "GenericStruct".to_string(), + fields: vec![ + ("value".to_string(), ParamType::U32), + ("description".to_string(), ParamType::StringArray(4)), + ], + }; + let enum_variants = EnumVariants::new(vec![ + ( + "container".to_string(), + ParamType::Struct { + generics: vec![ParamType::Struct { + name: "GenericStruct".to_string(), + fields: vec![ + ("value".to_string(), ParamType::U32), + ("description".to_string(), ParamType::StringArray(4)), + ], + generics: vec![ParamType::U32], + }], + name: "GenericStruct".to_string(), + fields: vec![ + ("value".to_string(), inner_struct.clone()), + ("description".to_string(), ParamType::StringArray(4)), + ], + }, + ), + ("value".to_string(), inner_struct.clone()), + ]) + .unwrap(); + let token = Token::Enum( + ( + 0u64, + Token::Struct(vec![ + Token::Struct(vec![ + Token::U32(42), + Token::StringArray(StaticStringToken::new("fuel".into(), Some(4))), + ]), + Token::StringArray(StaticStringToken::new("fuel".into(), Some(4))), + ]), + enum_variants, + ) + .into(), + ); + let output = token_to_string(&token).unwrap(); + assert_eq!(output, "(container:{{42, fuel}, fuel})"); + } +} diff --git a/forc-plugins/forc-client/src/op/mod.rs b/forc-plugins/forc-client/src/op/mod.rs index f11f8c63b7c..85ac01013a9 100644 --- a/forc-plugins/forc-client/src/op/mod.rs +++ b/forc-plugins/forc-client/src/op/mod.rs @@ -1,7 +1,9 @@ +mod call; mod deploy; mod run; mod submit; +pub use call::call; pub use deploy::{deploy, DeployedContract, DeployedExecutable, DeployedPackage}; pub use run::run; pub use submit::submit; diff --git a/forc-plugins/forc-client/src/util/node_url.rs b/forc-plugins/forc-client/src/util/node_url.rs index 58e9dcd87e1..404af4a34eb 100644 --- a/forc-plugins/forc-client/src/util/node_url.rs +++ b/forc-plugins/forc-client/src/util/node_url.rs @@ -6,6 +6,7 @@ use crate::NodeTarget; use super::target::Target; /// Returns the URL to use for connecting to Fuel Core node. +/// TODO: move to impl of NodeTarget pub fn get_node_url( node_target: &NodeTarget, manifest_network: &Option, @@ -13,14 +14,16 @@ pub fn get_node_url( let node_url = match ( node_target.testnet, node_target.mainnet, + node_target.devnet, node_target.target.clone(), node_target.node_url.clone(), ) { - (true, false, None, None) => Target::testnet().target_url(), - (false, true, None, None) => Target::mainnet().target_url(), - (false, false, Some(target), None) => target.target_url(), - (false, false, None, Some(node_url)) => node_url, - (false, false, None, None) => manifest_network + (true, false, false, None, None) => Target::testnet().target_url(), + (false, true, false, None, None) => Target::mainnet().target_url(), + (false, false, true, None, None) => Target::devnet().target_url(), + (false, false, false, Some(target), None) => target.target_url(), + (false, false, false, None, Some(node_url)) => node_url, + (false, false, false, None, None) => manifest_network .as_ref() .map(|nw| &nw.url[..]) .unwrap_or(crate::constants::NODE_URL) @@ -38,8 +41,9 @@ fn test_get_node_url_testnet() { let input = NodeTarget { target: None, node_url: None, - testnet: true, mainnet: false, + testnet: true, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); @@ -51,8 +55,9 @@ fn test_get_node_url_mainnet() { let input = NodeTarget { target: None, node_url: None, - testnet: false, mainnet: true, + testnet: false, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); @@ -64,8 +69,9 @@ fn test_get_node_url_target_mainnet() { let input = NodeTarget { target: Some(Target::Mainnet), node_url: None, - testnet: false, mainnet: false, + testnet: false, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); assert_eq!("https://mainnet.fuel.network", actual); @@ -76,8 +82,9 @@ fn test_get_node_url_target_testnet() { let input = NodeTarget { target: Some(Target::Testnet), node_url: None, - testnet: false, mainnet: false, + testnet: false, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); @@ -89,8 +96,9 @@ fn test_get_node_url_default() { let input = NodeTarget { target: None, node_url: None, - testnet: false, mainnet: false, + testnet: false, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); @@ -102,8 +110,9 @@ fn test_get_node_url_local() { let input = NodeTarget { target: Some(Target::Local), node_url: None, - testnet: false, mainnet: false, + testnet: false, + devnet: false, }; let actual = get_node_url(&input, &None).unwrap(); assert_eq!("http://127.0.0.1:4000", actual); @@ -117,8 +126,9 @@ fn test_get_node_url_local_testnet() { let input = NodeTarget { target: Some(Target::Local), node_url: None, - testnet: true, mainnet: false, + testnet: true, + devnet: false, }; get_node_url(&input, &None).unwrap(); } @@ -131,8 +141,9 @@ fn test_get_node_url_same_url() { let input = NodeTarget { target: Some(Target::Testnet), node_url: Some("testnet.fuel.network".to_string()), - testnet: false, mainnet: false, + testnet: false, + devnet: false, }; get_node_url(&input, &None).unwrap(); } diff --git a/forc-plugins/forc-client/src/util/target.rs b/forc-plugins/forc-client/src/util/target.rs index bbc5679e844..9e2f2e87a13 100644 --- a/forc-plugins/forc-client/src/util/target.rs +++ b/forc-plugins/forc-client/src/util/target.rs @@ -1,6 +1,6 @@ use crate::constants::{ - MAINNET_ENDPOINT_URL, MAINNET_EXPLORER_URL, NODE_URL, TESTNET_ENDPOINT_URL, - TESTNET_EXPLORER_URL, TESTNET_FAUCET_URL, + DEVNET_ENDPOINT_URL, DEVNET_FAUCET_URL, MAINNET_ENDPOINT_URL, MAINNET_EXPLORER_URL, NODE_URL, + TESTNET_ENDPOINT_URL, TESTNET_EXPLORER_URL, TESTNET_FAUCET_URL, }; use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; @@ -9,8 +9,9 @@ use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] /// Possible target values that forc-client can interact with. pub enum Target { - Testnet, Mainnet, + Testnet, + Devnet, Local, } @@ -23,8 +24,9 @@ impl Default for Target { impl Target { pub fn target_url(&self) -> String { let url = match self { - Target::Testnet => TESTNET_ENDPOINT_URL, Target::Mainnet => MAINNET_ENDPOINT_URL, + Target::Testnet => TESTNET_ENDPOINT_URL, + Target::Devnet => DEVNET_ENDPOINT_URL, Target::Local => NODE_URL, }; url.to_string() @@ -34,6 +36,7 @@ impl Target { match target_url { TESTNET_ENDPOINT_URL => Some(Target::Testnet), MAINNET_ENDPOINT_URL => Some(Target::Mainnet), + DEVNET_ENDPOINT_URL => Some(Target::Devnet), NODE_URL => Some(Target::Local), _ => None, } @@ -43,6 +46,10 @@ impl Target { Target::Local } + pub fn devnet() -> Self { + Target::Devnet + } + pub fn testnet() -> Self { Target::Testnet } @@ -53,16 +60,18 @@ impl Target { pub fn faucet_url(&self) -> Option { match self { - Target::Testnet => Some(TESTNET_FAUCET_URL.to_string()), Target::Mainnet => None, + Target::Testnet => Some(TESTNET_FAUCET_URL.to_string()), + Target::Devnet => Some(DEVNET_FAUCET_URL.to_string()), Target::Local => Some("http://localhost:3000".to_string()), } } pub fn explorer_url(&self) -> Option { match self { - Target::Testnet => Some(TESTNET_EXPLORER_URL.to_string()), Target::Mainnet => Some(MAINNET_EXPLORER_URL.to_string()), + Target::Testnet => Some(TESTNET_EXPLORER_URL.to_string()), + Target::Devnet => None, _ => None, } } @@ -76,11 +85,13 @@ impl FromStr for Target { "Fuel Sepolia Testnet" => Ok(Target::Testnet), "Ignition" => Ok(Target::Mainnet), "local" => Ok(Target::Local), + "Devnet" | "devnet" => Ok(Target::Devnet), _ => bail!( - "'{s}' is not a valid target name. Possible values: '{}', '{}', '{}'", + "'{s}' is not a valid target name. Possible values: '{}', '{}', '{}', '{}'", Target::Testnet, Target::Mainnet, - Target::Local + Target::Local, + Target::Devnet, ), } } @@ -89,8 +100,9 @@ impl FromStr for Target { impl std::fmt::Display for Target { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { - Target::Testnet => "Fuel Sepolia Testnet", Target::Mainnet => "Ignition", + Target::Testnet => "Fuel Sepolia Testnet", + Target::Devnet => "Devnet", Target::Local => "local", }; write!(f, "{}", s) diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index a8a35171eb0..db5caf16736 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -188,8 +188,6 @@ pub(crate) async fn select_account( let wallet_path = default_wallet_path(); let accounts = collect_user_accounts(&wallet_path, password)?; let account_balances = collect_account_balances(&accounts, provider).await?; - let consensus_parameters = provider.consensus_parameters().await?; - let base_asset_id = consensus_parameters.base_asset_id(); let total_balance = account_balances .iter() @@ -213,37 +211,6 @@ pub(crate) async fn select_account( }; anyhow::bail!(message) } - let selections = - format_base_asset_account_balances(&accounts, &account_balances, base_asset_id)?; - - let mut account_index; - loop { - account_index = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Wallet account") - .max_length(5) - .items(&selections[..]) - .default(0) - .interact()?; - - if accounts.contains_key(&account_index) { - break; - } - let options: Vec = accounts - .keys() - .map(|key| { - let raw_addr = format!("0x{key}"); - let checksum_addr = checksum_encode(&raw_addr)?; - Ok(checksum_addr) - }) - .collect::>>()?; - println_warning(&format!( - "\"{}\" is not a valid account.\nPlease choose a valid option from {}", - account_index, - options.join(","), - )); - } - - let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, password)?; // TODO: Do this via forc-wallet once the functionality is exposed. // TODO: calculate the number of transactions to sign and ask the user to confirm. @@ -256,7 +223,7 @@ pub(crate) async fn select_account( anyhow::bail!("User refused to sign"); } - let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + let wallet = select_local_wallet_account(password, provider).await?; Ok(ForcClientAccount::Wallet(wallet)) } SignerSelectionMode::Manual => { @@ -276,6 +243,50 @@ pub(crate) async fn select_account( } } +pub(crate) async fn select_local_wallet_account( + password: &str, + provider: &Provider, +) -> Result { + let wallet_path = default_wallet_path(); + let accounts = collect_user_accounts(&wallet_path, password)?; + let account_balances = collect_account_balances(&accounts, provider).await?; + let consensus_parameters = provider.consensus_parameters().await?; + let base_asset_id = consensus_parameters.base_asset_id(); + let selections = + format_base_asset_account_balances(&accounts, &account_balances, base_asset_id)?; + + let mut account_index; + loop { + account_index = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Wallet account") + .max_length(5) + .items(&selections[..]) + .default(0) + .interact()?; + + if accounts.contains_key(&account_index) { + break; + } + let options: Vec = accounts + .keys() + .map(|key| { + let raw_addr = format!("0x{key}"); + let checksum_addr = checksum_encode(&raw_addr)?; + Ok(checksum_addr) + }) + .collect::>>()?; + println_warning(&format!( + "\"{}\" is not a valid account.\nPlease choose a valid option from {}", + account_index, + options.join(","), + )); + } + + let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, password)?; + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + Ok(wallet) +} + pub async fn update_proxy_contract_target( account: &ForcClientAccount, proxy_contract_id: ContractId, diff --git a/forc-plugins/forc-client/test/data/contract_with_types/Forc.toml b/forc-plugins/forc-client/test/data/contract_with_types/Forc.toml new file mode 100644 index 00000000000..90dae6aa4f1 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_types/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract_with_types" diff --git a/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json new file mode 100644 index 00000000000..6e1b6fc610d --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-abi.json @@ -0,0 +1,720 @@ +{ + "programType": "contract", + "specVersion": "1", + "encodingVersion": "1", + "concreteTypes": [ + { + "type": "()", + "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "type": "(u64, bool)", + "concreteTypeId": "5a05acd79ded5b8adc972345c8309f451c54aa0c27b2873554ac4758191aad5f", + "metadataTypeId": 1 + }, + { + "type": "[u64; 10]", + "concreteTypeId": "34bdb7bae35dacc4188e2ab32eb559de01ccd4154def797134ebe16b310c33af", + "metadataTypeId": 2 + }, + { + "type": "b256", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + }, + { + "type": "enum GenericEnum>", + "concreteTypeId": "e8a118f329c0722601113b0179a2e3f73eae6abc483e97556284a6fc73b07197", + "metadataTypeId": 4, + "typeArguments": [ + "7e5af3efc0fd41ec44b9367746ca17af79224954690bed680461173162a62926" + ] + }, + { + "type": "enum GenericEnum", + "concreteTypeId": "14e956820212553da4f16588c7f10f7857d7e629c70c07e4e1695e093a06a0be", + "metadataTypeId": 4, + "typeArguments": [ + "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + ] + }, + { + "type": "enum Status", + "concreteTypeId": "66eef06c10a78bfec7643c7d553d635fc1edc145c4c1037d422dc6f9fc5ea100", + "metadataTypeId": 5 + }, + { + "type": "enum std::identity::Identity", + "concreteTypeId": "ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335", + "metadataTypeId": 6 + }, + { + "type": "enum std::option::Option", + "concreteTypeId": "d852149004cc9ec0bbe7dc4e37bffea1d41469b759512b6136f2e865a4c06e7d", + "metadataTypeId": 7, + "typeArguments": [ + "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + ] + }, + { + "type": "str", + "concreteTypeId": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a" + }, + { + "type": "str[10]", + "concreteTypeId": "338a25cb65b9251663dcce6362b744fe10aa849758299590f4efed5dd299bf50" + }, + { + "type": "struct ComplexStruct>", + "concreteTypeId": "4ce860126ad9d85fbf993112427156c992765879a44eddbe11eb508498897266", + "metadataTypeId": 12, + "typeArguments": [ + "7e5af3efc0fd41ec44b9367746ca17af79224954690bed680461173162a62926" + ] + }, + { + "type": "struct GenericStruct", + "concreteTypeId": "7e5af3efc0fd41ec44b9367746ca17af79224954690bed680461173162a62926", + "metadataTypeId": 13, + "typeArguments": [ + "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + ] + }, + { + "type": "struct User", + "concreteTypeId": "8771e33a50e92b7b9a2b3c66fd99f3c417e4c2d3aef1152b4d64e8df6169f831", + "metadataTypeId": 14 + }, + { + "type": "struct std::asset_id::AssetId", + "concreteTypeId": "c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974", + "metadataTypeId": 16 + }, + { + "type": "struct std::bytes::Bytes", + "concreteTypeId": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb", + "metadataTypeId": 17 + }, + { + "type": "struct std::u128::U128", + "concreteTypeId": "c4a4edf5dbd3026f55a64842d4610751fd4996e511a21367af515102dfb99a1b", + "metadataTypeId": 20 + }, + { + "type": "struct std::vec::Vec", + "concreteTypeId": "d5bfe1d4e1ace20166c9b50cadd47e862020561bde24f5189cfc2723f5ed76f4", + "metadataTypeId": 22, + "typeArguments": [ + "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + ] + }, + { + "type": "u16", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" + }, + { + "type": "u256", + "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e" + }, + { + "type": "u32", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + }, + { + "type": "u64", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "type": "u8", + "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" + } + ], + "metadataTypes": [ + { + "type": "(_, _)", + "metadataTypeId": 0, + "components": [ + { + "name": "__tuple_element", + "typeId": 14 + }, + { + "name": "__tuple_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "(_, _)", + "metadataTypeId": 1, + "components": [ + { + "name": "__tuple_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "__tuple_element", + "typeId": 3 + } + ] + }, + { + "type": "[_; 10]", + "metadataTypeId": 2, + "components": [ + { + "name": "__array_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "bool", + "metadataTypeId": 3 + }, + { + "type": "enum GenericEnum", + "metadataTypeId": 4, + "components": [ + { + "name": "container", + "typeId": 13, + "typeArguments": [ + { + "name": "", + "typeId": 8 + } + ] + }, + { + "name": "value", + "typeId": 8 + } + ], + "typeParameters": [ + 8 + ] + }, + { + "type": "enum Status", + "metadataTypeId": 5, + "components": [ + { + "name": "Active", + "typeId": 3 + }, + { + "name": "Pending", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "Inactive", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + } + ] + }, + { + "type": "enum std::identity::Identity", + "metadataTypeId": 6, + "components": [ + { + "name": "Address", + "typeId": 15 + }, + { + "name": "ContractId", + "typeId": 19 + } + ] + }, + { + "type": "enum std::option::Option", + "metadataTypeId": 7, + "components": [ + { + "name": "None", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "name": "Some", + "typeId": 8 + } + ], + "typeParameters": [ + 8 + ] + }, + { + "type": "generic T", + "metadataTypeId": 8 + }, + { + "type": "raw untyped ptr", + "metadataTypeId": 9 + }, + { + "type": "str[2]", + "metadataTypeId": 10 + }, + { + "type": "str[4]", + "metadataTypeId": 11 + }, + { + "type": "struct ComplexStruct", + "metadataTypeId": 12, + "components": [ + { + "name": "info", + "typeId": 0 + }, + { + "name": "status", + "typeId": 5 + }, + { + "name": "data", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "generic", + "typeId": 13, + "typeArguments": [ + { + "name": "", + "typeId": 8 + } + ] + } + ], + "typeParameters": [ + 8 + ] + }, + { + "type": "struct GenericStruct", + "metadataTypeId": 13, + "components": [ + { + "name": "value", + "typeId": 8 + }, + { + "name": "description", + "typeId": 11 + } + ], + "typeParameters": [ + 8 + ] + }, + { + "type": "struct User", + "metadataTypeId": 14, + "components": [ + { + "name": "name", + "typeId": 10 + }, + { + "name": "id", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "struct std::address::Address", + "metadataTypeId": 15, + "components": [ + { + "name": "bits", + "typeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + } + ] + }, + { + "type": "struct std::asset_id::AssetId", + "metadataTypeId": 16, + "components": [ + { + "name": "bits", + "typeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + } + ] + }, + { + "type": "struct std::bytes::Bytes", + "metadataTypeId": 17, + "components": [ + { + "name": "buf", + "typeId": 18 + }, + { + "name": "len", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "struct std::bytes::RawBytes", + "metadataTypeId": 18, + "components": [ + { + "name": "ptr", + "typeId": 9 + }, + { + "name": "cap", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "struct std::contract_id::ContractId", + "metadataTypeId": 19, + "components": [ + { + "name": "bits", + "typeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + } + ] + }, + { + "type": "struct std::u128::U128", + "metadataTypeId": 20, + "components": [ + { + "name": "upper", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "lower", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "struct std::vec::RawVec", + "metadataTypeId": 21, + "components": [ + { + "name": "ptr", + "typeId": 9 + }, + { + "name": "cap", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ], + "typeParameters": [ + 8 + ] + }, + { + "type": "struct std::vec::Vec", + "metadataTypeId": 22, + "components": [ + { + "name": "buf", + "typeId": 21, + "typeArguments": [ + { + "name": "", + "typeId": 8 + } + ] + }, + { + "name": "len", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ], + "typeParameters": [ + 8 + ] + } + ], + "functions": [ + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "34bdb7bae35dacc4188e2ab32eb559de01ccd4154def797134ebe16b310c33af" + } + ], + "name": "test_array", + "output": "34bdb7bae35dacc4188e2ab32eb559de01ccd4154def797134ebe16b310c33af", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + } + ], + "name": "test_b256", + "output": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb" + } + ], + "name": "test_bytes", + "output": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "4ce860126ad9d85fbf993112427156c992765879a44eddbe11eb508498897266" + } + ], + "name": "test_complex_struct", + "output": "4ce860126ad9d85fbf993112427156c992765879a44eddbe11eb508498897266", + "attributes": null + }, + { + "inputs": [], + "name": "test_empty", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": null + }, + { + "inputs": [], + "name": "test_empty_no_return", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "66eef06c10a78bfec7643c7d553d635fc1edc145c4c1037d422dc6f9fc5ea100" + } + ], + "name": "test_enum", + "output": "66eef06c10a78bfec7643c7d553d635fc1edc145c4c1037d422dc6f9fc5ea100", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "e8a118f329c0722601113b0179a2e3f73eae6abc483e97556284a6fc73b07197" + } + ], + "name": "test_enum_with_complex_generic", + "output": "e8a118f329c0722601113b0179a2e3f73eae6abc483e97556284a6fc73b07197", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "14e956820212553da4f16588c7f10f7857d7e629c70c07e4e1695e093a06a0be" + } + ], + "name": "test_enum_with_generic", + "output": "14e956820212553da4f16588c7f10f7857d7e629c70c07e4e1695e093a06a0be", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "d852149004cc9ec0bbe7dc4e37bffea1d41469b759512b6136f2e865a4c06e7d" + } + ], + "name": "test_option", + "output": "d852149004cc9ec0bbe7dc4e37bffea1d41469b759512b6136f2e865a4c06e7d", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a" + } + ], + "name": "test_str", + "output": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "338a25cb65b9251663dcce6362b744fe10aa849758299590f4efed5dd299bf50" + } + ], + "name": "test_str_array", + "output": "338a25cb65b9251663dcce6362b744fe10aa849758299590f4efed5dd299bf50", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a" + } + ], + "name": "test_str_slice", + "output": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "8771e33a50e92b7b9a2b3c66fd99f3c417e4c2d3aef1152b4d64e8df6169f831" + } + ], + "name": "test_struct", + "output": "8771e33a50e92b7b9a2b3c66fd99f3c417e4c2d3aef1152b4d64e8df6169f831", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "7e5af3efc0fd41ec44b9367746ca17af79224954690bed680461173162a62926" + } + ], + "name": "test_struct_with_generic", + "output": "7e5af3efc0fd41ec44b9367746ca17af79224954690bed680461173162a62926", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "5a05acd79ded5b8adc972345c8309f451c54aa0c27b2873554ac4758191aad5f" + } + ], + "name": "test_tuple", + "output": "5a05acd79ded5b8adc972345c8309f451c54aa0c27b2873554ac4758191aad5f", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "c4a4edf5dbd3026f55a64842d4610751fd4996e511a21367af515102dfb99a1b" + } + ], + "name": "test_u128", + "output": "c4a4edf5dbd3026f55a64842d4610751fd4996e511a21367af515102dfb99a1b", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" + } + ], + "name": "test_u16", + "output": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e" + } + ], + "name": "test_u256", + "output": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + } + ], + "name": "test_u32", + "output": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ], + "name": "test_u64", + "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" + } + ], + "name": "test_u8", + "output": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + } + ], + "name": "test_unit", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": null + }, + { + "inputs": [ + { + "name": "a", + "concreteTypeId": "d5bfe1d4e1ace20166c9b50cadd47e862020561bde24f5189cfc2723f5ed76f4" + } + ], + "name": "test_vector", + "output": "d5bfe1d4e1ace20166c9b50cadd47e862020561bde24f5189cfc2723f5ed76f4", + "attributes": null + }, + { + "inputs": [ + { + "name": "amount_to_transfer", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "asset_id", + "concreteTypeId": "c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974" + }, + { + "name": "recipient", + "concreteTypeId": "ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335" + } + ], + "name": "transfer", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": [ + { + "name": "payable", + "arguments": [] + } + ] + } + ], + "loggedTypes": [], + "messagesTypes": [], + "configurables": [] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-storage_slots.json b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-storage_slots.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types-storage_slots.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types.bin b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types.bin new file mode 100644 index 00000000000..fed08f7707e Binary files /dev/null and b/forc-plugins/forc-client/test/data/contract_with_types/contract_with_types.bin differ diff --git a/forc-plugins/forc-client/test/data/contract_with_types/src/main.sw b/forc-plugins/forc-client/test/data/contract_with_types/src/main.sw new file mode 100644 index 00000000000..a4f3c314764 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_types/src/main.sw @@ -0,0 +1,156 @@ +contract; + +use std::{asset::transfer, bytes::Bytes, context::balance_of, u128::U128}; + +struct GenericStruct { + value: T, + description: str[4], +} + +enum GenericEnum { + container: GenericStruct, + value: T, +} + +struct ComplexStruct { + info: (User, u64), + status: Status, + data: u64, + generic: GenericStruct, +} + +enum Status { + Active: bool, + Pending: u64, + Inactive: (), +} + +struct User { + name: str[2], + id: u64, +} + +abi MyContract { + fn test_empty_no_return(); + fn test_empty() -> (); + fn test_unit(a: ()) -> (); + fn test_u8(a: u8) -> u8; + fn test_u16(a: u16) -> u16; + fn test_u32(a: u32) -> u32; + fn test_u64(a: u64) -> u64; + fn test_u128(a: U128) -> U128; + fn test_u256(a: u256) -> u256; + fn test_b256(a: b256) -> b256; + fn test_bytes(a: Bytes) -> Bytes; + fn test_str(a: str) -> str; + fn test_str_array(a: str[10]) -> str[10]; + // str and str[] are the same type + fn test_str_slice(a: str) -> str; + fn test_tuple(a: (u64, bool)) -> (u64, bool); + fn test_array(a: [u64; 10]) -> [u64; 10]; + fn test_vector(a: Vec) -> Vec; + fn test_struct(a: User) -> User; + fn test_enum(a: Status) -> Status; + fn test_option(a: Option) -> Option; + + // complex functions + fn test_struct_with_generic(a: GenericStruct) -> GenericStruct; + fn test_enum_with_generic(a: GenericEnum) -> GenericEnum; + fn test_enum_with_complex_generic( + a: GenericEnum>, + ) -> GenericEnum>; + fn test_complex_struct(a: ComplexStruct>) -> ComplexStruct>; + + // payable functions + #[payable] + fn transfer( + amount_to_transfer: u64, + asset_id: AssetId, + recipient: Identity, + ); +} + +impl MyContract for Contract { + fn test_empty() -> () {} + fn test_empty_no_return() {} + fn test_unit(a: ()) -> () { + a + } + fn test_u8(a: u8) -> u8 { + a + } + fn test_u16(a: u16) -> u16 { + a + } + fn test_u32(a: u32) -> u32 { + a + } + fn test_u64(a: u64) -> u64 { + a + } + fn test_u128(a: U128) -> U128 { + a + } + fn test_u256(a: u256) -> u256 { + a + } + fn test_b256(a: b256) -> b256 { + a + } + fn test_bytes(a: Bytes) -> Bytes { + a + } + fn test_str(a: str) -> str { + a + } + fn test_str_array(a: str[10]) -> str[10] { + a + } + fn test_str_slice(a: str) -> str { + a + } + fn test_tuple(a: (u64, bool)) -> (u64, bool) { + a + } + fn test_array(a: [u64; 10]) -> [u64; 10] { + a + } + fn test_vector(a: Vec) -> Vec { + a + } + fn test_struct(a: User) -> User { + a + } + fn test_enum(a: Status) -> Status { + a + } + fn test_option(a: Option) -> Option { + a + } + + // complex functions + fn test_struct_with_generic(a: GenericStruct) -> GenericStruct { + a + } + fn test_enum_with_generic(a: GenericEnum) -> GenericEnum { + a + } + fn test_enum_with_complex_generic( + a: GenericEnum>, + ) -> GenericEnum> { + a + } + fn test_complex_struct(a: ComplexStruct>) -> ComplexStruct> { + a + } + + // payable functions + #[payable] + fn transfer( + amount_to_transfer: u64, + asset_id: AssetId, + recipient: Identity, + ) { + transfer(recipient, asset_id, amount_to_transfer); + } +} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 210c3a11aa3..5deaa08d777 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -361,6 +361,7 @@ async fn test_simple_deploy() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -403,6 +404,7 @@ async fn test_deploy_submit_only() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -450,6 +452,7 @@ async fn test_deploy_fresh_proxy() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -502,6 +505,7 @@ async fn test_proxy_contract_re_routes_call() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -560,6 +564,7 @@ async fn test_proxy_contract_re_routes_call() { target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -635,6 +640,7 @@ async fn test_non_owner_fails_to_set_target() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -739,6 +745,7 @@ async fn chunked_deploy() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -772,6 +779,7 @@ async fn chunked_deploy_re_routes_calls() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -815,6 +823,7 @@ async fn chunked_deploy_with_proxy_re_routes_call() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -848,6 +857,7 @@ async fn can_deploy_script() { target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -879,6 +889,7 @@ async fn deploy_script_calls() { target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -911,6 +922,7 @@ async fn deploy_script_calls() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, @@ -1002,6 +1014,7 @@ async fn can_deploy_predicates() { target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -1033,6 +1046,7 @@ async fn deployed_predicate_call() { target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -1180,6 +1194,7 @@ async fn call_with_forc_generated_overrides(node_url: &str, contract_id: Contrac target: None, testnet: false, mainnet: false, + devnet: false, }; let pkg = Pkg { path: Some(tmp_dir.path().display().to_string()), @@ -1290,6 +1305,7 @@ async fn offset_shifted_abi_works() { target: None, testnet: false, mainnet: false, + devnet: false, }; let cmd = cmd::Deploy { pkg, diff --git a/forc-plugins/forc-tx/Cargo.toml b/forc-plugins/forc-tx/Cargo.toml index 91fee672cf3..f0fd7f21002 100644 --- a/forc-plugins/forc-tx/Cargo.toml +++ b/forc-plugins/forc-tx/Cargo.toml @@ -22,6 +22,7 @@ devault.workspace = true forc-util.workspace = true fuel-tx = { workspace = true, features = ["random", "test-helpers"] } fuel-types = { workspace = true, features = ["serde"] } +fuels-core = { workspace = true } serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/forc-plugins/forc-tx/src/lib.rs b/forc-plugins/forc-tx/src/lib.rs index a5e0d9d8d09..194b22fa0cc 100644 --- a/forc-plugins/forc-tx/src/lib.rs +++ b/forc-plugins/forc-tx/src/lib.rs @@ -8,6 +8,7 @@ use fuel_tx::{ policies::{Policies, PolicyType}, Buildable, Chargeable, ConsensusParameters, }; +use fuels_core::types::transaction::TxPolicies; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use thiserror::Error; @@ -197,7 +198,7 @@ pub struct Script { } /// Flag set for specifying gas price and limit. -#[derive(Debug, Devault, Parser, Deserialize, Serialize)] +#[derive(Debug, Devault, Clone, Parser, Deserialize, Serialize)] pub struct Gas { /// Gas price for the transaction. #[clap(long = "gas-price")] @@ -208,6 +209,9 @@ pub struct Gas { /// Max fee for the transaction. #[clap(long)] pub max_fee: Option, + /// The tip for the transaction. + #[clap(long)] + pub tip: Option, } /// Block until which tx cannot be included. @@ -913,6 +917,22 @@ impl From for fuel_tx::Output { } } +impl From<&Gas> for TxPolicies { + fn from(gas: &Gas) -> Self { + let mut policies = TxPolicies::default(); + if let Some(max_fee) = gas.max_fee { + policies = policies.with_max_fee(max_fee); + } + if let Some(script_gas_limit) = gas.script_gas_limit { + policies = policies.with_script_gas_limit(script_gas_limit); + } + if let Some(tip) = gas.tip { + policies = policies.with_tip(tip); + } + policies + } +} + #[test] fn test_parse_create() { let cmd = r#" diff --git a/swayfmt/src/parse.rs b/swayfmt/src/parse.rs index 17befe22cc3..8cb185ee2e6 100644 --- a/swayfmt/src/parse.rs +++ b/swayfmt/src/parse.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use sway_ast::{attribute::Annotated, token::CommentedTokenStream, Module}; use sway_error::handler::{ErrorEmitted, Handler}; -fn with_handler( +pub fn with_handler( run: impl FnOnce(&Handler) -> Result, ) -> Result { let handler = <_>::default(); diff --git a/test/src/sdk-harness/test_projects/tx_fields/mod.rs b/test/src/sdk-harness/test_projects/tx_fields/mod.rs index 59c29f27442..a3d6739e7c2 100644 --- a/test/src/sdk-harness/test_projects/tx_fields/mod.rs +++ b/test/src/sdk-harness/test_projects/tx_fields/mod.rs @@ -1630,8 +1630,6 @@ mod outputs { use super::*; mod success { - use fuel_core_client::client::schema::schema::__fields::GasCosts::exp; - use super::*; #[tokio::test]