diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3982b92a03..c37546303a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ env: FUEL_CORE_VERSION: 0.22.0 FUEL_CORE_PATCH_BRANCH: RUST_VERSION: 1.74.0 - FORC_VERSION: 0.48.0 + FORC_VERSION: 0.49.1 FORC_PATCH_BRANCH: "" FORC_PATCH_REVISION: "" @@ -80,7 +80,7 @@ jobs: # TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. - name: Build Sway test projects w type paths - run: forc build --terse --json-abi-with-callpaths + run: forc build --terse --error-on-warnings --json-abi-with-callpaths working-directory: packages/fuels - uses: actions/upload-artifact@v2 @@ -97,6 +97,25 @@ jobs: !packages/fuels/tests/**/Forc.lock !packages/fuels/tests/.gitignore + # TODO: To be removed once experimental encoding is the default + - name: Build Sway test projects w experimental logs + run: forc build --terse --error-on-warnings --json-abi-with-callpaths --experimental-new-encoding + working-directory: packages/fuels + + - uses: actions/upload-artifact@v2 + with: + retention-days: 2 + name: sway-examples-w-experimental-logs + # cache only the sway build artifacts, skip all src files + path: | + packages/fuels/tests + !packages/fuels/tests/*.rs + !packages/fuels/tests/**/*.rs + !packages/fuels/tests/**/*.sw + !packages/fuels/tests/**/Forc.toml + !packages/fuels/tests/**/Forc.lock + !packages/fuels/tests/.gitignore + get-workspace-members: runs-on: ubuntu-latest outputs: @@ -137,7 +156,7 @@ jobs: toolchain: ${{ env.RUST_VERSION }} - name: Publish crate check - uses: katyo/publish-crates@v2 + uses: xgreenx/publish-crates@v1 with: dry-run: true check-repo: false @@ -182,6 +201,11 @@ jobs: args: - command: check_doc_unresolved_links args: + # TODO: To be removed once experimental encoding is the default + - command: test_experimental_logs + args: + download_sway_artifacts: sway-examples-w-experimental-logs + install_fuel_core: true steps: - name: Checkout repository uses: actions/checkout@v3 @@ -218,8 +242,9 @@ jobs: name: ${{ matrix.download_sway_artifacts }} path: packages/fuels/tests/ + # TODO: `test_experimental_logs` to be removed once experimental encoding is the default. - name: Install nextest - if: ${{ matrix.cargo_command == 'nextest' }} + if: ${{ matrix.cargo_command == 'nextest' || matrix.command == 'test_experimental_logs' }} uses: taiki-e/install-action@nextest - name: Install cargo-machete @@ -257,6 +282,11 @@ jobs: run: | ! cargo doc --document-private-items |& grep -A 6 "warning: unresolved link to" + # TODO: To be removed once experimental encoding is the default. + - name: Test experimental logs + if: ${{ matrix.command == 'test_experimental_logs' }} + run: RUSTFLAGS='--cfg experimental' cargo nextest run --test logs + publish: needs: - cargo-verifications @@ -279,7 +309,7 @@ jobs: mv ./dasel /usr/local/bin/dasel ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} Cargo.toml - name: Publish crate - uses: katyo/publish-crates@v2 + uses: xgreenx/publish-crates@v1 with: publish-delay: 30000 registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index d6d0f4b156..4ffe6aaaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ readme = "README.md" license = "Apache-2.0" repository = "https://github.com/FuelLabs/fuels-rs" rust-version = "1.74.0" -version = "0.54.0" +version = "0.55.0" [workspace.dependencies] Inflector = "0.11.4" @@ -89,10 +89,10 @@ fuel-types = { version = "0.43.1" } fuel-vm = { version = "0.43.1" } # Workspace projects -fuels = { version = "0.54.0", path = "./packages/fuels" } -fuels-accounts = { version = "0.54.0", path = "./packages/fuels-accounts", default-features = false } -fuels-code-gen = { version = "0.54.0", path = "./packages/fuels-code-gen", default-features = false } -fuels-core = { version = "0.54.0", path = "./packages/fuels-core", default-features = false } -fuels-macros = { version = "0.54.0", path = "./packages/fuels-macros", default-features = false } -fuels-programs = { version = "0.54.0", path = "./packages/fuels-programs", default-features = false } -fuels-test-helpers = { version = "0.54.0", path = "./packages/fuels-test-helpers", default-features = false } +fuels = { version = "0.55.0", path = "./packages/fuels" } +fuels-accounts = { version = "0.55.0", path = "./packages/fuels-accounts", default-features = false } +fuels-code-gen = { version = "0.55.0", path = "./packages/fuels-code-gen", default-features = false } +fuels-core = { version = "0.55.0", path = "./packages/fuels-core", default-features = false } +fuels-macros = { version = "0.55.0", path = "./packages/fuels-macros", default-features = false } +fuels-programs = { version = "0.55.0", path = "./packages/fuels-programs", default-features = false } +fuels-test-helpers = { version = "0.55.0", path = "./packages/fuels-test-helpers", default-features = false } diff --git a/docs/src/connecting/short-lived.md b/docs/src/connecting/short-lived.md index bad25ff842..7e8eb06085 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.54.0", features = ["fuel-core-lib"] } +fuels = { version = "0.55.0", features = ["fuel-core-lib"] } ``` ### RocksDB @@ -35,5 +35,5 @@ fuels = { version = "0.54.0", 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.54.0", features = ["rocksdb"] } +fuels = { version = "0.55.0", features = ["rocksdb"] } ``` diff --git a/docs/src/custom-transactions/transaction-builders.md b/docs/src/custom-transactions/transaction-builders.md index be821b5189..de8eaffbaf 100644 --- a/docs/src/custom-transactions/transaction-builders.md +++ b/docs/src/custom-transactions/transaction-builders.md @@ -52,20 +52,22 @@ We combine all of the inputs and outputs and set them on the builder: {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_io}} ``` -As we have used coins that require a signature, we sign the transaction builder with: +As we have used coins that require a signature, we have to add the signer to the transaction builder with: ```rust,ignore -{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_sign}} +{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_add_signer}} ``` > **Note** The signature is not created until the transaction is finalized with `build(&provider)` -We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The Account trait lets us use `adjust_for_fee()` for adjusting the transaction inputs if needed to cover the fee. The second argument to `adjust_for_fee()` is the total amount of the base asset that we expect our transaction to spend regardless of fees. In our case, this is the **ask_amount** we are transferring to the predicate. +We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The `Account` trait lets us use `adjust_for_fee()` for adjusting the transaction inputs if needed to cover the fee. The second argument to `adjust_for_fee()` is the total amount of the base asset that we expect our transaction to spend regardless of fees. In our case, this is the **ask_amount** we are transferring to the predicate. ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_adjust}} ``` +> **Note** It is recommended to add signers before calling `adjust_for_fee()` as the estimation will include the size of the witnesses. + We can also define transaction policies. For example, we can limit the gas price by doing the following: ```rust,ignore @@ -83,3 +85,13 @@ Finally, we verify the transaction succeeded and that the cold storage indeed ho ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_verify}} ``` + +## Building a transaction without signatures + +If you need to build the transaction without signatures, which is useful when estimating transaction costs or simulations, you can use the `build_without_signatures(&provider)` method and later sign the built transaction. + +```rust,ignore +{{#include ../../../packages/fuels/tests/contracts.rs:tb_build_without_signatures}} +``` + +> **Note** In contrast to adding signers to a transaction builder, when signing a built transaction, you must ensure that the order of signatures matches the order of signed inputs. Multiple signed inputs with the same owner will have the same witness index. diff --git a/docs/src/types/address.md b/docs/src/types/address.md index 8ced0feb63..e966caa088 100644 --- a/docs/src/types/address.md +++ b/docs/src/types/address.md @@ -1,6 +1,6 @@ # `Address` -Like `Bytes32`, `Address` is a wrapper on `[u8; 32]` with similar methods and implements the same traits (see [fuel-types documentation](https://docs.rs/fuel-types/{{versions.fuel-types}}/fuel_types/struct.Address.html)). +Like `Bytes32`, `Address` is a wrapper on `[u8; 32]` with similar methods and implements the same traits (see [fuel-types documentation](https://docs.rs/fuel-types/latest/fuel_types/struct.Address.html)). These are the main ways of creating an `Address`: diff --git a/docs/src/types/bytes32.md b/docs/src/types/bytes32.md index 4558e77b8a..70f6700503 100644 --- a/docs/src/types/bytes32.md +++ b/docs/src/types/bytes32.md @@ -14,6 +14,6 @@ These are the main ways of creating a `Bytes32`: {{#include ../../../examples/types/src/lib.rs:bytes32_format}} ``` -For a full list of implemented methods and traits, see the [fuel-types documentation](https://docs.rs/fuel-types/{{versions.fuel-types}}/fuel_types/struct.Bytes32.html). +For a full list of implemented methods and traits, see the [fuel-types documentation](https://docs.rs/fuel-types/latest/fuel_types/struct.Bytes32.html). > **Note:** In Fuel, there's a special type called `b256`, which is similar to `Bytes32`; also used to represent hashes, and it holds a 256-bit value. In Rust, through the SDK, this is represented as `Bits256(value)` where `value` is a `[u8; 32]`. If your contract method takes a `b256` as input, all you need to do is pass a `Bits256([u8; 32])` when calling it from the SDK. diff --git a/docs/src/wallets/signing.md b/docs/src/wallets/signing.md index b0144e1c0e..33f54fcfdd 100644 --- a/docs/src/wallets/signing.md +++ b/docs/src/wallets/signing.md @@ -1,19 +1,27 @@ # Signing -Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign_message`. Below is a full example of how to sign and recover a message. +Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign`. Below is a full example of how to sign and recover a message. ```rust,ignore {{#include ../../../packages/fuels-accounts/src/account.rs:sign_message}} ``` -## Signing a transaction +## Adding `Signers` to a transaction builder -Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signatures in the transaction builder and resolve the final transaction automatically. This is done by storing the secret keys of all signers until the final transaction is built. +Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built. -To sign a _transaction builder_ use the `wallet.sign_transaction`. Below is a full example of how to create a transaction and sign it. +Below is a full example of how to create a transaction builder and add signers to it. -> Note: When you sign a transaction builder the secret key is stored inside it and will not be resolved until you call `build()`! +> Note: When you add a `Signer` to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you call `build()`! ```rust,ignore -{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tx}} +{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tb}} +``` + +## Signing a built transaction + +If you have a built transaction and want to add a signature, you can use the `sign_with` method. + +```rust,ignore +{{#include ../../../packages/fuels/tests/contracts.rs:tx_sign_with}} ``` diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 4296f7d483..dfef678a0e 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -899,7 +899,7 @@ mod tests { // customize the builder... wallet.adjust_for_fee(&mut tb, 0).await?; - wallet.sign_transaction(&mut tb); + tb.add_signer(wallet.clone())?; let tx = tb.build(provider).await?; diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 8a412d25ac..d37d1a2864 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -3,9 +3,7 @@ mod tests { use std::str::FromStr; use fuels::{ - accounts::{ - predicate::Predicate, wallet::WalletUnlocked, Account, Signer, ViewOnlyAccount, - }, + accounts::{predicate::Predicate, wallet::WalletUnlocked, Account, ViewOnlyAccount}, core::constants::BASE_ASSET_ID, prelude::Result, test_helpers::{setup_single_asset_coins, setup_test_provider}, @@ -191,7 +189,8 @@ mod tests { // ANCHOR: transfer_multiple_transaction let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default()); - wallet_1.sign_transaction(&mut tb); + tb.add_signer(wallet_1.clone())?; + let tx = tb.build(&provider).await?; provider.send_transaction_and_await_commit(tx).await?; @@ -296,9 +295,9 @@ mod tests { let mut tb = tb.with_inputs(inputs).with_outputs(outputs); // ANCHOR_END: custom_tx_io - // ANCHOR: custom_tx_sign - hot_wallet.sign_transaction(&mut tb); - // ANCHOR_END: custom_tx_sign + // ANCHOR: custom_tx_add_signer + tb.add_signer(hot_wallet.clone())?; + // ANCHOR_END: custom_tx_add_signer // ANCHOR: custom_tx_adjust hot_wallet.adjust_for_fee(&mut tb, 100).await?; diff --git a/examples/predicates/src/lib.rs b/examples/predicates/src/lib.rs index a07385389d..b91d7927aa 100644 --- a/examples/predicates/src/lib.rs +++ b/examples/predicates/src/lib.rs @@ -2,14 +2,13 @@ mod tests { use fuels::{ accounts::{predicate::Predicate, Account}, + crypto::{Message, SecretKey}, prelude::*, types::B512, }; #[tokio::test] async fn predicate_example() -> Result<()> { - use fuels::accounts::fuel_crypto::SecretKey; - // ANCHOR: predicate_wallets let secret_key1: SecretKey = "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301" @@ -53,22 +52,10 @@ mod tests { }); // ANCHOR_END: predicate_coins - let data_to_sign = [0; 32]; - let signature1: B512 = wallet - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; - let signature2: B512 = wallet2 - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; - let signature3: B512 = wallet3 - .sign_message(data_to_sign) - .await? - .as_ref() - .try_into()?; + let data_to_sign = Message::new([0; 32]); + let signature1: B512 = wallet.sign(data_to_sign).await?.as_ref().try_into()?; + let signature2: B512 = wallet2.sign(data_to_sign).await?.as_ref().try_into()?; + let signature3: B512 = wallet3.sign(data_to_sign).await?.as_ref().try_into()?; let signatures = [signature1, signature2, signature3]; diff --git a/examples/providers/src/lib.rs b/examples/providers/src/lib.rs index 7b9758d1f8..f8e9f9d2a2 100644 --- a/examples/providers/src/lib.rs +++ b/examples/providers/src/lib.rs @@ -10,7 +10,7 @@ mod tests { // ANCHOR: connect_to_testnet use std::str::FromStr; - use fuels::{accounts::fuel_crypto::SecretKey, prelude::*}; + use fuels::{crypto::SecretKey, prelude::*}; // Create a provider pointing to the testnet. // This example will not work as the testnet does not support the new version of fuel-core diff --git a/examples/wallets/src/lib.rs b/examples/wallets/src/lib.rs index 9c05df30a2..ec263f21f8 100644 --- a/examples/wallets/src/lib.rs +++ b/examples/wallets/src/lib.rs @@ -23,7 +23,7 @@ mod tests { // ANCHOR: create_wallet_from_secret_key use std::str::FromStr; - use fuels::{accounts::fuel_crypto::SecretKey, prelude::*}; + use fuels::{crypto::SecretKey, prelude::*}; // Use the test helper to setup a test provider. let provider = setup_test_provider(vec![], vec![], None, None).await?; diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 1c0b1fceb1..54061a593e 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -2,9 +2,6 @@ use std::{collections::HashMap, fmt::Display}; use async_trait::async_trait; use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest}; -#[doc(no_inline)] -pub use fuel_crypto; -use fuel_crypto::Signature; use fuel_tx::{Output, Receipt, TxId, TxPointer, UtxoId}; use fuel_types::{AssetId, Bytes32, ContractId, Nonce}; use fuels_core::{ @@ -29,23 +26,6 @@ use crate::{ provider::{Provider, ResourceFilter}, }; -/// Trait for signing transactions and messages -/// -/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Signer: std::fmt::Debug + Send + Sync { - type Error: std::error::Error + Send + Sync; - - async fn sign_message>( - &self, - message: S, - ) -> std::result::Result; - - /// Signs the transaction - fn sign_transaction(&self, message: &mut impl TransactionBuilder); -} - #[derive(Debug)] pub struct AccountError(String); @@ -192,7 +172,9 @@ pub trait Account: ViewOnlyAccount { } // Add signatures to the builder if the underlying account is a wallet - fn add_witnessses(&self, _tb: &mut Tb) {} + fn add_witnesses(&self, _tb: &mut Tb) -> Result<()> { + Ok(()) + } /// Transfer funds from this account to another `Address`. /// Fails if amount for asset ID is larger than address's spendable coins. @@ -212,7 +194,7 @@ pub trait Account: ViewOnlyAccount { let mut tx_builder = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_policies); - self.add_witnessses(&mut tx_builder); + self.add_witnesses(&mut tx_builder)?; let used_base_amount = if asset_id == AssetId::BASE { amount } else { 0 }; self.adjust_for_fee(&mut tx_builder, used_base_amount) @@ -274,8 +256,9 @@ pub trait Account: ViewOnlyAccount { tx_policies, ); - self.add_witnessses(&mut tb); + self.add_witnesses(&mut tb)?; self.adjust_for_fee(&mut tb, balance).await?; + let tx = tb.build(provider).await?; let tx_id = tx.id(provider.chain_id()); @@ -308,8 +291,9 @@ pub trait Account: ViewOnlyAccount { tx_policies, ); - self.add_witnessses(&mut tb); + self.add_witnesses(&mut tb)?; self.adjust_for_fee(&mut tb, amount).await?; + let tx = tb.build(provider).await?; let tx_id = tx.id(provider.chain_id()); @@ -328,9 +312,12 @@ pub trait Account: ViewOnlyAccount { mod tests { use std::str::FromStr; - use fuel_crypto::{Message, SecretKey}; + use fuel_crypto::{Message, SecretKey, Signature}; use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction}; - use fuels_core::types::{transaction::Transaction, transaction_builders::DryRunner}; + use fuels_core::{ + traits::Signer, + types::{transaction::Transaction, transaction_builders::DryRunner}, + }; use rand::{rngs::StdRng, RngCore, SeedableRng}; use super::*; @@ -351,15 +338,13 @@ mod tests { // Create a wallet using the private key created above. let wallet = WalletUnlocked::new_from_private_key(secret, None); - let message = "my message"; - - let signature = wallet.sign_message(message).await?; + let message = Message::new("my message".as_bytes()); + let signature = wallet.sign(message).await?; // Check if signature is what we expect it to be assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?); // Recover address that signed the message - let message = Message::new(message); let recovered_address = signature.recover(&message)?; assert_eq!(wallet.address().hash(), recovered_address.hash()); @@ -371,6 +356,7 @@ mod tests { Ok(()) } + #[derive(Default)] struct MockDryRunner { c_param: ConsensusParameters, } @@ -390,7 +376,7 @@ mod tests { #[tokio::test] async fn sign_tx_and_verify() -> std::result::Result<(), Box> { - // ANCHOR: sign_tx + // ANCHOR: sign_tb let secret = SecretKey::from_str( "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1", )?; @@ -421,15 +407,11 @@ mod tests { ) }; - // Sign the transaction - wallet.sign_transaction(&mut tb); // Add the private key to the transaction builder - // ANCHOR_END: sign_tx + // Add `Signer` to the transaction builder + tb.add_signer(wallet.clone())?; + // ANCHOR_END: sign_tb - let tx = tb - .build(&MockDryRunner { - c_param: ConsensusParameters::default(), - }) - .await?; // Resolve signatures and add corresponding witness indexes + let tx = tb.build(&MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes // Extract the signature from the tx witnesses let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?; @@ -437,7 +419,7 @@ mod tests { // Sign the transaction manually let message = Message::from_bytes(*tx.id(0.into())); - let signature = Signature::sign(&wallet.private_key, &message); + let signature = wallet.sign(message).await?; // Check if the signatures are the same assert_eq!(signature, tx_signature); diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index b5ef6bda0d..d5b67e98e9 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -252,7 +252,7 @@ impl Provider { tx.precompute(&self.chain_id())?; let chain_info = self.chain_info().await?; - tx.check_without_signatures( + tx.check( chain_info.latest_block.header.height, self.consensus_parameters(), )?; diff --git a/packages/fuels-accounts/src/wallet.rs b/packages/fuels-accounts/src/wallet.rs index 1db3135659..b17eef2a8e 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -4,12 +4,15 @@ use async_trait::async_trait; use elliptic_curve::rand_core; use eth_keystore::KeystoreError; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; -use fuels_core::types::{ - bech32::{Bech32Address, FUEL_BECH32_HRP}, - errors::{Error, Result}, - input::Input, - transaction_builders::TransactionBuilder, - AssetId, +use fuels_core::{ + traits::Signer, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + AssetId, + }, }; use rand::{CryptoRng, Rng}; use thiserror::Error; @@ -17,7 +20,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ provider::{Provider, ProviderError}, - Account, AccountError, AccountResult, Signer, ViewOnlyAccount, + Account, AccountError, AccountResult, ViewOnlyAccount, }; pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'"; @@ -224,6 +227,10 @@ impl WalletUnlocked { .expect("Decrypted key should have a correct size"); Ok(Self::new_from_private_key(secret_key, provider)) } + + pub fn address(&self) -> &Bech32Address { + &self.address + } } impl ViewOnlyAccount for WalletUnlocked { @@ -255,26 +262,24 @@ impl Account for WalletUnlocked { .collect::>()) } - fn add_witnessses(&self, tb: &mut Tb) { - self.sign_transaction(tb); + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + + Ok(()) } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for WalletUnlocked { - type Error = WalletError; - async fn sign_message>( - &self, - message: S, - ) -> WalletResult { - let message = Message::new(message); + async fn sign(&self, message: Message) -> Result { let sig = Signature::sign(&self.private_key, &message); + Ok(sig) } - fn sign_transaction(&self, tb: &mut impl TransactionBuilder) { - tb.add_unresolved_signature(self.address().clone(), self.private_key); + fn address(&self) -> &Bech32Address { + &self.address } } @@ -314,15 +319,15 @@ mod tests { let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?; // sign a message using the above key. - let message = "Hello there!"; - let signature = wallet.sign_message(message).await?; + let message = Message::new("Hello there!".as_bytes()); + let signature = wallet.sign(message).await?; // Read from the encrypted JSON keystore and decrypt it. let path = Path::new(dir.path()).join(uuid); let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?; // Sign the same message as before and assert that the signature is the same. - let signature2 = recovered_wallet.sign_message(message).await?; + let signature2 = recovered_wallet.sign(message).await?; assert_eq!(signature, signature2); // Remove tempdir. diff --git a/packages/fuels-code-gen/src/program_bindings/utils.rs b/packages/fuels-code-gen/src/program_bindings/utils.rs index 56671a371b..75b81d8edb 100644 --- a/packages/fuels-code-gen/src/program_bindings/utils.rs +++ b/packages/fuels-code-gen/src/program_bindings/utils.rs @@ -141,7 +141,7 @@ pub(crate) fn tokenize_generics(generics: &[Ident]) -> (TokenStream, TokenStream pub(crate) fn sdk_provided_custom_types_lookup() -> HashMap { [ ("std::address::Address", "::fuels::types::Address"), - ("std::contract_id::AssetId", "::fuels::types::AssetId"), + ("std::asset_id::AssetId", "::fuels::types::AssetId"), ("std::b512::B512", "::fuels::types::B512"), ("std::bytes::Bytes", "::fuels::types::Bytes"), ("std::contract_id::ContractId", "::fuels::types::ContractId"), diff --git a/packages/fuels-core/Cargo.toml b/packages/fuels-core/Cargo.toml index 0bfaafa6b4..48531b88c4 100644 --- a/packages/fuels-core/Cargo.toml +++ b/packages/fuels-core/Cargo.toml @@ -29,10 +29,10 @@ serde_json = { workspace = true, default-features = true } sha2 = { workspace = true } thiserror = { workspace = true, default-features = false } uint = { workspace = true, default-features = false } -zeroize = { workspace = true, features = ["derive"] } [dev-dependencies] fuels-macros = { workspace = true } +tokio = { workspace = true, features = ["test-util"] } [features] default = ["std"] diff --git a/packages/fuels-core/src/codec/abi_decoder.rs b/packages/fuels-core/src/codec/abi_decoder.rs index 6b7434af87..2d7bcbd4ab 100644 --- a/packages/fuels-core/src/codec/abi_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder.rs @@ -1,10 +1,15 @@ mod bounded_decoder; +#[cfg(experimental)] +mod experimental_bounded_decoder; use crate::{ codec::abi_decoder::bounded_decoder::BoundedDecoder, types::{errors::Result, param_types::ParamType, Token}, }; +#[cfg(experimental)] +use crate::codec::abi_decoder::experimental_bounded_decoder::ExperimentalBoundedDecoder; + #[derive(Debug, Clone, Copy)] pub struct DecoderConfig { /// Entering a struct, array, tuple, enum or vector increases the depth. Decoding will fail if @@ -77,6 +82,20 @@ impl ABIDecoder { pub fn decode_multiple(&self, param_types: &[ParamType], bytes: &[u8]) -> Result> { BoundedDecoder::new(self.config).decode_multiple(param_types, bytes) } + + #[cfg(experimental)] + pub fn experimental_decode(&self, param_type: &ParamType, bytes: &[u8]) -> Result { + ExperimentalBoundedDecoder::new(self.config).decode(param_type, bytes) + } + + #[cfg(experimental)] + pub fn experimental_decode_multiple( + &self, + param_types: &[ParamType], + bytes: &[u8], + ) -> Result> { + ExperimentalBoundedDecoder::new(self.config).decode_multiple(param_types, bytes) + } } #[cfg(test)] diff --git a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs index 3ad13d0cc9..d7556c2ff6 100644 --- a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs @@ -4,7 +4,6 @@ use crate::{ checked_round_up_to_word_alignment, codec::DecoderConfig, constants::WORD_SIZE, - traits::Tokenizable, types::{ enum_variants::EnumVariants, errors::{error, Result}, @@ -36,7 +35,7 @@ impl BoundedDecoder { } } - pub fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { param_type.validate_is_decodable(self.config.max_depth)?; match param_type { // Unit, U8 and Bool are returned as u64 from receipt "Return" @@ -94,7 +93,7 @@ impl BoundedDecoder { ParamType::U256 => Self::decode_u256(bytes), ParamType::Bool => Self::decode_bool(bytes), ParamType::B256 => Self::decode_b256(bytes), - ParamType::RawSlice => self.decode_raw_slice(bytes), + ParamType::RawSlice => Self::decode_raw_slice(bytes), ParamType::StringSlice => Self::decode_string_slice(bytes), ParamType::StringArray(len) => Self::decode_string_array(bytes, *len), ParamType::Array(ref t, length) => { @@ -215,22 +214,10 @@ impl BoundedDecoder { }) } - fn decode_raw_slice(&mut self, bytes: &[u8]) -> Result { - let raw_slice_element = ParamType::U64; - let num_of_elements = - ParamType::calculate_num_of_elements(&raw_slice_element, bytes.len())?; - let param_type = ParamType::U64; - let (tokens, bytes_read) = - self.decode_params(std::iter::repeat(¶m_type).take(num_of_elements), bytes)?; - let elements = tokens - .into_iter() - .map(u64::from_token) - .collect::>>() - .map_err(|e| error!(InvalidData, "{e}"))?; - + fn decode_raw_slice(bytes: &[u8]) -> Result { Ok(Decoded { - token: Token::RawSlice(elements), - bytes_read, + token: Token::RawSlice(bytes.to_vec()), + bytes_read: bytes.len(), }) } diff --git a/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs b/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs new file mode 100644 index 0000000000..e8728bf08b --- /dev/null +++ b/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs @@ -0,0 +1,398 @@ +use std::{iter::repeat, str}; + +use crate::{ + codec::DecoderConfig, + constants::WORD_SIZE, + types::{ + enum_variants::EnumVariants, + errors::{error, Result}, + param_types::ParamType, + StaticStringToken, Token, U256, + }, +}; + +/// Is used to decode bytes into `Token`s from which types implementing `Tokenizable` can be +/// instantiated. Implements decoding limits to control resource usage. +pub(crate) struct ExperimentalBoundedDecoder { + depth_tracker: CounterWithLimit, + token_tracker: CounterWithLimit, +} + +const U8_BYTES_SIZE: usize = 1; +const U16_BYTES_SIZE: usize = 2; +const U32_BYTES_SIZE: usize = 4; +const U64_BYTES_SIZE: usize = WORD_SIZE; +const U128_BYTES_SIZE: usize = 2 * WORD_SIZE; +const U256_BYTES_SIZE: usize = 4 * WORD_SIZE; +const B256_BYTES_SIZE: usize = 4 * WORD_SIZE; +const LENGTH_BYTES_SIZE: usize = WORD_SIZE; +const DISCRIMINANT_BYTES_SIZE: usize = WORD_SIZE; + +impl ExperimentalBoundedDecoder { + pub(crate) fn new(config: DecoderConfig) -> Self { + let depth_tracker = CounterWithLimit::new(config.max_depth, "Depth"); + let token_tracker = CounterWithLimit::new(config.max_tokens, "Token"); + Self { + depth_tracker, + token_tracker, + } + } + + pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + self.decode_param(param_type, bytes).map(|x| x.token) + } + + pub(crate) fn decode_multiple( + &mut self, + param_types: &[ParamType], + bytes: &[u8], + ) -> Result> { + let (tokens, _) = self.decode_params(param_types, bytes)?; + + Ok(tokens) + } + + fn run_w_depth_tracking( + &mut self, + decoder: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.depth_tracker.increase()?; + let res = decoder(self); + self.depth_tracker.decrease(); + + res + } + + fn decode_param(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + self.token_tracker.increase()?; + match param_type { + ParamType::Unit => Self::decode_unit(), + ParamType::Bool => Self::decode_bool(bytes), + ParamType::U8 => Self::decode_u8(bytes), + ParamType::U16 => Self::decode_u16(bytes), + ParamType::U32 => Self::decode_u32(bytes), + ParamType::U64 => Self::decode_u64(bytes), + ParamType::U128 => Self::decode_u128(bytes), + ParamType::U256 => Self::decode_u256(bytes), + ParamType::B256 => Self::decode_b256(bytes), + ParamType::Bytes => Self::decode_bytes(bytes), + ParamType::String => Self::decode_std_string(bytes), + ParamType::RawSlice => Self::decode_raw_slice(bytes), + ParamType::StringArray(length) => Self::decode_string_array(bytes, *length), + ParamType::StringSlice => Self::decode_string_slice(bytes), + ParamType::Tuple(param_types) => { + self.run_w_depth_tracking(|ctx| ctx.decode_tuple(param_types, bytes)) + } + ParamType::Array(param_type, length) => { + self.run_w_depth_tracking(|ctx| ctx.decode_array(param_type, bytes, *length)) + } + ParamType::Vector(param_type) => { + self.run_w_depth_tracking(|ctx| ctx.decode_vector(param_type, bytes)) + } + + ParamType::Struct { fields, .. } => { + self.run_w_depth_tracking(|ctx| ctx.decode_struct(fields, bytes)) + } + ParamType::Enum { variants, .. } => { + self.run_w_depth_tracking(|ctx| ctx.decode_enum(bytes, variants)) + } + } + } + + fn decode_unit() -> Result { + Ok(Decoded { + token: Token::Unit, + bytes_read: 0, + }) + } + + fn decode_bool(bytes: &[u8]) -> Result { + let value = peek_u8(bytes)? != 0u8; + + Ok(Decoded { + token: Token::Bool(value), + bytes_read: U8_BYTES_SIZE, + }) + } + + fn decode_u8(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U8(peek_u8(bytes)?), + bytes_read: U8_BYTES_SIZE, + }) + } + + fn decode_u16(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U16(peek_u16(bytes)?), + bytes_read: U16_BYTES_SIZE, + }) + } + + fn decode_u32(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U32(peek_u32(bytes)?), + bytes_read: U32_BYTES_SIZE, + }) + } + + fn decode_u64(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U64(peek_u64(bytes)?), + bytes_read: U64_BYTES_SIZE, + }) + } + + fn decode_u128(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U128(peek_u128(bytes)?), + bytes_read: U128_BYTES_SIZE, + }) + } + + fn decode_u256(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U256(peek_u256(bytes)?), + bytes_read: U256_BYTES_SIZE, + }) + } + + fn decode_b256(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::B256(*peek_fixed::(bytes)?), + bytes_read: B256_BYTES_SIZE, + }) + } + + fn decode_bytes(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::Bytes(bytes.to_vec()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_std_string(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::String(str::from_utf8(bytes)?.to_string()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_raw_slice(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::RawSlice(bytes.to_vec()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_string_array(bytes: &[u8], length: usize) -> Result { + let bytes = peek(bytes, length)?; + let decoded = str::from_utf8(bytes)?.to_string(); + + Ok(Decoded { + token: Token::StringArray(StaticStringToken::new(decoded, Some(length))), + bytes_read: length, + }) + } + + fn decode_string_slice(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + let decoded = str::from_utf8(bytes)?.to_string(); + + Ok(Decoded { + token: Token::StringSlice(StaticStringToken::new(decoded, None)), + bytes_read: bytes.len(), + }) + } + + fn decode_tuple(&mut self, param_types: &[ParamType], bytes: &[u8]) -> Result { + let (tokens, bytes_read) = self.decode_params(param_types, bytes)?; + + Ok(Decoded { + token: Token::Tuple(tokens), + bytes_read, + }) + } + + fn decode_array( + &mut self, + param_type: &ParamType, + bytes: &[u8], + length: usize, + ) -> Result { + let (tokens, bytes_read) = self.decode_params(repeat(param_type).take(length), bytes)?; + + Ok(Decoded { + token: Token::Array(tokens), + bytes_read, + }) + } + + fn decode_vector(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = skip(bytes, LENGTH_BYTES_SIZE)?; + let (tokens, bytes_read) = self.decode_params(repeat(param_type).take(length), bytes)?; + + Ok(Decoded { + token: Token::Vector(tokens), + bytes_read: LENGTH_BYTES_SIZE + bytes_read, + }) + } + + fn decode_struct(&mut self, param_types: &[ParamType], bytes: &[u8]) -> Result { + let (tokens, bytes_read) = self.decode_params(param_types, bytes)?; + + Ok(Decoded { + token: Token::Struct(tokens), + bytes_read, + }) + } + + fn decode_enum(&mut self, bytes: &[u8], variants: &EnumVariants) -> Result { + let discriminant = peek_discriminant(bytes)?; + let variant_bytes = skip(bytes, DISCRIMINANT_BYTES_SIZE)?; + let selected_variant = variants.param_type_of_variant(discriminant)?; + + let decoded = self.decode_param(selected_variant, variant_bytes)?; + + Ok(Decoded { + token: Token::Enum(Box::new((discriminant, decoded.token, variants.clone()))), + bytes_read: DISCRIMINANT_BYTES_SIZE + decoded.bytes_read, + }) + } + + fn decode_params<'a>( + &mut self, + param_types: impl IntoIterator, + bytes: &[u8], + ) -> Result<(Vec, usize)> { + let mut tokens = vec![]; + let mut bytes_read = 0; + + for param_type in param_types { + let decoded = self.decode_param(param_type, skip(bytes, bytes_read)?)?; + tokens.push(decoded.token); + bytes_read += decoded.bytes_read; + } + + Ok((tokens, bytes_read)) + } +} + +#[derive(Debug, Clone)] +struct Decoded { + token: Token, + bytes_read: usize, +} + +struct CounterWithLimit { + count: usize, + max: usize, + name: String, +} + +impl CounterWithLimit { + fn new(max: usize, name: impl Into) -> Self { + Self { + count: 0, + max, + name: name.into(), + } + } + + fn increase(&mut self) -> Result<()> { + self.count += 1; + if self.count > self.max { + return Err(error!( + InvalidType, + "{} limit ({}) reached while decoding. Try increasing it.", self.name, self.max + )); + } + + Ok(()) + } + + fn decrease(&mut self) { + if self.count > 0 { + self.count -= 1; + } + } +} + +fn peek_u8(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u8::from_be_bytes(*slice)) +} + +fn peek_u16(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u16::from_be_bytes(*slice)) +} + +fn peek_u32(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u32::from_be_bytes(*slice)) +} + +fn peek_u64(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u64::from_be_bytes(*slice)) +} + +fn peek_u128(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u128::from_be_bytes(*slice)) +} + +fn peek_u256(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(U256::from(*slice)) +} + +fn peek_length(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + + u64::from_be_bytes(*slice) + .try_into() + .map_err(|_| error!(InvalidData, "could not convert `u64` to `usize`")) +} + +fn peek_discriminant(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u64::from_be_bytes(*slice)) +} + +fn peek(data: &[u8], len: usize) -> Result<&[u8]> { + (len <= data.len()).then_some(&data[..len]).ok_or(error!( + InvalidData, + "tried to read `{len}` bytes but only had `{}` remaining!", + data.len() + )) +} + +fn peek_fixed(data: &[u8]) -> Result<&[u8; LEN]> { + let slice_w_correct_length = peek(data, LEN)?; + Ok(slice_w_correct_length + .try_into() + .expect("peek(data, len) must return a slice of length `len` or error out")) +} + +fn skip(slice: &[u8], num_bytes: usize) -> Result<&[u8]> { + (num_bytes <= slice.len()) + .then_some(&slice[num_bytes..]) + .ok_or(error!( + InvalidData, + "tried to consume `{num_bytes}` bytes but only had `{}` remaining!", + slice.len() + )) +} diff --git a/packages/fuels-core/src/codec/abi_encoder.rs b/packages/fuels-core/src/codec/abi_encoder.rs index 3585a52499..b20e2795cc 100644 --- a/packages/fuels-core/src/codec/abi_encoder.rs +++ b/packages/fuels-core/src/codec/abi_encoder.rs @@ -2,7 +2,6 @@ use fuel_types::bytes::padded_len_usize; use crate::{ checked_round_up_to_word_alignment, - constants::WORD_SIZE, types::{ errors::Result, pad_u16, pad_u32, @@ -83,7 +82,7 @@ impl ABIEncoder { Token::Enum(arg_enum) => Self::encode_enum(arg_enum)?, Token::Tuple(arg_tuple) => Self::encode_tuple(arg_tuple)?, Token::Unit => vec![Self::encode_unit()], - Token::RawSlice(data) => Self::encode_raw_slice(data)?, + Token::RawSlice(data) => Self::encode_raw_slice(data.to_vec())?, Token::Bytes(data) => Self::encode_bytes(data.to_vec())?, // `String` in Sway has the same memory layout as the bytes type Token::String(string) => Self::encode_bytes(string.clone().into_bytes())?, @@ -190,16 +189,17 @@ impl ABIEncoder { ]) } - fn encode_raw_slice(data: &[u64]) -> Result> { - let encoded_data = data - .iter() - .map(|&word| Self::encode_u64(word)) - .collect::>(); + fn encode_raw_slice(mut data: Vec) -> Result> { + let len = data.len(); - let num_bytes = data.len() * WORD_SIZE; - let len = Self::encode_u64(num_bytes as u64); + zeropad_to_word_alignment(&mut data); - Ok(vec![Data::Dynamic(encoded_data), len]) + let encoded_data = vec![Data::Inline(data)]; + + Ok(vec![ + Data::Dynamic(encoded_data), + Self::encode_u64(len as u64), + ]) } fn encode_string_slice(arg_string: &StaticStringToken) -> Result> { @@ -248,6 +248,7 @@ mod tests { use super::*; use crate::{ codec::first_four_bytes_of_sha256_hash, + constants::WORD_SIZE, types::{enum_variants::EnumVariants, param_types::ParamType}, }; @@ -1202,16 +1203,12 @@ mod tests { let encoded_bytes = ABIEncoder::encode(&[token])?.resolve(offset); // assert - let ptr = vec![0, 0, 0, 0, 0, 0, 0, 56]; - let len = vec![0, 0, 0, 0, 0, 0, 0, 24]; - let data = [ - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 2], - [0, 0, 0, 0, 0, 0, 0, 3], - ] - .concat(); + let ptr = [0, 0, 0, 0, 0, 0, 0, 56].to_vec(); + let len = [0, 0, 0, 0, 0, 0, 0, 3].to_vec(); + let data = [1, 2, 3].to_vec(); + let padding = [0, 0, 0, 0, 0].to_vec(); - let expected_encoded_bytes = [ptr, len, data].concat(); + let expected_encoded_bytes = [ptr, len, data, padding].concat(); assert_eq!(expected_encoded_bytes, encoded_bytes); diff --git a/packages/fuels-core/src/codec/logs.rs b/packages/fuels-core/src/codec/logs.rs index 1daae05ed5..920c164c79 100644 --- a/packages/fuels-core/src/codec/logs.rs +++ b/packages/fuels-core/src/codec/logs.rs @@ -10,12 +10,12 @@ use fuel_tx::{ContractId, Receipt}; use crate::{ codec::{ABIDecoder, DecoderConfig}, traits::{Parameterize, Tokenizable}, - types::{ - errors::{error, Error, Result}, - param_types::ParamType, - }, + types::errors::{error, Error, Result}, }; +#[cfg(not(experimental))] +use crate::types::param_types::ParamType; + #[derive(Clone)] pub struct LogFormatter { formatter: fn(DecoderConfig, &[u8]) -> Result, @@ -34,11 +34,19 @@ impl LogFormatter { decoder_config: DecoderConfig, bytes: &[u8], ) -> Result { - Self::can_decode_log_with_type::()?; - let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?; + #[cfg(not(experimental))] + let token = { + Self::can_decode_log_with_type::()?; + ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)? + }; + + #[cfg(experimental)] + let token = ABIDecoder::new(decoder_config).experimental_decode(&T::param_type(), bytes)?; + Ok(format!("{:?}", T::from_token(token)?)) } + #[cfg(not(experimental))] fn can_decode_log_with_type() -> Result<()> { match T::param_type() { // String slices can not be decoded from logs as they are encoded as ptr, len @@ -188,8 +196,14 @@ impl LogDecoder { .extract_log_id_and_data() .filter_map(|(log_id, bytes)| { target_ids.contains(&log_id).then(|| { + #[cfg(experimental)] + let token = ABIDecoder::new(self.decoder_config) + .experimental_decode(&T::param_type(), &bytes)?; + + #[cfg(not(experimental))] let token = ABIDecoder::new(self.decoder_config).decode(&T::param_type(), &bytes)?; + T::from_token(token) }) }) diff --git a/packages/fuels-core/src/traits.rs b/packages/fuels-core/src/traits.rs index fa9fe98b4a..0d394ba76e 100644 --- a/packages/fuels-core/src/traits.rs +++ b/packages/fuels-core/src/traits.rs @@ -1,5 +1,7 @@ mod parameterize; +mod signer; mod tokenizable; pub use parameterize::*; +pub use signer::*; pub use tokenizable::*; diff --git a/packages/fuels-core/src/traits/signer.rs b/packages/fuels-core/src/traits/signer.rs new file mode 100644 index 0000000000..6172dbc145 --- /dev/null +++ b/packages/fuels-core/src/traits/signer.rs @@ -0,0 +1,14 @@ +use async_trait::async_trait; +use fuel_crypto::{Message, Signature}; + +use crate::types::{bech32::Bech32Address, errors::Result}; + +/// Trait for signing transactions and messages +/// +/// Implement this trait to support different signing modes, e.g. hardware wallet, hosted etc. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Signer: 'static { + async fn sign(&self, message: Message) -> Result; + fn address(&self) -> &Bech32Address; +} diff --git a/packages/fuels-core/src/types.rs b/packages/fuels-core/src/types.rs index a20d7cdcc1..60e7f57a4e 100644 --- a/packages/fuels-core/src/types.rs +++ b/packages/fuels-core/src/types.rs @@ -91,7 +91,7 @@ pub enum Token { Struct(Vec), Enum(Box), Tuple(Vec), - RawSlice(Vec), + RawSlice(Vec), Bytes(Vec), String(String), } diff --git a/packages/fuels-core/src/types/core/identity.rs b/packages/fuels-core/src/types/core/identity.rs index 3df2384093..7d7615dc16 100644 --- a/packages/fuels-core/src/types/core/identity.rs +++ b/packages/fuels-core/src/types/core/identity.rs @@ -1,3 +1,4 @@ +use crate::types::bech32::{Bech32Address, Bech32ContractId}; use fuel_tx::{Address, ContractId}; use fuels_macros::{Parameterize, Tokenizable, TryFrom}; use serde::{Deserialize, Serialize}; @@ -26,3 +27,78 @@ impl AsRef<[u8]> for Identity { } } } + +impl From<&Address> for Identity { + fn from(address: &Address) -> Self { + Self::Address(*address) + } +} +impl From
for Identity { + fn from(address: Address) -> Self { + Self::Address(address) + } +} + +impl From<&ContractId> for Identity { + fn from(contract_id: &ContractId) -> Self { + Self::ContractId(*contract_id) + } +} +impl From for Identity { + fn from(contract_id: ContractId) -> Self { + Self::ContractId(contract_id) + } +} + +impl From<&Bech32Address> for Identity { + fn from(data: &Bech32Address) -> Self { + Self::Address(data.into()) + } +} +impl From for Identity { + fn from(data: Bech32Address) -> Self { + Self::Address(data.into()) + } +} + +impl From<&Bech32ContractId> for Identity { + fn from(data: &Bech32ContractId) -> Self { + Self::ContractId(data.into()) + } +} +impl From for Identity { + fn from(data: Bech32ContractId) -> Self { + Self::ContractId(data.into()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn test_bech32() { + let b32_str = "fuel1dved7k25uxadatl7l5kql309jnw07dcn4t3a6x9hm9nxyjcpqqns50p7n2"; + let bech32_contract_id = Bech32ContractId::from_str(b32_str).unwrap(); + + let bech32_contract_id_borrowed = &bech32_contract_id.clone(); + let identity: Identity = bech32_contract_id_borrowed.into(); + assert_eq!( + identity, + Identity::ContractId(bech32_contract_id.clone().into()) + ); + + let identity: Identity = bech32_contract_id.clone().into(); + assert_eq!(identity, Identity::ContractId(bech32_contract_id.into())); + + let bech32_address = Bech32Address::from_str(b32_str).unwrap(); + + let bech32_address_borrowed = &bech32_address.clone(); + let identity: Identity = bech32_address_borrowed.into(); + assert_eq!(identity, Identity::Address(bech32_address.clone().into())); + + let identity: Identity = bech32_address.clone().into(); + assert_eq!(identity, Identity::Address(bech32_address.clone().into())); + } +} diff --git a/packages/fuels-core/src/types/core/raw_slice.rs b/packages/fuels-core/src/types/core/raw_slice.rs index d4e0b0cd62..0eb37a8fe7 100644 --- a/packages/fuels-core/src/types/core/raw_slice.rs +++ b/packages/fuels-core/src/types/core/raw_slice.rs @@ -1,21 +1,21 @@ #[derive(Debug, PartialEq, Clone, Eq)] // `RawSlice` is a mapping of the contract type "untyped raw slice" -- currently the only way of // returning dynamically sized data from a script. -pub struct RawSlice(pub Vec); +pub struct RawSlice(pub Vec); -impl From for Vec { - fn from(raw_slice: RawSlice) -> Vec { +impl From for Vec { + fn from(raw_slice: RawSlice) -> Vec { raw_slice.0 } } -impl PartialEq> for RawSlice { - fn eq(&self, other: &Vec) -> bool { +impl PartialEq> for RawSlice { + fn eq(&self, other: &Vec) -> bool { self.0 == *other } } -impl PartialEq for Vec { +impl PartialEq for Vec { fn eq(&self, other: &RawSlice) -> bool { *self == other.0 } diff --git a/packages/fuels-core/src/types/errors.rs b/packages/fuels-core/src/types/errors.rs index afcb497c69..5e42bf6088 100644 --- a/packages/fuels-core/src/types/errors.rs +++ b/packages/fuels-core/src/types/errors.rs @@ -71,6 +71,11 @@ impl From for Error { } } +impl From for Error { + fn from(err: ValidityError) -> Error { + Error::ValidationError(format!("{:?}", err)) + } +} + impl_error_from!(InvalidData, bech32::Error); impl_error_from!(InvalidData, TryFromSliceError); -impl_error_from!(ValidationError, ValidityError); diff --git a/packages/fuels-core/src/types/param_types.rs b/packages/fuels-core/src/types/param_types.rs index e23b13203e..c2bff0e2f8 100644 --- a/packages/fuels-core/src/types/param_types.rs +++ b/packages/fuels-core/src/types/param_types.rs @@ -16,19 +16,23 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum ParamType { + Unit, + Bool, U8, U16, U32, U64, U128, U256, - Bool, B256, - Unit, + Bytes, + String, + RawSlice, + StringArray(usize), + StringSlice, + Tuple(Vec), Array(Box, usize), Vector(Box), - StringSlice, - StringArray(usize), Struct { fields: Vec, generics: Vec, @@ -37,10 +41,6 @@ pub enum ParamType { variants: EnumVariants, generics: Vec, }, - Tuple(Vec), - RawSlice, - Bytes, - String, } pub enum ReturnLocation { diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index b26fcc213c..9eab467aa0 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -1,10 +1,14 @@ #![cfg(feature = "std")] -use std::{collections::HashMap, iter::repeat_with}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + iter::repeat, +}; use async_trait::async_trait; use fuel_asm::{op, GTFArgs, RegId}; -use fuel_crypto::{Message as CryptoMessage, SecretKey, Signature}; +use fuel_crypto::{Message as CryptoMessage, Signature}; use fuel_tx::{ field::{Inputs, WitnessLimit, Witnesses}, policies::{Policies, PolicyType}, @@ -13,11 +17,12 @@ use fuel_tx::{ Witness, }; use fuel_types::{bytes::padded_len_usize, canonical::Serialize, Bytes32, ChainId, Salt}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use itertools::Itertools; use crate::{ constants::{BASE_ASSET_ID, SIGNATURE_WITNESS_SIZE, WITNESS_STATIC_SIZE, WORD_SIZE}, offsets, + traits::Signer, types::{ bech32::Bech32Address, coin::Coin, @@ -56,11 +61,9 @@ impl DryRunner for &T { } } -#[derive(Debug, Clone, Default, Zeroize, ZeroizeOnDrop)] -struct UnresolvedSignatures { - #[zeroize(skip)] - addr_idx_offset_map: HashMap, - secret_keys: Vec, +#[derive(Debug, Clone, Default)] +struct UnresolvedWitnessIndexes { + owner_to_idx_offset: HashMap, } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -68,6 +71,11 @@ pub trait BuildableTransaction { type TxType: Transaction; async fn build(self, provider: &impl DryRunner) -> Result; + + /// Building without signatures will set the witness indexes of signed coins in the + /// order as they appear in the inputs. Multiple coins with the same owner will have + /// the same witness index. Make sure you sign the built transaction in the expected order. + async fn build_without_signatures(self, provider: &impl DryRunner) -> Result; } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -77,6 +85,13 @@ impl BuildableTransaction for ScriptTransactionBuilder { async fn build(self, provider: &impl DryRunner) -> Result { self.build(provider).await } + + async fn build_without_signatures(mut self, provider: &impl DryRunner) -> Result { + self.set_witness_indexes(); + self.unresolved_signers = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -86,13 +101,20 @@ impl BuildableTransaction for CreateTransactionBuilder { async fn build(self, provider: &impl DryRunner) -> Result { self.build(provider).await } + + async fn build_without_signatures(mut self, provider: &impl DryRunner) -> Result { + self.set_witness_indexes(); + self.unresolved_signers = Default::default(); + + self.build(provider).await + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait TransactionBuilder: BuildableTransaction + Send + Clone { +pub trait TransactionBuilder: BuildableTransaction + Send { type TxType: Transaction; - fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); + fn add_signer(&mut self, signer: impl Signer + Send + Sync) -> Result<&mut Self>; async fn fee_checked_from_tx( &self, provider: &impl DryRunner, @@ -115,19 +137,45 @@ macro_rules! impl_tx_trait { impl TransactionBuilder for $ty { type TxType = $tx_ty; - fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey) { - let index_offset = self.unresolved_signatures.secret_keys.len() as u64; - self.unresolved_signatures.secret_keys.push(secret_key); - self.unresolved_signatures - .addr_idx_offset_map - .insert(owner, index_offset); + fn add_signer(&mut self, signer: impl Signer + Send + Sync) -> Result<&mut Self> { + let address = signer.address(); + if self + .unresolved_witness_indexes + .owner_to_idx_offset + .contains_key(address) + { + return Err(error!( + InvalidData, + "Already added `Signer` with address: `{address}`" + )); + } + + let index_offset = self.unresolved_signers.len() as u64; + self.unresolved_witness_indexes + .owner_to_idx_offset + .insert(address.clone(), index_offset); + self.unresolved_signers.push(Box::new(signer)); + + Ok(self) } async fn fee_checked_from_tx( &self, provider: &impl DryRunner, ) -> Result> { - let mut tx = BuildableTransaction::build(self.clone(), provider).await?; + let mut fee_estimation_tb = self.clone_without_signers(); + + // Add a temporary witness for every `Signer` to include them in the fee + // estimation. + let witness: Witness = Signature::default().as_ref().into(); + fee_estimation_tb + .witnesses_mut() + .extend(repeat(witness).take(self.unresolved_signers.len())); + + let mut tx = + BuildableTransaction::build_without_signatures(fee_estimation_tb, provider) + .await?; + let consensus_parameters = provider.consensus_parameters(); if tx.is_using_predicates() { @@ -141,8 +189,10 @@ macro_rules! impl_tx_trait { )) } - fn with_tx_policies(self, tx_policies: TxPolicies) -> Self { - self.with_tx_policies(tx_policies) + fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { + self.tx_policies = tx_policies; + + self } fn with_inputs(mut self, inputs: Vec) -> Self { @@ -186,6 +236,21 @@ macro_rules! impl_tx_trait { } impl $ty { + fn set_witness_indexes(&mut self) { + self.unresolved_witness_indexes.owner_to_idx_offset = self + .inputs() + .iter() + .filter_map(|input| match input { + Input::ResourceSigned { resource } => Some(resource.owner()), + _ => None, + }) + .unique() + .cloned() + .enumerate() + .map(|(idx, owner)| (owner, idx as u64)) + .collect(); + } + fn generate_fuel_policies(&self, network_min_gas_price: u64) -> Policies { let mut policies = Policies::default(); policies.set(PolicyType::MaxFee, self.tx_policies.max_fee()); @@ -214,7 +279,7 @@ macro_rules! impl_tx_trait { fn num_witnesses(&self) -> Result { let num_witnesses = self.witnesses().len(); - if num_witnesses + self.unresolved_signatures.secret_keys.len() > 256 { + if num_witnesses + self.unresolved_signers.len() > 256 { return Err(error!( InvalidData, "tx can not have more than 256 witnesses" @@ -226,8 +291,8 @@ macro_rules! impl_tx_trait { fn calculate_witnesses_size(&self) -> Option { let witnesses_size = calculate_witnesses_size(&self.witnesses); - let signature_size = - SIGNATURE_WITNESS_SIZE * self.unresolved_signatures.secret_keys.len(); + let signature_size = SIGNATURE_WITNESS_SIZE + * self.unresolved_witness_indexes.owner_to_idx_offset.len(); Some(padded_len_usize(witnesses_size + signature_size) as u64) } @@ -235,7 +300,15 @@ macro_rules! impl_tx_trait { }; } -#[derive(Debug, Clone, Default)] +impl Debug for dyn Signer + Send + Sync { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signer") + .field("address", &self.address()) + .finish() + } +} + +#[derive(Debug, Default)] pub struct ScriptTransactionBuilder { pub script: Vec, pub script_data: Vec, @@ -244,10 +317,11 @@ pub struct ScriptTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub gas_estimation_tolerance: f32, - unresolved_signatures: UnresolvedSignatures, + unresolved_witness_indexes: UnresolvedWitnessIndexes, + unresolved_signers: Vec>, } -#[derive(Debug, Clone, Default)] +#[derive(Default)] pub struct CreateTransactionBuilder { pub bytecode_length: u64, pub bytecode_witness_index: u8, @@ -257,7 +331,8 @@ pub struct CreateTransactionBuilder { pub witnesses: Vec, pub tx_policies: TxPolicies, pub salt: Salt, - unresolved_signatures: UnresolvedSignatures, + unresolved_witness_indexes: UnresolvedWitnessIndexes, + unresolved_signers: Vec>, } impl_tx_trait!(ScriptTransactionBuilder, ScriptTransaction); @@ -272,10 +347,7 @@ impl ScriptTransactionBuilder { 0 }; - let num_witnesses = self.num_witnesses()?; - let tx = self - .resolve_fuel_tx_provider(base_offset, num_witnesses, &provider) - .await?; + let tx = self.resolve_fuel_tx(base_offset, &provider).await?; Ok(ScriptTransaction { tx, @@ -285,10 +357,12 @@ impl ScriptTransactionBuilder { // When dry running a tx with `utxo_validation` off, the node will not validate signatures. // However, the node will check if the right number of witnesses is present. - // This function will create empty witnesses such that the total length matches the expected one. + // This function will create witnesses from a default `Signature` such that the total length matches the expected one. + // Using a `Signature` ensures that the calculated fee includes the fee generated by the witnesses. fn create_dry_run_witnesses(&self, num_witnesses: u8) -> Vec { - let unresolved_witnesses_len = self.unresolved_signatures.addr_idx_offset_map.len(); - repeat_with(Default::default) + let unresolved_witnesses_len = self.unresolved_witness_indexes.owner_to_idx_offset.len(); + let witness: Witness = Signature::default().as_ref().into(); + repeat(witness) .take(num_witnesses as usize + unresolved_witnesses_len) .collect() } @@ -353,12 +427,12 @@ impl ScriptTransactionBuilder { Ok(()) } - async fn resolve_fuel_tx_provider( + async fn resolve_fuel_tx( self, base_offset: usize, - num_witnesses: u8, provider: &impl DryRunner, ) -> Result