diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b765a2c8e2..18bb6f8722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: RUSTFLAGS: "-D warnings" FUEL_CORE_VERSION: 0.20.4 RUST_VERSION: 1.71.1 - FORC_VERSION: 0.44.0 + FORC_VERSION: 0.45.0 FORC_PATCH_BRANCH: "" FORC_PATCH_REVISION: "" @@ -60,7 +60,7 @@ jobs: working-directory: packages/fuels - name: Build Sway test projects - run: forc build --terse + run: forc build --terse --error-on-warnings working-directory: packages/fuels - uses: actions/upload-artifact@v2 @@ -173,6 +173,8 @@ jobs: install_fuel_core: true - cargo_command: test args: --doc --workspace + - cargo_command: machete + args: --skip-target-dir - command: test_wasm args: - command: check_doc_anchors_valid @@ -215,6 +217,10 @@ jobs: if: ${{ matrix.cargo_command == 'nextest' }} uses: taiki-e/install-action@nextest + - name: Install cargo-machete + if: ${{ matrix.cargo_command == 'machete' }} + uses: taiki-e/install-action@cargo-machete + - name: Cargo (workspace-level) if: ${{ matrix.cargo_command && !matrix.package }} run: cargo ${{ matrix.cargo_command }} ${{ matrix.args }} diff --git a/.gitignore b/.gitignore index 239d9f4ffe..75d9a30a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ packages/fuels/tests/**/**/target/ packages/fuels/tests/**/**/.gitignore # Don't add lock files in the fuels directory. -packages/fuels/Forc.lock \ No newline at end of file +packages/fuels/Forc.lock diff --git a/Cargo.toml b/Cargo.toml index 9bfd08e275..0a6a2dbcae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,24 +6,25 @@ # https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html#details resolver = "2" members = [ - "examples/abigen", - "examples/contracts", - "examples/cookbook", - "examples/debugging", - "examples/predicates", - "examples/providers", - "examples/rust_bindings", - "examples/types", - "examples/wallets", - "packages/fuels", - "packages/fuels-accounts", - "packages/fuels-code-gen", - "packages/fuels-core", - "packages/fuels-macros", - "packages/fuels-programs", - "packages/fuels-test-helpers", - "packages/wasm-tests", - "scripts/check-docs", + "examples/codec", + "examples/contracts", + "examples/cookbook", + "examples/debugging", + "examples/macros", + "examples/predicates", + "examples/providers", + "examples/rust_bindings", + "examples/types", + "examples/wallets", + "packages/fuels", + "packages/fuels-accounts", + "packages/fuels-code-gen", + "packages/fuels-core", + "packages/fuels-macros", + "packages/fuels-programs", + "packages/fuels-test-helpers", + "packages/wasm-tests", + "scripts/check-docs", ] [workspace.package] @@ -34,14 +35,14 @@ readme = "README.md" license = "Apache-2.0" repository = "https://github.com/FuelLabs/fuels-rs" rust-version = "1.71.1" -version = "0.46.0" +version = "0.48.0" [workspace.dependencies] Inflector = "0.11.4" async-trait = { version = "0.1.73", default-features = false } bech32 = "0.9.1" bytes = { version = "1.4.0", default-features = false } -chrono = "0.4.26" +chrono = "0.4.27" elliptic-curve = { version = "0.13.5", default-features = false } eth-keystore = "0.5.0" fuel-abi-types = "0.3.0" @@ -51,18 +52,19 @@ itertools = "0.11.0" portpicker = "0.1.1" proc-macro2 = "1.0.66" quote = "1.0.32" -rand = { version = "0.8.5", default-features = false, features = ["std_rng", "getrandom"] } +rand = { version = "0.8.5", default-features = false, features = [ + "std_rng", + "getrandom", +] } regex = "1.9.3" serde = { version = "1.0.183", default-features = false } serde_json = "1.0.104" serde_with = { version = "3.2.0", default-features = false } sha2 = { version = "0.10.7", default-features = false } -strum = "0.25.0" -strum_macros = "0.25.2" syn = "2.0.28" tai64 = { version = "4.0.0", default-features = false } tempfile = { version = "3.7.1", default-features = false } -thiserror = { version = "1.0.46", default-features = false } +thiserror = { version = "1.0.47", default-features = false } tokio = { version = "1.31.0", default-features = false } trybuild = "1.0.82" which = { version = "4.4.0", default-features = false } @@ -83,10 +85,10 @@ fuel-types = { version = "0.35.3", default-features = false } fuel-vm = "0.35.3" # Workspace projects -fuels = { version = "0.46.0", path = "./packages/fuels" } -fuels-accounts = { version = "0.46.0", path = "./packages/fuels-accounts", default-features = false } -fuels-code-gen = { version = "0.46.0", path = "./packages/fuels-code-gen", default-features = false } -fuels-core = { version = "0.46.0", path = "./packages/fuels-core", default-features = false } -fuels-macros = { version = "0.46.0", path = "./packages/fuels-macros", default-features = false } -fuels-programs = { version = "0.46.0", path = "./packages/fuels-programs", default-features = false } -fuels-test-helpers = { version = "0.46.0", path = "./packages/fuels-test-helpers", default-features = false } +fuels = { version = "0.48.0", path = "./packages/fuels" } +fuels-accounts = { version = "0.48.0", path = "./packages/fuels-accounts", default-features = false } +fuels-code-gen = { version = "0.48.0", path = "./packages/fuels-code-gen", default-features = false } +fuels-core = { version = "0.48.0", path = "./packages/fuels-core", default-features = false } +fuels-macros = { version = "0.48.0", path = "./packages/fuels-macros", default-features = false } +fuels-programs = { version = "0.48.0", path = "./packages/fuels-programs", default-features = false } +fuels-test-helpers = { version = "0.48.0", path = "./packages/fuels-test-helpers", default-features = false } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 592c792737..7c899d51a2 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,6 +8,7 @@ - [Running a short-lived Fuel node with the SDK](./connecting/short-lived.md) - [Rocksdb](./connecting/rocksdb.md) - [Querying the blockchain](./connecting/querying.md) + - [Retrying upon errors](./connecting/retrying.md) - [Accounts](./accounts.md) - [Managing wallets](./wallets/index.md) - [Creating a wallet from a private key](./wallets/private-keys.md) @@ -22,6 +23,7 @@ - [The `abigen!` macro](abigen/the-abigen-macro.md) - [Deploying contracts](./deploying/index.md) - [Configurable constants](./deploying/configurable-constants.md) + - [Storage slots](./deploying/storage-slots.md) - [Interacting with contracts](./deploying/interacting-with-contracts.md) - [The FuelVM Binary file](./deploying/the-fuelvm-binary-file.md) - [Calling contracts](./calling-contracts/index.md) @@ -54,6 +56,9 @@ - [B512](./types/B512.md) - [EvmAddress](./types/evm_address.md) - [Vectors](./types/vectors.md) +- [Codec](./codec/index.md) + - [Encoding](./codec/encoding.md) + - [Decoding](./codec/decoding.md) - [API Reference](./reference.md) - [Testing](./testing/index.md) - [Testing basics](./testing/basics.md) @@ -66,7 +71,7 @@ - [Debugging](./debugging/index.md) - [The Function selector](./debugging/function-selector.md) - [Glossary](./glossary.md) -- [Contributing](./contributing/contributing.md) +- [Contributing](./contributing/CONTRIBUTING.md) - [Integration tests structure](./contributing/tests-structure.md) - [Command Line Interfaces](./cli/index.md) - [`fuels-abi-cli`](./cli/fuels-abi-cli.md) diff --git a/docs/src/abigen/the-abigen-macro.md b/docs/src/abigen/the-abigen-macro.md index 805c01ea41..d9aa541205 100644 --- a/docs/src/abigen/the-abigen-macro.md +++ b/docs/src/abigen/the-abigen-macro.md @@ -21,7 +21,7 @@ where: So, an `abigen!` which generates bindings for two contracts and one script looks like this: ```rust,ignore -{{#include ../../../examples/abigen/src/lib.rs:multiple_abigen_program_types}} +{{#include ../../../examples/macros/src/lib.rs:multiple_abigen_program_types}} ``` ## How does the generated code look? diff --git a/docs/src/calling-contracts/index.md b/docs/src/calling-contracts/index.md index 80eaa8f9c4..93c724c8de 100644 --- a/docs/src/calling-contracts/index.md +++ b/docs/src/calling-contracts/index.md @@ -15,4 +15,10 @@ Here's an example. Suppose your Sway contract has two ABI methods called `initia The example above uses all the default configurations and performs a simple contract call. -Next, we'll see how we can further configure the many different parameters in a contract call +Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:submit_response_contract}} +``` + +Next, we'll see how we can further configure the many different parameters in a contract call. diff --git a/docs/src/calling-contracts/low-level-calls.md b/docs/src/calling-contracts/low-level-calls.md index 3b726f4182..78d4a305bd 100644 --- a/docs/src/calling-contracts/low-level-calls.md +++ b/docs/src/calling-contracts/low-level-calls.md @@ -15,7 +15,7 @@ Your caller contract should call `std::low_level_call::call_with_function_select {{#include ../../../packages/fuels/tests/contracts/low_level_caller/src/main.sw:low_level_call_contract}} ``` -On the SDK side, you can construct an encoded function selector using the `fuels::core::fn_selector` macro, and encoded calldata using the `fuels::core::calldata` macro. +On the SDK side, you can construct an encoded function selector using the `fuels::core::fn_selector!` macro, and encoded calldata using the `fuels::core::calldata!` macro. E.g. to call the following function on the target contract: diff --git a/docs/src/calling-contracts/multicalls.md b/docs/src/calling-contracts/multicalls.md index 7920831df4..472de7203d 100644 --- a/docs/src/calling-contracts/multicalls.md +++ b/docs/src/calling-contracts/multicalls.md @@ -16,6 +16,12 @@ Next, you provide the prepared calls to your `MultiContractCallHandler` and opti > **Note:** any transaction parameters configured on separate contract calls are disregarded in favor of the parameters provided to `MultiContractCallHandler`. +Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:submit_response_multicontract}} +``` + ## Output values To get the output values of the bundled calls, you need to provide explicit type annotations when saving the result of `call()` or `simulate()` to a variable: diff --git a/docs/src/calling-contracts/tx-params.md b/docs/src/calling-contracts/tx-params.md index 2b8fcfaf4e..3b7bdb64a8 100644 --- a/docs/src/calling-contracts/tx-params.md +++ b/docs/src/calling-contracts/tx-params.md @@ -17,13 +17,9 @@ You can configure these parameters by creating an instance of `TxParameters` and -You can also use `TxParameters::default()` to use the default values: +You can also use `TxParameters::default()` to use the default values. If `gas_price` or `gas_limit` is set to `None`, the SDK will use the network's default value: -```rust,ignore -{{#include ../../../packages/fuels-core/src/utils/constants.rs:default_tx_parameters}} -``` - This way: ```rust,ignore diff --git a/docs/src/codec/decoding.md b/docs/src/codec/decoding.md new file mode 100644 index 0000000000..33a7d0b588 --- /dev/null +++ b/docs/src/codec/decoding.md @@ -0,0 +1,49 @@ +# Decoding + +Be sure to read the [prerequisites](./index.md#prerequisites-for-decodingencoding) to decoding. + +Decoding is done via the [`ABIDecoder`](https://docs.rs/fuels/latest/fuels/core/codec/struct.ABIDecoder.html): + +```rust,ignore +{{#include ../../../examples/codec/src/lib.rs:decoding_example}} +``` + +First into a [`Token`](https://docs.rs/fuels/latest/fuels/types/enum.Token.html), then via the [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) trait, into the desired type. + +If the type came from [`abigen!`](../abigen/index.md) (or uses the [`::fuels::macros::TryFrom`](https://docs.rs/fuels/latest/fuels/macros/derive.TryFrom.html) derivation) then you can also use `try_into` to convert bytes into a type that implements both [`Parameterize`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html) and [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html): + +```rust,ignore +{{#include ../../../examples/codec/src/lib.rs:decoding_example_try_into}} +``` + +Under the hood, [`try_from_bytes`](https://docs.rs/fuels/latest/fuels/core/codec/fn.try_from_bytes.html) is being called, which does what the preceding example did. + +## Configuring the decoder + +The decoder can be configured to limit its resource expenditure: + +```rust,ignore +{{#include ../../../examples/codec/src/lib.rs:configuring_the_decoder}} +``` + + + +For an explanation of each configuration value visit the `DecoderConfig`. + + + +The default values for the `DecoderConfig` are: + +```rust,ignore +{{#include ../../../packages/fuels-core/src/codec/abi_decoder.rs:default_decoder_config}} +``` + +## Configuring the decoder for contract/script calls + +You can also configure the decoder used to decode the return value of the contract method: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:contract_decoder_config}} +``` + +The same method is available for script calls. diff --git a/docs/src/codec/encoding.md b/docs/src/codec/encoding.md new file mode 100644 index 0000000000..86905f71c8 --- /dev/null +++ b/docs/src/codec/encoding.md @@ -0,0 +1,20 @@ +# Encoding + +Be sure to read the [prerequisites](./index.md#prerequisites-for-decodingencoding) to encoding. + +Encoding is done via the [`ABIEncoder`](https://docs.rs/fuels/latest/fuels/core/codec/struct.ABIEncoder.html): + +```rust,ignore +{{#include ../../../examples/codec/src/lib.rs:encoding_example}} +``` + +Note that the return type of `encode` is `UnresolvedBytes`. The encoding cannot be finished until we know at which memory address this data is to be loaded. If you don't use heap types (`::std::vec::Vec`, `::fuels::types::Bytes`, `::std::string::String`), then you can safely `.resolve(0)` to get the encoded bytes. + +There is also a shortcut-macro that can encode multiple types which implement [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html): + +```rust,ignore +{{#include ../../../examples/codec/src/lib.rs:encoding_example_w_macro}} +``` + +> Note: +> The above example will call `.resolve(0)`. Don't use it if you're encoding heap types. diff --git a/docs/src/codec/index.md b/docs/src/codec/index.md new file mode 100644 index 0000000000..ba23d7cb88 --- /dev/null +++ b/docs/src/codec/index.md @@ -0,0 +1,45 @@ +# Codec + +Encoding and decoding are done as per [the fuel spec](https://specs.fuel.network/master/abi/argument-encoding.html). To this end, `fuels` makes use of the [ABIEncoder](https://docs.rs/fuels/latest/fuels/core/codec/struct.ABIEncoder.html) and the [ABIDecoder](https://docs.rs/fuels/latest/fuels/core/codec/struct.ABIDecoder.html). + +## Prerequisites for decoding/encoding + +To encode a type, you must first convert it into a [`Token`](https://docs.rs/fuels/latest/fuels/types/enum.Token.html). This is commonly done by implementing the [Tokenizable](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) trait. + +To decode, you also need to provide a [`ParamType`](https://docs.rs/fuels/latest/fuels/types/param_types/enum.ParamType.html) describing the schema of the type in question. This is commonly done by implementing the [Parameterize](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html) trait. + +All types generated by the [`abigen!`](../abigen/index.md) macro implement both the [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) and [`Parameterize`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html) traits. + +`fuels` also contains implementations for: + +- [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) for the `fuels`-owned types listed [here](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html#implementors) as well as [for some foreign types](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html#foreign-impls) (such as `u8`, `u16`, `std::vec::Vec`, etc.). +- [`Parameterize`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html) for the `fuels`-owned types listed [here](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html#implementors) as well as [for some foreign types](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html#foreign-impls) (such as `u8`, `u16`, `std::vec::Vec`, etc.). + +## Deriving the traits + +Both [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) and [`Parameterize`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html) can be derived for `struct`s and `enum`s if all inner types implement the derived traits: + +```rust,ignore +{{#include ../../../examples/macros/src/lib.rs:deriving_traits}} +``` + +> Note: +> Deriving [`Tokenizable`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Tokenizable.html) on `enum`s requires that all variants also implement [`Parameterize`](https://docs.rs/fuels/latest/fuels/core/traits/trait.Parameterize.html). + +### Tweaking the derivation + +#### Changing the location of imports + +The derived code expects that the `fuels` package is accessible through `::fuels`. If this is not the case then the derivation macro needs to be given the locations of `fuels::types` and `fuels::core`. + +```rust,ignore +{{#include ../../../examples/macros/src/lib.rs:deriving_traits_paths}} +``` + +#### Generating no-std code + +If you want `no-std` generated code: + +```rust,ignore +{{#include ../../../examples/macros/src/lib.rs:deriving_traits_nostd}} +``` diff --git a/docs/src/connecting/external-node.md b/docs/src/connecting/external-node.md index b00cae3f82..08e3b47fa9 100644 --- a/docs/src/connecting/external-node.md +++ b/docs/src/connecting/external-node.md @@ -14,7 +14,7 @@ In the code example, we connected a new provider to the Testnet node and created > **Note:** New wallets on the Testnet will not have any assets! They can be obtained by providing the wallet address to the faucet at > ->[faucet-beta-3.fuel.network](https://faucet-beta-3.fuel.network) +>[faucet-beta-4.fuel.network](https://faucet-beta-4.fuel.network) > > Once the assets have been transferred to the wallet, you can reuse it in other tests by providing the private key! > diff --git a/docs/src/connecting/retrying.md b/docs/src/connecting/retrying.md new file mode 100644 index 0000000000..ab51cb11cd --- /dev/null +++ b/docs/src/connecting/retrying.md @@ -0,0 +1,34 @@ +# Retrying requests + +The [`Provider`](https://docs.rs/fuels/0.47.0/fuels/accounts/provider/struct.Provider.html) can be configured to retry a request upon receiving a `io::Error`. + +> Note: Currently all node errors are received as `io::Error`s. So, if configured, a retry will happen even if, for example, a transaction failed to verify. + +We can configure the number of retry attempts and the retry strategy as detailed below. + +## RetryConfig + +The retry behavior can be altered by giving a custom `RetryConfig`. It allows for configuring the maximum number of attempts and the interval strategy used. + +```rust, ignore +{{#include ../../../packages/fuels-accounts/src/provider/retry_util.rs:retry_config}} +``` + +```rust, ignore +{{#include ../../../examples/providers/src/lib.rs:configure_retry}} +``` + +## Interval strategy - Backoff + +`Backoff` defines different strategies for managing intervals between retry attempts. +Each strategy allows you to customize the waiting time before a new attempt based on the number of attempts made. + +### Variants + +- `Linear(Duration)`: `Default` Increases the waiting time linearly with each attempt. +- `Exponential(Duration)`: Doubles the waiting time with each attempt. +- `Fixed(Duration)`: Uses a constant waiting time between attempts. + +```rust, ignore +{{#include ../../../packages/fuels-accounts/src/provider/retry_util.rs:backoff}} +``` diff --git a/docs/src/connecting/short-lived.md b/docs/src/connecting/short-lived.md index 00e0761e5b..8744d3e315 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.46.0", features = ["fuel-core-lib"] } +fuels = { version = "0.48.0", features = ["fuel-core-lib"] } ``` ### RocksDb @@ -35,5 +35,5 @@ fuels = { version = "0.46.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.46.0", features = ["rocksdb"] } +fuels = { version = "0.48.0", features = ["rocksdb"] } ``` diff --git a/docs/src/contributing/CONTRIBUTING.md b/docs/src/contributing/CONTRIBUTING.md new file mode 100644 index 0000000000..bdad1d7524 --- /dev/null +++ b/docs/src/contributing/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to the Fuel Rust SDK + +Thanks for your interest in contributing to the Fuel Rust SDK! + +This document outlines the process for installing dependencies, setting up for development, and conventions for contributing.` + +If you run into any difficulties getting started, you can always ask questions on our [Discourse](https://forum.fuel.network/). + +## Finding something to work on + +You may contribute to the project in many ways, some of which involve coding knowledge and some which do not. A few examples include: + +- Reporting bugs +- Adding new features or bugfixes for which there is already an open issue +- Making feature requests + +Check out our [Help Wanted](https://github.com/FuelLabs/fuels-rs/labels/help%20wanted) or [Good First Issues](https://github.com/FuelLabs/fuels-rs/labels/good%20first%20issue) to find a suitable task. + +If you are planning something big, for example, changes related to multiple components or changes to current behaviors, make sure to [open an issue](https://github.com/FuelLabs/fuels-rs/issues/new) to discuss with us before starting on the implementation. + +## Contribution flow + +This is a rough outline of what a contributor's workflow looks like: + +- Make sure what you want to contribute is already tracked as an issue. + - We may discuss the problem and solution in the issue. +- Create a Git branch from where you want to base your work. This is usually master. +- Write code, add test cases, and commit your work. +- Run tests and make sure all tests pass. +- Add the breaking label to your PR if the PR contains any breaking changes. +- Push your changes to a branch in your fork of the repository and submit a pull request. + - Make sure to mention the issue created in step 1 in the commit message. +- Your PR will be reviewed, and some changes may be requested. + - Your PR must be re-reviewed and approved once you've made changes. + - Use GitHub's 'update branch' button if the PR becomes outdated. + - If there are conflicts, you can merge and resolve them locally. Then push to your PR branch. Any changes to the branch will require a re-review. +- Our CI system (Github Actions) automatically tests all authorized pull requests. +- Use GitHub to merge the PR once approved. + +Thanks for your contributions! + +## Linking issues + +Pull requests should be linked to at least one issue in the same repo. + +If the pull request resolves the relevant issues, and you want GitHub to close these issues automatically after it merged into the default branch, you can use the syntax (`KEYWORD #ISSUE-NUMBER`) like this: + +```sh +close #123 +``` + +If the pull request links an issue but does not close it, you can use the keyword `ref` like this: + +```sh +ref #456 +``` + +Multiple issues should use full syntax for each issue and be separated by a comma, like: + +```sh +close #123, ref #456 +``` diff --git a/docs/src/contributing/contributing.md b/docs/src/contributing/contributing.md deleted file mode 100644 index 40e6dfb2b7..0000000000 --- a/docs/src/contributing/contributing.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing to `fuels-rs` - -> **note** This page is still a work in progress. diff --git a/docs/src/deploying/storage-slots.md b/docs/src/deploying/storage-slots.md new file mode 100644 index 0000000000..11bfd3162e --- /dev/null +++ b/docs/src/deploying/storage-slots.md @@ -0,0 +1,13 @@ +# Overriding storage slots + +If you use storage in your contract, the default storage values will be generated in a JSON file (e.g. `my_contract-storage_slots.json`) by the Sway compiler. These are loaded automatically for you when you load a contract binary. If you wish to override some of the defaults, you need to provide the corresponding storage slots manually: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:storage_slots_override}} +``` + +If you don't have the slot storage file (`my_contract-storage_slots.json` example from above) for some reason, or you don't wish to load any of the default values, you can disable the auto-loading of storage slots: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:storage_slots_disable_autoload}} +``` diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 02529d1e19..11223424ea 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -91,10 +91,10 @@ cargo test -- --nocapture Add these dependencies on your `Cargo.toml`: ```toml -fuels = "0.46" +fuels = "0.48" ``` -> **Note** We're using version `0.46` of the SDK, which is the latest version at the time of this writing. +> **Note** We're using version `0.48` of the SDK, which is the latest version at the time of this writing. And then, in your Rust file that's going to make use of the SDK: diff --git a/docs/src/running-scripts.md b/docs/src/running-scripts.md index 919ac259d6..6f38973bb7 100644 --- a/docs/src/running-scripts.md +++ b/docs/src/running-scripts.md @@ -6,6 +6,12 @@ You can run a script using its JSON-ABI and the path to its binary file. You can {{#include ../../packages/fuels/tests/scripts.rs:script_with_arguments}} ```` +Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows: + +```rust,ignore +{{#include ../../packages/fuels/tests/scripts.rs:submit_response_script}} +``` + ## Running scripts with transaction parameters The method for passing transaction parameters is the same as [with contracts](./calling-contracts/tx-params.md). As a reminder, the workflow would look like this: @@ -26,13 +32,13 @@ Script calls provide the same logging functions, `decode_logs()` and `decode_log Scripts use the same interfaces for setting external contracts as [contract methods](./calling-contracts/other-contracts.md). -Below is an example that uses `set_contracts(&[&contract_instance, ...])`. +Below is an example that uses `with_contracts(&[&contract_instance, ...])`. ```rust,ignore {{#include ../../packages/fuels/tests/logs.rs:external_contract}} ``` -And this is an example that uses `set_contract_ids(&[&contract_id, ...])`. +And this is an example that uses `with_contract_ids(&[&contract_id, ...])`. ```rust,ignore {{#include ../../packages/fuels/tests/logs.rs:external_contract_ids}} diff --git a/docs/src/types/custom_types.md b/docs/src/types/custom_types.md index 4137dfa9f4..5fe79d9bd4 100644 --- a/docs/src/types/custom_types.md +++ b/docs/src/types/custom_types.md @@ -22,20 +22,6 @@ After using the `abigen!` macro, `CounterConfig` will be accessible in your Rust You can freely use your custom types (structs or enums) within this scope. That also means passing custom types to functions and receiving custom types from function calls. -## Manual decoding - -Suppose you wish to decode raw bytes into a type used in your contract and the `abigen!` generated this type, then you can use `try_into`: - -```rust,ignore -{{#include ../../../packages/fuels/tests/types_contracts.rs:manual_decode}} -``` - -Otherwise, for native types such as `u8`, `u32`,...,`ContractId` and others, you must use `::fuels::core::try_from_bytes`: - -```rust,ignore -{{#include ../../../examples/rust_bindings/src/lib.rs:manual_decode_native}} -``` - ## Generics The Fuel Rust SDK supports both generic enums and generic structs. If you're already familiar with Rust, it's your typical `struct MyStruct` type of generics support. diff --git a/docs/src/wallets/mnemonic-wallet.md b/docs/src/wallets/mnemonic-wallet.md index 40749ef2b6..9dd4daf98f 100644 --- a/docs/src/wallets/mnemonic-wallet.md +++ b/docs/src/wallets/mnemonic-wallet.md @@ -2,7 +2,7 @@ A mnemonic phrase is a cryptographically-generated sequence of words that's used to derive a private key. For instance: `"oblige salon price punch saddle immune slogan rare snap desert retire surprise";` would generate the address `0xdf9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185`. -In addition to that, we also support [Hierarchical Deterministic Wallets](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets) and [derivation paths](https://learnmeabitcoin.com/technical/derivation-paths). You may recognize the string `"m/44'/60'/0'/0/0"` from somewhere; that's a derivation path. In simple terms, it's a way to derive many wallets from a single root wallet. +In addition to that, we also support [Hierarchical Deterministic Wallets](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets) and [derivation paths](https://thebitcoinmanual.com/articles/btc-derivation-path/). You may recognize the string `"m/44'/60'/0'/0/0"` from somewhere; that's a derivation path. In simple terms, it's a way to derive many wallets from a single root wallet. The SDK gives you two wallets from mnemonic instantiation methods: one that takes a derivation path (`Wallet::new_from_mnemonic_phrase_with_path`) and one that uses the default derivation path, in case you don't want or don't need to configure that (`Wallet::new_from_mnemonic_phrase`). diff --git a/examples/abigen/src/lib.rs b/examples/abigen/src/lib.rs deleted file mode 100644 index b333442ebd..0000000000 --- a/examples/abigen/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[cfg(test)] -mod tests { - use fuels::prelude::*; - - #[tokio::test] - async fn example_of_abigen_usage() -> Result<()> { - // ANCHOR: multiple_abigen_program_types - abigen!( - Contract(name="ContractA", abi="packages/fuels/tests/bindings/sharing_types/contract_a/out/debug/contract_a-abi.json"), - Contract(name="ContractB", abi="packages/fuels/tests/bindings/sharing_types/contract_b/out/debug/contract_b-abi.json"), - Script(name="MyScript", abi="packages/fuels/tests/scripts/arguments/out/debug/arguments-abi.json"), - Predicate(name="MyPredicateEncoder", abi="packages/fuels/tests/predicates/basic_predicate/out/debug/basic_predicate-abi.json"), - ); - // ANCHOR_END: multiple_abigen_program_types - - Ok(()) - } -} diff --git a/examples/abigen/Cargo.toml b/examples/codec/Cargo.toml similarity index 80% rename from examples/abigen/Cargo.toml rename to examples/codec/Cargo.toml index c452190cf1..a97f79f13f 100644 --- a/examples/abigen/Cargo.toml +++ b/examples/codec/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fuels-example-abigen" +name = "fuels-example-codec" version = { workspace = true } authors = { workspace = true } edition = { workspace = true } @@ -7,7 +7,7 @@ homepage = { workspace = true } license = { workspace = true } publish = false repository = { workspace = true } -description = "Fuel Rust SDK abigen examples." +description = "Fuel Rust SDK codec examples." [dev-dependencies] fuels = { workspace = true } diff --git a/examples/codec/src/lib.rs b/examples/codec/src/lib.rs new file mode 100644 index 0000000000..8d5fff8e3d --- /dev/null +++ b/examples/codec/src/lib.rs @@ -0,0 +1,101 @@ +#[cfg(test)] +mod tests { + use fuels::{core::codec::DecoderConfig, types::errors::Result}; + + #[test] + fn encoding_a_type() -> Result<()> { + //ANCHOR: encoding_example + use fuels::{ + core::{codec::ABIEncoder, traits::Tokenizable}, + macros::Tokenizable, + types::unresolved_bytes::UnresolvedBytes, + }; + + #[derive(Tokenizable)] + struct MyStruct { + field: u64, + } + + let instance = MyStruct { field: 101 }; + let encoded: UnresolvedBytes = ABIEncoder::encode(&[instance.into_token()])?; + let load_memory_address: u64 = 0x100; + let _: Vec = encoded.resolve(load_memory_address); + //ANCHOR_END: encoding_example + + Ok(()) + } + #[test] + fn encoding_via_macro() -> Result<()> { + //ANCHOR: encoding_example_w_macro + use fuels::{core::codec::calldata, macros::Tokenizable}; + + #[derive(Tokenizable)] + struct MyStruct { + field: u64, + } + let _: Vec = calldata!(MyStruct { field: 101 }, MyStruct { field: 102 })?; + //ANCHOR_END: encoding_example_w_macro + + Ok(()) + } + + #[test] + fn decoding_example() -> Result<()> { + // ANCHOR: decoding_example + use fuels::{ + core::{ + codec::ABIDecoder, + traits::{Parameterize, Tokenizable}, + }, + macros::{Parameterize, Tokenizable}, + types::Token, + }; + + #[derive(Parameterize, Tokenizable)] + struct MyStruct { + field: u64, + } + + let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101]; + + let token: Token = ABIDecoder::default().decode(&MyStruct::param_type(), bytes)?; + + let _: MyStruct = MyStruct::from_token(token)?; + // ANCHOR_END: decoding_example + + Ok(()) + } + + #[test] + fn decoding_example_try_into() -> Result<()> { + // ANCHOR: decoding_example_try_into + use fuels::macros::{Parameterize, Tokenizable, TryFrom}; + + #[derive(Parameterize, Tokenizable, TryFrom)] + struct MyStruct { + field: u64, + } + + let bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 101]; + + let _: MyStruct = bytes.try_into()?; + // ANCHOR_END: decoding_example_try_into + + Ok(()) + } + + #[test] + fn configuring_the_decoder() -> Result<()> { + // ANCHOR: configuring_the_decoder + + use fuels::core::codec::ABIDecoder; + + ABIDecoder::new(DecoderConfig { + max_depth: 5, + max_tokens: 100, + }); + // ANCHOR_END: configuring_the_decoder + + Ok(()) + } +} diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 8d7d9524ad..8c9e74c002 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -1,14 +1,20 @@ #[cfg(test)] mod tests { - use fuels::types::errors::{error, Error, Result}; - use fuels::types::Bits256; + use fuels::{ + core::codec::DecoderConfig, + prelude::{LoadConfiguration, StorageConfiguration}, + types::{ + errors::{error, Error, Result}, + Bits256, + }, + }; #[tokio::test] async fn instantiate_client() -> Result<()> { // ANCHOR: instantiate_client use fuels::{ - client::FuelClient, fuel_node::{Config, FuelService}, + prelude::Provider, }; // Run the fuel node. @@ -17,8 +23,8 @@ mod tests { .map_err(|err| error!(InfrastructureError, "{err}"))?; // Create a client that will talk to the node created above. - let client = FuelClient::from(server.bound_address); - assert!(client.health().await?); + let client = Provider::from(server.bound_address).await?; + assert!(client.healthy().await?); // ANCHOR_END: instantiate_client Ok(()) } @@ -105,7 +111,7 @@ mod tests { .await?; // ANCHOR_END: contract_call_cost_estimation - assert_eq!(transaction_cost.gas_used, 333); + assert_eq!(transaction_cost.gas_used, 331); Ok(()) } @@ -138,7 +144,8 @@ mod tests { let key = Bytes32::from([1u8; 32]); let value = Bytes32::from([2u8; 32]); let storage_slot = StorageSlot::new(key, value); - let storage_configuration = StorageConfiguration::from(vec![storage_slot]); + let storage_configuration = + StorageConfiguration::default().add_slot_overrides([storage_slot]); let configuration = LoadConfiguration::default() .with_storage_configuration(storage_configuration) .with_salt(salt); @@ -192,6 +199,18 @@ mod tests { assert_eq!(52, response.value); // ANCHOR_END: use_deployed_contract + // ANCHOR: submit_response_contract + let response = contract_instance + .methods() + .initialize_counter(42) + .submit() + .await?; + + let value = response.response().await?.value; + + // ANCHOR_END: submit_response_contract + assert_eq!(42, value); + Ok(()) } @@ -389,7 +408,7 @@ mod tests { let amount = 100; let response = contract_methods - .increment_from_contract_then_mint(called_contract_id, amount, address) + .mint_then_increment_from_contract(called_contract_id, amount, address) .call() .await; @@ -401,7 +420,7 @@ mod tests { // ANCHOR: dependency_estimation_manual let response = contract_methods - .increment_from_contract_then_mint(called_contract_id, amount, address) + .mint_then_increment_from_contract(called_contract_id, amount, address) .append_variable_outputs(1) .with_contract_ids(&[called_contract_id.into()]) .call() @@ -414,7 +433,7 @@ mod tests { // ANCHOR: dependency_estimation let response = contract_methods - .increment_from_contract_then_mint(called_contract_id, amount, address) + .mint_then_increment_from_contract(called_contract_id, amount, address) .estimate_tx_dependencies(Some(2)) .await? .call() @@ -590,6 +609,14 @@ mod tests { assert_eq!(counter, 42); assert_eq!(array, [42; 2]); + // ANCHOR: submit_response_multicontract + let submitted_tx = multi_call_handler.submit().await?; + let (counter, array): (u64, [u64; 2]) = submitted_tx.response().await?.value; + // ANCHOR_END: submit_response_multicontract + + assert_eq!(counter, 42); + assert_eq!(array, [42; 2]); + Ok(()) } @@ -630,7 +657,7 @@ mod tests { .await?; // ANCHOR_END: multi_call_cost_estimation - assert_eq!(transaction_cost.gas_used, 546); + assert_eq!(transaction_cost.gas_used, 542); Ok(()) } @@ -744,8 +771,8 @@ mod tests { a: true, b: [1, 2, 3], }, - SizedAsciiString::<4>::try_from("fuel").unwrap() - ); + SizedAsciiString::<4>::try_from("fuel")? + )?; caller_contract_instance .methods() @@ -791,4 +818,65 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn configure_the_return_value_decoder() -> Result<()> { + use fuels::prelude::*; + + setup_program_test!( + Wallets("wallet"), + Abigen(Contract( + name = "MyContract", + project = "packages/fuels/tests/contracts/contract_test" + )), + Deploy( + name = "contract_instance", + contract = "MyContract", + wallet = "wallet" + ) + ); + + // ANCHOR: contract_decoder_config + let _ = contract_instance + .methods() + .initialize_counter(42) + .with_decoder_config(DecoderConfig { + max_depth: 10, + max_tokens: 20_00, + }) + .call() + .await?; + // ANCHOR_END: contract_decoder_config + + Ok(()) + } + + #[tokio::test] + async fn storage_slots_override() -> Result<()> { + { + // ANCHOR: storage_slots_override + use fuels::{programs::contract::Contract, tx::StorageSlot}; + let slot_override = StorageSlot::new([1; 32].into(), [2; 32].into()); + let storage_config = + StorageConfiguration::default().add_slot_overrides([slot_override]); + + let load_config = + LoadConfiguration::default().with_storage_configuration(storage_config); + let _: Result = Contract::load_from("...", load_config); + // ANCHOR_END: storage_slots_override + } + + { + // ANCHOR: storage_slots_disable_autoload + use fuels::programs::contract::Contract; + let storage_config = StorageConfiguration::default().with_autoload(false); + + let load_config = + LoadConfiguration::default().with_storage_configuration(storage_config); + let _: Result = Contract::load_from("...", load_config); + // ANCHOR_END: storage_slots_disable_autoload + } + + Ok(()) + } } diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index ea30f03bee..1d7fccd4fb 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -1,9 +1,11 @@ #[cfg(test)] mod tests { - use fuels::types::Bits256; use fuels::{ prelude::Result, - types::transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, + types::{ + transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, + Bits256, + }, }; #[tokio::test] @@ -115,7 +117,7 @@ mod tests { // ANCHOR: custom_chain_provider let node_config = Config::local_node(); - let (_provider, _bound_address) = + let _provider = setup_test_provider(coins, vec![], Some(node_config), Some(chain_config)).await; // ANCHOR_END: custom_chain_provider Ok(()) @@ -136,7 +138,7 @@ mod tests { let (coins, _) = setup_multiple_assets_coins(wallet_1.address(), NUM_ASSETS, NUM_COINS, AMOUNT); - let (provider, _) = setup_test_provider(coins, vec![], None, None).await; + let provider = setup_test_provider(coins, vec![], None, None).await; wallet_1.set_provider(provider.clone()); wallet_2.set_provider(provider.clone()); @@ -164,12 +166,17 @@ mod tests { // ANCHOR_END: transfer_multiple_inout // ANCHOR: transfer_multiple_transaction - let mut tb = - ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxParameters::default()); + let network_info = provider.network_info().await?; + let mut tb = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + TxParameters::default(), + network_info, + ); wallet_1.sign_transaction(&mut tb); let tx = tb.build()?; - provider.send_transaction(tx).await?; + provider.send_transaction_and_await_commit(tx).await?; let balances = wallet_2.get_balances().await?; diff --git a/examples/debugging/src/lib.rs b/examples/debugging/src/lib.rs index 3b7ee92211..ef3424f348 100644 --- a/examples/debugging/src/lib.rs +++ b/examples/debugging/src/lib.rs @@ -60,11 +60,13 @@ mod tests { } #[test] - fn test_macros() { + fn test_macros() -> Result<()> { let function_selector = fn_selector!(initialize_counter(u64)); - let call_data = calldata!(42u64); + let call_data = calldata!(42u64)?; assert_eq!(vec![0, 0, 0, 0, 171, 100, 229, 242], function_selector); assert_eq!(vec![0, 0, 0, 0, 0, 0, 0, 42], call_data); + + Ok(()) } } diff --git a/examples/macros/Cargo.toml b/examples/macros/Cargo.toml new file mode 100644 index 0000000000..6be83c5d0d --- /dev/null +++ b/examples/macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fuels-example-macros" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = false +repository = { workspace = true } +description = "Fuel Rust SDK macro examples." + +[dev-dependencies] +fuels = { workspace = true } +tokio = { workspace = true, features = ["full"] } diff --git a/examples/macros/src/lib.rs b/examples/macros/src/lib.rs new file mode 100644 index 0000000000..1a95404059 --- /dev/null +++ b/examples/macros/src/lib.rs @@ -0,0 +1,65 @@ +extern crate alloc; + +#[cfg(test)] +mod tests { + use fuels::prelude::*; + + #[test] + fn example_of_abigen_usage() { + // ANCHOR: multiple_abigen_program_types + abigen!( + Contract(name="ContractA", abi="packages/fuels/tests/bindings/sharing_types/contract_a/out/debug/contract_a-abi.json"), + Contract(name="ContractB", abi="packages/fuels/tests/bindings/sharing_types/contract_b/out/debug/contract_b-abi.json"), + Script(name="MyScript", abi="packages/fuels/tests/scripts/arguments/out/debug/arguments-abi.json"), + Predicate(name="MyPredicateEncoder", abi="packages/fuels/tests/predicates/basic_predicate/out/debug/basic_predicate-abi.json"), + ); + // ANCHOR_END: multiple_abigen_program_types + } + + #[test] + fn macro_deriving() { + // ANCHOR: deriving_traits + use fuels::macros::{Parameterize, Tokenizable}; + + #[derive(Parameterize, Tokenizable)] + struct MyStruct { + field_a: u8, + } + + #[derive(Parameterize, Tokenizable)] + enum SomeEnum { + A(MyStruct), + B(Vec), + } + // ANCHOR_END: deriving_traits + } + #[test] + fn macro_deriving_extra() { + { + use fuels::{ + core as fuels_core_elsewhere, + macros::{Parameterize, Tokenizable}, + types as fuels_types_elsewhere, + }; + + // ANCHOR: deriving_traits_paths + #[derive(Parameterize, Tokenizable)] + #[FuelsCorePath = "fuels_core_elsewhere"] + #[FuelsTypesPath = "fuels_types_elsewhere"] + pub struct SomeStruct { + field_a: u64, + } + // ANCHOR_END: deriving_traits_paths + } + { + // ANCHOR: deriving_traits_nostd + use fuels::macros::{Parameterize, Tokenizable}; + #[derive(Parameterize, Tokenizable)] + #[NoStd] + pub struct SomeStruct { + field_a: u64, + } + // ANCHOR_END: deriving_traits_nostd + } + } +} diff --git a/examples/predicates/src/lib.rs b/examples/predicates/src/lib.rs index 083ee76aba..7f4f3913a1 100644 --- a/examples/predicates/src/lib.rs +++ b/examples/predicates/src/lib.rs @@ -44,7 +44,7 @@ mod tests { }) .collect::>(); - let (provider, _) = setup_test_provider(all_coins, vec![], None, None).await; + let provider = setup_test_provider(all_coins, vec![], None, None).await; [&mut wallet, &mut wallet2, &mut wallet3, &mut receiver] .iter_mut() diff --git a/examples/providers/src/lib.rs b/examples/providers/src/lib.rs index 759554286d..0da4b44ed4 100644 --- a/examples/providers/src/lib.rs +++ b/examples/providers/src/lib.rs @@ -1,11 +1,10 @@ #[cfg(test)] mod tests { + use std::time::Duration; + use fuels::prelude::Result; #[tokio::test] - /// This test will not work for as no endpoint supports the new `fuel-core` release yet - /// TODO: https://github.com/FuelLabs/fuels-rs/issues/978 - #[ignore] async fn connect_to_fuel_node() { // ANCHOR: connect_to_testnet use std::str::FromStr; @@ -15,7 +14,7 @@ mod tests { // Create a provider pointing to the testnet. // This example will not work as the testnet does not support the new version of fuel-core // yet - let provider = Provider::connect("beta-3.fuel.network").await.unwrap(); + let provider = Provider::connect("beta-4.fuel.network").await.unwrap(); // Setup a private key let secret = @@ -29,8 +28,13 @@ mod tests { dbg!(wallet.address().to_string()); // ANCHOR_END: connect_to_testnet + let provider = setup_test_provider(vec![], vec![], None, None).await; + let port = provider.url().split(':').last().unwrap(); + // ANCHOR: local_node_address - let _provider = Provider::connect("127.0.0.1:4000").await.unwrap(); + let _provider = Provider::connect(format!("127.0.0.1:{port}")) + .await + .unwrap(); // ANCHOR_END: local_node_address } @@ -59,7 +63,12 @@ mod tests { ); // ANCHOR_END: setup_single_asset - let (provider, _) = setup_test_provider(coins.clone(), vec![], None, None).await; + // ANCHOR: configure_retry + let retry_config = RetryConfig::new(3, Backoff::Fixed(Duration::from_secs(2)))?; + let provider = setup_test_provider(coins.clone(), vec![], None, None) + .await + .with_retry_config(retry_config); + // ANCHOR_END: configure_retry // ANCHOR_END: setup_test_blockchain // ANCHOR: get_coins diff --git a/examples/rust_bindings/Cargo.toml b/examples/rust_bindings/Cargo.toml index 132e942948..0d274ffa6c 100644 --- a/examples/rust_bindings/Cargo.toml +++ b/examples/rust_bindings/Cargo.toml @@ -15,3 +15,4 @@ fuels-macros = { workspace = true } proc-macro2 = { workspace = true } rand = { workspace = true } tokio = { workspace = true, features = ["full"] } +fuels-code-gen = { workspace = true } diff --git a/examples/rust_bindings/src/lib.rs b/examples/rust_bindings/src/lib.rs index fb258a9dfe..b7ab092f1d 100644 --- a/examples/rust_bindings/src/lib.rs +++ b/examples/rust_bindings/src/lib.rs @@ -1,9 +1,6 @@ #[cfg(test)] mod tests { - use fuels::{ - core::codec::try_from_bytes, - prelude::{AssetId, ContractId, Result}, - }; + use fuels::prelude::Result; #[tokio::test] #[allow(unused_variables)] @@ -75,21 +72,4 @@ mod tests { } Ok(()) } - - #[test] - fn manual_decode_of_native_types() -> Result<()> { - // ANCHOR: manual_decode_native - let contract_id_bytes = [0xFF; 32]; - let contract_id = ContractId::new(contract_id_bytes); - - let asset_id_bytes = [0xFF; 32]; - let asset_id = AssetId::new(asset_id_bytes); - - let bytes: Vec = [contract_id_bytes, asset_id_bytes].concat(); - let expected: (ContractId, AssetId) = try_from_bytes(&bytes)?; - - assert_eq!(expected, (contract_id, asset_id)); - // ANCHOR_END: manual_decode_native - Ok(()) - } } diff --git a/examples/rust_bindings/src/rust_bindings_formatted.rs b/examples/rust_bindings/src/rust_bindings_formatted.rs index 7320c52763..3c6b02f5ab 100644 --- a/examples/rust_bindings/src/rust_bindings_formatted.rs +++ b/examples/rust_bindings/src/rust_bindings_formatted.rs @@ -1,15 +1,34 @@ pub mod abigen_bindings { pub mod my_contract_mod { + use ::fuels::{ + accounts::{Account, ViewOnlyAccount}, + core::{ + codec, + traits::{Parameterize, Tokenizable}, + Configurables, + }, + programs::{ + contract::{self, ContractCallHandler}, + logs::{self, LogDecoder}, + }, + types::{bech32::Bech32ContractId, errors::Result, AssetId}, + }; + pub struct MyContract { contract_id: Bech32ContractId, account: T, log_decoder: LogDecoder, } impl MyContract { - pub fn new(contract_id: Bech32ContractId, account: T) -> Self { - let log_decoder = LogDecoder { - type_lookup: logs::log_type_lookup(&[], contract_id.clone().into()), - }; + pub fn new( + contract_id: impl ::core::convert::Into, + account: T, + ) -> Self { + let contract_id: Bech32ContractId = contract_id.into(); + let log_decoder = LogDecoder::new(logs::log_formatters_lookup( + vec![], + contract_id.clone().into(), + )); Self { contract_id, account, @@ -22,18 +41,18 @@ pub mod abigen_bindings { pub fn account(&self) -> T { self.account.clone() } - pub fn with_account(&self, mut account: U) -> Result> { - Ok(MyContract { + pub fn with_account(&self, account: U) -> Result> { + ::core::result::Result::Ok(MyContract { contract_id: self.contract_id.clone(), account, log_decoder: self.log_decoder.clone(), }) } - pub async fn get_balances(&self) -> Result> { + pub async fn get_balances(&self) -> Result<::std::collections::HashMap> { ViewOnlyAccount::try_provider(&self.account)? .get_contract_balances(&self.contract_id) .await - .map_err(Into::into) + .map_err(::std::convert::Into::into) } pub fn methods(&self) -> MyContractMethods { MyContractMethods { @@ -51,13 +70,10 @@ pub mod abigen_bindings { impl MyContractMethods { #[doc = "Calls the contract's `initialize_counter` function"] pub fn initialize_counter(&self, value: u64) -> ContractCallHandler { - Contract::method_hash( + contract::method_hash( self.contract_id.clone(), - self.account, - function_selector::resolve_fn_selector( - "initialize_counter", - &[::param_type()], - ), + self.account.clone(), + codec::resolve_fn_selector("initialize_counter", &[u64::param_type()]), &[Tokenizable::into_token(value)], self.log_decoder.clone(), false, @@ -66,21 +82,18 @@ pub mod abigen_bindings { } #[doc = "Calls the contract's `increment_counter` function"] pub fn increment_counter(&self, value: u64) -> ContractCallHandler { - Contract::method_hash( + contract::method_hash( self.contract_id.clone(), - self.account, - function_selector::resolve_fn_selector( - "increment_counter", - &[::param_type()], - ), - &[Tokenizable::into_token(value)], + self.account.clone(), + codec::resolve_fn_selector("increment_counter", &[u64::param_type()]), + &[value.into_token()], self.log_decoder.clone(), false, ) .expect("method not found (this should never happen)") } } - impl SettableContract for MyContract { + impl contract::SettableContract for MyContract { fn id(&self) -> Bech32ContractId { self.contract_id.clone() } @@ -90,11 +103,11 @@ pub mod abigen_bindings { } #[derive(Clone, Debug, Default)] pub struct MyContractConfigurables { - offsets_with_data: Vec<(u64, Vec)>, + offsets_with_data: ::std::vec::Vec<(u64, ::std::vec::Vec)>, } impl MyContractConfigurables { pub fn new() -> Self { - Default::default() + ::std::default::Default::default() } } impl From for Configurables { @@ -104,7 +117,7 @@ pub mod abigen_bindings { } } } - pub use abigen_bindings::my_contract_mod::MyContract; pub use abigen_bindings::my_contract_mod::MyContractConfigurables; pub use abigen_bindings::my_contract_mod::MyContractMethods; + diff --git a/examples/wallets/src/lib.rs b/examples/wallets/src/lib.rs index eb34427108..10808f78ac 100644 --- a/examples/wallets/src/lib.rs +++ b/examples/wallets/src/lib.rs @@ -8,7 +8,7 @@ mod tests { use fuels::prelude::*; // Use the test helper to setup a test provider. - let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await; + let provider = setup_test_provider(vec![], vec![], None, None).await; // Create the wallet. let _wallet = WalletUnlocked::new_random(Some(provider)); @@ -24,7 +24,7 @@ mod tests { use fuels::{accounts::fuel_crypto::SecretKey, prelude::*}; // Use the test helper to setup a test provider. - let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await; + let provider = setup_test_provider(vec![], vec![], None, None).await; // Setup the private key. let secret = SecretKey::from_str( @@ -46,7 +46,7 @@ mod tests { "oblige salon price punch saddle immune slogan rare snap desert retire surprise"; // Use the test helper to setup a test provider. - let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await; + let provider = setup_test_provider(vec![], vec![], None, None).await; // Create first account from mnemonic phrase. let _wallet = WalletUnlocked::new_from_mnemonic_phrase_with_path( @@ -74,7 +74,7 @@ mod tests { let mut rng = rand::thread_rng(); // Use the test helper to setup a test provider. - let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await; + let provider = setup_test_provider(vec![], vec![], None, None).await; let password = "my_master_password"; @@ -100,7 +100,7 @@ mod tests { "oblige salon price punch saddle immune slogan rare snap desert retire surprise"; // Use the test helper to setup a test provider. - let (provider, _address) = setup_test_provider(vec![], vec![], None, None).await; + let provider = setup_test_provider(vec![], vec![], None, None).await; // Create first account from mnemonic phrase. let wallet = WalletUnlocked::new_from_mnemonic_phrase(phrase, Some(provider))?; @@ -252,7 +252,7 @@ mod tests { amount_per_coin, ); // ANCHOR_END: multiple_assets_coins - let (provider, _socket_addr) = setup_test_provider(coins.clone(), vec![], None, None).await; + let provider = setup_test_provider(coins.clone(), vec![], None, None).await; wallet.set_provider(provider); // ANCHOR_END: multiple_assets_wallet Ok(()) @@ -293,7 +293,7 @@ mod tests { let assets = vec![asset_base, asset_1, asset_2]; let coins = setup_custom_assets_coins(wallet.address(), &assets); - let (provider, _socket_addr) = setup_test_provider(coins, vec![], None, None).await; + let provider = setup_test_provider(coins, vec![], None, None).await; wallet.set_provider(provider); // ANCHOR_END: custom_assets_wallet // ANCHOR: custom_assets_wallet_short diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 323f99ab35..f4603e084a 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -11,7 +11,6 @@ description = "Fuel Rust SDK accounts." [dependencies] async-trait = { workspace = true, default-features = false } -bytes = { workspace = true, features = ["serde"] } chrono = { workspace = true } elliptic-curve = { workspace = true, default-features = false } eth-keystore = { workspace = true } @@ -23,10 +22,7 @@ fuel-types = { workspace = true, features = ["random"] } fuel-vm = { workspace = true } fuels-core = { workspace = true } hex = { workspace = true, default-features = false, features = ["std"] } -itertools = { workspace = true } rand = { workspace = true, default-features = false } -serde = { workspace = true, default-features = true, features = ["derive"] } -sha2 = { workspace = true, default-features = false } tai64 = { workspace = true, features = ["serde"] } thiserror = { workspace = true, default-features = false } tokio = { workspace = true, features = ["full"] } diff --git a/packages/fuels-accounts/src/accounts_utils.rs b/packages/fuels-accounts/src/accounts_utils.rs index 3ae833285d..fb39660e81 100644 --- a/packages/fuels-accounts/src/accounts_utils.rs +++ b/packages/fuels-accounts/src/accounts_utils.rs @@ -2,7 +2,12 @@ use fuel_tx::{ConsensusParameters, Output, Receipt}; use fuel_types::MessageId; use fuels_core::{ constants::BASE_ASSET_ID, - types::{bech32::Bech32Address, input::Input, transaction_builders::TransactionBuilder}, + types::{ + bech32::Bech32Address, + errors::{error, Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + }, }; pub fn extract_message_id(receipts: &[Receipt]) -> Option { @@ -13,10 +18,10 @@ pub fn calculate_base_amount_with_fee( tb: &impl TransactionBuilder, consensus_params: &ConsensusParameters, previous_base_amount: u64, -) -> u64 { +) -> Result { let transaction_fee = tb - .fee_checked_from_tx(consensus_params) - .expect("Error calculating TransactionFee"); + .fee_checked_from_tx(consensus_params)? + .ok_or(error!(InvalidData, "Error calculating TransactionFee"))?; let mut new_base_amount = transaction_fee.max_fee() + previous_base_amount; @@ -31,7 +36,8 @@ pub fn calculate_base_amount_with_fee( if !is_consuming_utxos && new_base_amount == 0 { new_base_amount = MIN_AMOUNT; } - new_base_amount + + Ok(new_base_amount) } // Replace the current base asset inputs of a tx builder with the provided ones. diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index c070d50bde..dc8986ac09 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -259,15 +259,18 @@ pub trait Account: ViewOnlyAccount { tx_parameters: TxParameters, ) -> Result<(TxId, Vec)> { let provider = self.try_provider()?; + let network_info = provider.network_info().await?; let inputs = self.get_asset_inputs_for_amount(asset_id, amount).await?; //let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount); - let consensus_parameters = provider.consensus_parameters(); - - let tx_builder = ScriptTransactionBuilder::prepare_transfer(inputs, vec![], tx_parameters) - .with_consensus_parameters(consensus_parameters); + let tx_builder = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + tx_parameters, + network_info, + ); let mut outputs = self.get_asset_outputs_for_amount(to, asset_id, amount); let expected_change_output = @@ -284,16 +287,13 @@ pub trait Account: ViewOnlyAccount { let tx = self .add_fee_resources(tx_builder, previous_base_amount) .await?; - - let chain_id = consensus_parameters.chain_id.into(); - dbg!(&tx.id(chain_id)); - dbg!(&tx.inputs()); - dbg!(&tx.outputs()); - - let tx_id = provider.send_transaction(tx.clone()).await?; + let tx_id = provider.send_transaction_and_await_commit(tx).await?; self.cache(&tx, chain_id); - //let receipts = provider.get_receipts(&tx_id).await? - let receipts = vec![]; + + let receipts = provider + .tx_status(&tx_id) + .await? + .take_receipts_checked(None)?; Ok((tx_id, receipts)) } @@ -315,6 +315,7 @@ pub trait Account: ViewOnlyAccount { tx_parameters: TxParameters, ) -> std::result::Result<(String, Vec), Error> { let provider = self.try_provider()?; + let network_info = provider.network_info().await?; let zeroes = Bytes32::zeroed(); let plain_contract_id: ContractId = to.into(); @@ -335,8 +336,6 @@ pub trait Account: ViewOnlyAccount { ]; // Build transaction and sign it - let params = provider.consensus_parameters(); - let tb = ScriptTransactionBuilder::prepare_contract_transfer( plain_contract_id, balance, @@ -344,8 +343,8 @@ pub trait Account: ViewOnlyAccount { inputs, outputs, tx_parameters, - ) - .with_consensus_parameters(params); + network_info, + ); // if we are not transferring the base asset, previous base amount is 0 let base_amount = if asset_id == AssetId::default() { @@ -356,9 +355,13 @@ pub trait Account: ViewOnlyAccount { let tx = self.add_fee_resources(tb, base_amount).await?; - let tx_id = provider.send_transaction(tx.clone()).await?; + let tx_id = provider.send_transaction_and_await_commit(tx).await?; self.cache(&tx, params.chain_id); - let receipts = provider.get_receipts(&tx_id).await?; + + let receipts = provider + .tx_status(&tx_id) + .await? + .take_receipts_checked(None)?; Ok((tx_id.to_string(), receipts)) } @@ -373,6 +376,7 @@ pub trait Account: ViewOnlyAccount { tx_parameters: TxParameters, ) -> std::result::Result<(TxId, MessageId, Vec), Error> { let provider = self.try_provider()?; + let network_info = provider.network_info().await?; let inputs = self .get_asset_inputs_for_amount(BASE_ASSET_ID, amount) @@ -383,11 +387,16 @@ pub trait Account: ViewOnlyAccount { amount, inputs, tx_parameters, + network_info, ); let tx = self.add_fee_resources(tb, amount).await?; - let tx_id = provider.send_transaction(tx).await?; - let receipts = provider.get_receipts(&tx_id).await?; + let tx_id = provider.send_transaction_and_await_commit(tx).await?; + + let receipts = provider + .tx_status(&tx_id) + .await? + .take_receipts_checked(None)?; let message_id = extract_message_id(&receipts) .expect("MessageId could not be retrieved from tx receipts."); @@ -402,7 +411,7 @@ mod tests { use fuel_crypto::{Message, SecretKey}; use fuel_tx::{Address, Output}; - use fuels_core::types::transaction::Transaction; + use fuels_core::types::{transaction::Transaction, transaction_builders::NetworkInfo}; use rand::{rngs::StdRng, RngCore, SeedableRng}; use super::*; @@ -451,6 +460,12 @@ mod tests { )?; let wallet = WalletUnlocked::new_from_private_key(secret, None); + let network_info = NetworkInfo { + consensus_parameters: Default::default(), + max_gas_per_tx: 0, + min_gas_price: 0, + gas_costs: Default::default(), + }; // Set up a transaction let mut tb = { let input_coin = Input::ResourceSigned { @@ -473,6 +488,7 @@ mod tests { vec![input_coin], vec![output_coin], Default::default(), + network_info, ) }; @@ -492,14 +508,14 @@ mod tests { assert_eq!(signature, tx_signature); // Check if the signature is what we expect it to be - assert_eq!(signature, Signature::from_str("df91e8ae723165f9a28b70910e3da41300da413607065618522f3084c9f051114acb1b51a836bd63c3d84a1ac904bf37b82ef03973c19026b266d04872f170a6")?); + assert_eq!(signature, Signature::from_str("51198e39c541cd3197785fd8add8cdbec3dc5aba7f8fbb23eb09455dd1003a8b78d94f247df8e1577805ea7eebd6d58336393942fd98484609e9e7d6d7a55f28")?); // Recover the address that signed the transaction let recovered_address = signature.recover(&message)?; assert_eq!(wallet.address().hash(), recovered_address.hash()); - // Verify the signature + // Verify signature signature.verify(&recovered_address, &message)?; // ANCHOR_END: sign_tx diff --git a/packages/fuels-accounts/src/predicate.rs b/packages/fuels-accounts/src/predicate.rs index 88fa96b54b..0389b9763b 100644 --- a/packages/fuels-accounts/src/predicate.rs +++ b/packages/fuels-accounts/src/predicate.rs @@ -166,10 +166,8 @@ impl Account for Predicate { previous_base_amount: u64, ) -> Result { let consensus_parameters = self.try_provider()?.consensus_parameters(); - tb = tb.with_consensus_parameters(consensus_parameters); - let new_base_amount = - calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount); + calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount)?; let new_base_inputs = self .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 19cd9ef638..438dd4395b 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, fmt::Debug, io}; +use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr}; + +mod retry_util; +mod retryable_client; use chrono::{DateTime, Utc}; #[cfg(feature = "fuel-core-lib")] @@ -6,13 +9,12 @@ use fuel_core::service::{Config, FuelService}; use fuel_core_client::client::{ pagination::{PageDirection, PaginatedResult, PaginationRequest}, types::{balance::Balance, contract::ContractBalance, TransactionStatus}, - FuelClient, }; use fuel_tx::{AssetId, ConsensusParameters, Receipt, ScriptExecutionResult, TxId, UtxoId}; use fuel_types::{Address, Bytes32, ChainId, MessageId, Nonce}; use fuel_vm::state::ProgramState; use fuels_core::{ - constants::{BASE_ASSET_ID, DEFAULT_GAS_ESTIMATION_TOLERANCE, MAX_GAS_PER_TX}, + constants::{BASE_ASSET_ID, DEFAULT_GAS_ESTIMATION_TOLERANCE}, types::{ bech32::{Bech32Address, Bech32ContractId}, block::Block, @@ -24,12 +26,17 @@ use fuels_core::{ message_proof::MessageProof, node_info::NodeInfo, transaction::Transaction, + transaction_builders::NetworkInfo, transaction_response::TransactionResponse, + tx_status::TxStatus, }, }; +pub use retry_util::{Backoff, RetryConfig}; use tai64::Tai64; use thiserror::Error; +use crate::provider::retryable_client::RetryableClient; + type ProviderResult = std::result::Result; #[derive(Debug)] @@ -116,8 +123,10 @@ impl Default for ResourceFilter { #[derive(Debug, Error)] pub enum ProviderError { // Every IO error in the context of Provider comes from the gql client - #[error(transparent)] + #[error("Client request error: {0}")] ClientRequestError(#[from] io::Error), + #[error("Receipts have not yet been propagated. Retry the request later.")] + ReceiptsNotPropagatedYet, } impl From for Error { @@ -131,19 +140,52 @@ impl From for Error { /// of `FuelClient`, directly, which provides a broader API. #[derive(Debug, Clone)] pub struct Provider { - pub client: FuelClient, - pub consensus_parameters: ConsensusParameters, + client: RetryableClient, + consensus_parameters: ConsensusParameters, } impl Provider { - pub fn new(client: FuelClient, consensus_parameters: ConsensusParameters) -> Self { - Self { + pub fn new(url: impl AsRef, consensus_parameters: ConsensusParameters) -> Result { + let client = RetryableClient::new(&url, Default::default())?; + + Ok(Self { client, consensus_parameters, - } + }) + } + + pub async fn from(addr: impl Into) -> Result { + let addr = addr.into(); + Self::connect(format!("http://{addr}")).await + } + + pub async fn healthy(&self) -> Result { + Ok(self.client.health().await?) + } + + /// Connects to an existing node at the given address. + pub async fn connect(url: impl AsRef) -> Result { + let client = RetryableClient::new(&url, Default::default())?; + let consensus_parameters = client.chain_info().await?.consensus_parameters.into(); + + Ok(Self { + client, + consensus_parameters, + }) + } + + pub fn url(&self) -> &str { + self.client.url() } /// Sends a transaction to the underlying Provider's client. + pub async fn send_transaction_and_await_commit(&self, tx: T) -> Result { + let tx_id = self.send_transaction(tx.clone()).await?; + self.client.await_transaction_commit(&tx_id).await?; + + Ok(tx_id) + } + pub async fn send_transaction(&self, tx: T) -> Result { let tolerance = 0.0; let TransactionCost { @@ -176,46 +218,46 @@ impl Provider { &self.consensus_parameters(), )?; - let tx_id = self.submit_tx(tx.clone()).await?; - + let tx_id = self.client.submit(&tx.into()).await?; Ok(tx_id) } - pub async fn get_receipts(&self, tx_id: &TxId) -> Result> { - let tx_status = self.client.transaction_status(tx_id).await?; - let receipts = self.client.receipts(tx_id).await?.map_or(vec![], |v| v); - Self::if_failure_generate_error(&tx_status, &receipts)?; - Ok(receipts) - } - - fn if_failure_generate_error(status: &TransactionStatus, receipts: &[Receipt]) -> Result<()> { - if let TransactionStatus::Failure { - reason, - program_state, - .. - } = status - { - let revert_id = program_state - .and_then(|state| match state { - ProgramState::Revert(revert_id) => Some(revert_id), - _ => None, - }) - .expect("Transaction failed without a `revert_id`"); + pub async fn tx_status(&self, tx_id: &TxId) -> ProviderResult { + let fetch_receipts = || async { + let receipts = self.client.receipts(tx_id).await?; + receipts.ok_or_else(|| ProviderError::ReceiptsNotPropagatedYet) + }; - return Err(Error::RevertTransactionError { - reason: reason.to_string(), - revert_id, - receipts: receipts.to_owned(), - }); - } + let tx_status = self.client.transaction_status(tx_id).await?; + let status = match tx_status { + TransactionStatus::Success { .. } => { + let receipts = fetch_receipts().await?; + TxStatus::Success { receipts } + } + TransactionStatus::Failure { + reason, + program_state, + .. + } => { + let receipts = fetch_receipts().await?; + let revert_id = program_state + .and_then(|state| match state { + ProgramState::Revert(revert_id) => Some(revert_id), + _ => None, + }) + .expect("Transaction failed without a `revert_id`"); + + TxStatus::Revert { + receipts, + reason, + id: revert_id, + } + } + TransactionStatus::Submitted { .. } => TxStatus::Submitted, + TransactionStatus::SqueezedOut { .. } => TxStatus::SqueezedOut, + }; - Ok(()) - } - - async fn submit_tx(&self, tx: impl Transaction) -> ProviderResult { - let tx_id = self.client.submit(&tx.into()).await?; - //self.client.await_transaction_commit(&tx_id).await?; - Ok(tx_id) + Ok(status) } #[cfg(feature = "fuel-core-lib")] @@ -225,13 +267,6 @@ impl Provider { Ok(FuelClient::from(srv.bound_address)) } - /// Connects to an existing node at the given address. - pub async fn connect(url: impl AsRef) -> Result { - let client = FuelClient::new(url).map_err(|err| error!(InfrastructureError, "{err}"))?; - let consensus_parameters = client.chain_info().await?.consensus_parameters.into(); - Ok(Provider::new(client, consensus_parameters)) - } - pub async fn chain_info(&self) -> ProviderResult { Ok(self.client.chain_info().await?.into()) } @@ -240,6 +275,13 @@ impl Provider { self.consensus_parameters } + pub async fn network_info(&self) -> ProviderResult { + let node_info = self.node_info().await?; + let chain_info = self.chain_info().await?; + + Ok(NetworkInfo::new(node_info, chain_info)) + } + pub fn chain_id(&self) -> ChainId { self.consensus_parameters.chain_id } @@ -526,7 +568,7 @@ impl Provider { let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE); // Remove limits from an existing Transaction for accurate gas estimation - let dry_run_tx = Self::generate_dry_run_tx(tx.clone()); + let dry_run_tx = self.generate_dry_run_tx(tx.clone()); let gas_used = self .get_gas_used_with_tolerance(dry_run_tx.clone(), tolerance) .await?; @@ -550,9 +592,10 @@ impl Provider { } // Remove limits from an existing Transaction to get an accurate gas estimation - fn generate_dry_run_tx(tx: T) -> T { - // Simulate the contract call with MAX_GAS_PER_TX to get the complete gas_used - tx.clone().with_gas_limit(MAX_GAS_PER_TX).with_gas_price(0) + fn generate_dry_run_tx(&self, tx: T) -> T { + // Simulate the contract call with max gas to get the complete gas_used + let max_gas_per_tx = self.consensus_parameters.max_gas_per_tx; + tx.clone().with_gas_limit(max_gas_per_tx).with_gas_price(0) } // Increase estimated gas by the provided tolerance @@ -612,4 +655,9 @@ impl Provider { .map(Into::into); Ok(proof) } + + pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self { + self.client.set_retry_config(retry_config); + self + } } diff --git a/packages/fuels-accounts/src/provider/retry_util.rs b/packages/fuels-accounts/src/provider/retry_util.rs new file mode 100644 index 0000000000..3847fecfcf --- /dev/null +++ b/packages/fuels-accounts/src/provider/retry_util.rs @@ -0,0 +1,338 @@ +use std::{fmt::Debug, future::Future, num::NonZeroU32, time::Duration}; + +use fuels_core::types::errors::{error, Error, Result as SdkResult}; + +/// A set of strategies to control retry intervals between attempts. +/// +/// The `Backoff` enum defines different strategies for managing intervals between retry attempts. +/// Each strategy allows you to customize the waiting time before a new attempt based on the +/// number of attempts made. +/// +/// # Variants +/// +/// - `Linear(Duration)`: Increases the waiting time linearly with each attempt. +/// - `Exponential(Duration)`: Doubles the waiting time with each attempt. +/// - `Fixed(Duration)`: Uses a constant waiting time between attempts. +/// +/// # Examples +/// +/// ```rust +/// use std::time::Duration; +/// use fuels_accounts::provider::Backoff; +/// +/// let linear_backoff = Backoff::Linear(Duration::from_secs(2)); +/// let exponential_backoff = Backoff::Exponential(Duration::from_secs(1)); +/// let fixed_backoff = Backoff::Fixed(Duration::from_secs(5)); +/// ``` +//ANCHOR: backoff +#[derive(Debug, Clone)] +pub enum Backoff { + Linear(Duration), + Exponential(Duration), + Fixed(Duration), +} +//ANCHOR_END: backoff + +impl Default for Backoff { + fn default() -> Self { + Backoff::Linear(Duration::from_millis(10)) + } +} + +impl Backoff { + pub fn wait_duration(&self, attempt: u32) -> Duration { + match self { + Backoff::Linear(base_duration) => *base_duration * (attempt + 1), + Backoff::Exponential(base_duration) => *base_duration * 2u32.pow(attempt), + Backoff::Fixed(interval) => *interval, + } + } +} + +/// Configuration for controlling retry behavior. +/// +/// The `RetryConfig` struct encapsulates the configuration parameters for controlling the retry behavior +/// of asynchronous actions. It includes the maximum number of attempts and the interval strategy from +/// the `Backoff` enum that determines how much time to wait between retry attempts. +/// +/// # Fields +/// +/// - `max_attempts`: The maximum number of attempts before giving up. +/// - `interval`: The chosen interval strategy from the `Backoff` enum. +/// +/// # Examples +/// +/// ```rust +/// use std::num::NonZeroUsize; +/// use std::time::Duration; +/// use fuels_accounts::provider::{Backoff, RetryConfig}; +/// +/// let max_attempts = 5; +/// let interval_strategy = Backoff::Exponential(Duration::from_secs(1)); +/// +/// let retry_config = RetryConfig::new(max_attempts, interval_strategy).unwrap(); +/// ``` +// ANCHOR: retry_config +#[derive(Clone, Debug)] +pub struct RetryConfig { + max_attempts: NonZeroU32, + interval: Backoff, +} +// ANCHOR_END: retry_config + +impl RetryConfig { + pub fn new(max_attempts: u32, interval: Backoff) -> SdkResult { + let max_attempts = NonZeroU32::new(max_attempts) + .ok_or_else(|| error!(InvalidData, "`max_attempts` must be greater than 0."))?; + + Ok(RetryConfig { + max_attempts, + interval, + }) + } +} + +impl Default for RetryConfig { + fn default() -> Self { + Self { + max_attempts: NonZeroU32::new(1).expect("Should not fail!"), + interval: Default::default(), + } + } +} + +/// Retries an asynchronous action with customizable retry behavior. +/// +/// This function takes an asynchronous action represented by a closure `action`. +/// The action is executed repeatedly with backoff and retry logic based on the +/// provided `retry_config` and the `should_retry` condition. +/// +/// The `action` closure should return a `Future` that resolves to a `Result`, +/// where `T` represents the success type and `K` represents the error type. +/// +/// # Parameters +/// +/// - `action`: The asynchronous action to be retried. +/// - `retry_config`: A reference to the retry configuration. +/// - `should_retry`: A closure that determines whether to retry based on the result. +/// +/// # Return +/// +/// Returns `Ok(T)` if the action succeeds without requiring further retries. +/// Returns `Err(Error)` if the maximum number of attempts is reached and the action +/// still fails. If a retryable error occurs during the attempts, the error will +/// be returned if the `should_retry` condition allows further retries. +pub(crate) async fn retry( + mut action: impl FnMut() -> Fut, + retry_config: &RetryConfig, + should_retry: ShouldRetry, +) -> T +where + Fut: Future, + ShouldRetry: Fn(&T) -> bool, +{ + let mut last_result = None; + + for attempt in 0..retry_config.max_attempts.into() { + let result = action().await; + + if should_retry(&result) { + last_result = Some(result) + } else { + return result; + } + + tokio::time::sleep(retry_config.interval.wait_duration(attempt)).await; + } + + last_result.expect("Should not happen") +} + +#[cfg(test)] +mod tests { + mod retry_until { + use std::time::{Duration, Instant}; + + use fuels_core::{ + error, + types::errors::{Error, Result}, + }; + use tokio::sync::Mutex; + + use crate::provider::{retry_util, Backoff, RetryConfig}; + + #[tokio::test] + async fn returns_last_received_response() -> Result<()> { + // given + let err_msgs = ["Err1", "Err2", "Err3"]; + let number_of_attempts = Mutex::new(0usize); + + let will_always_fail = || async { + let msg = err_msgs[*number_of_attempts.lock().await]; + *number_of_attempts.lock().await += 1; + + msg + }; + + let should_retry_fn = |_res: &_| -> bool { true }; + + let retry_options = RetryConfig::new(3, Backoff::Linear(Duration::from_millis(10)))?; + + // when + let response = + retry_util::retry(will_always_fail, &retry_options, should_retry_fn).await; + + // then + assert_eq!(response, "Err3"); + + Ok(()) + } + + #[tokio::test] + async fn stops_retrying_when_predicate_is_satistfied() -> Result<()> { + // given + let values = Mutex::new(vec![1, 2, 3]); + + let will_always_fail = || async { values.lock().await.pop().unwrap() }; + + let should_retry_fn = |res: &i32| *res != 2; + + let retry_options = RetryConfig::new(3, Backoff::Linear(Duration::from_millis(10)))?; + + // when + let response = + retry_util::retry(will_always_fail, &retry_options, should_retry_fn).await; + + // then + assert_eq!(response, 2); + + Ok(()) + } + + #[tokio::test] + async fn retry_respects_delay_between_attempts_fixed() -> Result<()> { + // given + let timestamps: Mutex> = Mutex::new(vec![]); + + let will_fail_and_record_timestamp = || async { + timestamps.lock().await.push(Instant::now()); + Result::<()>::Err(Error::InvalidData("Error".to_string())) + }; + + let should_retry_fn = |_res: &_| -> bool { true }; + + let retry_options = RetryConfig::new(3, Backoff::Fixed(Duration::from_millis(100)))?; + + // when + let _ = retry_util::retry( + will_fail_and_record_timestamp, + &retry_options, + should_retry_fn, + ) + .await; + + // then + let timestamps_vec = timestamps.lock().await.clone(); + + let timestamps_spaced_out_at_least_100_mills = timestamps_vec + .iter() + .zip(timestamps_vec.iter().skip(1)) + .all(|(current_timestamp, the_next_timestamp)| { + the_next_timestamp.duration_since(*current_timestamp) + >= Duration::from_millis(100) + }); + + assert!( + timestamps_spaced_out_at_least_100_mills, + "Retry did not wait for the specified time between attempts." + ); + + Ok(()) + } + + #[tokio::test] + async fn retry_respects_delay_between_attempts_linear() -> Result<()> { + // given + let timestamps: Mutex> = Mutex::new(vec![]); + + let will_fail_and_record_timestamp = || async { + timestamps.lock().await.push(Instant::now()); + Result::<()>::Err(Error::InvalidData("Error".to_string())) + }; + + let should_retry_fn = |_res: &_| -> bool { true }; + + let retry_options = RetryConfig::new(3, Backoff::Linear(Duration::from_millis(100)))?; + + // when + let _ = retry_util::retry( + will_fail_and_record_timestamp, + &retry_options, + should_retry_fn, + ) + .await; + + // then + let timestamps_vec = timestamps.lock().await.clone(); + + let timestamps_spaced_out_at_least_100_mills = timestamps_vec + .iter() + .zip(timestamps_vec.iter().skip(1)) + .enumerate() + .all(|(attempt, (current_timestamp, the_next_timestamp))| { + the_next_timestamp.duration_since(*current_timestamp) + >= (Duration::from_millis(100) * (attempt + 1) as u32) + }); + + assert!( + timestamps_spaced_out_at_least_100_mills, + "Retry did not wait for the specified time between attempts." + ); + + Ok(()) + } + + #[tokio::test] + async fn retry_respects_delay_between_attempts_exponential() -> Result<()> { + // given + let timestamps: Mutex> = Mutex::new(vec![]); + + let will_fail_and_record_timestamp = || async { + timestamps.lock().await.push(Instant::now()); + Result::<()>::Err(error!(InvalidData, "Error")) + }; + + let should_retry_fn = |_res: &_| -> bool { true }; + + let retry_options = + RetryConfig::new(3, Backoff::Exponential(Duration::from_millis(100)))?; + + // when + let _ = retry_util::retry( + will_fail_and_record_timestamp, + &retry_options, + should_retry_fn, + ) + .await; + + // then + let timestamps_vec = timestamps.lock().await.clone(); + + let timestamps_spaced_out_at_least_100_mills = timestamps_vec + .iter() + .zip(timestamps_vec.iter().skip(1)) + .enumerate() + .all(|(attempt, (current_timestamp, the_next_timestamp))| { + the_next_timestamp.duration_since(*current_timestamp) + >= (Duration::from_millis(100) * (2_usize.pow((attempt) as u32)) as u32) + }); + + assert!( + timestamps_spaced_out_at_least_100_mills, + "Retry did not wait for the specified time between attempts." + ); + + Ok(()) + } + } +} diff --git a/packages/fuels-accounts/src/provider/retryable_client.rs b/packages/fuels-accounts/src/provider/retryable_client.rs new file mode 100644 index 0000000000..89db3c1e30 --- /dev/null +++ b/packages/fuels-accounts/src/provider/retryable_client.rs @@ -0,0 +1,223 @@ +use std::{future::Future, io}; + +use fuel_core_client::client::{ + pagination::{PaginatedResult, PaginationRequest}, + types, + types::{primitives::BlockId, TransactionResponse, TransactionStatus}, + FuelClient, +}; +use fuel_tx::{Receipt, Transaction, TxId, UtxoId}; +use fuel_types::{Address, AssetId, BlockHeight, ContractId, MessageId, Nonce}; +use fuels_core::{ + error, + types::errors::{Error, Result}, +}; + +use crate::provider::{retry_util, RetryConfig}; + +#[derive(Debug, Clone)] +pub(crate) struct RetryableClient { + client: FuelClient, + url: String, + retry_config: RetryConfig, +} + +impl RetryableClient { + pub(crate) fn new(url: impl AsRef, retry_config: RetryConfig) -> Result { + let url = url.as_ref().to_string(); + let client = FuelClient::new(&url).map_err(|err| error!(InfrastructureError, "{err}"))?; + Ok(Self { + client, + retry_config, + url, + }) + } + + pub(crate) fn url(&self) -> &str { + &self.url + } + + pub(crate) fn set_retry_config(&mut self, retry_config: RetryConfig) { + self.retry_config = retry_config; + } + + async fn our_retry(&self, action: impl Fn() -> Fut) -> io::Result + where + Fut: Future>, + { + retry_util::retry(action, &self.retry_config, |result| result.is_err()).await + } + + // DELEGATION START + pub async fn health(&self) -> io::Result { + self.our_retry(|| self.client.health()).await + } + + pub async fn transaction(&self, id: &TxId) -> io::Result> { + self.our_retry(|| self.client.transaction(id)).await + } + + pub(crate) async fn chain_info(&self) -> io::Result { + self.our_retry(|| self.client.chain_info()).await + } + + pub async fn await_transaction_commit(&self, id: &TxId) -> io::Result { + self.our_retry(|| self.client.await_transaction_commit(id)) + .await + } + + pub async fn submit(&self, tx: &Transaction) -> io::Result { + self.our_retry(|| self.client.submit(tx)).await + } + + pub async fn receipts(&self, id: &TxId) -> io::Result>> { + retry_util::retry( + || self.client.receipts(id), + &self.retry_config, + |result| !matches!(result, Ok(Some(_))), + ) + .await + } + + pub async fn transaction_status(&self, id: &TxId) -> io::Result { + self.our_retry(|| self.client.transaction_status(id)).await + } + + pub async fn node_info(&self) -> io::Result { + self.our_retry(|| self.client.node_info()).await + } + pub async fn dry_run(&self, tx: &Transaction) -> io::Result> { + self.our_retry(|| self.client.dry_run(tx)).await + } + + pub async fn dry_run_opt( + &self, + tx: &Transaction, + utxo_validation: Option, + ) -> io::Result> { + self.our_retry(|| self.client.dry_run_opt(tx, utxo_validation)) + .await + } + + pub async fn coins( + &self, + owner: &Address, + asset_id: Option<&AssetId>, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(move || self.client.coins(owner, asset_id, request.clone())) + .await + } + + pub async fn coins_to_spend( + &self, + owner: &Address, + spend_query: Vec<(AssetId, u64, Option)>, + excluded_ids: Option<(Vec, Vec)>, + ) -> io::Result>> { + self.client + .coins_to_spend(owner, spend_query, excluded_ids) + .await + } + + pub async fn balance(&self, owner: &Address, asset_id: Option<&AssetId>) -> io::Result { + self.our_retry(|| self.client.balance(owner, asset_id)) + .await + } + + pub async fn contract_balance( + &self, + id: &ContractId, + asset: Option<&AssetId>, + ) -> io::Result { + self.our_retry(|| self.client.contract_balance(id, asset)) + .await + } + + pub async fn contract_balances( + &self, + contract: &ContractId, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.contract_balances(contract, request.clone())) + .await + } + + pub async fn balances( + &self, + owner: &Address, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.balances(owner, request.clone())) + .await + } + + pub async fn transactions( + &self, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.transactions(request.clone())) + .await + } + + pub async fn transactions_by_owner( + &self, + owner: &Address, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.transactions_by_owner(owner, request.clone())) + .await + } + + pub async fn produce_blocks( + &self, + blocks_to_produce: u64, + start_timestamp: Option, + ) -> io::Result { + self.our_retry(|| { + self.client + .produce_blocks(blocks_to_produce, start_timestamp) + }) + .await + } + + pub async fn block(&self, id: &BlockId) -> io::Result> { + self.our_retry(|| self.client.block(id)).await + } + + pub async fn blocks( + &self, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.blocks(request.clone())).await + } + + pub async fn messages( + &self, + owner: Option<&Address>, + request: PaginationRequest, + ) -> io::Result> { + self.our_retry(|| self.client.messages(owner, request.clone())) + .await + } + + /// Request a merkle proof of an output message. + pub async fn message_proof( + &self, + transaction_id: &TxId, + message_id: &MessageId, + commit_block_id: Option<&BlockId>, + commit_block_height: Option, + ) -> io::Result> { + self.our_retry(|| { + self.client.message_proof( + transaction_id, + message_id, + commit_block_id, + commit_block_height, + ) + }) + .await + } + // DELEGATION END +} diff --git a/packages/fuels-accounts/src/wallet.rs b/packages/fuels-accounts/src/wallet.rs index dd49a78f23..ace10de21b 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -274,12 +274,10 @@ impl Account for WalletUnlocked { previous_base_amount: u64, ) -> Result { let consensus_parameters = self.try_provider()?.consensus_parameters(); - tb = tb.with_consensus_parameters(consensus_parameters); - self.sign_transaction(&mut tb); let new_base_amount = - calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount); + calculate_base_amount_with_fee(&tb, &consensus_parameters, previous_base_amount)?; let new_base_inputs = self .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) diff --git a/packages/fuels-code-gen/Cargo.toml b/packages/fuels-code-gen/Cargo.toml index 9c7c6fbacd..b6c1e609dd 100644 --- a/packages/fuels-code-gen/Cargo.toml +++ b/packages/fuels-code-gen/Cargo.toml @@ -18,3 +18,6 @@ quote = { workspace = true } regex = { workspace = true } serde_json = { workspace = true } syn = { workspace = true } + +[package.metadata.cargo-machete] +ignored = ["Inflector"] diff --git a/packages/fuels-code-gen/src/program_bindings/abigen.rs b/packages/fuels-code-gen/src/program_bindings/abigen.rs index 4870fd3e5e..16fe429416 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen.rs @@ -54,10 +54,10 @@ impl Abigen { } fn wasm_paths_hotfix(code: &TokenStream) -> TokenStream { [ - (r#"::\s*std\s*::\s*string"#, "::alloc::string"), - (r#"::\s*std\s*::\s*format"#, "::alloc::format"), - (r#"::\s*std\s*::\s*vec"#, "::alloc::vec"), - (r#"::\s*std\s*::\s*boxed"#, "::alloc::boxed"), + (r"::\s*std\s*::\s*string", "::alloc::string"), + (r"::\s*std\s*::\s*format", "::alloc::format"), + (r"::\s*std\s*::\s*vec", "::alloc::vec"), + (r"::\s*std\s*::\s*boxed", "::alloc::boxed"), ] .map(|(reg_expr_str, substitute)| (Regex::new(reg_expr_str).unwrap(), substitute)) .into_iter() diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs index e7ca18dae2..91ab078d92 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs @@ -40,7 +40,7 @@ pub(crate) fn contract_bindings( pub struct #name { contract_id: ::fuels::types::bech32::Bech32ContractId, account: T, - log_decoder: ::fuels::programs::logs::LogDecoder + log_decoder: ::fuels::core::codec::LogDecoder } impl #name @@ -50,7 +50,7 @@ pub(crate) fn contract_bindings( account: T, ) -> Self { let contract_id: ::fuels::types::bech32::Bech32ContractId = contract_id.into(); - let log_decoder = ::fuels::programs::logs::LogDecoder { log_formatters: #log_formatters }; + let log_decoder = ::fuels::core::codec::LogDecoder::new(#log_formatters); Self { contract_id, account, log_decoder } } @@ -62,7 +62,7 @@ pub(crate) fn contract_bindings( self.account.clone() } - pub fn with_account(&self, mut account: U) -> ::fuels::types::errors::Result<#name> { + pub fn with_account(&self, account: U) -> ::fuels::types::errors::Result<#name> { ::core::result::Result::Ok(#name { contract_id: self.contract_id.clone(), account, log_decoder: self.log_decoder.clone()}) } @@ -86,7 +86,7 @@ pub(crate) fn contract_bindings( pub struct #methods_name { contract_id: ::fuels::types::bech32::Bech32ContractId, account: T, - log_decoder: ::fuels::programs::logs::LogDecoder + log_decoder: ::fuels::core::codec::LogDecoder } impl #methods_name { @@ -100,7 +100,7 @@ pub(crate) fn contract_bindings( self.contract_id.clone() } - fn log_decoder(&self) -> ::fuels::programs::logs::LogDecoder { + fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder { self.log_decoder.clone() } } diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs index 47d7e9cd57..c0739d2ca8 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs @@ -40,7 +40,7 @@ pub(crate) fn script_bindings( pub struct #name{ account: T, binary: ::std::vec::Vec, - log_decoder: ::fuels::programs::logs::LogDecoder + log_decoder: ::fuels::core::codec::LogDecoder } impl #name @@ -51,11 +51,11 @@ pub(crate) fn script_bindings( Self { account, binary, - log_decoder: ::fuels::programs::logs::LogDecoder {log_formatters: #log_formatters_lookup} + log_decoder: ::fuels::core::codec::LogDecoder::new(#log_formatters_lookup) } } - pub fn with_account(self, mut account: U) -> ::fuels::types::errors::Result<#name> { + pub fn with_account(self, account: U) -> ::fuels::types::errors::Result<#name> { ::core::result::Result::Ok(#name { account, binary: self.binary, log_decoder: self.log_decoder}) } @@ -67,7 +67,7 @@ pub(crate) fn script_bindings( self } - pub fn log_decoder(&self) -> ::fuels::programs::logs::LogDecoder { + pub fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder { self.log_decoder.clone() } diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/configurables.rs b/packages/fuels-code-gen/src/program_bindings/abigen/configurables.rs index c1abc56e08..765ada0288 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/configurables.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/configurables.rs @@ -59,7 +59,7 @@ fn generate_struct_impl( configurable_struct_name: &Ident, resolved_configurables: &[ResolvedConfigurable], ) -> TokenStream { - let setter_methods = generate_setter_methods(resolved_configurables); + let builder_methods = generate_builder_methods(resolved_configurables); quote! { impl #configurable_struct_name { @@ -67,12 +67,12 @@ fn generate_struct_impl( ::std::default::Default::default() } - #setter_methods + #builder_methods } } } -fn generate_setter_methods(resolved_configurables: &[ResolvedConfigurable]) -> TokenStream { +fn generate_builder_methods(resolved_configurables: &[ResolvedConfigurable]) -> TokenStream { let methods = resolved_configurables.iter().map( |ResolvedConfigurable { name, @@ -81,6 +81,7 @@ fn generate_setter_methods(resolved_configurables: &[ResolvedConfigurable]) -> T }| { let encoder_code = generate_encoder_code(ttype); quote! { + #[allow(non_snake_case)] pub fn #name(mut self, value: #ttype) -> Self{ self.offsets_with_data.push((#offset, #encoder_code)); self diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/logs.rs b/packages/fuels-code-gen/src/program_bindings/abigen/logs.rs index 15b476029c..c1c1c0e802 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/logs.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/logs.rs @@ -10,7 +10,7 @@ pub(crate) fn log_formatters_instantiation_code( ) -> TokenStream { let resolved_logs = resolve_logs(logged_types); let log_id_log_formatter_pairs = generate_log_id_log_formatter_pairs(&resolved_logs); - quote! {::fuels::programs::logs::log_formatters_lookup(vec![#(#log_id_log_formatter_pairs),*], #contract_id)} + quote! {::fuels::core::codec::log_formatters_lookup(vec![#(#log_id_log_formatter_pairs),*], #contract_id)} } #[derive(Debug)] @@ -31,7 +31,7 @@ fn resolve_logs(logged_types: &[FullLoggedType]) -> Vec { ResolvedLog { log_id: l.log_id, log_formatter: quote! { - ::fuels::programs::logs::LogFormatter::new::<#resolved_type>() + ::fuels::core::codec::LogFormatter::new::<#resolved_type>() }, } }) diff --git a/packages/fuels-core/Cargo.toml b/packages/fuels-core/Cargo.toml index cce1aaedbd..9afe9a7172 100644 --- a/packages/fuels-core/Cargo.toml +++ b/packages/fuels-core/Cargo.toml @@ -10,30 +10,28 @@ rust-version = { workspace = true } description = "Fuel Rust SDK core." [dependencies] -bech32 = "0.9.0" -chrono = "0.4.2" +bech32 = { workspace = true } +chrono = { workspace = true } fuel-abi-types = { workspace = true } fuel-asm = { workspace = true } -fuel-core = { workspace = true, default-features = false, optional = true } fuel-core-chain-config = { workspace = true } fuel-core-client = { workspace = true, optional = true } +fuel-crypto = { workspace = true } fuel-tx = { workspace = true } fuel-types = { workspace = true, features = ["default"] } fuel-vm = { workspace = true } -fuel-crypto = { workspace = true } fuels-macros = { workspace = true } hex = { workspace = true, features = ["std"] } itertools = { workspace = true } -proc-macro2 = { workspace = true } -regex = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, default-features = true } sha2 = { workspace = true } -strum = { workspace = true } -strum_macros = { workspace = true } thiserror = { workspace = true, default-features = false } uint = { version = "0.9.5", default-features = false } +[dev-dependencies] +fuels-macros = { workspace = true } + [features] default = ["std"] std = ["dep:fuel-core-client"] diff --git a/packages/fuels-core/src/codec.rs b/packages/fuels-core/src/codec.rs index c70f449692..644ea96883 100644 --- a/packages/fuels-core/src/codec.rs +++ b/packages/fuels-core/src/codec.rs @@ -1,21 +1,24 @@ mod abi_decoder; mod abi_encoder; mod function_selector; +mod logs; pub use abi_decoder::*; pub use abi_encoder::*; pub use function_selector::*; +pub use logs::*; use crate::{ traits::{Parameterize, Tokenizable}, types::errors::Result, }; -pub fn try_from_bytes(bytes: &[u8]) -> Result +/// Decodes `bytes` into type `T` following the schema defined by T's `Parameterize` impl +pub fn try_from_bytes(bytes: &[u8], decoder_config: DecoderConfig) -> Result where T: Parameterize + Tokenizable, { - let token = ABIDecoder::decode_single(&T::param_type(), bytes)?; + let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?; T::from_token(token) } @@ -32,7 +35,7 @@ mod tests { fn can_convert_bytes_into_tuple() -> Result<()> { let tuple_in_bytes: Vec = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2]; - let the_tuple: (u64, u32) = try_from_bytes(&tuple_in_bytes)?; + let the_tuple: (u64, u32) = try_from_bytes(&tuple_in_bytes, DecoderConfig::default())?; assert_eq!(the_tuple, (1, 2)); @@ -43,11 +46,17 @@ mod tests { fn can_convert_all_from_bool_to_u64() -> Result<()> { let bytes: Vec = vec![0xFF; WORD_SIZE]; - assert!(try_from_bytes::(&bytes)?); - assert_eq!(try_from_bytes::(&bytes)?, u8::MAX); - assert_eq!(try_from_bytes::(&bytes)?, u16::MAX); - assert_eq!(try_from_bytes::(&bytes)?, u32::MAX); - assert_eq!(try_from_bytes::(&bytes)?, u64::MAX); + macro_rules! test_decode { + ($($for_type: ident),*) => { + $(assert_eq!( + try_from_bytes::<$for_type>(&bytes, DecoderConfig::default())?, + $for_type::MAX + );)* + }; + } + + assert!(try_from_bytes::(&bytes, DecoderConfig::default())?); + test_decode!(u8, u16, u32, u64); Ok(()) } @@ -56,18 +65,17 @@ mod tests { fn can_convert_native_types() -> Result<()> { let bytes = [0xFF; 32]; - assert_eq!( - try_from_bytes::
(&bytes)?, - Address::new(bytes.as_slice().try_into()?) - ); - assert_eq!( - try_from_bytes::(&bytes)?, - ContractId::new(bytes.as_slice().try_into()?) - ); - assert_eq!( - try_from_bytes::(&bytes)?, - AssetId::new(bytes.as_slice().try_into()?) - ); + macro_rules! test_decode { + ($($for_type: ident),*) => { + $(assert_eq!( + try_from_bytes::<$for_type>(&bytes, DecoderConfig::default())?, + $for_type::new(bytes.as_slice().try_into()?) + );)* + }; + } + + test_decode!(Address, ContractId, AssetId); + Ok(()) } } diff --git a/packages/fuels-core/src/codec/abi_decoder.rs b/packages/fuels-core/src/codec/abi_decoder.rs index ecacff9e90..eee62d75d7 100644 --- a/packages/fuels-core/src/codec/abi_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder.rs @@ -1,383 +1,81 @@ -use std::{convert::TryInto, str}; - -use fuel_types::bytes::padded_len_usize; +mod bounded_decoder; use crate::{ - constants::WORD_SIZE, - traits::Tokenizable, - types::{ - enum_variants::EnumVariants, - errors::{error, Error, Result}, - param_types::ParamType, - StaticStringToken, Token, U256, - }, + codec::abi_decoder::bounded_decoder::BoundedDecoder, + types::{errors::Result, param_types::ParamType, Token}, }; -const U128_BYTES_SIZE: usize = 2 * WORD_SIZE; -const U256_BYTES_SIZE: usize = 4 * WORD_SIZE; -const B256_BYTES_SIZE: usize = 4 * WORD_SIZE; +#[derive(Debug, Clone, Copy)] +pub struct DecoderConfig { + /// Entering a struct, array, tuple, enum or vector increases the depth. Decoding will fail if + /// the current depth becomes greater than `max_depth` configured here. + pub max_depth: usize, + /// Every decoded Token will increase the token count. Decoding will fail if the current + /// token count becomes greater than `max_tokens` configured here. + pub max_tokens: usize, +} -#[derive(Debug, Clone)] -struct DecodeResult { - token: Token, - bytes_read: usize, +// ANCHOR: default_decoder_config +impl Default for DecoderConfig { + fn default() -> Self { + Self { + max_depth: 45, + max_tokens: 10_000, + } + } } +// ANCHOR_END: default_decoder_config -pub struct ABIDecoder; +#[derive(Default)] +pub struct ABIDecoder { + config: DecoderConfig, +} impl ABIDecoder { - /// Decodes types described by `param_types` into their respective `Token`s - /// using the data in `bytes` and `receipts`. + pub fn new(config: DecoderConfig) -> Self { + Self { config } + } + + /// Decodes `bytes` following the schema described in `param_type` into its respective `Token`. /// /// # Arguments /// - /// * `param_types`: The ParamType's of the types we expect are encoded - /// inside `bytes` and `receipts`. + /// * `param_type`: The `ParamType` of the type we expect is encoded + /// inside `bytes`. /// * `bytes`: The bytes to be used in the decoding process. /// # Examples /// /// ``` /// use fuels_core::codec::ABIDecoder; - /// use fuels_core::types::{param_types::ParamType, Token}; + /// use fuels_core::traits::Tokenizable; + /// use fuels_core::types::param_types::ParamType; + /// + /// let decoder = ABIDecoder::default(); /// - /// let tokens = ABIDecoder::decode(&[ParamType::U8, ParamType::U8], &[0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2]).unwrap(); + /// let token = decoder.decode(&ParamType::U8, &[0, 0, 0, 0, 0, 0, 0, 7]).unwrap(); /// - /// assert_eq!(tokens, vec![Token::U8(1), Token::U8(2)]) + /// assert_eq!(u8::from_token(token).unwrap(), 7u8); /// ``` - pub fn decode(param_types: &[ParamType], bytes: &[u8]) -> Result> { - let (tokens, _) = Self::decode_multiple(param_types, bytes)?; - - Ok(tokens) - } - - /// The same as `decode` just for a single type. Used in most cases since - /// contract functions can only return one type. - pub fn decode_single(param_type: &ParamType, bytes: &[u8]) -> Result { - Ok(Self::decode_param(param_type, bytes)?.token) - } - - fn decode_param(param_type: &ParamType, bytes: &[u8]) -> Result { - if param_type.contains_nested_heap_types() { - return Err(error!( - InvalidData, - "Type {param_type:?} contains nested heap types (`Vec` or `Bytes`), this is not supported." - )); - } - match param_type { - ParamType::Unit => Self::decode_unit(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::Bool => Self::decode_bool(bytes), - ParamType::B256 => Self::decode_b256(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) => Self::decode_array(t, bytes, *length), - ParamType::Struct { fields, .. } => Self::decode_struct(fields, bytes), - ParamType::Enum { variants, .. } => Self::decode_enum(bytes, variants), - ParamType::Tuple(types) => Self::decode_tuple(types, bytes), - ParamType::Vector(param_type) => Self::decode_vector(param_type, bytes), - ParamType::Bytes => Self::decode_bytes(bytes), - ParamType::String => Self::decode_std_string(bytes), - } - } - - fn decode_bytes(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::Bytes(bytes.to_vec()), - bytes_read: bytes.len(), - }) - } - - fn decode_std_string(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::String(str::from_utf8(bytes)?.to_string()), - bytes_read: bytes.len(), - }) - } - - fn decode_vector(param_type: &ParamType, bytes: &[u8]) -> Result { - let num_of_elements = ParamType::calculate_num_of_elements(param_type, bytes.len())?; - let (tokens, bytes_read) = Self::decode_multiple(vec![param_type; num_of_elements], bytes)?; - - Ok(DecodeResult { - token: Token::Vector(tokens), - bytes_read, - }) - } - - fn decode_tuple(param_types: &[ParamType], bytes: &[u8]) -> Result { - let (tokens, bytes_read) = Self::decode_multiple(param_types, bytes)?; - - Ok(DecodeResult { - token: Token::Tuple(tokens), - bytes_read, - }) - } - - fn decode_struct(param_types: &[ParamType], bytes: &[u8]) -> Result { - let (tokens, bytes_read) = Self::decode_multiple(param_types, bytes)?; - - Ok(DecodeResult { - token: Token::Struct(tokens), - bytes_read, - }) - } - - fn decode_multiple<'a>( - param_types: impl IntoIterator, - bytes: &[u8], - ) -> Result<(Vec, usize)> { - let mut results = vec![]; - - let mut bytes_read = 0; - - for param_type in param_types { - let res = Self::decode_param(param_type, skip(bytes, bytes_read)?)?; - bytes_read += res.bytes_read; - results.push(res.token); - } - - Ok((results, bytes_read)) - } - - fn decode_array(param_type: &ParamType, bytes: &[u8], length: usize) -> Result { - let (tokens, bytes_read) = Self::decode_multiple(&vec![param_type.clone(); length], bytes)?; - - Ok(DecodeResult { - token: Token::Array(tokens), - bytes_read, - }) - } - - fn decode_raw_slice(bytes: &[u8]) -> Result { - let raw_slice_element = ParamType::U64; - let num_of_elements = - ParamType::calculate_num_of_elements(&raw_slice_element, bytes.len())?; - let (tokens, bytes_read) = - Self::decode_multiple(&vec![ParamType::U64; num_of_elements], bytes)?; - let elements = tokens - .into_iter() - .map(u64::from_token) - .collect::>>() - .map_err(|e| error!(InvalidData, "{e}"))?; - - Ok(DecodeResult { - token: Token::RawSlice(elements), - bytes_read, - }) - } - - fn decode_string_slice(bytes: &[u8]) -> Result { - let decoded = str::from_utf8(bytes)?; - - Ok(DecodeResult { - token: Token::StringSlice(StaticStringToken::new(decoded.into(), None)), - bytes_read: decoded.len(), - }) - } - - fn decode_string_array(bytes: &[u8], length: usize) -> Result { - let encoded_len = padded_len_usize(length); - let encoded_str = peek(bytes, encoded_len)?; - - let decoded = str::from_utf8(&encoded_str[..length])?; - let result = DecodeResult { - token: Token::StringArray(StaticStringToken::new(decoded.into(), Some(length))), - bytes_read: encoded_len, - }; - Ok(result) - } - - fn decode_b256(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::B256(*peek_fixed::<32>(bytes)?), - bytes_read: B256_BYTES_SIZE, - }) - } - - fn decode_bool(bytes: &[u8]) -> Result { - // Grab last byte of the word and compare it to 0x00 - let b = peek_u8(bytes)? != 0u8; - - let result = DecodeResult { - token: Token::Bool(b), - bytes_read: WORD_SIZE, - }; - - Ok(result) - } - - fn decode_u128(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U128(peek_u128(bytes)?), - bytes_read: U128_BYTES_SIZE, - }) - } - - fn decode_u256(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U256(peek_u256(bytes)?), - bytes_read: U256_BYTES_SIZE, - }) - } - - fn decode_u64(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U64(peek_u64(bytes)?), - bytes_read: WORD_SIZE, - }) - } - - fn decode_u32(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U32(peek_u32(bytes)?), - bytes_read: WORD_SIZE, - }) - } - - fn decode_u16(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U16(peek_u16(bytes)?), - bytes_read: WORD_SIZE, - }) - } - - fn decode_u8(bytes: &[u8]) -> Result { - Ok(DecodeResult { - token: Token::U8(peek_u8(bytes)?), - bytes_read: WORD_SIZE, - }) + pub fn decode(&self, param_type: &ParamType, bytes: &[u8]) -> Result { + BoundedDecoder::new(self.config).decode(param_type, bytes) } - fn decode_unit(bytes: &[u8]) -> Result { - // We don't need the data, we're doing this purely as a bounds - // check. - peek_fixed::(bytes)?; - Ok(DecodeResult { - token: Token::Unit, - bytes_read: WORD_SIZE, - }) - } - - /// The encoding follows the ABI specs defined - /// [here](https://github.com/FuelLabs/fuel-specs/blob/1be31f70c757d8390f74b9e1b3beb096620553eb/specs/protocol/abi.md) + /// Same as `decode` but decodes multiple `ParamType`s in one go. + /// # Examples + /// ``` + /// use fuels_core::codec::ABIDecoder; + /// use fuels_core::types::param_types::ParamType; + /// use fuels_core::types::Token; /// - /// # Arguments + /// let decoder = ABIDecoder::default(); + /// let data: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8]; /// - /// * `data`: slice of encoded data on whose beginning we're expecting an encoded enum - /// * `variants`: all types that this particular enum type could hold - fn decode_enum(bytes: &[u8], variants: &EnumVariants) -> Result { - let enum_width = variants.compute_encoding_width_of_enum(); - - let discriminant = peek_u32(bytes)? as u8; - let selected_variant = variants.param_type_of_variant(discriminant)?; - - let words_to_skip = enum_width - selected_variant.compute_encoding_width(); - let enum_content_bytes = skip(bytes, words_to_skip * WORD_SIZE)?; - let result = Self::decode_token_in_enum(enum_content_bytes, variants, selected_variant)?; - - let selector = Box::new((discriminant, result.token, variants.clone())); - Ok(DecodeResult { - token: Token::Enum(selector), - bytes_read: enum_width * WORD_SIZE, - }) - } - - fn decode_token_in_enum( - bytes: &[u8], - variants: &EnumVariants, - selected_variant: &ParamType, - ) -> Result { - // Enums that contain only Units as variants have only their discriminant encoded. - // Because of this we construct the Token::Unit rather than calling `decode_param` - if variants.only_units_inside() { - Ok(DecodeResult { - token: Token::Unit, - bytes_read: 0, - }) - } else { - Self::decode_param(selected_variant, bytes) - } - } -} - -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_u64(bytes: &[u8]) -> Result { - let slice = peek_fixed::(bytes)?; - Ok(u64::from_be_bytes(*slice)) -} - -fn peek_u32(bytes: &[u8]) -> Result { - const BYTES: usize = std::mem::size_of::(); - - let slice = peek_fixed::(bytes)?; - let bytes = slice[WORD_SIZE - BYTES..] - .try_into() - .expect("peek_u32: You must use a slice containing exactly 4B."); - Ok(u32::from_be_bytes(bytes)) -} - -fn peek_u16(bytes: &[u8]) -> Result { - const BYTES: usize = std::mem::size_of::(); - - let slice = peek_fixed::(bytes)?; - let bytes = slice[WORD_SIZE - BYTES..] - .try_into() - .expect("peek_u16: You must use a slice containing exactly 2B."); - Ok(u16::from_be_bytes(bytes)) -} - -fn peek_u8(bytes: &[u8]) -> Result { - const BYTES: usize = std::mem::size_of::(); - - let slice = peek_fixed::(bytes)?; - let bytes = slice[WORD_SIZE - BYTES..] - .try_into() - .expect("peek_u8: You must use a slice containing exactly 1B."); - Ok(u8::from_be_bytes(bytes)) -} - -fn peek_fixed(data: &[u8]) -> Result<&[u8; LEN]> { - let slice_w_correct_length = peek(data, LEN)?; - Ok(<&[u8; LEN]>::try_from(slice_w_correct_length) - .expect("peek(data,len) must return a slice of length `len` or error out")) -} - -fn peek(data: &[u8], len: usize) -> Result<&[u8]> { - if len > data.len() { - Err(error!( - InvalidData, - "tried to read {len} bytes from response but only had {} remaining!", - data.len() - )) - } else { - Ok(&data[..len]) - } -} - -fn skip(slice: &[u8], num_bytes: usize) -> Result<&[u8]> { - if num_bytes > slice.len() { - Err(error!( - InvalidData, - "tried to consume {num_bytes} bytes from response but only had {} remaining!", - slice.len() - )) - } else { - Ok(&slice[num_bytes..]) + /// let tokens = decoder.decode_multiple(&[ParamType::U8, ParamType::U8], &data).unwrap(); + /// + /// assert_eq!(tokens, vec![Token::U8(7), Token::U8(8)]); + /// ``` + pub fn decode_multiple(&self, param_types: &[ParamType], bytes: &[u8]) -> Result> { + BoundedDecoder::new(self.config).decode_multiple(param_types, bytes) } } @@ -386,12 +84,16 @@ mod tests { use std::vec; use super::*; + use crate::{ + constants::WORD_SIZE, + types::{enum_variants::EnumVariants, errors::Error, StaticStringToken}, + }; #[test] fn decode_int() -> Result<()> { let data = [0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff]; - let decoded = ABIDecoder::decode_single(&ParamType::U32, &data)?; + let decoded = ABIDecoder::default().decode(&ParamType::U32, &data)?; assert_eq!(decoded, Token::U32(u32::MAX)); Ok(()) @@ -411,7 +113,7 @@ mod tests { 0xff, ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![ Token::U32(u32::MAX), @@ -430,7 +132,7 @@ mod tests { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00, ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![Token::Bool(true), Token::Bool(false)]; @@ -446,7 +148,7 @@ mod tests { 0xf3, 0x1e, 0x93, 0xb, ]; - let decoded = ABIDecoder::decode_single(&ParamType::B256, &data)?; + let decoded = ABIDecoder::default().decode(&ParamType::B256, &data)?; assert_eq!(decoded, Token::B256(data)); Ok(()) @@ -462,7 +164,7 @@ mod tests { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00, // Hello ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![ Token::StringArray(StaticStringToken::new( @@ -485,7 +187,7 @@ mod tests { 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, // entence ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![Token::StringSlice(StaticStringToken::new( "This is a full sentence".into(), @@ -504,7 +206,7 @@ mod tests { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![Token::Array(vec![Token::U8(255), Token::U8(42)])]; assert_eq!(decoded, expected); @@ -526,7 +228,7 @@ mod tests { generics: vec![], }; - let decoded = ABIDecoder::decode_single(¶m_type, &data)?; + let decoded = ABIDecoder::default().decode(¶m_type, &data)?; let expected = Token::Struct(vec![Token::U8(1), Token::Bool(true)]); @@ -537,7 +239,7 @@ mod tests { #[test] fn decode_bytes() -> Result<()> { let data = [0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05]; - let decoded = ABIDecoder::decode_single(&ParamType::Bytes, &data)?; + let decoded = ABIDecoder::default().decode(&ParamType::Bytes, &data)?; let expected = Token::Bytes(data.to_vec()); @@ -564,7 +266,7 @@ mod tests { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, ]; - let decoded = ABIDecoder::decode(&types, &data)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &data)?; let expected = vec![Token::Enum(Box::new((0, Token::U32(42), inner_enum_types)))]; assert_eq!(decoded, expected); @@ -613,7 +315,7 @@ mod tests { .flatten() .collect(); - let decoded = ABIDecoder::decode_single(&struct_type, &data)?; + let decoded = ABIDecoder::default().decode(&struct_type, &data)?; let expected = Token::Struct(vec![ Token::Enum(Box::new((1, Token::U32(12345), inner_enum_types))), @@ -655,7 +357,7 @@ mod tests { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, ]; - let decoded = ABIDecoder::decode_single(&nested_struct, &data)?; + let decoded = ABIDecoder::default().decode(&nested_struct, &data)?; let my_nested_struct = vec![ Token::U16(10), @@ -723,7 +425,7 @@ mod tests { 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, // str data ]; - let decoded = ABIDecoder::decode(&types, &bytes)?; + let decoded = ABIDecoder::default().decode_multiple(&types, &bytes)?; // Expected tokens let foo = Token::Struct(vec![ @@ -765,7 +467,7 @@ mod tests { generics: vec![], }; - let actual = ABIDecoder::decode_single(&struct_type, &data)?; + let actual = ABIDecoder::default().decode(&struct_type, &data)?; let expected = Token::Struct(vec![Token::Unit, Token::U64(u64::MAX)]); assert_eq!(actual, expected); @@ -782,7 +484,7 @@ mod tests { generics: vec![], }; - let result = ABIDecoder::decode_single(&enum_w_only_units, &data)?; + let result = ABIDecoder::default().decode(&enum_w_only_units, &data)?; let expected_enum = Token::Enum(Box::new((1, Token::Unit, variants))); assert_eq!(result, expected_enum); @@ -799,7 +501,7 @@ mod tests { generics: vec![], }; - let result = ABIDecoder::decode_single(&enum_type, &data); + let result = ABIDecoder::default().decode(&enum_type, &data); let error = result.expect_err("Should have resulted in an error"); @@ -807,4 +509,177 @@ mod tests { assert!(matches!(error, Error::InvalidData(str) if str.starts_with(expected_msg))); Ok(()) } + + #[test] + fn max_depth_surpassed() { + const MAX_DEPTH: usize = 2; + let config = DecoderConfig { + max_depth: MAX_DEPTH, + ..Default::default() + }; + let msg = format!("Depth limit ({MAX_DEPTH}) reached while decoding. Try increasing it."); + // for each nested enum so that it may read the discriminant + let data = [0; MAX_DEPTH * WORD_SIZE]; + + [nested_struct, nested_enum, nested_tuple, nested_array] + .iter() + .map(|fun| fun(MAX_DEPTH + 1)) + .for_each(|param_type| { + assert_decoding_failed_w_data(config, ¶m_type, &msg, &data); + }) + } + + #[test] + fn depth_is_not_reached() { + const MAX_DEPTH: usize = 3; + const ACTUAL_DEPTH: usize = MAX_DEPTH - 1; + + // enough data to decode 2*ACTUAL_DEPTH enums (discriminant + u8 = 2*WORD_SIZE) + let data = [0; 2 * ACTUAL_DEPTH * (WORD_SIZE * 2)]; + let config = DecoderConfig { + max_depth: MAX_DEPTH, + ..Default::default() + }; + + [nested_struct, nested_enum, nested_tuple, nested_array] + .into_iter() + .map(|fun| fun(ACTUAL_DEPTH)) + .map(|param_type| { + // Wrapping everything in a structure so that we may check whether the depth is + // decremented after finishing every struct field. + ParamType::Struct { + fields: vec![param_type.clone(), param_type], + generics: vec![], + } + }) + .for_each(|param_type| { + ABIDecoder::new(config).decode(¶m_type, &data).unwrap(); + }) + } + + #[test] + fn too_many_tokens() { + let config = DecoderConfig { + max_tokens: 3, + ..Default::default() + }; + + let data = [0; 3 * WORD_SIZE]; + let el = ParamType::U8; + for param_type in [ + ParamType::Struct { + fields: vec![el.clone(); 3], + generics: vec![], + }, + ParamType::Tuple(vec![el.clone(); 3]), + ParamType::Array(Box::new(el.clone()), 3), + ParamType::Vector(Box::new(el)), + ] { + assert_decoding_failed_w_data( + config, + ¶m_type, + "Token limit (3) reached while decoding. Try increasing it.", + &data, + ); + } + } + + #[test] + fn vectors_of_zst_are_not_supported() { + let param_type = ParamType::Vector(Box::new(ParamType::StringArray(0))); + + let err = ABIDecoder::default() + .decode(¶m_type, &[]) + .expect_err("Vectors of ZST should be prohibited"); + + let Error::InvalidType(msg) = err else { + panic!("Expected error of type InvalidType") + }; + assert_eq!( + msg, + "Cannot calculate the number of elements because the type is zero-sized." + ); + } + + #[test] + fn token_count_is_being_reset_between_decodings() { + // given + let config = DecoderConfig { + max_tokens: 3, + ..Default::default() + }; + + let param_type = ParamType::Array(Box::new(ParamType::StringArray(0)), 2); + + let decoder = ABIDecoder::new(config); + decoder.decode(¶m_type, &[]).unwrap(); + + // when + let result = decoder.decode(¶m_type, &[]); + + // then + result.expect("Element count to be reset"); + } + + fn assert_decoding_failed_w_data( + config: DecoderConfig, + param_type: &ParamType, + msg: &str, + data: &[u8], + ) { + let decoder = ABIDecoder::new(config); + + let err = decoder.decode(param_type, data); + + let Err(Error::InvalidType(actual_msg)) = err else { + panic!("Unexpected an InvalidType error! Got: {err:?}"); + }; + assert_eq!(actual_msg, msg); + } + + fn nested_struct(depth: usize) -> ParamType { + let fields = if depth == 1 { + vec![] + } else { + vec![nested_struct(depth - 1)] + }; + + ParamType::Struct { + fields, + generics: vec![], + } + } + + fn nested_enum(depth: usize) -> ParamType { + let fields = if depth == 1 { + vec![ParamType::U8] + } else { + vec![nested_enum(depth - 1)] + }; + + ParamType::Enum { + variants: EnumVariants::new(fields).unwrap(), + generics: vec![], + } + } + + fn nested_array(depth: usize) -> ParamType { + let field = if depth == 1 { + ParamType::U8 + } else { + nested_array(depth - 1) + }; + + ParamType::Array(Box::new(field), 1) + } + + fn nested_tuple(depth: usize) -> ParamType { + let fields = if depth == 1 { + vec![ParamType::U8] + } else { + vec![nested_tuple(depth - 1)] + }; + + ParamType::Tuple(fields) + } } diff --git a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs new file mode 100644 index 0000000000..4c5066c24a --- /dev/null +++ b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs @@ -0,0 +1,459 @@ +use std::{convert::TryInto, str}; + +use fuel_types::bytes::padded_len_usize; + +use crate::{ + codec::DecoderConfig, + constants::WORD_SIZE, + traits::Tokenizable, + types::{ + enum_variants::EnumVariants, + errors::{error, 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 BoundedDecoder { + depth_tracker: CounterWithLimit, + token_tracker: CounterWithLimit, +} + +const U128_BYTES_SIZE: usize = 2 * WORD_SIZE; +const U256_BYTES_SIZE: usize = 4 * WORD_SIZE; +const B256_BYTES_SIZE: usize = 4 * WORD_SIZE; + +impl BoundedDecoder { + 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::is_type_decodable(param_type)?; + Ok(self.decode_param(param_type, bytes)?.token) + } + + pub(crate) fn decode_multiple( + &mut self, + param_types: &[ParamType], + bytes: &[u8], + ) -> Result> { + for param_type in param_types { + Self::is_type_decodable(param_type)?; + } + let (tokens, _) = self.decode_params(param_types, bytes)?; + + Ok(tokens) + } + + fn is_type_decodable(param_type: &ParamType) -> Result<()> { + if param_type.contains_nested_heap_types() { + Err(error!( + InvalidType, + "Type {param_type:?} contains nested heap types (`Vec` or `Bytes`), this is not supported." + )) + } else { + Ok(()) + } + } + + 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(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::Bool => Self::decode_bool(bytes), + ParamType::B256 => Self::decode_b256(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) => { + self.run_w_depth_tracking(|ctx| ctx.decode_array(t, bytes, *length)) + } + 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)) + } + ParamType::Tuple(types) => { + self.run_w_depth_tracking(|ctx| ctx.decode_tuple(types, bytes)) + } + ParamType::Vector(param_type) => { + // although nested vectors cannot be decoded yet, depth tracking still occurs for future + // proofing + self.run_w_depth_tracking(|ctx| ctx.decode_vector(param_type, bytes)) + } + ParamType::Bytes => Self::decode_bytes(bytes), + ParamType::String => Self::decode_std_string(bytes), + } + } + + fn decode_bytes(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::Bytes(bytes.to_vec()), + bytes_read: bytes.len(), + }) + } + + fn decode_std_string(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::String(str::from_utf8(bytes)?.to_string()), + bytes_read: bytes.len(), + }) + } + + fn decode_vector(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + let num_of_elements = ParamType::calculate_num_of_elements(param_type, bytes.len())?; + let (tokens, bytes_read) = + self.decode_params(std::iter::repeat(param_type).take(num_of_elements), bytes)?; + + Ok(Decoded { + token: Token::Vector(tokens), + bytes_read, + }) + } + + 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_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_params<'a>( + &mut self, + param_types: impl IntoIterator, + bytes: &[u8], + ) -> Result<(Vec, usize)> { + let mut results = vec![]; + + let mut bytes_read = 0; + + for param_type in param_types { + let res = self.decode_param(param_type, skip(bytes, bytes_read)?)?; + bytes_read += res.bytes_read; + results.push(res.token); + } + + Ok((results, bytes_read)) + } + + fn decode_array( + &mut self, + param_type: &ParamType, + bytes: &[u8], + length: usize, + ) -> Result { + let (tokens, bytes_read) = + self.decode_params(std::iter::repeat(param_type).take(length), bytes)?; + + Ok(Decoded { + token: Token::Array(tokens), + bytes_read, + }) + } + + 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}"))?; + + Ok(Decoded { + token: Token::RawSlice(elements), + bytes_read, + }) + } + + fn decode_string_slice(bytes: &[u8]) -> Result { + let decoded = str::from_utf8(bytes)?; + + Ok(Decoded { + token: Token::StringSlice(StaticStringToken::new(decoded.into(), None)), + bytes_read: decoded.len(), + }) + } + + fn decode_string_array(bytes: &[u8], length: usize) -> Result { + let encoded_len = padded_len_usize(length); + let encoded_str = peek(bytes, encoded_len)?; + + let decoded = str::from_utf8(&encoded_str[..length])?; + let result = Decoded { + token: Token::StringArray(StaticStringToken::new(decoded.into(), Some(length))), + bytes_read: encoded_len, + }; + Ok(result) + } + + fn decode_b256(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::B256(*peek_fixed::<32>(bytes)?), + bytes_read: B256_BYTES_SIZE, + }) + } + + fn decode_bool(bytes: &[u8]) -> Result { + // Grab last byte of the word and compare it to 0x00 + let b = peek_u8(bytes)? != 0u8; + + let result = Decoded { + token: Token::Bool(b), + bytes_read: WORD_SIZE, + }; + + Ok(result) + } + + 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_u64(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U64(peek_u64(bytes)?), + bytes_read: WORD_SIZE, + }) + } + + fn decode_u32(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U32(peek_u32(bytes)?), + bytes_read: WORD_SIZE, + }) + } + + fn decode_u16(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U16(peek_u16(bytes)?), + bytes_read: WORD_SIZE, + }) + } + + fn decode_u8(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U8(peek_u8(bytes)?), + bytes_read: WORD_SIZE, + }) + } + + fn decode_unit(bytes: &[u8]) -> Result { + // We don't need the data, we're doing this purely as a bounds + // check. + peek_fixed::(bytes)?; + Ok(Decoded { + token: Token::Unit, + bytes_read: WORD_SIZE, + }) + } + + /// The encoding follows the ABI specs defined + /// [here](https://github.com/FuelLabs/fuel-specs/blob/1be31f70c757d8390f74b9e1b3beb096620553eb/specs/protocol/abi.md) + /// + /// # Arguments + /// + /// * `data`: slice of encoded data on whose beginning we're expecting an encoded enum + /// * `variants`: all types that this particular enum type could hold + fn decode_enum(&mut self, bytes: &[u8], variants: &EnumVariants) -> Result { + let enum_width = variants.compute_encoding_width_of_enum(); + + let discriminant = peek_u32(bytes)? as u8; + let selected_variant = variants.param_type_of_variant(discriminant)?; + + let words_to_skip = enum_width - selected_variant.compute_encoding_width(); + let enum_content_bytes = skip(bytes, words_to_skip * WORD_SIZE)?; + let result = self.decode_token_in_enum(enum_content_bytes, variants, selected_variant)?; + + let selector = Box::new((discriminant, result.token, variants.clone())); + Ok(Decoded { + token: Token::Enum(selector), + bytes_read: enum_width * WORD_SIZE, + }) + } + + fn decode_token_in_enum( + &mut self, + bytes: &[u8], + variants: &EnumVariants, + selected_variant: &ParamType, + ) -> Result { + // Enums that contain only Units as variants have only their discriminant encoded. + // Because of this we construct the Token::Unit rather than calling `decode_param` + if variants.only_units_inside() { + Ok(Decoded { + token: Token::Unit, + bytes_read: 0, + }) + } else { + self.decode_param(selected_variant, bytes) + } + } +} + +#[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 { + Err(error!( + InvalidType, + "{} limit ({}) reached while decoding. Try increasing it.", self.name, self.max + )) + } else { + Ok(()) + } + } + + fn decrease(&mut self) { + if self.count > 0 { + self.count -= 1; + } + } +} + +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_u64(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u64::from_be_bytes(*slice)) +} + +fn peek_u32(bytes: &[u8]) -> Result { + const BYTES: usize = std::mem::size_of::(); + + let slice = peek_fixed::(bytes)?; + let bytes = slice[WORD_SIZE - BYTES..] + .try_into() + .expect("peek_u32: You must use a slice containing exactly 4B."); + Ok(u32::from_be_bytes(bytes)) +} + +fn peek_u16(bytes: &[u8]) -> Result { + const BYTES: usize = std::mem::size_of::(); + + let slice = peek_fixed::(bytes)?; + let bytes = slice[WORD_SIZE - BYTES..] + .try_into() + .expect("peek_u16: You must use a slice containing exactly 2B."); + Ok(u16::from_be_bytes(bytes)) +} + +fn peek_u8(bytes: &[u8]) -> Result { + const BYTES: usize = std::mem::size_of::(); + + let slice = peek_fixed::(bytes)?; + let bytes = slice[WORD_SIZE - BYTES..] + .try_into() + .expect("peek_u8: You must use a slice containing exactly 1B."); + Ok(u8::from_be_bytes(bytes)) +} + +fn peek_fixed(data: &[u8]) -> Result<&[u8; LEN]> { + let slice_w_correct_length = peek(data, LEN)?; + Ok(<&[u8; LEN]>::try_from(slice_w_correct_length) + .expect("peek(data,len) must return a slice of length `len` or error out")) +} + +fn peek(data: &[u8], len: usize) -> Result<&[u8]> { + if len > data.len() { + Err(error!( + InvalidData, + "tried to read {len} bytes from response but only had {} remaining!", + data.len() + )) + } else { + Ok(&data[..len]) + } +} + +fn skip(slice: &[u8], num_bytes: usize) -> Result<&[u8]> { + if num_bytes > slice.len() { + Err(error!( + InvalidData, + "tried to consume {num_bytes} bytes from response but only had {} remaining!", + slice.len() + )) + } else { + Ok(&slice[num_bytes..]) + } +} diff --git a/packages/fuels-core/src/codec/function_selector.rs b/packages/fuels-core/src/codec/function_selector.rs index bb3db9dbc5..2e04683eee 100644 --- a/packages/fuels-core/src/codec/function_selector.rs +++ b/packages/fuels-core/src/codec/function_selector.rs @@ -108,7 +108,8 @@ pub use fn_selector; #[macro_export] macro_rules! calldata { ( $($arg: expr),* ) => { - ::fuels::core::codec::ABIEncoder::encode(&[$(::fuels::core::traits::Tokenizable::into_token($arg)),*]).unwrap().resolve(0) + ::fuels::core::codec::ABIEncoder::encode(&[$(::fuels::core::traits::Tokenizable::into_token($arg)),*]) + .map(|ub| ub.resolve(0)) } } diff --git a/packages/fuels-programs/src/logs.rs b/packages/fuels-core/src/codec/logs.rs similarity index 84% rename from packages/fuels-programs/src/logs.rs rename to packages/fuels-core/src/codec/logs.rs index 6c90e09b06..98930dd293 100644 --- a/packages/fuels-programs/src/logs.rs +++ b/packages/fuels-core/src/codec/logs.rs @@ -10,15 +10,16 @@ use fuel_abi_types::error_codes::{ FAILED_SEND_MESSAGE_SIGNAL, FAILED_TRANSFER_TO_ADDRESS_SIGNAL, }; use fuel_tx::{ContractId, Receipt}; -use fuels_core::{ - codec::try_from_bytes, + +use crate::{ + codec::{try_from_bytes, DecoderConfig}, traits::{Parameterize, Tokenizable}, types::errors::{error, Error, Result}, }; #[derive(Clone)] pub struct LogFormatter { - formatter: fn(&[u8]) -> Result, + formatter: fn(DecoderConfig, &[u8]) -> Result, type_id: TypeId, } @@ -30,16 +31,19 @@ impl LogFormatter { } } - fn format_log(bytes: &[u8]) -> Result { - Ok(format!("{:?}", try_from_bytes::(bytes)?)) + fn format_log( + decoder_config: DecoderConfig, + bytes: &[u8], + ) -> Result { + Ok(format!("{:?}", try_from_bytes::(bytes, decoder_config)?)) } pub fn can_handle_type(&self) -> bool { TypeId::of::() == self.type_id } - pub fn format(&self, bytes: &[u8]) -> Result { - (self.formatter)(bytes) + pub fn format(&self, decoder_config: DecoderConfig, bytes: &[u8]) -> Result { + (self.formatter)(decoder_config, bytes) } } @@ -59,7 +63,8 @@ pub struct LogId(ContractId, u64); #[derive(Debug, Clone, Default)] pub struct LogDecoder { /// A mapping of LogId and param-type - pub log_formatters: HashMap, + log_formatters: HashMap, + decoder_config: DecoderConfig, } #[derive(Debug)] @@ -84,6 +89,18 @@ impl LogResult { } impl LogDecoder { + pub fn new(log_formatters: HashMap) -> Self { + Self { + log_formatters, + decoder_config: Default::default(), + } + } + + pub fn set_decoder_config(&mut self, decoder_config: DecoderConfig) -> &mut Self { + self.decoder_config = decoder_config; + self + } + /// Get all logs results from the given receipts as `Result` pub fn decode_logs(&self, receipts: &[Receipt]) -> LogResult { let results = receipts @@ -107,10 +124,10 @@ impl LogDecoder { data ) }) - .and_then(|log_formatter| log_formatter.format(data)) + .and_then(|log_formatter| log_formatter.format(self.decoder_config, data)) } - fn decode_last_log(&self, receipts: &[Receipt]) -> Result { + pub(crate) fn decode_last_log(&self, receipts: &[Receipt]) -> Result { receipts .iter() .rev() @@ -120,7 +137,7 @@ impl LogDecoder { .and_then(|(log_id, data)| self.format_log(&log_id, &data)) } - fn decode_last_two_logs(&self, receipts: &[Receipt]) -> Result<(String, String)> { + pub(crate) fn decode_last_two_logs(&self, receipts: &[Receipt]) -> Result<(String, String)> { let res = receipts .iter() .rev() @@ -160,7 +177,7 @@ impl LogDecoder { .filter_map(|(log_id, data)| { target_ids .contains(&log_id) - .then_some(try_from_bytes(&data)) + .then_some(try_from_bytes(&data, self.decoder_config)) }) .collect() } diff --git a/packages/fuels-core/src/types.rs b/packages/fuels-core/src/types.rs index d423909f2c..b1236dfae4 100644 --- a/packages/fuels-core/src/types.rs +++ b/packages/fuels-core/src/types.rs @@ -16,6 +16,7 @@ pub mod enum_variants; pub mod errors; pub mod param_types; pub mod transaction_builders; +pub mod tx_status; pub mod unresolved_bytes; mod wrappers; diff --git a/packages/fuels-core/src/types/bech32.rs b/packages/fuels-core/src/types/bech32.rs index 56faf974e1..2d7fab01cf 100644 --- a/packages/fuels-core/src/types/bech32.rs +++ b/packages/fuels-core/src/types/bech32.rs @@ -3,12 +3,14 @@ use std::{ str::FromStr, }; -use crate::types::Bits256; use bech32::{FromBase32, ToBase32, Variant::Bech32m}; use fuel_tx::{Address, Bytes32, ContractId, ContractIdExt}; use fuel_types::AssetId; -use crate::types::errors::{Error, Result}; +use crate::types::{ + errors::{Error, Result}, + Bits256, +}; // Fuel Network human-readable part for bech32 encoding pub const FUEL_BECH32_HRP: &str = "fuel"; diff --git a/packages/fuels-core/src/types/core/sized_ascii_string.rs b/packages/fuels-core/src/types/core/sized_ascii_string.rs index e23dadf50c..1eccc98509 100644 --- a/packages/fuels-core/src/types/core/sized_ascii_string.rs +++ b/packages/fuels-core/src/types/core/sized_ascii_string.rs @@ -1,8 +1,9 @@ use std::fmt::{Debug, Display, Formatter}; -use crate::types::errors::{error, Error, Result}; use serde::{Deserialize, Serialize}; +use crate::types::errors::{error, Error, Result}; + // To be used when interacting with contracts which have string slices in their ABI. // The FuelVM strings only support ascii characters. #[derive(Debug, PartialEq, Clone, Eq)] diff --git a/packages/fuels-core/src/types/param_types.rs b/packages/fuels-core/src/types/param_types.rs index a615f41926..c292dd1e82 100644 --- a/packages/fuels-core/src/types/param_types.rs +++ b/packages/fuels-core/src/types/param_types.rs @@ -24,8 +24,6 @@ pub enum ParamType { U256, Bool, B256, - // The Unit ParamType is used for unit variants in Enums. The corresponding type field is `()`, - // similar to Rust. Unit, Array(Box, usize), Vector(Box), @@ -70,6 +68,12 @@ impl ParamType { available_bytes: usize, ) -> Result { let memory_size = param_type.compute_encoding_width() * WORD_SIZE; + if memory_size == 0 { + return Err(error!( + InvalidType, + "Cannot calculate the number of elements because the type is zero-sized." + )); + } let remainder = available_bytes % memory_size; if remainder != 0 { return Err(error!( @@ -469,8 +473,6 @@ fn try_str_slice(the_type: &Type) -> Result> { fn try_array(the_type: &Type) -> Result> { if let Some(len) = extract_array_len(&the_type.type_field) { - if the_type.components.len() != 1 {} - return match the_type.components.as_slice() { [single_type] => { let array_type = single_type.try_into()?; @@ -504,6 +506,7 @@ fn try_primitive(the_type: &Type) -> Result> { #[cfg(test)] mod tests { + use super::*; use crate::types::param_types::ParamType; diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index a7c4184d55..076c076c8c 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -5,11 +5,11 @@ use std::collections::HashMap; use fuel_asm::{op, GTFArgs, RegId}; use fuel_crypto::{Message as CryptoMessage, SecretKey, Signature}; use fuel_tx::{ - field::Witnesses, Cacheable, ConsensusParameters, Create, Input as FuelInput, Output, Script, - StorageSlot, Transaction as FuelTransaction, TransactionFee, TxPointer, UniqueIdentifier, - Witness, + field::{GasLimit, GasPrice, Witnesses}, + Cacheable, ConsensusParameters, Create, Input as FuelInput, Output, Script, StorageSlot, + Transaction as FuelTransaction, TransactionFee, TxPointer, UniqueIdentifier, Witness, }; -use fuel_types::{bytes::padded_len_usize, Bytes32, Salt}; +use fuel_types::{bytes::padded_len_usize, Bytes32, ChainId, MemLayout, Salt}; use fuel_vm::{checked_transaction::EstimatePredicates, gas::GasCosts}; use crate::{ @@ -23,11 +23,35 @@ use crate::{ input::Input, message::Message, transaction::{CreateTransaction, ScriptTransaction, Transaction, TxParameters}, + unresolved_bytes::UnresolvedBytes, Address, AssetId, ContractId, }, }; -use super::unresolved_bytes::UnresolvedBytes; +use super::{chain_info::ChainInfo, node_info::NodeInfo}; + +#[derive(Debug, Clone)] +pub struct NetworkInfo { + pub consensus_parameters: ConsensusParameters, + pub max_gas_per_tx: u64, + pub min_gas_price: u64, + pub gas_costs: GasCosts, +} + +impl NetworkInfo { + pub fn new(node_info: NodeInfo, chain_info: ChainInfo) -> Self { + Self { + max_gas_per_tx: chain_info.consensus_parameters.max_gas_per_tx, + consensus_parameters: chain_info.consensus_parameters.into(), + min_gas_price: node_info.min_gas_price, + gas_costs: chain_info.gas_costs, + } + } + + pub fn chain_id(&self) -> ChainId { + self.consensus_parameters.chain_id + } +} #[derive(Debug, Clone, Default)] struct UnresolvedSignatures { @@ -40,7 +64,7 @@ pub trait TransactionBuilder: Send { fn build(self) -> Result; fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); - fn fee_checked_from_tx(&self, params: &ConsensusParameters) -> Option; + fn fee_checked_from_tx(&self, params: &ConsensusParameters) -> Result>; fn with_maturity(self, maturity: u32) -> Self; fn with_gas_price(self, gas_price: u64) -> Self; fn with_gas_limit(self, gas_limit: u64) -> Self; @@ -48,7 +72,6 @@ pub trait TransactionBuilder: Send { fn with_inputs(self, inputs: Vec) -> Self; fn with_outputs(self, outputs: Vec) -> Self; fn with_witnesses(self, witnesses: Vec) -> Self; - fn with_consensus_parameters(self, consensus_parameters: ConsensusParameters) -> Self; fn inputs(&self) -> &Vec; fn inputs_mut(&mut self) -> &mut Vec; fn outputs(&self) -> &Vec; @@ -63,29 +86,21 @@ macro_rules! impl_tx_trait { type TxType = $tx_ty; fn build(self) -> Result<$tx_ty> { let uses_predicates = self.is_using_predicates(); - let (base_offset, consensus_parameters) = if uses_predicates { - let consensus_params = self - .consensus_parameters - .ok_or(error!( - TransactionBuildError, - "predicate inputs require consensus parameters. Use `.set_consensus_parameters()`."))?; - (self.base_offset(&consensus_params), consensus_params) + let base_offset = if uses_predicates { + self.base_offset() } else { - // If no ConsensusParameters have been set, we can use the default instead of - // erroring out since the tx doesn't use predicates - (0, self.consensus_parameters.unwrap_or_default()) + 0 }; + let network_info = self.network_info.clone(); + let num_witnesses = self.num_witnesses()?; - let mut tx = - self.resolve_fuel_tx(base_offset, num_witnesses, &consensus_parameters)?; + let mut tx = self.resolve_fuel_tx(base_offset, num_witnesses)?; - tx.precompute(&consensus_parameters.chain_id)?; + tx.precompute(&network_info.chain_id())?; if uses_predicates { - // TODO: Fetch `GasCosts` from the `fuel-core`: - // https://github.com/FuelLabs/fuel-core/issues/1221 - tx.estimate_predicates(&consensus_parameters, &GasCosts::default())?; + estimate_predicates(&mut tx, &network_info)?; }; Ok($tx_ty { tx }) @@ -94,12 +109,17 @@ macro_rules! impl_tx_trait { fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey) { let index_offset = self.unresolved_signatures.secret_keys.len() as u8; self.unresolved_signatures.secret_keys.push(secret_key); - self.unresolved_signatures.addr_idx_offset_map.insert(owner, index_offset); + self.unresolved_signatures + .addr_idx_offset_map + .insert(owner, index_offset); } - fn fee_checked_from_tx(&self, params: &ConsensusParameters) -> Option { - let tx = &self.clone().build().expect("error in build").tx; - TransactionFee::checked_from_tx(params, tx) + fn fee_checked_from_tx( + &self, + params: &ConsensusParameters, + ) -> Result> { + let tx = self.clone().build()?.tx; + Ok(TransactionFee::checked_from_tx(params, &tx)) } fn with_maturity(mut self, maturity: u32) -> Self { @@ -108,19 +128,20 @@ macro_rules! impl_tx_trait { } fn with_gas_price(mut self, gas_price: u64) -> Self { - self.gas_price = gas_price; + self.gas_price = Some(gas_price); self } fn with_gas_limit(mut self, gas_limit: u64) -> Self { - self.gas_limit = gas_limit; + self.gas_limit = Some(gas_limit); self } - fn with_tx_params(self, tx_params: TxParameters) -> Self { - self.with_gas_limit(tx_params.gas_limit()) - .with_gas_price(tx_params.gas_price()) - .with_maturity(tx_params.maturity().into()) + fn with_tx_params(mut self, tx_params: TxParameters) -> Self { + self.gas_limit = tx_params.gas_limit(); + self.gas_price = tx_params.gas_price(); + + self.with_maturity(tx_params.maturity().into()) } fn with_inputs(mut self, inputs: Vec) -> Self { @@ -138,14 +159,6 @@ macro_rules! impl_tx_trait { self } - fn with_consensus_parameters( - mut self, - consensus_parameters: ConsensusParameters, - ) -> Self { - self.consensus_parameters = Some(consensus_parameters); - self - } - fn inputs(&self) -> &Vec { self.inputs.as_ref() } @@ -179,12 +192,13 @@ macro_rules! impl_tx_trait { } fn num_witnesses(&self) -> Result { - let num_witnesses = self - .witnesses() - .len(); + let num_witnesses = self.witnesses().len(); if num_witnesses + self.unresolved_signatures.secret_keys.len() > 256 { - return Err(error!(InvalidData, "tx can not have more than 256 witnesses")); + return Err(error!( + InvalidData, + "tx can not have more than 256 witnesses" + )); } Ok(num_witnesses as u8) @@ -193,24 +207,24 @@ macro_rules! impl_tx_trait { }; } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct ScriptTransactionBuilder { - pub gas_price: u64, - pub gas_limit: u64, + pub gas_price: Option, + pub gas_limit: Option, pub maturity: u32, pub script: Vec, pub script_data: Vec, pub inputs: Vec, pub outputs: Vec, pub witnesses: Vec, - pub(crate) consensus_parameters: Option, + pub(crate) network_info: NetworkInfo, unresolved_signatures: UnresolvedSignatures, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct CreateTransactionBuilder { - pub gas_price: u64, - pub gas_limit: u64, + pub gas_price: Option, + pub gas_limit: Option, pub maturity: u32, pub bytecode_length: u64, pub bytecode_witness_index: u8, @@ -219,7 +233,7 @@ pub struct CreateTransactionBuilder { pub outputs: Vec, pub witnesses: Vec, pub salt: Salt, - pub(crate) consensus_parameters: Option, + pub(crate) network_info: NetworkInfo, unresolved_signatures: UnresolvedSignatures, } @@ -227,15 +241,28 @@ impl_tx_trait!(ScriptTransactionBuilder, ScriptTransaction); impl_tx_trait!(CreateTransactionBuilder, CreateTransaction); impl ScriptTransactionBuilder { - fn resolve_fuel_tx( - self, - base_offset: usize, - num_witnesses: u8, - consensus_parameters: &ConsensusParameters, - ) -> Result