diff --git a/Cargo.lock b/Cargo.lock index b0c7e8122fe..b3cf2582ba4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2964,6 +2964,7 @@ dependencies = [ "dirs 5.0.1", "fd-lock", "forc-tracing 0.64.0", + "fuel-abi-types", "fuel-tx", "hex", "paste", diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 2dbbc45f20d..4dec37de12b 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -11,6 +11,7 @@ use crate::{ use anyhow::{anyhow, bail, Context, Error, Result}; use byte_unit::{Byte, UnitType}; use forc_tracing::{println_action_green, println_warning}; +use forc_util::configurables::ConfigurableDeclarations; use forc_util::{ default_output_directory, find_file_name, kebab_to_snake_case, print_compiling, print_on_failure, print_warnings, @@ -266,6 +267,8 @@ pub struct PrintOpts { pub ir: PrintIr, /// Output build errors and warnings in reverse order. pub reverse_order: bool, + /// Generate a JSON file at the given path to use for overriding configurable slots. + pub configurable_override_file: Option, } #[derive(Default, Clone)] @@ -2149,6 +2152,7 @@ pub fn build_with_options(build_options: &BuildOpts) -> Result { build_target, member_filter, experimental, + print: print_opts, .. } = &build_options; @@ -2229,6 +2233,15 @@ pub fn build_with_options(build_options: &BuildOpts) -> Result { if let Some(outfile) = &debug_outfile { built_package.write_debug_info(outfile.as_ref())?; } + if let Some(path) = &print_opts.configurable_override_file { + if let ProgramABI::Fuel(program_abi) = &built_package.program_abi { + let config_decls = ConfigurableDeclarations::try_from(program_abi.clone())?; + let config_decls_str = serde_json::to_string_pretty(&config_decls)?; + fs::write(path, config_decls_str)?; + } else { + bail!("Only Fuel ABI configurable generation is supported"); + } + } built_package.write_output(minify, &pkg_manifest.project.name, &output_dir)?; built_workspace.push(Arc::new(built_package)); } diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index 5553a2fe06b..d1955951436 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -84,11 +84,21 @@ pub struct Command { #[clap(long, verbatim_doc_comment, name = "JSON_FILE_PATH")] pub override_storage_slots: Option, + /// Override configurable slot initialization. + /// + /// By default, configurable slots are initialized with the values defined in the configurable block in + /// the sway code. You can override the initialization by providing the file path to a JSON file + /// containing the overridden values. + /// + /// Use forc build --generate_configurable_slots_file to generate the file. + #[clap(long, verbatim_doc_comment)] + pub override_configurable_slots: Option, + /// Disable the "new encoding" feature #[clap(long)] pub no_encoding_v1: bool, /// AWS KMS signer arn. If present forc-deploy will automatically use AWS KMS signer instead of forc-wallet. - #[clap(long)] + #[clap(long, name = "AWS_KMS_ARN")] pub aws_kms_signer: Option, } diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index a5c71a41f79..bcfc01b28d8 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -16,7 +16,7 @@ use anyhow::{bail, Context, Result}; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, PackageManifestFile}; use forc_tracing::{println_action_green, println_warning}; -use forc_util::default_output_directory; +use forc_util::{configurables::ConfigurableDeclarations, default_output_directory}; use forc_wallet::utils::default_wallet_path; use fuel_core_client::client::types::{ChainInfo, TransactionStatus}; use fuel_core_client::client::FuelClient; @@ -29,7 +29,10 @@ use fuels::{ types::{bech32::Bech32ContractId, transaction_builders::Blob}, }; use fuels_accounts::{provider::Provider, Account, ViewOnlyAccount}; -use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; +use fuels_core::{ + codec::ABIEncoder, + types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}, +}; use futures::FutureExt; use pkg::{manifest::build_profile::ExperimentalFlags, BuildProfile, BuiltPackage}; use serde::{Deserialize, Serialize}; @@ -513,9 +516,26 @@ pub async fn deploy_pkg( let node_url = provider.url(); let client = FuelClient::new(node_url)?; - let bytecode = &compiled.bytecode.bytes; + let mut bytecode = compiled.bytecode.bytes.clone(); let storage_slots = resolve_storage_slots(command, compiled)?; + if let Some(config_slots) = &command.override_configurable_slots { + let mut config_values_with_offset: Vec<_> = vec![]; + let config_decls = ConfigurableDeclarations::from_file(&PathBuf::from_str(config_slots)?)?; + for decl in config_decls.declarations.values() { + let config_type = crate::util::encode::Type::from_str(&decl.config_type)?; + let token = crate::util::encode::Token::from_type_and_value(&config_type, &decl.value)?; + let abi_encoder = ABIEncoder::new(Default::default()); + let encoded_val = abi_encoder.encode(&[token.0])?; + let offset = decl.offset as usize; + config_values_with_offset.push((offset, encoded_val)); + } + + for (offset, val) in config_values_with_offset { + bytecode[offset..offset + val.len()].copy_from_slice(&val); + } + } + let contract = Contract::from(bytecode.clone()); let root = contract.root(); let state_root = Contract::initial_state_root(storage_slots.iter()); @@ -651,6 +671,7 @@ fn build_opts_from_cmd(cmd: &cmd::Deploy) -> pkg::BuildOpts { bytecode_spans: false, ir: cmd.print.ir(), reverse_order: cmd.print.reverse_order, + configurable_override_file: cmd.print.configurable_override_file.clone(), }, time_phases: cmd.print.time_phases, metrics_outfile: cmd.print.metrics_outfile.clone(), diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index 5e58835fca0..3e55cacc848 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -257,6 +257,7 @@ fn build_opts_from_cmd(cmd: &cmd::Run) -> pkg::BuildOpts { bytecode_spans: false, ir: cmd.print.ir(), reverse_order: cmd.print.reverse_order, + configurable_override_file: cmd.print.configurable_override_file.clone(), }, minify: pkg::MinifyOpts { json_abi: cmd.minify.json_abi, diff --git a/forc-plugins/forc-client/src/util/encode.rs b/forc-plugins/forc-client/src/util/encode.rs index f1ab46793e4..80a74048aad 100644 --- a/forc-plugins/forc-client/src/util/encode.rs +++ b/forc-plugins/forc-client/src/util/encode.rs @@ -1,7 +1,7 @@ use anyhow::Context; use fuel_abi_types::abi::full_program::FullTypeApplication; +use fuels::types::{Bits256, StaticStringToken, U256}; use std::str::FromStr; -use sway_types::u256::U256; /// A wrapper around fuels_core::types::Token, which enables serde de/serialization. #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -14,8 +14,12 @@ pub(crate) enum Type { U16, U32, U64, + U128, U256, + B256, Bool, + String, + StringArray(usize), } impl TryFrom<&FullTypeApplication> for Type { @@ -53,14 +57,29 @@ impl Token { Ok(Token(fuels_core::types::Token::U64(u64_val))) } Type::U256 => { - let v = value.parse::().context("u256 literal out of range")?; - let bytes = v.to_be_bytes(); - Ok(Token(fuels_core::types::Token::U256(bytes.into()))) + let v = value.parse::().context("Invalid value for U256")?; + Ok(Token(fuels_core::types::Token::U256(v))) } Type::Bool => { let bool_val = value.parse::()?; Ok(Token(fuels_core::types::Token::Bool(bool_val))) } + Type::U128 => { + let u128_val = value.parse::()?; + Ok(Token(fuels_core::types::Token::U128(u128_val))) + } + Type::B256 => { + let bits256 = Bits256::from_hex_str(value)?; + Ok(Token(fuels_core::types::Token::B256(bits256.0))) + } + Type::String => { + let s = value.to_string(); + Ok(Token(fuels_core::types::Token::String(s))) + } + Type::StringArray(len) => { + let s = StaticStringToken::new(value.to_string(), Some(*len)); + Ok(Token(fuels_core::types::Token::StringArray(s))) + } } } } @@ -75,9 +94,27 @@ impl FromStr for Type { "u16" => Ok(Type::U16), "u32" => Ok(Type::U32), "u64" => Ok(Type::U64), + "u128" => Ok(Type::U128), "u256" => Ok(Type::U256), "bool" => Ok(Type::Bool), - other => anyhow::bail!("{other} type is not supported."), + "b256" => Ok(Type::B256), + "String" => Ok(Type::String), + other => { + // Use a regular expression to check for string slice syntax + let re = forc_util::Regex::new(r"^str\[(\d+)\]$").expect("Invalid regex pattern"); + if let Some(captures) = re.captures(other) { + // Extract the number inside the brackets + let len = captures + .get(1) + .ok_or_else(|| anyhow::anyhow!("Invalid string slice length"))? + .as_str() + .parse::() + .map_err(|_| anyhow::anyhow!("Invalid number for string slice length"))?; + Ok(Type::StringArray(len)) + } else { + anyhow::bail!("{other} type is not supported.") + } + } } } } @@ -85,6 +122,8 @@ impl FromStr for Type { #[cfg(test)] mod tests { use super::*; + use fuel_abi_types::abi::full_program::{FullTypeApplication, FullTypeDeclaration}; + use fuels::types::StaticStringToken; #[test] fn test_token_generation_success() { @@ -92,29 +131,123 @@ mod tests { let u16_token = Token::from_type_and_value(&Type::U16, "1").unwrap(); let u32_token = Token::from_type_and_value(&Type::U32, "1").unwrap(); let u64_token = Token::from_type_and_value(&Type::U64, "1").unwrap(); + let u128_token = Token::from_type_and_value(&Type::U128, "1").unwrap(); + let u256_token = Token::from_type_and_value(&Type::U256, "1").unwrap(); + let b256_token = Token::from_type_and_value( + &Type::B256, + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); let bool_token = Token::from_type_and_value(&Type::Bool, "true").unwrap(); + let string_token = Token::from_type_and_value(&Type::String, "Hello, World!").unwrap(); + let string_array_token = + Token::from_type_and_value(&Type::StringArray(5), "Hello").unwrap(); - let generated_tokens = [u8_token, u16_token, u32_token, u64_token, bool_token]; + let generated_tokens = [ + u8_token, + u16_token, + u32_token, + u64_token, + u128_token, + u256_token, + b256_token, + bool_token, + string_token, + string_array_token, + ]; let expected_tokens = [ Token(fuels_core::types::Token::U8(1)), Token(fuels_core::types::Token::U16(1)), Token(fuels_core::types::Token::U32(1)), Token(fuels_core::types::Token::U64(1)), + Token(fuels_core::types::Token::U128(1)), + Token(fuels_core::types::Token::U256(fuels::types::U256([ + 1, 0, 0, 0, + ]))), + Token(fuels_core::types::Token::B256([0; 32])), Token(fuels_core::types::Token::Bool(true)), + Token(fuels_core::types::Token::String( + "Hello, World!".to_string(), + )), + Token(fuels_core::types::Token::StringArray( + StaticStringToken::new("Hello".to_string(), Some(5)), + )), ]; assert_eq!(generated_tokens, expected_tokens) } #[test] - #[should_panic] fn test_token_generation_fail_type_mismatch() { - Token::from_type_and_value(&Type::U8, "false").unwrap(); + for (arg_type, value, error_msg) in [ + (Type::U8, "false", "invalid digit found in string"), + (Type::U16, "false", "invalid digit found in string"), + (Type::U32, "false", "invalid digit found in string"), + (Type::U64, "false", "invalid digit found in string"), + (Type::U128, "false", "invalid digit found in string"), + (Type::U256, "false", "Invalid value for U256"), + (Type::B256, "false", "Odd number of digits"), + (Type::B256, "0x123", "Odd number of digits"), + ( + Type::Bool, + "Hello", + "provided string was not `true` or `false`", + ), + ] { + assert_eq!( + Token::from_type_and_value(&arg_type, value) + .expect_err("should panic") + .to_string(), + error_msg + ); + } + } + + #[test] + fn test_token_generation_fail_out_of_range() { + for (arg_type, value, error_msg) in [ + (Type::U8, "256", "number too large to fit in target type"), + (Type::U16, "65536", "number too large to fit in target type"), + ( + Type::U32, + "4294967296", + "number too large to fit in target type", + ), + ( + Type::U64, + "18446744073709551616", + "number too large to fit in target type", + ), + ( + Type::U128, + "340282366920938463463374607431768211456", + "number too large to fit in target type", + ), + ( + Type::U256, + "115792089237316195423570985008687907853269984665640564039457584007913129639936", + "Invalid value for U256", + ), + ( + Type::B256, + "0x10000000000000000000000000000000000000000000000000000000000000000", + "Odd number of digits", + ), + ] { + assert_eq!( + Token::from_type_and_value(&arg_type, value) + .expect_err("should panic") + .to_string(), + error_msg + ); + } } #[test] fn test_type_generation_success() { - let possible_type_list = ["()", "u8", "u16", "u32", "u64", "u256", "bool"]; + let possible_type_list = [ + "()", "u8", "u16", "u32", "u64", "u128", "u256", "b256", "bool", "String", "str[5]", + ]; let types = possible_type_list .iter() .map(|type_str| Type::from_str(type_str)) @@ -127,8 +260,12 @@ mod tests { Type::U16, Type::U32, Type::U64, + Type::U128, Type::U256, + Type::B256, Type::Bool, + Type::String, + Type::StringArray(5), ]; assert_eq!(types, expected_types) } @@ -139,4 +276,41 @@ mod tests { let invalid_type_str = "u2"; Type::from_str(invalid_type_str).unwrap(); } + + #[test] + #[should_panic(expected = "str[abc] type is not supported.")] + fn test_type_generation_fail_invalid_string_slice() { + let invalid_type_str = "str[abc]"; + Type::from_str(invalid_type_str).unwrap(); + } + + #[test] + fn test_try_from_full_type_application() { + let type_application = FullTypeApplication { + type_decl: FullTypeDeclaration { + type_field: "u8".to_string(), + components: Default::default(), + type_parameters: Default::default(), + }, + name: "none".to_string(), + type_arguments: vec![], + }; + let generated_type = Type::try_from(&type_application).unwrap(); + assert_eq!(generated_type, Type::U8); + } + + #[test] + #[should_panic(expected = "str[abc] type is not supported.")] + fn test_try_from_full_type_application_invalid_string_slice() { + let type_application = FullTypeApplication { + type_decl: FullTypeDeclaration { + type_field: "str[abc]".to_string(), + components: Default::default(), + type_parameters: Default::default(), + }, + name: "none".to_string(), + type_arguments: vec![], + }; + Type::try_from(&type_application).unwrap(); + } } diff --git a/forc-plugins/forc-client/test/data/contract_with_configurables/.gitignore b/forc-plugins/forc-client/test/data/contract_with_configurables/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_configurables/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/forc-plugins/forc-client/test/data/contract_with_configurables/Forc.toml b/forc-plugins/forc-client/test/data/contract_with_configurables/Forc.toml new file mode 100644 index 00000000000..a30b1f64490 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_configurables/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract_with_configurables" + +[dependencies] +std = { path = "../../../../../sway-lib-std/" } + diff --git a/forc-plugins/forc-client/test/data/contract_with_configurables/contract_with_configurables-abi.json b/forc-plugins/forc-client/test/data/contract_with_configurables/contract_with_configurables-abi.json new file mode 100644 index 00000000000..a7076a012bd --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_configurables/contract_with_configurables-abi.json @@ -0,0 +1,136 @@ +{ + "programType": "contract", + "specVersion": "1", + "encodingVersion": "1", + "concreteTypes": [ + { + "type": "(bool, u8, u16, u32, u64, u256, b256, str[4])", + "concreteTypeId": "bb312f998479ccee7bd35192143d546be33c1d6865813eca0b67e3feb9c30a09", + "metadataTypeId": 0 + }, + { + "type": "b256", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + }, + { + "type": "bool", + "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "type": "str[4]", + "concreteTypeId": "94f0fa95c830be5e4f711963e83259fe7e8bc723278ab6ec34449e791a99b53a" + }, + { + "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": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "name": "__tuple_element", + "typeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" + }, + { + "name": "__tuple_element", + "typeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" + }, + { + "name": "__tuple_element", + "typeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + }, + { + "name": "__tuple_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "__tuple_element", + "typeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e" + }, + { + "name": "__tuple_element", + "typeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + }, + { + "name": "__tuple_element", + "typeId": "94f0fa95c830be5e4f711963e83259fe7e8bc723278ab6ec34449e791a99b53a" + } + ] + } + ], + "functions": [ + { + "inputs": [], + "name": "return_configurables", + "output": "bb312f998479ccee7bd35192143d546be33c1d6865813eca0b67e3feb9c30a09", + "attributes": null + } + ], + "loggedTypes": [], + "messagesTypes": [], + "configurables": [ + { + "name": "BOOL", + "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", + "offset": 4528 + }, + { + "name": "U8", + "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b", + "offset": 4600 + }, + { + "name": "U16", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef", + "offset": 4544 + }, + { + "name": "U32", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc", + "offset": 4584 + }, + { + "name": "U64", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0", + "offset": 4592 + }, + { + "name": "U256", + "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e", + "offset": 4552 + }, + { + "name": "B256", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b", + "offset": 4496 + }, + { + "name": "STR_4", + "concreteTypeId": "94f0fa95c830be5e4f711963e83259fe7e8bc723278ab6ec34449e791a99b53a", + "offset": 4536 + } + ] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/test/data/contract_with_configurables/override-configurables.json b/forc-plugins/forc-client/test/data/contract_with_configurables/override-configurables.json new file mode 100644 index 00000000000..2b5906a6350 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_configurables/override-configurables.json @@ -0,0 +1,42 @@ +{ + "U16": { + "configType": "u16", + "offset": 4544, + "value": "2" + }, + "BOOL": { + "configType": "bool", + "offset": 4528, + "value": "false" + }, + "STR_4": { + "configType": "str[4]", + "offset": 4536, + "value": "test" + }, + "U256": { + "configType": "u256", + "offset": 4552, + "value": "0x0000000000000000000000000000000000000000000000000000000000000005" + }, + "U32": { + "configType": "u32", + "offset": 4584, + "value": "3" + }, + "U64": { + "configType": "u64", + "offset": 4592, + "value": "4" + }, + "B256": { + "configType": "b256", + "offset": 4496, + "value": "0x0606060606060606060606060606060606060606060606060606060606060606" + }, + "U8": { + "configType": "u8", + "offset": 4600, + "value": "1" + } +} diff --git a/forc-plugins/forc-client/test/data/contract_with_configurables/src/main.sw b/forc-plugins/forc-client/test/data/contract_with_configurables/src/main.sw new file mode 100644 index 00000000000..e1e0fabf564 --- /dev/null +++ b/forc-plugins/forc-client/test/data/contract_with_configurables/src/main.sw @@ -0,0 +1,22 @@ +contract; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + U16: u16 = 16, + U32: u32 = 32, + U64: u64 = 63, + U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256, + B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101, + STR_4: str[4] = __to_str_array("fuel"), +} + +abi TestContract { + fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4]); +} + +impl TestContract for Contract { + fn return_configurables() -> (bool, u8, u16, u32, u64, u256, b256, str[4]) { + (BOOL, U8, U16, U32, U64, U256, B256, STR_4) + } +} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index eb3c8e259a8..7f976664777 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -5,12 +5,12 @@ use forc_client::{ util::{account::ForcClientAccount, tx::update_proxy_contract_target}, NodeTarget, }; -use forc_pkg::manifest::Proxy; +use forc_pkg::{manifest::Proxy, BuildProfile}; use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; use fuels::{ macros::abigen, - types::{transaction::TxPolicies, AsciiString, Bits256}, + types::{transaction::TxPolicies, AsciiString, Bits256, SizedAsciiString}, }; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; @@ -793,3 +793,75 @@ async fn chunked_deploy_with_proxy_re_routes_call() { node.kill().unwrap(); } + +#[tokio::test] +async fn configurable_override() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("contract_with_configurables"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let override_config_file = tmp_dir.path().join("override-configurables.json"); + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + override_configurable_slots: Some(format!("{}", override_config_file.display())), + build_profile: BuildProfile::RELEASE.to_string(), + ..Default::default() + }; + let deployed_contract = deploy(cmd).await.unwrap().remove(0); + + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + let contract_id = deployed_contract.id; + abigen!(Contract( + name = "ContractWithConfigurables", + abi = "forc-plugins/forc-client/test/data/contract_with_configurables/contract_with_configurables-abi.json" + )); + + let instance = ContractWithConfigurables::new(contract_id, wallet_unlocked); + + let (b_val, u8_val, u16_val, u32_val, u64_val, u256_val, b256_val, str_arr_val) = instance + .methods() + .return_configurables() + .call() + .await + .unwrap() + .value; + + assert!(!b_val); + assert_eq!(u8_val, 1); + assert_eq!(u16_val, 2); + assert_eq!(u32_val, 3); + assert_eq!(u64_val, 4); + let expected_u256 = fuels::types::U256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(); + assert_eq!(u256_val, expected_u256); + let expected_b256 = fuels::types::Bits256::from_hex_str( + "0x0606060606060606060606060606060606060606060606060606060606060606", + ) + .unwrap(); + assert_eq!(b256_val, expected_b256); + let expected_str_arr = SizedAsciiString::new("test".to_string()).unwrap(); + assert_eq!(str_arr_val, expected_str_arr); + + node.kill().unwrap(); +} diff --git a/forc-util/Cargo.toml b/forc-util/Cargo.toml index 47e702cec70..97d96cabb65 100644 --- a/forc-util/Cargo.toml +++ b/forc-util/Cargo.toml @@ -16,6 +16,7 @@ clap = { workspace = true, features = ["cargo", "derive", "env"] } dirs.workspace = true fd-lock.workspace = true forc-tracing.workspace = true +fuel-abi-types.workspace = true fuel-tx = { workspace = true, features = ["serde"], optional = true } hex.workspace = true paste.workspace = true diff --git a/forc-util/src/configurables.rs b/forc-util/src/configurables.rs new file mode 100644 index 00000000000..e14d3ff6a67 --- /dev/null +++ b/forc-util/src/configurables.rs @@ -0,0 +1,118 @@ +use fuel_abi_types::abi::program::{ConcreteTypeId, ProgramABI}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::Path}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurableDeclarations { + #[serde(flatten)] + pub declarations: HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurableDeclaration { + /// Name of the configurable field. + pub config_type: String, + /// Offset of the configurable field. + pub offset: u64, + /// Value of the configurable field. + pub value: String, +} + +impl ConfigurableDeclaration { + pub fn new(config_type: String, offset: u64, value: String) -> Self { + Self { + config_type, + offset, + value, + } + } +} + +impl ConfigurableDeclarations { + pub fn new(declarations: HashMap) -> Self { + Self { declarations } + } + + /// Read `ConfigurableDeclarations` from json file at given `path`. + pub fn from_file(path: &Path) -> anyhow::Result { + let decls = std::fs::read_to_string(path).map_err(|e| { + anyhow::anyhow!( + "failed to read to configurable manifest from {}, error: {}", + path.display(), + e + ) + })?; + let decls: ConfigurableDeclarations = serde_json::from_str(&decls)?; + Ok(decls) + } +} + +impl TryFrom for ConfigurableDeclarations { + type Error = anyhow::Error; + + fn try_from(value: ProgramABI) -> Result { + let concrete_type_lookup: HashMap<&ConcreteTypeId, &str> = value + .concrete_types + .iter() + .map(|conc_type| (&conc_type.concrete_type_id, conc_type.type_field.as_str())) + .collect(); + + let configurables = value + .configurables + .unwrap_or_default() + .iter() + .map(|configurable| { + let config_name = configurable.name.as_str(); + let config_concrete_type_id = &configurable.concrete_type_id; + let config_type_str: &str = concrete_type_lookup + .get(config_concrete_type_id) + .ok_or_else(|| { + anyhow::anyhow!("missing {config_name} type declaration in program abi.") + })?; + let offset = configurable.offset; + + let decl = ConfigurableDeclaration::new( + config_type_str.to_string(), + offset, + "".to_string(), + ); + + Ok((config_name.to_string(), decl)) + }) + .collect::>>()?; + + Ok(Self::new(configurables)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_configurable_decl() { + let decl = r#"{ "configType": "Type A", "offset": 120, "value": "Value" }"#; + let decl_parsed: ConfigurableDeclaration = serde_json::from_str(decl).unwrap(); + + assert_eq!(decl_parsed.config_type, "Type A".to_string()); + assert_eq!(decl_parsed.offset, 120); + assert_eq!(decl_parsed.value, "Value".to_string()) + } + + #[test] + fn test_configurable_decls() { + let decls = r#"{ "configName": {"configType": "Name", "offset": 120, "value": "Value"} }"#; + let decls_parsed: ConfigurableDeclarations = serde_json::from_str(decls).unwrap(); + + assert_eq!(decls_parsed.declarations.len(), 1); + + let decl_parsed = decls_parsed.declarations.iter().next().unwrap(); + + assert_eq!(decl_parsed.0, "configName"); + assert_eq!(decl_parsed.1.config_type, "Name"); + assert_eq!(decl_parsed.1.offset, 120); + assert_eq!(decl_parsed.1.value, "Value"); + } +} diff --git a/forc-util/src/lib.rs b/forc-util/src/lib.rs index a630f741023..5eb318137c1 100644 --- a/forc-util/src/lib.rs +++ b/forc-util/src/lib.rs @@ -23,6 +23,7 @@ use sway_error::{ use sway_types::{LineCol, LineColRange, SourceEngine, Span}; use sway_utils::constants; +pub mod configurables; pub mod fs_locking; pub mod restricted; diff --git a/forc/src/cli/commands/test.rs b/forc/src/cli/commands/test.rs index 5ffa1c51d75..34d6378229a 100644 --- a/forc/src/cli/commands/test.rs +++ b/forc/src/cli/commands/test.rs @@ -243,6 +243,7 @@ fn opts_from_cmd(cmd: Command) -> forc_test::TestOpts { bytecode_spans: false, ir: cmd.build.print.ir(), reverse_order: cmd.build.print.reverse_order, + configurable_override_file: cmd.build.print.configurable_override_file.clone(), }, time_phases: cmd.build.print.time_phases, metrics_outfile: cmd.build.print.metrics_outfile, diff --git a/forc/src/cli/shared.rs b/forc/src/cli/shared.rs index 28b38465df3..54ce3e5b011 100644 --- a/forc/src/cli/shared.rs +++ b/forc/src/cli/shared.rs @@ -105,6 +105,9 @@ pub struct Print { /// Output compilation metrics into the specified file. #[clap(long)] pub metrics_outfile: Option, + /// Generate a JSON file at the given path to use for overriding configurable slots. + #[clap(long, name = "OUTPUT_PATH")] + pub configurable_override_file: Option, } impl Print { diff --git a/forc/src/ops/forc_build.rs b/forc/src/ops/forc_build.rs index 49c0c27f955..a890401d314 100644 --- a/forc/src/ops/forc_build.rs +++ b/forc/src/ops/forc_build.rs @@ -28,6 +28,7 @@ fn opts_from_cmd(cmd: BuildCommand) -> pkg::BuildOpts { bytecode_spans: false, ir: cmd.build.print.ir(), reverse_order: cmd.build.print.reverse_order, + configurable_override_file: cmd.build.print.configurable_override_file, }, time_phases: cmd.build.print.time_phases, metrics_outfile: cmd.build.print.metrics_outfile, diff --git a/forc/src/ops/forc_contract_id.rs b/forc/src/ops/forc_contract_id.rs index f81441637a5..9744936c55e 100644 --- a/forc/src/ops/forc_contract_id.rs +++ b/forc/src/ops/forc_contract_id.rs @@ -63,6 +63,7 @@ fn build_opts_from_cmd(cmd: &ContractIdCommand) -> pkg::BuildOpts { bytecode_spans: false, ir: cmd.print.ir(), reverse_order: cmd.print.reverse_order, + configurable_override_file: cmd.print.configurable_override_file.clone(), }, time_phases: cmd.print.time_phases, metrics_outfile: cmd.print.metrics_outfile.clone(), diff --git a/forc/src/ops/forc_predicate_root.rs b/forc/src/ops/forc_predicate_root.rs index 1381aec392e..d5cfb9f7bda 100644 --- a/forc/src/ops/forc_predicate_root.rs +++ b/forc/src/ops/forc_predicate_root.rs @@ -32,6 +32,7 @@ fn build_opts_from_cmd(cmd: PredicateRootCommand) -> pkg::BuildOpts { bytecode_spans: false, ir: cmd.print.ir(), reverse_order: cmd.print.reverse_order, + configurable_override_file: cmd.print.configurable_override_file.clone(), }, time_phases: cmd.print.time_phases, metrics_outfile: cmd.print.metrics_outfile, diff --git a/test/src/e2e_vm_tests/harness.rs b/test/src/e2e_vm_tests/harness.rs index e2a3236db07..b0fadcec377 100644 --- a/test/src/e2e_vm_tests/harness.rs +++ b/test/src/e2e_vm_tests/harness.rs @@ -288,6 +288,7 @@ pub(crate) async fn compile_to_bytes(file_name: &str, run_config: &RunConfig) -> bytecode_spans: run_config.print_bytecode, ir: run_config.print_ir.clone(), reverse_order: false, + configurable_override_file: None, }, pkg: forc_pkg::PkgOpts { path: Some(format!(