diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c83368f58..fe58a33f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ env: CARGO_TERM_COLOR: always DASEL_VERSION: https://github.com/TomWright/dasel/releases/download/v2.3.6/dasel_linux_amd64 RUSTFLAGS: "-D warnings" - FUEL_CORE_VERSION: 0.38.0 + FUEL_CORE_VERSION: 0.39.0 FUEL_CORE_PATCH_BRANCH: "" FUEL_CORE_PATCH_REVISION: "" RUST_VERSION: 1.79.0 diff --git a/Cargo.toml b/Cargo.toml index 1742b7803..a507c97ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ readme = "README.md" license = "Apache-2.0" repository = "https://github.com/FuelLabs/fuels-rs" rust-version = "1.79.0" -version = "0.66.7" +version = "0.66.8" [workspace.dependencies] Inflector = "0.11.4" @@ -48,6 +48,7 @@ async-trait = { version = "0.1.74", default-features = false } bech32 = "0.9.1" bytes = { version = "1.5.0", default-features = false } chrono = "0.4.31" +cynic = { version = "2.2", default-features = false } elliptic-curve = { version = "0.13.8", default-features = false } eth-keystore = "0.5.0" flate2 = { version = "1.0", default-features = false } @@ -85,14 +86,14 @@ octocrab = { version = "0.39", default-features = false } dotenv = { version = "0.15", default-features = false } # Dependencies from the `fuel-core` repository: -fuel-core = { version = "0.38.0", default-features = false, features = [ +fuel-core = { version = "0.39.0", default-features = false, features = [ "wasm-executor", ] } -fuel-core-chain-config = { version = "0.38.0", default-features = false } -fuel-core-client = { version = "0.38.0", default-features = false } -fuel-core-poa = { version = "0.38.0", default-features = false } -fuel-core-services = { version = "0.38.0", default-features = false } -fuel-core-types = { version = "0.38.0", default-features = false } +fuel-core-chain-config = { version = "0.39.0", default-features = false } +fuel-core-client = { version = "0.39.0", default-features = false } +fuel-core-poa = { version = "0.39.0", default-features = false } +fuel-core-services = { version = "0.39.0", default-features = false } +fuel-core-types = { version = "0.39.0", default-features = false } # Dependencies from the `fuel-vm` repository: fuel-asm = { version = "0.58.0" } @@ -104,11 +105,11 @@ fuel-types = { version = "0.58.0" } fuel-vm = { version = "0.58.0" } # Workspace projects -fuels = { version = "0.66.7", path = "./packages/fuels", default-features = false } -fuels-accounts = { version = "0.66.7", path = "./packages/fuels-accounts", default-features = false } -fuels-code-gen = { version = "0.66.7", path = "./packages/fuels-code-gen", default-features = false } -fuels-core = { version = "0.66.7", path = "./packages/fuels-core", default-features = false } -fuels-macros = { version = "0.66.7", path = "./packages/fuels-macros", default-features = false } -fuels-programs = { version = "0.66.7", path = "./packages/fuels-programs", default-features = false } -fuels-test-helpers = { version = "0.66.7", path = "./packages/fuels-test-helpers", default-features = false } -versions-replacer = { version = "0.66.7", path = "./scripts/versions-replacer", default-features = false } +fuels = { version = "0.66.8", path = "./packages/fuels", default-features = false } +fuels-accounts = { version = "0.66.8", path = "./packages/fuels-accounts", default-features = false } +fuels-code-gen = { version = "0.66.8", path = "./packages/fuels-code-gen", default-features = false } +fuels-core = { version = "0.66.8", path = "./packages/fuels-core", default-features = false } +fuels-macros = { version = "0.66.8", path = "./packages/fuels-macros", default-features = false } +fuels-programs = { version = "0.66.8", path = "./packages/fuels-programs", default-features = false } +fuels-test-helpers = { version = "0.66.8", path = "./packages/fuels-test-helpers", default-features = false } +versions-replacer = { version = "0.66.8", path = "./scripts/versions-replacer", default-features = false } diff --git a/docs/src/connecting/short-lived.md b/docs/src/connecting/short-lived.md index 6476a2781..df9df5085 100644 --- a/docs/src/connecting/short-lived.md +++ b/docs/src/connecting/short-lived.md @@ -27,7 +27,7 @@ let wallet = launch_provider_and_get_wallet().await?; The `fuel-core-lib` feature allows us to run a `fuel-core` node without installing the `fuel-core` binary on the local machine. Using the `fuel-core-lib` feature flag entails downloading all the dependencies needed to run the fuel-core node. ```rust,ignore -fuels = { version = "0.66.7", features = ["fuel-core-lib"] } +fuels = { version = "0.66.8", features = ["fuel-core-lib"] } ``` ### RocksDB @@ -35,5 +35,5 @@ fuels = { version = "0.66.7", features = ["fuel-core-lib"] } The `rocksdb` is an additional feature that, when combined with `fuel-core-lib`, provides persistent storage capabilities while using `fuel-core` as a library. ```rust,ignore -fuels = { version = "0.66.7", features = ["rocksdb"] } +fuels = { version = "0.66.8", features = ["rocksdb"] } ``` diff --git a/e2e/tests/providers.rs b/e2e/tests/providers.rs index cc2ddbf62..cd7b289fd 100644 --- a/e2e/tests/providers.rs +++ b/e2e/tests/providers.rs @@ -1214,3 +1214,70 @@ async fn contract_call_with_impersonation() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn is_account_query_test() -> Result<()> { + { + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.provider().unwrap().clone(); + + let blob = Blob::new(vec![1; 100]); + let blob_id = blob.id(); + + let is_account = provider.is_user_account(blob_id).await?; + assert!(is_account); + + let mut tb = BlobTransactionBuilder::default().with_blob(blob); + wallet.adjust_for_fee(&mut tb, 0).await?; + wallet.add_witnesses(&mut tb)?; + let tx = tb.build(provider.clone()).await?; + + provider + .send_transaction_and_await_commit(tx) + .await? + .check(None)?; + + let is_account = provider.is_user_account(blob_id).await?; + assert!(!is_account); + } + { + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.provider().unwrap().clone(); + + let contract = Contract::load_from( + "sway/contracts/contract_test/out/release/contract_test.bin", + LoadConfiguration::default(), + )?; + let contract_id = contract.contract_id(); + + let is_account = provider.is_user_account(*contract_id).await?; + assert!(is_account); + + contract.deploy(&wallet, TxPolicies::default()).await?; + + let is_account = provider.is_user_account(*contract_id).await?; + assert!(!is_account); + } + { + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.provider().unwrap().clone(); + + let mut tb = ScriptTransactionBuilder::default(); + wallet.adjust_for_fee(&mut tb, 0).await?; + wallet.add_witnesses(&mut tb)?; + let tx = tb.build(provider.clone()).await?; + + let tx_id = tx.id(provider.chain_id()); + let is_account = provider.is_user_account(tx_id).await?; + assert!(is_account); + + provider + .send_transaction_and_await_commit(tx) + .await? + .check(None)?; + let is_account = provider.is_user_account(tx_id).await?; + assert!(!is_account); + } + + Ok(()) +} diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index aa7c14d58..34dea20b8 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -12,6 +12,7 @@ description = "Fuel Rust SDK accounts." [dependencies] async-trait = { workspace = true, default-features = false } chrono = { workspace = true } +cynic = { workspace = true, optional = true } elliptic-curve = { workspace = true, default-features = false } eth-keystore = { workspace = true, optional = true } fuel-core-client = { workspace = true, optional = true } @@ -33,6 +34,10 @@ fuel-tx = { workspace = true, features = ["test-helpers", "random"] } tempfile = { workspace = true } tokio = { workspace = true, features = ["test-util"] } +[build-dependencies] +cynic = { workspace = true, features = ["default"], optional = true } +fuel-core-client = { workspace = true, optional = true } + [features] default = ["std"] coin-cache = ["tokio?/time"] @@ -41,4 +46,5 @@ std = [ "dep:tokio", "fuel-core-client/default", "dep:eth-keystore", + "dep:cynic", ] diff --git a/packages/fuels-accounts/build.rs b/packages/fuels-accounts/build.rs new file mode 100644 index 000000000..4314a65b2 --- /dev/null +++ b/packages/fuels-accounts/build.rs @@ -0,0 +1,15 @@ +fn main() { + #[cfg(feature = "std")] + { + use std::fs; + + fs::create_dir_all("target").expect("Unable to create target directory"); + fs::write( + "target/fuel-core-client-schema.sdl", + fuel_core_client::SCHEMA_SDL, + ) + .expect("Unable to write schema file"); + + println!("cargo:rerun-if-changed=build.rs"); + } +} diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 5e7439eac..862b3414f 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -711,6 +711,10 @@ impl Provider { Ok(proof) } + pub async fn is_user_account(&self, address: impl Into) -> Result { + self.client.is_user_account(*address.into()).await + } + pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self { self.client.set_retry_config(retry_config); diff --git a/packages/fuels-accounts/src/provider/retryable_client.rs b/packages/fuels-accounts/src/provider/retryable_client.rs index e25064ba9..7de393f01 100644 --- a/packages/fuels-accounts/src/provider/retryable_client.rs +++ b/packages/fuels-accounts/src/provider/retryable_client.rs @@ -1,5 +1,7 @@ use std::{future::Future, io}; +use custom_queries::{IsUserAccountQuery, IsUserAccountVariables}; +use cynic::QueryBuilder; use fuel_core_client::client::{ pagination::{PaginatedResult, PaginationRequest}, types::{ @@ -311,4 +313,54 @@ impl RetryableClient { .map(|contract| contract.is_some()) } // DELEGATION END + + pub async fn is_user_account(&self, address: [u8; 32]) -> Result { + let blob_id = BlobId::from(address); + let contract_id = ContractId::from(address); + let transaction_id = TransactionId::from(address); + + let query = IsUserAccountQuery::build(IsUserAccountVariables { + blob_id: blob_id.into(), + contract_id: contract_id.into(), + transaction_id: transaction_id.into(), + }); + + let response = self.client.query(query).await?; + + let is_resource = response.blob.is_some() + || response.contract.is_some() + || response.transaction.is_some(); + + Ok(!is_resource) + } +} + +mod custom_queries { + use fuel_core_client::client::schema::blob::BlobIdFragment; + use fuel_core_client::client::schema::schema; + use fuel_core_client::client::schema::{ + contract::ContractIdFragment, tx::TransactionIdFragment, BlobId, ContractId, TransactionId, + }; + + #[derive(cynic::QueryVariables, Debug)] + pub struct IsUserAccountVariables { + pub blob_id: BlobId, + pub contract_id: ContractId, + pub transaction_id: TransactionId, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic( + graphql_type = "Query", + variables = "IsUserAccountVariables", + schema_path = "./target/fuel-core-client-schema.sdl" + )] + pub struct IsUserAccountQuery { + #[arguments(id: $blob_id)] + pub blob: Option, + #[arguments(id: $contract_id)] + pub contract: Option, + #[arguments(id: $transaction_id)] + pub transaction: Option, + } } diff --git a/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs b/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs index 2040e3e96..c047bcf89 100644 --- a/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs +++ b/packages/fuels-accounts/src/provider/supported_fuel_core_version.rs @@ -1 +1 @@ -pub const SUPPORTED_FUEL_CORE_VERSION: semver::Version = semver::Version::new(0, 38, 0); +pub const SUPPORTED_FUEL_CORE_VERSION: semver::Version = semver::Version::new(0, 39, 0); diff --git a/packages/fuels-core/Cargo.toml b/packages/fuels-core/Cargo.toml index 1d42366aa..4c1ff67d4 100644 --- a/packages/fuels-core/Cargo.toml +++ b/packages/fuels-core/Cargo.toml @@ -28,6 +28,7 @@ itertools = { workspace = true } postcard = { version = "1", default-features = true, features = ["alloc"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, default-features = true } +sha2 = { workspace = true } thiserror = { workspace = true, default-features = false } uint = { workspace = true, default-features = false } diff --git a/packages/fuels-core/src/types.rs b/packages/fuels-core/src/types.rs index da04de9d9..484fd1239 100644 --- a/packages/fuels-core/src/types.rs +++ b/packages/fuels-core/src/types.rs @@ -17,6 +17,7 @@ pub mod transaction_builders; pub mod tx_status; mod wrappers; pub use dry_runner::*; +pub mod checksum_address; pub type ByteArray = [u8; 8]; pub type Selector = Vec; diff --git a/packages/fuels-core/src/types/checksum_address.rs b/packages/fuels-core/src/types/checksum_address.rs new file mode 100644 index 000000000..42b4c0d76 --- /dev/null +++ b/packages/fuels-core/src/types/checksum_address.rs @@ -0,0 +1,138 @@ +use crate::types::errors::{Error, Result}; +use sha2::{Digest, Sha256}; + +pub fn checksum_encode(address: &str) -> Result { + let trimmed = address.trim_start_matches("0x"); + pre_validate(trimmed)?; + + let lowercase = trimmed.to_ascii_lowercase(); + + let hash = Sha256::digest(lowercase.as_bytes()); + let mut checksum = String::with_capacity(trimmed.len()); + + for (i, addr_char) in lowercase.chars().enumerate() { + let hash_byte = hash[i / 2]; + let hash_nibble = if i % 2 == 0 { + // even index: high nibble + (hash_byte >> 4) & 0x0F + } else { + // odd index: low nibble + hash_byte & 0x0F + }; + + // checksum rule + if hash_nibble > 7 { + checksum.push(addr_char.to_ascii_uppercase()); + } else { + checksum.push(addr_char); + } + } + + Ok(format!("0x{checksum}")) +} + +fn pre_validate(s: &str) -> Result<()> { + if s.len() != 64 { + return Err(Error::Codec("invalid address length".to_string())); + } + + if !s.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(Error::Codec( + "address contains invalid characters".to_string(), + )); + } + + Ok(()) +} + +pub fn is_checksum_valid(address: &str) -> bool { + let Ok(checksum) = checksum_encode(address) else { + return false; + }; + + let address_normalized = if address.starts_with("0x") { + address.to_string() + } else { + format!("0x{}", address) + }; + + checksum == address_normalized +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use fuel_core_client::client::schema::Address; + + use super::*; + + const VALID_CHECKSUM: [&str; 4] = [ + "0x9cfB2CAd509D417ec40b70ebE1DD72a3624D46fdD1Ea5420dBD755CE7f4Dc897", + "0x54944e5B8189827e470e5a8bAcFC6C3667397DC4E1EEF7EF3519d16D6D6c6610", + "c36bE0E14d3EAf5d8D233e0F4a40b3b4e48427D25F84C460d2B03B242A38479e", + "a1184D77D0D08A064E03b2bd9f50863e88faDdea4693A05cA1ee9B1732ea99B7", + ]; + const INVALID_CHECKSUM: [&str; 8] = [ + "0x587aa0482482efEa0234752d1ad9a9c438D1f34D2859b8bef2d56A432cB68e33", + "0xe10f526B192593793b7a1559aA91445faba82a1d669e3eb2DCd17f9c121b24b1", + "6b63804cFbF9856e68e5B6e7aEf238dc8311ec55bec04df774003A2c96E0418e", + "81f3A10b61828580D06cC4c7b0ed8f59b9Fb618bE856c55d33deCD95489A1e23", + // all lower + "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "7e2becd64cd598da59b4d1064b711661898656c6b1f4918a787156b8965dc83c", + // all caps + "0x26183FBE7375045250865947695DFC12500DCC43EFB9102B4E8C4D3C20009DCB", + "577E424EE53A16E6A85291FEABC8443862495F74AC39A706D2DD0B9FC16955EB", + ]; + const INVALID_LEN: [&str; 6] = [ + // too short + "0x1234567890abcdef", + // too long + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234", + // 65 characters + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1", + // 63 characters + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde", + "", + "0x", + ]; + const INVALID_CHARACTERS: &str = + "0xGHIJKL7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + + #[test] + fn will_detect_valid_checksums() { + for valid in VALID_CHECKSUM.iter() { + assert!(is_checksum_valid(valid)); + } + } + + #[test] + fn will_detect_invalid_checksums() { + for invalid in INVALID_CHECKSUM.iter() { + assert!(!is_checksum_valid(invalid)); + } + } + + #[test] + fn can_construct_address_from_checksum() { + let checksum = checksum_encode(INVALID_CHECKSUM[0]).expect("should encode"); + Address::from_str(&checksum).expect("should be valid address"); + } + + #[test] + fn will_detect_invalid_lengths() { + for invalid in INVALID_LEN.iter() { + let result = checksum_encode(invalid).expect_err("should not encode"); + assert!(result.to_string().contains("invalid address length")); + } + } + + #[test] + fn will_detect_invalid_characters() { + let result = checksum_encode(INVALID_CHARACTERS).expect_err("should not encode"); + assert!(result + .to_string() + .contains("address contains invalid characters")); + } +} diff --git a/packages/fuels-test-helpers/src/service.rs b/packages/fuels-test-helpers/src/service.rs index f6cb9213d..9fc6f1430 100644 --- a/packages/fuels-test-helpers/src/service.rs +++ b/packages/fuels-test-helpers/src/service.rs @@ -93,6 +93,7 @@ impl FuelService { max_queries_depth: 16, max_queries_complexity: 80000, max_queries_recursive_depth: 16, + max_queries_resolver_recursive_depth: 1, max_queries_directives: 10, max_concurrent_queries: 1024, request_body_bytes_limit: 16 * 1024 * 1024,