diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/Cargo.toml b/docs/beta-4/fuels-rs/packages/fuels-accounts/Cargo.toml new file mode 100644 index 00000000..323f99ab --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "fuels-accounts" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +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 } +fuel-core = { workspace = true, default-features = false, optional = true } +fuel-core-client = { workspace = true, features = ["default"] } +fuel-crypto = { workspace = true, features = ["random"] } +fuel-tx = { workspace = true } +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"] } + +[dev-dependencies] +hex = { workspace = true, default-features = false, features = ["std"] } +tempfile = { workspace = true } + +[features] +default = ["std"] +std = ["fuels-core/std"] diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/src/accounts_utils.rs b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/accounts_utils.rs new file mode 100644 index 00000000..fb39660e --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/accounts_utils.rs @@ -0,0 +1,79 @@ +use fuel_tx::{ConsensusParameters, Output, Receipt}; +use fuel_types::MessageId; +use fuels_core::{ + constants::BASE_ASSET_ID, + types::{ + bech32::Bech32Address, + errors::{error, Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + }, +}; + +pub fn extract_message_id(receipts: &[Receipt]) -> Option { + receipts.iter().find_map(|m| m.message_id()) +} + +pub fn calculate_base_amount_with_fee( + tb: &impl TransactionBuilder, + consensus_params: &ConsensusParameters, + previous_base_amount: u64, +) -> Result { + let transaction_fee = tb + .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; + + // If the tx doesn't consume any UTXOs, attempting to repeat it will lead to an + // error due to non unique tx ids (e.g. repeated contract call with configured gas cost of 0). + // Here we enforce a minimum amount on the base asset to avoid this + let is_consuming_utxos = tb + .inputs() + .iter() + .any(|input| !matches!(input, Input::Contract { .. })); + const MIN_AMOUNT: u64 = 1; + if !is_consuming_utxos && new_base_amount == 0 { + new_base_amount = MIN_AMOUNT; + } + + Ok(new_base_amount) +} + +// Replace the current base asset inputs of a tx builder with the provided ones. +// Only signed resources and coin predicates are replaced, the remaining inputs are kept. +// Messages that contain data are also kept since we don't know who will consume the data. +pub fn adjust_inputs( + tb: &mut impl TransactionBuilder, + new_base_inputs: impl IntoIterator, +) { + let adjusted_inputs = tb + .inputs() + .iter() + .filter(|input| { + input.contains_data() + || !matches!(input , Input::ResourceSigned { resource , .. } + | Input::ResourcePredicate { resource, .. } if resource.asset_id() == BASE_ASSET_ID) + }) + .cloned() + .chain(new_base_inputs) + .collect(); + + *tb.inputs_mut() = adjusted_inputs +} + +pub fn adjust_outputs( + tb: &mut impl TransactionBuilder, + address: &Bech32Address, + new_base_amount: u64, +) { + let is_base_change_present = tb.outputs().iter().any(|output| { + matches!(output , Output::Change { asset_id , .. } + if asset_id == & BASE_ASSET_ID) + }); + + if !is_base_change_present && new_base_amount != 0 { + tb.outputs_mut() + .push(Output::change(address.into(), 0, BASE_ASSET_ID)); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/src/lib.rs b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/lib.rs new file mode 100644 index 00000000..97faf8e8 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/lib.rs @@ -0,0 +1,422 @@ +use std::{collections::HashMap, fmt::Display}; + +use async_trait::async_trait; +use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest}; +#[doc(no_inline)] +pub use fuel_crypto; +use fuel_crypto::Signature; +use fuel_tx::{Output, Receipt, TxId, TxPointer, UtxoId}; +use fuel_types::{AssetId, Bytes32, ContractId, MessageId}; +use fuels_core::{ + constants::BASE_ASSET_ID, + types::{ + bech32::{Bech32Address, Bech32ContractId}, + coin::Coin, + coin_type::CoinType, + errors::{Error, Result}, + input::Input, + message::Message, + transaction::TxParameters, + transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, + transaction_response::TransactionResponse, + }, +}; +use provider::ResourceFilter; + +use crate::{accounts_utils::extract_message_id, provider::Provider}; + +mod accounts_utils; +pub mod predicate; +pub mod provider; +pub mod wallet; + +/// Trait for signing transactions and messages +/// +/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Signer: std::fmt::Debug + Send + Sync { + type Error: std::error::Error + Send + Sync; + + async fn sign_message>( + &self, + message: S, + ) -> std::result::Result; + + /// Signs the transaction + fn sign_transaction(&self, message: &mut impl TransactionBuilder); +} + +#[derive(Debug)] +pub struct AccountError(String); + +impl AccountError { + pub fn no_provider() -> Self { + Self("No provider was setup: make sure to set_provider in your account!".to_string()) + } +} + +impl Display for AccountError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for AccountError {} + +impl From for Error { + fn from(e: AccountError) -> Self { + Error::AccountError(e.0) + } +} + +type AccountResult = std::result::Result; + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone { + fn address(&self) -> &Bech32Address; + + fn try_provider(&self) -> AccountResult<&Provider>; + + async fn get_transactions( + &self, + request: PaginationRequest, + ) -> Result> { + Ok(self + .try_provider()? + .get_transactions_by_owner(self.address(), request) + .await?) + } + + /// Gets all unspent coins of asset `asset_id` owned by the account. + async fn get_coins(&self, asset_id: AssetId) -> Result> { + Ok(self + .try_provider()? + .get_coins(self.address(), asset_id) + .await?) + } + + /// Get the balance of all spendable coins `asset_id` for address `address`. This is different + /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead + /// of the UTXOs. + async fn get_asset_balance(&self, asset_id: &AssetId) -> Result { + self.try_provider()? + .get_asset_balance(self.address(), *asset_id) + .await + .map_err(Into::into) + } + + /// Gets all unspent messages owned by the account. + async fn get_messages(&self) -> Result> { + Ok(self.try_provider()?.get_messages(self.address()).await?) + } + + /// Get all the spendable balances of all assets for the account. This is different from getting + /// the coins because we are only returning the sum of UTXOs coins amount and not the UTXOs + /// coins themselves. + async fn get_balances(&self) -> Result> { + self.try_provider()? + .get_balances(self.address()) + .await + .map_err(Into::into) + } + + // /// Get some spendable resources (coins and messages) of asset `asset_id` owned by the account + // /// that add up at least to amount `amount`. The returned coins (UTXOs) are actual coins that + // /// can be spent. The number of UXTOs is optimized to prevent dust accumulation. + async fn get_spendable_resources( + &self, + asset_id: AssetId, + amount: u64, + ) -> Result> { + let filter = ResourceFilter { + from: self.address().clone(), + asset_id, + amount, + ..Default::default() + }; + self.try_provider()? + .get_spendable_resources(filter) + .await + .map_err(Into::into) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Account: ViewOnlyAccount { + /// Returns a vector consisting of `Input::Coin`s and `Input::Message`s for the given + /// asset ID and amount. The `witness_index` is the position of the witness (signature) + /// in the transaction's list of witnesses. In the validation process, the node will + /// use the witness at this index to validate the coins returned by this method. + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + ) -> Result>; + + /// Returns a vector containing the output coin and change output given an asset and amount + fn get_asset_outputs_for_amount( + &self, + to: &Bech32Address, + asset_id: AssetId, + amount: u64, + ) -> Vec { + vec![ + Output::coin(to.into(), amount, asset_id), + // Note that the change will be computed by the node. + // Here we only have to tell the node who will own the change and its asset ID. + Output::change(self.address().into(), 0, asset_id), + ] + } + + async fn add_fee_resources( + &self, + tb: Tb, + previous_base_amount: u64, + ) -> Result; + + /// Transfer funds from this account to another `Address`. + /// Fails if amount for asset ID is larger than address's spendable coins. + /// Returns the transaction ID that was sent and the list of receipts. + async fn transfer( + &self, + to: &Bech32Address, + amount: u64, + asset_id: AssetId, + tx_parameters: TxParameters, + ) -> Result<(TxId, Vec)> { + let provider = self.try_provider()?; + + 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, outputs, tx_parameters) + .with_consensus_parameters(consensus_parameters); + + // if we are not transferring the base asset, previous base amount is 0 + let previous_base_amount = if asset_id == AssetId::default() { + amount + } else { + 0 + }; + + let tx = self + .add_fee_resources(tx_builder, previous_base_amount) + .await?; + + let tx_id = provider.send_transaction(tx).await?; + let receipts = provider.get_receipts(&tx_id).await?; + + Ok((tx_id, receipts)) + } + + /// Unconditionally transfers `balance` of type `asset_id` to + /// the contract at `to`. + /// Fails if balance for `asset_id` is larger than this account's spendable balance. + /// Returns the corresponding transaction ID and the list of receipts. + /// + /// CAUTION !!! + /// + /// This will transfer coins to a contract, possibly leading + /// to the PERMANENT LOSS OF COINS if not used with care. + async fn force_transfer_to_contract( + &self, + to: &Bech32ContractId, + balance: u64, + asset_id: AssetId, + tx_parameters: TxParameters, + ) -> std::result::Result<(String, Vec), Error> { + let provider = self.try_provider()?; + + let zeroes = Bytes32::zeroed(); + let plain_contract_id: ContractId = to.into(); + + let mut inputs = vec![Input::contract( + UtxoId::new(zeroes, 0), + zeroes, + zeroes, + TxPointer::default(), + plain_contract_id, + )]; + + inputs.extend(self.get_asset_inputs_for_amount(asset_id, balance).await?); + + let outputs = vec![ + Output::contract(0, zeroes, zeroes), + Output::change(self.address().into(), 0, asset_id), + ]; + + // Build transaction and sign it + let params = provider.consensus_parameters(); + + let tb = ScriptTransactionBuilder::prepare_contract_transfer( + plain_contract_id, + balance, + asset_id, + inputs, + outputs, + tx_parameters, + ) + .with_consensus_parameters(params); + + // if we are not transferring the base asset, previous base amount is 0 + let base_amount = if asset_id == AssetId::default() { + balance + } else { + 0 + }; + + let tx = self.add_fee_resources(tb, base_amount).await?; + + let tx_id = provider.send_transaction(tx).await?; + let receipts = provider.get_receipts(&tx_id).await?; + + Ok((tx_id.to_string(), receipts)) + } + + /// Withdraws an amount of the base asset to + /// an address on the base chain. + /// Returns the transaction ID, message ID and the list of receipts. + async fn withdraw_to_base_layer( + &self, + to: &Bech32Address, + amount: u64, + tx_parameters: TxParameters, + ) -> std::result::Result<(TxId, MessageId, Vec), Error> { + let provider = self.try_provider()?; + + let inputs = self + .get_asset_inputs_for_amount(BASE_ASSET_ID, amount) + .await?; + + let tb = ScriptTransactionBuilder::prepare_message_to_output( + to.into(), + amount, + inputs, + tx_parameters, + ); + + 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 message_id = extract_message_id(&receipts) + .expect("MessageId could not be retrieved from tx receipts."); + + Ok((tx_id, message_id, receipts)) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use fuel_crypto::{Message, SecretKey}; + use fuel_tx::{Address, Output}; + use fuels_core::types::transaction::Transaction; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + use super::*; + use crate::wallet::WalletUnlocked; + + #[tokio::test] + async fn sign_and_verify() -> std::result::Result<(), Box> { + // ANCHOR: sign_message + let mut rng = StdRng::seed_from_u64(2322u64); + let mut secret_seed = [0u8; 32]; + rng.fill_bytes(&mut secret_seed); + + let secret = secret_seed + .as_slice() + .try_into() + .expect("The seed size is valid"); + + // Create a wallet using the private key created above. + let wallet = WalletUnlocked::new_from_private_key(secret, None); + + let message = "my message"; + + let signature = wallet.sign_message(message).await?; + + // Check if signature is what we expect it to be + assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?); + + // Recover address that signed the message + let message = Message::new(message); + let recovered_address = signature.recover(&message)?; + + assert_eq!(wallet.address().hash(), recovered_address.hash()); + + // Verify signature + signature.verify(&recovered_address, &message)?; + // ANCHOR_END: sign_message + + Ok(()) + } + + #[tokio::test] + async fn sign_tx_and_verify() -> std::result::Result<(), Box> { + // ANCHOR: sign_tx + let secret = SecretKey::from_str( + "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1", + )?; + let wallet = WalletUnlocked::new_from_private_key(secret, None); + + // Set up a transaction + let mut tb = { + let input_coin = Input::ResourceSigned { + resource: CoinType::Coin(Coin { + amount: 10000000, + owner: wallet.address().clone(), + ..Default::default() + }), + }; + + let output_coin = Output::coin( + Address::from_str( + "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077", + )?, + 1, + Default::default(), + ); + + ScriptTransactionBuilder::prepare_transfer( + vec![input_coin], + vec![output_coin], + Default::default(), + ) + }; + + // Sign the transaction + wallet.sign_transaction(&mut tb); // Add the private key to the transaction builder + let tx = tb.build()?; // Resolve signatures and add corresponding witness indexes + + // Extract the signature from the tx witnesses + let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?; + let tx_signature = Signature::from_bytes(bytes); + + // Sign the transaction manually + let message = Message::from_bytes(*tx.id(0.into())); + let signature = Signature::sign(&wallet.private_key, &message); + + // Check if the signatures are the same + assert_eq!(signature, tx_signature); + + // Check if the signature is what we expect it to be + assert_eq!(signature, Signature::from_str("d7027be16db0aada625ac8cd438f9b6187bd74465495ba39511c1ad72b7bb10af4ef582c94cc33433f7a1eb4f2ad21c471473947f5f645e90924ba273e2cee7f")?); + + // Recover the address that signed the transaction + let recovered_address = signature.recover(&message)?; + + assert_eq!(wallet.address().hash(), recovered_address.hash()); + + // Verify the signature + signature.verify(&recovered_address, &message)?; + // ANCHOR_END: sign_tx + + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/src/predicate.rs b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/predicate.rs new file mode 100644 index 00000000..510d00db --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/predicate.rs @@ -0,0 +1,163 @@ +use std::{fmt::Debug, fs}; + +use fuel_tx::ConsensusParameters; +use fuel_types::AssetId; +use fuels_core::{ + constants::BASE_ASSET_ID, + types::{ + bech32::Bech32Address, errors::Result, input::Input, + transaction_builders::TransactionBuilder, unresolved_bytes::UnresolvedBytes, + }, + Configurables, +}; + +use crate::{ + accounts_utils::{adjust_inputs, adjust_outputs, calculate_base_amount_with_fee}, + provider::Provider, + Account, AccountError, AccountResult, ViewOnlyAccount, +}; + +#[derive(Debug, Clone)] +pub struct Predicate { + address: Bech32Address, + code: Vec, + data: UnresolvedBytes, + provider: Option, +} + +impl Predicate { + pub fn address(&self) -> &Bech32Address { + &self.address + } + + pub fn code(&self) -> &Vec { + &self.code + } + + pub fn data(&self) -> &UnresolvedBytes { + &self.data + } + + pub fn provider(&self) -> Option<&Provider> { + self.provider.as_ref() + } + + pub fn set_provider(&mut self, provider: Provider) { + self.address = Self::calculate_address(&self.code, provider.chain_id().into()); + self.provider = Some(provider); + } + + pub fn with_provider(self, provider: Provider) -> Self { + let address = Self::calculate_address(&self.code, provider.chain_id().into()); + Self { + address, + provider: Some(provider), + ..self + } + } + + pub fn calculate_address(code: &[u8], chain_id: u64) -> Bech32Address { + fuel_tx::Input::predicate_owner(code, &chain_id.into()).into() + } + + fn consensus_parameters(&self) -> ConsensusParameters { + self.provider() + .map(|p| p.consensus_parameters()) + .unwrap_or_default() + } + + /// Uses default `ConsensusParameters` + pub fn from_code(code: Vec) -> Self { + Self { + address: Self::calculate_address(&code, ConsensusParameters::default().chain_id.into()), + code, + data: Default::default(), + provider: None, + } + } + + /// Uses default `ConsensusParameters` + pub fn load_from(file_path: &str) -> Result { + let code = fs::read(file_path)?; + Ok(Self::from_code(code)) + } + + pub fn with_data(mut self, data: UnresolvedBytes) -> Self { + self.data = data; + self + } + + pub fn with_code(self, code: Vec) -> Self { + let address = Self::calculate_address(&code, self.consensus_parameters().chain_id.into()); + Self { + code, + address, + ..self + } + } + + pub fn with_configurables(mut self, configurables: impl Into) -> Self { + let configurables: Configurables = configurables.into(); + configurables.update_constants_in(&mut self.code); + let address = + Self::calculate_address(&self.code, self.consensus_parameters().chain_id.into()); + self.address = address; + self + } +} + +impl ViewOnlyAccount for Predicate { + fn address(&self) -> &Bech32Address { + self.address() + } + + fn try_provider(&self) -> AccountResult<&Provider> { + self.provider.as_ref().ok_or(AccountError::no_provider()) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Account for Predicate { + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + ) -> Result> { + Ok(self + .get_spendable_resources(asset_id, amount) + .await? + .into_iter() + .map(|resource| { + Input::resource_predicate(resource, self.code.clone(), self.data.clone()) + }) + .collect::>()) + } + + /// Add base asset inputs to the transaction to cover the estimated fee. + /// The original base asset amount cannot be calculated reliably from + /// the existing transaction inputs because the selected resources may exceed + /// the required amount to avoid dust. Therefore we require it as an argument. + /// + /// Requires contract inputs to be at the start of the transactions inputs vec + /// so that their indexes are retained + async fn add_fee_resources( + &self, + mut tb: Tb, + 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)?; + + let new_base_inputs = self + .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) + .await?; + + adjust_inputs(&mut tb, new_base_inputs); + adjust_outputs(&mut tb, self.address(), new_base_amount); + + tb.build() + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/src/provider.rs b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/provider.rs new file mode 100644 index 00000000..46662108 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/provider.rs @@ -0,0 +1,617 @@ +use std::{collections::HashMap, fmt::Debug, io}; + +use chrono::{DateTime, Utc}; +#[cfg(feature = "fuel-core-lib")] +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}, + types::{ + bech32::{Bech32Address, Bech32ContractId}, + block::Block, + chain_info::ChainInfo, + coin::Coin, + coin_type::CoinType, + errors::{error, Error, Result}, + message::Message, + message_proof::MessageProof, + node_info::NodeInfo, + transaction::Transaction, + transaction_response::TransactionResponse, + }, +}; +use tai64::Tai64; +use thiserror::Error; + +type ProviderResult = std::result::Result; + +#[derive(Debug)] +pub struct TransactionCost { + pub min_gas_price: u64, + pub gas_price: u64, + pub gas_used: u64, + pub metered_bytes_size: u64, + pub total_fee: u64, +} + +pub(crate) struct ResourceQueries { + utxos: Vec, + messages: Vec, + asset_id: AssetId, + amount: u64, +} + +impl ResourceQueries { + pub fn new( + utxo_ids: Vec, + message_nonces: Vec, + asset_id: AssetId, + amount: u64, + ) -> Self { + Self { + utxos: utxo_ids, + messages: message_nonces, + asset_id, + amount, + } + } + + pub fn exclusion_query(&self) -> Option<(Vec, Vec)> { + if self.utxos.is_empty() && self.messages.is_empty() { + return None; + } + + Some((self.utxos.clone(), self.messages.clone())) + } + + pub fn spend_query(&self) -> Vec<(AssetId, u64, Option)> { + vec![(self.asset_id, self.amount, None)] + } +} + +// ANCHOR: resource_filter +pub struct ResourceFilter { + pub from: Bech32Address, + pub asset_id: AssetId, + pub amount: u64, + pub excluded_utxos: Vec, + pub excluded_message_nonces: Vec, +} +// ANCHOR_END: resource_filter + +impl ResourceFilter { + pub fn owner(&self) -> Address { + (&self.from).into() + } + + pub(crate) fn resource_queries(&self) -> ResourceQueries { + ResourceQueries::new( + self.excluded_utxos.clone(), + self.excluded_message_nonces.clone(), + self.asset_id, + self.amount, + ) + } +} + +impl Default for ResourceFilter { + fn default() -> Self { + Self { + from: Default::default(), + asset_id: BASE_ASSET_ID, + amount: Default::default(), + excluded_utxos: Default::default(), + excluded_message_nonces: Default::default(), + } + } +} + +#[derive(Debug, Error)] +pub enum ProviderError { + // Every IO error in the context of Provider comes from the gql client + #[error(transparent)] + ClientRequestError(#[from] io::Error), +} + +impl From for Error { + fn from(e: ProviderError) -> Self { + Error::ProviderError(e.to_string()) + } +} + +/// Encapsulates common client operations in the SDK. +/// Note that you may also use `client`, which is an instance +/// of `FuelClient`, directly, which provides a broader API. +#[derive(Debug, Clone)] +pub struct Provider { + pub client: FuelClient, + pub consensus_parameters: ConsensusParameters, +} + +impl Provider { + pub fn new(client: FuelClient, consensus_parameters: ConsensusParameters) -> Self { + Self { + client, + consensus_parameters, + } + } + + /// Sends a transaction to the underlying Provider's client. + pub async fn send_transaction(&self, tx: T) -> Result { + let tolerance = 0.0; + let TransactionCost { + gas_used, + min_gas_price, + .. + } = self + .estimate_transaction_cost(tx.clone(), Some(tolerance)) + .await?; + + if gas_used > tx.gas_limit() { + return Err(error!( + ProviderError, + "gas_limit({}) is lower than the estimated gas_used({})", + tx.gas_limit(), + gas_used + )); + } else if min_gas_price > tx.gas_price() { + return Err(error!( + ProviderError, + "gas_price({}) is lower than the required min_gas_price({})", + tx.gas_price(), + min_gas_price + )); + } + + let chain_info = self.chain_info().await?; + tx.check_without_signatures( + chain_info.latest_block.header.height, + &self.consensus_parameters(), + )?; + + let tx_id = self.submit_tx(tx.clone()).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`"); + + return Err(Error::RevertTransactionError { + reason: reason.to_string(), + revert_id, + receipts: receipts.to_owned(), + }); + } + + 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) + } + + #[cfg(feature = "fuel-core-lib")] + /// Launches a local `fuel-core` network based on provided config. + pub async fn launch(config: Config) -> Result { + let srv = FuelService::new_node(config).await.unwrap(); + 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()) + } + + pub fn consensus_parameters(&self) -> ConsensusParameters { + self.consensus_parameters + } + + pub fn chain_id(&self) -> ChainId { + self.consensus_parameters.chain_id + } + + pub async fn node_info(&self) -> ProviderResult { + Ok(self.client.node_info().await?.into()) + } + + pub async fn checked_dry_run(&self, tx: T) -> Result> { + let receipts = self.dry_run(tx).await?; + Self::has_script_succeeded(&receipts)?; + + Ok(receipts) + } + + fn has_script_succeeded(receipts: &[Receipt]) -> Result<()> { + receipts + .iter() + .find_map(|receipt| match receipt { + Receipt::ScriptResult { result, .. } + if *result != ScriptExecutionResult::Success => + { + Some(format!("{result:?}")) + } + _ => None, + }) + .map(|error_message| { + Err(Error::RevertTransactionError { + reason: error_message, + revert_id: 0, + receipts: receipts.to_owned(), + }) + }) + .unwrap_or(Ok(())) + } + + pub async fn dry_run(&self, tx: T) -> Result> { + let receipts = self.client.dry_run(&tx.into()).await?; + + Ok(receipts) + } + + pub async fn dry_run_no_validation(&self, tx: T) -> Result> { + let receipts = self.client.dry_run_opt(&tx.into(), Some(false)).await?; + + Ok(receipts) + } + + /// Gets all unspent coins owned by address `from`, with asset ID `asset_id`. + pub async fn get_coins( + &self, + from: &Bech32Address, + asset_id: AssetId, + ) -> ProviderResult> { + let mut coins: Vec = vec![]; + + let mut cursor = None; + + loop { + let res = self + .client + .coins( + &from.into(), + Some(&asset_id), + PaginationRequest { + cursor: cursor.clone(), + results: 100, + direction: PageDirection::Forward, + }, + ) + .await?; + + if res.results.is_empty() { + break; + } + coins.extend(res.results.into_iter().map(Into::into)); + cursor = res.cursor; + } + + Ok(coins) + } + + /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to + /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number + /// of coins (UXTOs) is optimized to prevent dust accumulation. + pub async fn get_spendable_resources( + &self, + filter: ResourceFilter, + ) -> ProviderResult> { + let queries = filter.resource_queries(); + + let res = self + .client + .coins_to_spend( + &filter.owner(), + queries.spend_query(), + queries.exclusion_query(), + ) + .await? + .into_iter() + .flatten() + .map(|c| CoinType::try_from(c).map_err(ProviderError::ClientRequestError)) + .collect::>>()?; + Ok(res) + } + + /// Get the balance of all spendable coins `asset_id` for address `address`. This is different + /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead + /// of the UTXOs. + pub async fn get_asset_balance( + &self, + address: &Bech32Address, + asset_id: AssetId, + ) -> ProviderResult { + self.client + .balance(&address.into(), Some(&asset_id)) + .await + .map_err(Into::into) + } + + /// Get the balance of all spendable coins `asset_id` for contract with id `contract_id`. + pub async fn get_contract_asset_balance( + &self, + contract_id: &Bech32ContractId, + asset_id: AssetId, + ) -> ProviderResult { + self.client + .contract_balance(&contract_id.into(), Some(&asset_id)) + .await + .map_err(Into::into) + } + + /// Get all the spendable balances of all assets for address `address`. This is different from + /// getting the coins because we are only returning the numbers (the sum of UTXOs coins amount + /// for each asset id) and not the UTXOs coins themselves + pub async fn get_balances( + &self, + address: &Bech32Address, + ) -> ProviderResult> { + // We don't paginate results because there are likely at most ~100 different assets in one + // wallet + let pagination = PaginationRequest { + cursor: None, + results: 9999, + direction: PageDirection::Forward, + }; + let balances_vec = self + .client + .balances(&address.into(), pagination) + .await? + .results; + let balances = balances_vec + .into_iter() + .map( + |Balance { + owner: _, + amount, + asset_id, + }| (asset_id.to_string(), amount), + ) + .collect(); + Ok(balances) + } + + /// Get all balances of all assets for the contract with id `contract_id`. + pub async fn get_contract_balances( + &self, + contract_id: &Bech32ContractId, + ) -> ProviderResult> { + // We don't paginate results because there are likely at most ~100 different assets in one + // wallet + let pagination = PaginationRequest { + cursor: None, + results: 9999, + direction: PageDirection::Forward, + }; + + let balances_vec = self + .client + .contract_balances(&contract_id.into(), pagination) + .await? + .results; + let balances = balances_vec + .into_iter() + .map( + |ContractBalance { + contract: _, + amount, + asset_id, + }| (asset_id, amount), + ) + .collect(); + Ok(balances) + } + + pub async fn get_transaction_by_id( + &self, + tx_id: &TxId, + ) -> ProviderResult> { + Ok(self.client.transaction(tx_id).await?.map(Into::into)) + } + + pub async fn get_transactions( + &self, + request: PaginationRequest, + ) -> ProviderResult> { + let pr = self.client.transactions(request).await?; + + Ok(PaginatedResult { + cursor: pr.cursor, + results: pr.results.into_iter().map(Into::into).collect(), + has_next_page: pr.has_next_page, + has_previous_page: pr.has_previous_page, + }) + } + + // Get transaction(s) by owner + pub async fn get_transactions_by_owner( + &self, + owner: &Bech32Address, + request: PaginationRequest, + ) -> ProviderResult> { + let pr = self + .client + .transactions_by_owner(&owner.into(), request) + .await?; + + Ok(PaginatedResult { + cursor: pr.cursor, + results: pr.results.into_iter().map(Into::into).collect(), + has_next_page: pr.has_next_page, + has_previous_page: pr.has_previous_page, + }) + } + + pub async fn latest_block_height(&self) -> ProviderResult { + Ok(self.chain_info().await?.latest_block.header.height) + } + + pub async fn latest_block_time(&self) -> ProviderResult>> { + Ok(self.chain_info().await?.latest_block.header.time) + } + + pub async fn produce_blocks( + &self, + blocks_to_produce: u64, + start_time: Option>, + ) -> io::Result { + let start_time = start_time.map(|time| Tai64::from_unix(time.timestamp()).0); + self.client + .produce_blocks(blocks_to_produce, start_time) + .await + .map(Into::into) + } + + /// Get block by id. + pub async fn block(&self, block_id: &Bytes32) -> ProviderResult> { + let block = self.client.block(block_id).await?.map(Into::into); + Ok(block) + } + + // - Get block(s) + pub async fn get_blocks( + &self, + request: PaginationRequest, + ) -> ProviderResult> { + let pr = self.client.blocks(request).await?; + + Ok(PaginatedResult { + cursor: pr.cursor, + results: pr.results.into_iter().map(Into::into).collect(), + has_next_page: pr.has_next_page, + has_previous_page: pr.has_previous_page, + }) + } + + pub async fn estimate_transaction_cost( + &self, + tx: T, + tolerance: Option, + ) -> Result { + let NodeInfo { min_gas_price, .. } = self.node_info().await?; + let gas_price = std::cmp::max(tx.gas_price(), min_gas_price); + 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 gas_used = self + .get_gas_used_with_tolerance(dry_run_tx.clone(), tolerance) + .await?; + + // Update the tx with estimated gas_used and correct gas price to calculate the total_fee + let dry_run_tx = dry_run_tx + .with_gas_price(gas_price) + .with_gas_limit(gas_used); + + let transaction_fee = dry_run_tx + .fee_checked_from_tx(&self.consensus_parameters) + .expect("Error calculating TransactionFee"); + + Ok(TransactionCost { + min_gas_price, + gas_price, + gas_used, + metered_bytes_size: dry_run_tx.metered_bytes_size() as u64, + total_fee: transaction_fee.max_fee(), + }) + } + + // Remove limits from an existing Transaction to get an accurate gas estimation + 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 + async fn get_gas_used_with_tolerance( + &self, + tx: T, + tolerance: f64, + ) -> Result { + let gas_used = self.get_gas_used(&self.dry_run_no_validation(tx).await?); + Ok((gas_used as f64 * (1.0 + tolerance)) as u64) + } + + fn get_gas_used(&self, receipts: &[Receipt]) -> u64 { + receipts + .iter() + .rfind(|r| matches!(r, Receipt::ScriptResult { .. })) + .map(|script_result| { + script_result + .gas_used() + .expect("could not retrieve gas used from ScriptResult") + }) + .unwrap_or(0) + } + + pub async fn get_messages(&self, from: &Bech32Address) -> ProviderResult> { + let pagination = PaginationRequest { + cursor: None, + results: 100, + direction: PageDirection::Forward, + }; + Ok(self + .client + .messages(Some(&from.into()), pagination) + .await? + .results + .into_iter() + .map(Into::into) + .collect()) + } + + pub async fn get_message_proof( + &self, + tx_id: &TxId, + message_id: &MessageId, + commit_block_id: Option<&Bytes32>, + commit_block_height: Option, + ) -> ProviderResult> { + let proof = self + .client + .message_proof( + tx_id, + message_id, + commit_block_id.map(Into::into), + commit_block_height.map(Into::into), + ) + .await? + .map(Into::into); + Ok(proof) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-accounts/src/wallet.rs b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/wallet.rs new file mode 100644 index 00000000..4aaa9f51 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-accounts/src/wallet.rs @@ -0,0 +1,416 @@ +use std::{fmt, ops, path::Path}; + +use async_trait::async_trait; +use elliptic_curve::rand_core; +use eth_keystore::KeystoreError; +use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; +use fuels_core::{ + constants::BASE_ASSET_ID, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + AssetId, + }, +}; +use rand::{CryptoRng, Rng}; +use thiserror::Error; + +use crate::{ + accounts_utils::{adjust_inputs, adjust_outputs, calculate_base_amount_with_fee}, + provider::{Provider, ProviderError}, + Account, AccountError, AccountResult, Signer, ViewOnlyAccount, +}; + +pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'"; + +#[derive(Error, Debug)] +/// Error thrown by the Wallet module +pub enum WalletError { + /// Error propagated from the hex crate. + #[error(transparent)] + Hex(#[from] hex::FromHexError), + /// Error propagated by parsing of a slice + #[error("Failed to parse slice")] + Parsing(#[from] std::array::TryFromSliceError), + /// Keystore error + #[error(transparent)] + KeystoreError(#[from] KeystoreError), + #[error(transparent)] + FuelCrypto(#[from] fuel_crypto::Error), + #[error(transparent)] + ProviderError(#[from] ProviderError), + #[error("Called `try_provider` method on wallet where no provider was set up")] + NoProviderError, +} + +impl From for Error { + fn from(e: WalletError) -> Self { + Error::WalletError(e.to_string()) + } +} + +type WalletResult = std::result::Result; + +/// A FuelVM-compatible wallet that can be used to list assets, balances and more. +/// +/// Note that instances of the `Wallet` type only know their public address, and as a result can +/// only perform read-only operations. +/// +/// In order to sign messages or send transactions, a `Wallet` must first call [`Wallet::unlock`] +/// with a valid private key to produce a [`WalletUnlocked`]. +#[derive(Clone)] +pub struct Wallet { + /// The wallet's address. The wallet's address is derived + /// from the first 32 bytes of SHA-256 hash of the wallet's public key. + pub(crate) address: Bech32Address, + provider: Option, +} + +/// A `WalletUnlocked` is equivalent to a [`Wallet`] whose private key is known and stored +/// alongside in-memory. Knowing the private key allows a `WalletUlocked` to sign operations, send +/// transactions, and more. +#[derive(Clone, Debug)] +pub struct WalletUnlocked { + wallet: Wallet, + pub(crate) private_key: SecretKey, +} + +impl Wallet { + /// Construct a Wallet from its given public address. + pub fn from_address(address: Bech32Address, provider: Option) -> Self { + Self { address, provider } + } + + pub fn provider(&self) -> Option<&Provider> { + self.provider.as_ref() + } + + pub fn set_provider(&mut self, provider: Provider) { + self.provider = Some(provider); + } + + pub fn address(&self) -> &Bech32Address { + &self.address + } + + /// Unlock the wallet with the given `private_key`. + /// + /// The private key will be stored in memory until `wallet.lock()` is called or until the + /// wallet is `drop`ped. + pub fn unlock(self, private_key: SecretKey) -> WalletUnlocked { + WalletUnlocked { + wallet: self, + private_key, + } + } +} + +impl ViewOnlyAccount for Wallet { + fn address(&self) -> &Bech32Address { + self.address() + } + + fn try_provider(&self) -> AccountResult<&Provider> { + self.provider.as_ref().ok_or(AccountError::no_provider()) + } +} + +impl WalletUnlocked { + /// Lock the wallet by `drop`ping the private key from memory. + pub fn lock(self) -> Wallet { + self.wallet + } + + // NOTE: Rather than providing a `DerefMut` implementation, we wrap the `set_provider` method + // directly. This is because we should not allow the user a `&mut` handle to the inner `Wallet` + // as this could lead to ending up with a `WalletUnlocked` in an inconsistent state (e.g. the + // private key doesn't match the inner wallet's public key). + pub fn set_provider(&mut self, provider: Provider) { + self.wallet.set_provider(provider); + } + + /// Creates a new wallet with a random private key. + pub fn new_random(provider: Option) -> Self { + let mut rng = rand::thread_rng(); + let private_key = SecretKey::random(&mut rng); + Self::new_from_private_key(private_key, provider) + } + + /// Creates a new wallet from the given private key. + pub fn new_from_private_key(private_key: SecretKey, provider: Option) -> Self { + let public = PublicKey::from(&private_key); + let hashed = public.hash(); + let address = Bech32Address::new(FUEL_BECH32_HRP, hashed); + Wallet::from_address(address, provider).unlock(private_key) + } + + /// Creates a new wallet from a mnemonic phrase. + /// The default derivation path is used. + pub fn new_from_mnemonic_phrase( + phrase: &str, + provider: Option, + ) -> WalletResult { + let path = format!("{DEFAULT_DERIVATION_PATH_PREFIX}/0'/0/0"); + Self::new_from_mnemonic_phrase_with_path(phrase, provider, &path) + } + + /// Creates a new wallet from a mnemonic phrase. + /// It takes a path to a BIP32 derivation path. + pub fn new_from_mnemonic_phrase_with_path( + phrase: &str, + provider: Option, + path: &str, + ) -> WalletResult { + let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, path)?; + + Ok(Self::new_from_private_key(secret_key, provider)) + } + + /// Creates a new wallet and stores its encrypted version in the given path. + pub fn new_from_keystore( + dir: P, + rng: &mut R, + password: S, + provider: Option, + ) -> WalletResult<(Self, String)> + where + P: AsRef, + R: Rng + CryptoRng + rand_core::CryptoRng, + S: AsRef<[u8]>, + { + let (secret, uuid) = eth_keystore::new(dir, rng, password, None)?; + + let secret_key = + SecretKey::try_from(secret.as_slice()).expect("A new secret should be correct size"); + + let wallet = Self::new_from_private_key(secret_key, provider); + + Ok((wallet, uuid)) + } + + /// Encrypts the wallet's private key with the given password and saves it + /// to the given path. + pub fn encrypt(&self, dir: P, password: S) -> WalletResult + where + P: AsRef, + S: AsRef<[u8]>, + { + let mut rng = rand::thread_rng(); + + Ok(eth_keystore::encrypt_key( + dir, + &mut rng, + *self.private_key, + password, + None, + )?) + } + + /// Recreates a wallet from an encrypted JSON wallet given the provided path and password. + pub fn load_keystore( + keypath: P, + password: S, + provider: Option, + ) -> WalletResult + where + P: AsRef, + S: AsRef<[u8]>, + { + let secret = eth_keystore::decrypt_key(keypath, password)?; + let secret_key = SecretKey::try_from(secret.as_slice()) + .expect("Decrypted key should have a correct size"); + Ok(Self::new_from_private_key(secret_key, provider)) + } +} + +impl ViewOnlyAccount for WalletUnlocked { + fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + fn try_provider(&self) -> AccountResult<&Provider> { + self.provider.as_ref().ok_or(AccountError::no_provider()) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Account for WalletUnlocked { + /// Returns a vector consisting of `Input::Coin`s and `Input::Message`s for the given + /// asset ID and amount. The `witness_index` is the position of the witness (signature) + /// in the transaction's list of witnesses. In the validation process, the node will + /// use the witness at this index to validate the coins returned by this method. + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + ) -> Result> { + Ok(self + .get_spendable_resources(asset_id, amount) + .await? + .into_iter() + .map(Input::resource_signed) + .collect::>()) + } + + async fn add_fee_resources( + &self, + mut tb: Tb, + 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)?; + + let new_base_inputs = self + .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount) + .await?; + + adjust_inputs(&mut tb, new_base_inputs); + adjust_outputs(&mut tb, self.address(), new_base_amount); + + tb.build() + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for WalletUnlocked { + type Error = WalletError; + async fn sign_message>( + &self, + message: S, + ) -> WalletResult { + let message = Message::new(message); + let sig = Signature::sign(&self.private_key, &message); + Ok(sig) + } + + fn sign_transaction(&self, tb: &mut impl TransactionBuilder) { + tb.add_unresolved_signature(self.address().clone(), self.private_key); + } +} + +impl fmt::Debug for Wallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Wallet") + .field("address", &self.address) + .finish() + } +} + +impl ops::Deref for WalletUnlocked { + type Target = Wallet; + fn deref(&self) -> &Self::Target { + &self.wallet + } +} + +/// Generates a random mnemonic phrase given a random number generator and the number of words to +/// generate, `count`. +pub fn generate_mnemonic_phrase(rng: &mut R, count: usize) -> WalletResult { + Ok(fuel_crypto::generate_mnemonic_phrase(rng, count)?) +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use super::*; + + #[tokio::test] + async fn encrypted_json_keystore() -> Result<()> { + let dir = tempdir()?; + let mut rng = rand::thread_rng(); + + // Create a wallet to be stored in the keystore. + let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?; + + // sign a message using the above key. + let message = "Hello there!"; + let signature = wallet.sign_message(message).await?; + + // Read from the encrypted JSON keystore and decrypt it. + let path = Path::new(dir.path()).join(uuid); + let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?; + + // Sign the same message as before and assert that the signature is the same. + let signature2 = recovered_wallet.sign_message(message).await?; + assert_eq!(signature, signature2); + + // Remove tempdir. + assert!(std::fs::remove_file(&path).is_ok()); + Ok(()) + } + + #[tokio::test] + async fn mnemonic_generation() -> Result<()> { + let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 12)?; + + let _wallet = WalletUnlocked::new_from_mnemonic_phrase(&mnemonic, None)?; + Ok(()) + } + + #[tokio::test] + async fn wallet_from_mnemonic_phrase() -> Result<()> { + let phrase = + "oblige salon price punch saddle immune slogan rare snap desert retire surprise"; + + // Create first account from mnemonic phrase. + let wallet = + WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?; + + let expected_plain_address = + "df9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185"; + let expected_address = "fuel1m7wsumrvtaw6d6pwtcd809627ejzhk69pggvg0cvdyg2yynqqxzseuzply"; + + assert_eq!(wallet.address().hash().to_string(), expected_plain_address); + assert_eq!(wallet.address().to_string(), expected_address); + + // Create a second account from the same phrase. + let wallet2 = + WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/1'/0/0")?; + + let expected_second_plain_address = + "261191b0164a24fd0fd51566ec5e5b0b9ba8fb2d42dc9cf7dbbd6f23d2742759"; + let expected_second_address = + "fuel1ycgervqkfgj06r74z4nwchjmpwd637edgtwfea7mh4hj85n5yavszjk4cc"; + + assert_eq!( + wallet2.address().hash().to_string(), + expected_second_plain_address + ); + assert_eq!(wallet2.address().to_string(), expected_second_address); + + Ok(()) + } + + #[tokio::test] + async fn encrypt_and_store_wallet_from_mnemonic() -> Result<()> { + let dir = tempdir()?; + + let phrase = + "oblige salon price punch saddle immune slogan rare snap desert retire surprise"; + + // Create first account from mnemonic phrase. + let wallet = + WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?; + + let uuid = wallet.encrypt(&dir, "password")?; + + let path = Path::new(dir.path()).join(uuid); + + let recovered_wallet = WalletUnlocked::load_keystore(&path, "password", None)?; + + assert_eq!(wallet.address(), recovered_wallet.address()); + + // Remove tempdir. + assert!(std::fs::remove_file(&path).is_ok()); + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/Cargo.toml b/docs/beta-4/fuels-rs/packages/fuels-core/Cargo.toml new file mode 100644 index 00000000..f2e759eb --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "fuels-core" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +description = "Fuel Rust SDK core." + +[dependencies] +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 } +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 } + +[features] +default = ["std"] +std = ["dep:fuel-core-client"] diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/codec.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec.rs new file mode 100644 index 00000000..c70f4496 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec.rs @@ -0,0 +1,73 @@ +mod abi_decoder; +mod abi_encoder; +mod function_selector; + +pub use abi_decoder::*; +pub use abi_encoder::*; +pub use function_selector::*; + +use crate::{ + traits::{Parameterize, Tokenizable}, + types::errors::Result, +}; + +pub fn try_from_bytes(bytes: &[u8]) -> Result +where + T: Parameterize + Tokenizable, +{ + let token = ABIDecoder::decode_single(&T::param_type(), bytes)?; + + T::from_token(token) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + constants::WORD_SIZE, + types::{Address, AssetId, ContractId}, + }; + + #[test] + 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)?; + + assert_eq!(the_tuple, (1, 2)); + + Ok(()) + } + + #[test] + 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); + + Ok(()) + } + + #[test] + 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()?) + ); + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_decoder.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_decoder.rs new file mode 100644 index 00000000..ecacff9e --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_decoder.rs @@ -0,0 +1,810 @@ +use std::{convert::TryInto, str}; + +use fuel_types::bytes::padded_len_usize; + +use crate::{ + constants::WORD_SIZE, + traits::Tokenizable, + types::{ + enum_variants::EnumVariants, + errors::{error, Error, Result}, + param_types::ParamType, + StaticStringToken, Token, U256, + }, +}; + +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)] +struct DecodeResult { + token: Token, + bytes_read: usize, +} + +pub struct ABIDecoder; + +impl ABIDecoder { + /// Decodes types described by `param_types` into their respective `Token`s + /// using the data in `bytes` and `receipts`. + /// + /// # Arguments + /// + /// * `param_types`: The ParamType's of the types we expect are encoded + /// inside `bytes` and `receipts`. + /// * `bytes`: The bytes to be used in the decoding process. + /// # Examples + /// + /// ``` + /// use fuels_core::codec::ABIDecoder; + /// use fuels_core::types::{param_types::ParamType, Token}; + /// + /// 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(); + /// + /// assert_eq!(tokens, vec![Token::U8(1), Token::U8(2)]) + /// ``` + 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, + }) + } + + 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) + /// + /// # 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(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..]) + } +} + +#[cfg(test)] +mod tests { + use std::vec; + + use super::*; + + #[test] + fn decode_int() -> Result<()> { + let data = [0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff]; + + let decoded = ABIDecoder::decode_single(&ParamType::U32, &data)?; + + assert_eq!(decoded, Token::U32(u32::MAX)); + Ok(()) + } + + #[test] + fn decode_multiple_int() -> Result<()> { + let types = vec![ + ParamType::U32, + ParamType::U8, + ParamType::U16, + ParamType::U64, + ]; + let data = [ + 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![ + Token::U32(u32::MAX), + Token::U8(u8::MAX), + Token::U16(u16::MAX), + Token::U64(u64::MAX), + ]; + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_bool() -> Result<()> { + let types = vec![ParamType::Bool, ParamType::Bool]; + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00, + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![Token::Bool(true), Token::Bool(false)]; + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_b256() -> Result<()> { + let data = [ + 0xd5, 0x57, 0x9c, 0x46, 0xdf, 0xcc, 0x7f, 0x18, 0x20, 0x70, 0x13, 0xe6, 0x5b, 0x44, + 0xe4, 0xcb, 0x4e, 0x2c, 0x22, 0x98, 0xf4, 0xac, 0x45, 0x7b, 0xa8, 0xf8, 0x27, 0x43, + 0xf3, 0x1e, 0x93, 0xb, + ]; + + let decoded = ABIDecoder::decode_single(&ParamType::B256, &data)?; + + assert_eq!(decoded, Token::B256(data)); + Ok(()) + } + + #[test] + fn decode_string_array() -> Result<()> { + let types = vec![ParamType::StringArray(23), ParamType::StringArray(5)]; + let data = [ + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, // This is + 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x73, // a full s + 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x00, // entence + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00, // Hello + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![ + Token::StringArray(StaticStringToken::new( + "This is a full sentence".into(), + Some(23), + )), + Token::StringArray(StaticStringToken::new("Hello".into(), Some(5))), + ]; + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_string_slice() -> Result<()> { + let types = vec![ParamType::StringSlice]; + let data = [ + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, // This is + 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x73, // a full s + 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, // entence + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![Token::StringSlice(StaticStringToken::new( + "This is a full sentence".into(), + None, + ))]; + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_array() -> Result<()> { + // Create a parameter type for u8[2]. + let types = vec![ParamType::Array(Box::new(ParamType::U8), 2)]; + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![Token::Array(vec![Token::U8(255), Token::U8(42)])]; + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_struct() -> Result<()> { + // struct MyStruct { + // foo: u8, + // bar: bool, + // } + + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + ]; + let param_type = ParamType::Struct { + fields: vec![ParamType::U8, ParamType::Bool], + generics: vec![], + }; + + let decoded = ABIDecoder::decode_single(¶m_type, &data)?; + + let expected = Token::Struct(vec![Token::U8(1), Token::Bool(true)]); + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_bytes() -> Result<()> { + let data = [0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05]; + let decoded = ABIDecoder::decode_single(&ParamType::Bytes, &data)?; + + let expected = Token::Bytes(data.to_vec()); + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_enum() -> Result<()> { + // enum MyEnum { + // x: u32, + // y: bool, + // } + + let types = vec![ParamType::U32, ParamType::Bool]; + let inner_enum_types = EnumVariants::new(types)?; + let types = vec![ParamType::Enum { + variants: inner_enum_types.clone(), + generics: vec![], + }]; + + // "0" discriminant and 42 enum value + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, + ]; + + let decoded = ABIDecoder::decode(&types, &data)?; + + let expected = vec![Token::Enum(Box::new((0, Token::U32(42), inner_enum_types)))]; + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decoder_will_skip_enum_padding_and_decode_next_arg() -> Result<()> { + // struct MyStruct { + // par1: MyEnum, + // par2: u32 + // } + + // enum MyEnum { + // x: b256, + // y: u32, + // } + + let types = vec![ParamType::B256, ParamType::U32]; + let inner_enum_types = EnumVariants::new(types)?; + + let fields = vec![ + ParamType::Enum { + variants: inner_enum_types.clone(), + generics: vec![], + }, + ParamType::U32, + ]; + let struct_type = ParamType::Struct { + fields, + generics: vec![], + }; + + let enum_discriminant_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1]; + let enum_data_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30, 0x39]; + // this padding is due to the biggest variant of MyEnum being 3 WORDs bigger than the chosen variant + let enum_padding_enc = vec![0x0; 3 * WORD_SIZE]; + let struct_par2_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD4, 0x31]; + let data: Vec = vec![ + enum_discriminant_enc, + enum_padding_enc, + enum_data_enc, + struct_par2_enc, + ] + .into_iter() + .flatten() + .collect(); + + let decoded = ABIDecoder::decode_single(&struct_type, &data)?; + + let expected = Token::Struct(vec![ + Token::Enum(Box::new((1, Token::U32(12345), inner_enum_types))), + Token::U32(54321), + ]); + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn decode_nested_struct() -> Result<()> { + // struct Foo { + // x: u16, + // y: Bar, + // } + // + // struct Bar { + // a: bool, + // b: u8[2], + // } + + let fields = vec![ + ParamType::U16, + ParamType::Struct { + fields: vec![ + ParamType::Bool, + ParamType::Array(Box::new(ParamType::U8), 2), + ], + generics: vec![], + }, + ]; + let nested_struct = ParamType::Struct { + fields, + generics: vec![], + }; + + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + ]; + + let decoded = ABIDecoder::decode_single(&nested_struct, &data)?; + + let my_nested_struct = vec![ + Token::U16(10), + Token::Struct(vec![ + Token::Bool(true), + Token::Array(vec![Token::U8(1), Token::U8(2)]), + ]), + ]; + + assert_eq!(decoded, Token::Struct(my_nested_struct)); + Ok(()) + } + + #[test] + fn decode_comprehensive() -> Result<()> { + // struct Foo { + // x: u16, + // y: Bar, + // } + // + // struct Bar { + // a: bool, + // b: u8[2], + // } + + // fn: long_function(Foo,u8[2],b256,str[3],str) + + // Parameters + let fields = vec![ + ParamType::U16, + ParamType::Struct { + fields: vec![ + ParamType::Bool, + ParamType::Array(Box::new(ParamType::U8), 2), + ], + generics: vec![], + }, + ]; + let nested_struct = ParamType::Struct { + fields, + generics: vec![], + }; + + let u8_arr = ParamType::Array(Box::new(ParamType::U8), 2); + let b256 = ParamType::B256; + let s = ParamType::StringArray(3); + let ss = ParamType::StringSlice; + + let types = [nested_struct, u8_arr, b256, s, ss]; + + let bytes = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, // foo.x == 10u16 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // foo.y.a == true + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // foo.b.0 == 1u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // foo.b.1 == 2u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // u8[2].0 == 1u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // u8[2].0 == 2u8 + 0xd5, 0x57, 0x9c, 0x46, 0xdf, 0xcc, 0x7f, 0x18, // b256 + 0x20, 0x70, 0x13, 0xe6, 0x5b, 0x44, 0xe4, 0xcb, // b256 + 0x4e, 0x2c, 0x22, 0x98, 0xf4, 0xac, 0x45, 0x7b, // b256 + 0xa8, 0xf8, 0x27, 0x43, 0xf3, 0x1e, 0x93, 0xb, // b256 + 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, // str[3] + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, // str data + 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x73, // str data + 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, // str data + ]; + + let decoded = ABIDecoder::decode(&types, &bytes)?; + + // Expected tokens + let foo = Token::Struct(vec![ + Token::U16(10), + Token::Struct(vec![ + Token::Bool(true), + Token::Array(vec![Token::U8(1), Token::U8(2)]), + ]), + ]); + + let u8_arr = Token::Array(vec![Token::U8(1), Token::U8(2)]); + + let b256 = Token::B256([ + 0xd5, 0x57, 0x9c, 0x46, 0xdf, 0xcc, 0x7f, 0x18, 0x20, 0x70, 0x13, 0xe6, 0x5b, 0x44, + 0xe4, 0xcb, 0x4e, 0x2c, 0x22, 0x98, 0xf4, 0xac, 0x45, 0x7b, 0xa8, 0xf8, 0x27, 0x43, + 0xf3, 0x1e, 0x93, 0xb, + ]); + + let ss = Token::StringSlice(StaticStringToken::new( + "This is a full sentence".into(), + None, + )); + + let s = Token::StringArray(StaticStringToken::new("foo".into(), Some(3))); + + let expected: Vec = vec![foo, u8_arr, b256, s, ss]; + + assert_eq!(decoded, expected); + Ok(()) + } + + #[test] + fn units_in_structs_are_decoded_as_one_word() -> Result<()> { + let data = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]; + let struct_type = ParamType::Struct { + fields: vec![ParamType::Unit, ParamType::U64], + generics: vec![], + }; + + let actual = ABIDecoder::decode_single(&struct_type, &data)?; + + let expected = Token::Struct(vec![Token::Unit, Token::U64(u64::MAX)]); + assert_eq!(actual, expected); + Ok(()) + } + + #[test] + fn enums_with_all_unit_variants_are_decoded_from_one_word() -> Result<()> { + let data = [0, 0, 0, 0, 0, 0, 0, 1]; + let types = vec![ParamType::Unit, ParamType::Unit]; + let variants = EnumVariants::new(types)?; + let enum_w_only_units = ParamType::Enum { + variants: variants.clone(), + generics: vec![], + }; + + let result = ABIDecoder::decode_single(&enum_w_only_units, &data)?; + + let expected_enum = Token::Enum(Box::new((1, Token::Unit, variants))); + assert_eq!(result, expected_enum); + Ok(()) + } + + #[test] + fn out_of_bounds_discriminant_is_detected() -> Result<()> { + let data = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2]; + let types = vec![ParamType::U32]; + let variants = EnumVariants::new(types)?; + let enum_type = ParamType::Enum { + variants, + generics: vec![], + }; + + let result = ABIDecoder::decode_single(&enum_type, &data); + + let error = result.expect_err("Should have resulted in an error"); + + let expected_msg = "Discriminant '1' doesn't point to any variant: "; + assert!(matches!(error, Error::InvalidData(str) if str.starts_with(expected_msg))); + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_encoder.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_encoder.rs new file mode 100644 index 00000000..6d53e049 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/abi_encoder.rs @@ -0,0 +1,1203 @@ +use fuel_types::bytes::padded_len_usize; +use itertools::Itertools; + +use crate::{ + constants::WORD_SIZE, + types::{ + errors::Result, + pad_string, pad_u16, pad_u32, pad_u8, + unresolved_bytes::{Data, UnresolvedBytes}, + EnumSelector, StaticStringToken, Token, U256, + }, +}; + +pub struct ABIEncoder; + +impl ABIEncoder { + /// Encodes `Token`s in `args` following the ABI specs defined + /// [here](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md) + pub fn encode(args: &[Token]) -> Result { + let data = Self::encode_tokens(args)?; + + Ok(UnresolvedBytes::new(data)) + } + + fn encode_tokens(tokens: &[Token]) -> Result> { + tokens + .iter() + .map(Self::encode_token) + .flatten_ok() + .collect::>>() + } + + fn encode_token(arg: &Token) -> Result> { + let encoded_token = match arg { + Token::U8(arg_u8) => vec![Self::encode_u8(*arg_u8)], + Token::U16(arg_u16) => vec![Self::encode_u16(*arg_u16)], + Token::U32(arg_u32) => vec![Self::encode_u32(*arg_u32)], + Token::U64(arg_u64) => vec![Self::encode_u64(*arg_u64)], + Token::U128(arg_u128) => vec![Self::encode_u128(*arg_u128)], + Token::U256(arg_u256) => vec![Self::encode_u256(*arg_u256)], + Token::Bool(arg_bool) => vec![Self::encode_bool(*arg_bool)], + Token::B256(arg_bits256) => vec![Self::encode_b256(arg_bits256)], + Token::Array(arg_array) => Self::encode_array(arg_array)?, + Token::Vector(data) => Self::encode_vector(data)?, + Token::StringSlice(arg_string) => Self::encode_string_slice(arg_string)?, + Token::StringArray(arg_string) => vec![Self::encode_string_array(arg_string)?], + Token::Struct(arg_struct) => Self::encode_struct(arg_struct)?, + Token::Enum(arg_enum) => Self::encode_enum(arg_enum)?, + Token::Tuple(arg_tuple) => Self::encode_tuple(arg_tuple)?, + Token::Unit => vec![Self::encode_unit()], + Token::RawSlice(data) => Self::encode_raw_slice(data)?, + Token::Bytes(data) => Self::encode_bytes(data.to_vec())?, + // `String` in Sway has the same memory layout as the bytes type + Token::String(string) => Self::encode_bytes(string.clone().into_bytes())?, + }; + + Ok(encoded_token) + } + + fn encode_unit() -> Data { + Data::Inline(vec![0; WORD_SIZE]) + } + + fn encode_tuple(arg_tuple: &[Token]) -> Result> { + Self::encode_tokens(arg_tuple) + } + + fn encode_struct(subcomponents: &[Token]) -> Result> { + Self::encode_tokens(subcomponents) + } + + fn encode_array(arg_array: &[Token]) -> Result> { + Self::encode_tokens(arg_array) + } + + fn encode_b256(arg_bits256: &[u8; 32]) -> Data { + Data::Inline(arg_bits256.to_vec()) + } + + fn encode_bool(arg_bool: bool) -> Data { + Data::Inline(pad_u8(u8::from(arg_bool)).to_vec()) + } + + fn encode_u128(arg_u128: u128) -> Data { + Data::Inline(arg_u128.to_be_bytes().to_vec()) + } + + fn encode_u256(arg_u256: U256) -> Data { + let mut bytes = [0u8; 32]; + arg_u256.to_big_endian(&mut bytes); + Data::Inline(bytes.to_vec()) + } + + fn encode_u64(arg_u64: u64) -> Data { + Data::Inline(arg_u64.to_be_bytes().to_vec()) + } + + fn encode_u32(arg_u32: u32) -> Data { + Data::Inline(pad_u32(arg_u32).to_vec()) + } + + fn encode_u16(arg_u16: u16) -> Data { + Data::Inline(pad_u16(arg_u16).to_vec()) + } + + fn encode_u8(arg_u8: u8) -> Data { + Data::Inline(pad_u8(arg_u8).to_vec()) + } + + fn encode_enum(selector: &EnumSelector) -> Result> { + let (discriminant, token_within_enum, variants) = selector; + + let mut encoded_enum = vec![Self::encode_discriminant(*discriminant)]; + + // Enums that contain only Units as variants have only their discriminant encoded. + if !variants.only_units_inside() { + let variant_param_type = variants.param_type_of_variant(*discriminant)?; + let padding_amount = variants.compute_padding_amount(variant_param_type); + + encoded_enum.push(Data::Inline(vec![0; padding_amount])); + + let token_data = Self::encode_token(token_within_enum)?; + encoded_enum.extend(token_data); + } + + Ok(encoded_enum) + } + + fn encode_discriminant(discriminant: u8) -> Data { + Self::encode_u8(discriminant) + } + + fn encode_vector(data: &[Token]) -> Result> { + let encoded_data = Self::encode_tokens(data)?; + let cap = data.len() as u64; + let len = data.len() as u64; + + // A vector is expected to be encoded as 3 WORDs -- a ptr, a cap and a + // len. This means that we must place the encoded vector elements + // somewhere else. Hence the use of Data::Dynamic which will, when + // resolved, leave behind in its place only a pointer to the actual + // data. + Ok(vec![ + Data::Dynamic(encoded_data), + Self::encode_u64(cap), + Self::encode_u64(len), + ]) + } + + fn encode_raw_slice(data: &[u64]) -> Result> { + let encoded_data = data + .iter() + .map(|&word| Self::encode_u64(word)) + .collect::>(); + + let num_bytes = data.len() * WORD_SIZE; + + let len = Self::encode_u64(num_bytes as u64); + Ok(vec![Data::Dynamic(encoded_data), len]) + } + + fn encode_string_slice(arg_string: &StaticStringToken) -> Result> { + let encoded_data = Data::Inline(arg_string.get_encodable_str()?.as_bytes().to_vec()); + + let num_bytes = arg_string.get_encodable_str()?.len(); + let len = Self::encode_u64(num_bytes as u64); + Ok(vec![Data::Dynamic(vec![encoded_data]), len]) + } + + fn encode_string_array(arg_string: &StaticStringToken) -> Result { + Ok(Data::Inline(pad_string(arg_string.get_encodable_str()?))) + } + + fn encode_bytes(mut data: Vec) -> Result> { + let len = data.len(); + + zeropad_to_word_alignment(&mut data); + + let cap = data.len() as u64; + let encoded_data = vec![Data::Inline(data)]; + + Ok(vec![ + Data::Dynamic(encoded_data), + Self::encode_u64(cap), + Self::encode_u64(len as u64), + ]) + } +} + +fn zeropad_to_word_alignment(data: &mut Vec) { + let padded_length = padded_len_usize(data.len()); + data.resize(padded_length, 0); +} + +#[cfg(test)] +mod tests { + use std::slice; + + use itertools::chain; + use sha2::{Digest, Sha256}; + + use super::*; + use crate::{ + codec::first_four_bytes_of_sha256_hash, + types::{enum_variants::EnumVariants, param_types::ParamType}, + }; + + const VEC_METADATA_SIZE: usize = 3 * WORD_SIZE; + const DISCRIMINANT_SIZE: usize = WORD_SIZE; + + #[test] + fn encode_function_signature() { + let fn_signature = "entry_one(u64)"; + + let result = first_four_bytes_of_sha256_hash(fn_signature); + + println!("Encoded function selector for ({fn_signature}): {result:#0x?}"); + + assert_eq!(result, [0x0, 0x0, 0x0, 0x0, 0x0c, 0x36, 0xcb, 0x9c]); + } + + #[test] + fn encode_function_with_u32_type() -> Result<()> { + // @todo eventually we must update the json abi examples in here. + // They're in the old format. + // + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"u32"}], + // "name":"entry_one", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "entry_one(u32)"; + let arg = Token::U32(u32::MAX); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0xb7, 0x9e, 0xf7, 0x43]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_u32_type_multiple_args() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"first","type":"u32"},{"name":"second","type":"u32"}], + // "name":"takes_two", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_two(u32,u32)"; + let first = Token::U32(u32::MAX); + let second = Token::U32(u32::MAX); + + let args: Vec = vec![first, second]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, + ]; + + let expected_fn_selector = [0x0, 0x0, 0x0, 0x0, 0xa7, 0x07, 0xb0, 0x8e]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_fn_selector); + Ok(()) + } + + #[test] + fn encode_function_with_u64_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"u64"}], + // "name":"entry_one", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "entry_one(u64)"; + let arg = Token::U64(u64::MAX); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x0c, 0x36, 0xcb, 0x9c]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_bool_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"bool"}], + // "name":"bool_check", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "bool_check(bool)"; + let arg = Token::Bool(true); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x66, 0x8f, 0xff, 0x58]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_two_different_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"first","type":"u32"},{"name":"second","type":"bool"}], + // "name":"takes_two_types", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_two_types(u32,bool)"; + let first = Token::U32(u32::MAX); + let second = Token::Bool(true); + + let args: Vec = vec![first, second]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0xf5, 0x40, 0x73, 0x2b]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_bits256_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"b256"}], + // "name":"takes_bits256", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_bits256(b256)"; + + let mut hasher = Sha256::new(); + hasher.update("test string".as_bytes()); + + let arg = hasher.finalize(); + + let arg = Token::B256(arg.into()); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [ + 0xd5, 0x57, 0x9c, 0x46, 0xdf, 0xcc, 0x7f, 0x18, 0x20, 0x70, 0x13, 0xe6, 0x5b, 0x44, + 0xe4, 0xcb, 0x4e, 0x2c, 0x22, 0x98, 0xf4, 0xac, 0x45, 0x7b, 0xa8, 0xf8, 0x27, 0x43, + 0xf3, 0x1e, 0x93, 0xb, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x01, 0x49, 0x42, 0x96]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_array_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"u8[3]"}], + // "name":"takes_integer_array", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_integer_array(u8[3])"; + + // Keeping the construction of the arguments array separate for better readability. + let first = Token::U8(1); + let second = Token::U8(2); + let third = Token::U8(3); + + let arg = vec![first, second, third]; + let arg_array = Token::Array(arg); + + let args: Vec = vec![arg_array]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x2c, 0x5a, 0x10, 0x2e]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_string_array_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"str[23]"}], + // "name":"takes_string", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_string(str[23])"; + + let args: Vec = vec![Token::StringArray(StaticStringToken::new( + "This is a full sentence".into(), + Some(23), + ))]; + + let expected_encoded_abi = [ + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, + 0x20, 0x73, 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x00, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0xd5, 0x6e, 0x76, 0x51]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_string_slice_type() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"str"}], + // "name":"takes_string", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_string(str)"; + + let args: Vec = vec![Token::StringSlice(StaticStringToken::new( + "This is a full sentence".into(), + None, + ))]; + + let expected_encoded_abi = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // str at data index 16 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, // str of lenght 23 + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, // + 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x73, // + 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, // + ]; + + let expected_function_selector = [0, 0, 0, 0, 239, 77, 222, 230]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_struct() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"MyStruct"}], + // "name":"takes_my_struct", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_my_struct(MyStruct)"; + + // struct MyStruct { + // foo: u8, + // bar: bool, + // } + + let foo = Token::U8(1); + let bar = Token::Bool(true); + + // Create the custom struct token using the array of tuples above + let arg = Token::Struct(vec![foo, bar]); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0xa8, 0x1e, 0x8d, 0xd7]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_function_with_enum() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"MyEnum"}], + // "name":"takes_my_enum", + // "outputs": [] + // } + // ] + // "#; + + let fn_signature = "takes_my_enum(MyEnum)"; + + // enum MyEnum { + // x: u32, + // y: bool, + // } + let types = vec![ParamType::U32, ParamType::Bool]; + let params = EnumVariants::new(types)?; + + // An `EnumSelector` indicating that we've chosen the first Enum variant, + // whose value is 42 of the type ParamType::U32 and that the Enum could + // have held any of the other types present in `params`. + + let enum_selector = Box::new((0, Token::U32(42), params)); + + let arg = Token::Enum(enum_selector); + + let args: Vec = vec![arg]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x35, 0x5c, 0xa6, 0xfa]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + // The encoding follows the ABI specs defined [here](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md) + #[test] + fn enums_are_sized_to_fit_the_biggest_variant() -> Result<()> { + // Our enum has two variants: B256, and U64. So the enum will set aside + // 256b of space or 4 WORDS because that is the space needed to fit the + // largest variant(B256). + let types = vec![ParamType::B256, ParamType::U64]; + let enum_variants = EnumVariants::new(types)?; + let enum_selector = Box::new((1, Token::U64(42), enum_variants)); + + let encoded = ABIEncoder::encode(slice::from_ref(&Token::Enum(enum_selector)))?.resolve(0); + + let enum_discriminant_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1]; + let u64_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a]; + let enum_padding = vec![0x0; 24]; + + // notice the ordering, first the discriminant, then the necessary + // padding and then the value itself. + let expected: Vec = [enum_discriminant_enc, enum_padding, u64_enc] + .into_iter() + .flatten() + .collect(); + + assert_eq!(hex::encode(expected), hex::encode(encoded)); + Ok(()) + } + + #[test] + fn encoding_enums_with_deeply_nested_types() -> Result<()> { + /* + enum DeeperEnum { + v1: bool, + v2: str[10] + } + */ + let types = vec![ParamType::Bool, ParamType::StringArray(10)]; + let deeper_enum_variants = EnumVariants::new(types)?; + let deeper_enum_token = + Token::StringArray(StaticStringToken::new("0123456789".into(), Some(10))); + + let str_enc = vec![ + b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, + ]; + let deeper_enum_discriminant_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1]; + + /* + struct StructA { + some_enum: DeeperEnum + some_number: u32 + } + */ + + let fields = vec![ + ParamType::Enum { + variants: deeper_enum_variants.clone(), + generics: vec![], + }, + ParamType::Bool, + ]; + let struct_a_type = ParamType::Struct { + fields, + generics: vec![], + }; + + let struct_a_token = Token::Struct(vec![ + Token::Enum(Box::new((1, deeper_enum_token, deeper_enum_variants))), + Token::U32(11332), + ]); + let some_number_enc = vec![0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2c, 0x44]; + + /* + enum TopLevelEnum { + v1: StructA, + v2: bool, + v3: u64 + } + */ + + let types = vec![struct_a_type, ParamType::Bool, ParamType::U64]; + let top_level_enum_variants = EnumVariants::new(types)?; + let top_level_enum_token = + Token::Enum(Box::new((0, struct_a_token, top_level_enum_variants))); + let top_lvl_discriminant_enc = vec![0x0; 8]; + + let encoded = ABIEncoder::encode(slice::from_ref(&top_level_enum_token))?.resolve(0); + + let correct_encoding: Vec = [ + top_lvl_discriminant_enc, + deeper_enum_discriminant_enc, + str_enc, + some_number_enc, + ] + .into_iter() + .flatten() + .collect(); + + assert_eq!(hex::encode(correct_encoding), hex::encode(encoded)); + Ok(()) + } + + #[test] + fn encode_function_with_nested_structs() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type":"function", + // "inputs": [{"name":"arg","type":"Foo"}], + // "name":"takes_my_nested_struct", + // "outputs": [] + // } + // ] + // "#; + + // struct Foo { + // x: u16, + // y: Bar, + // } + // + // struct Bar { + // a: bool, + // b: u8[2], + // } + + let fn_signature = "takes_my_nested_struct(Foo)"; + + let args: Vec = vec![Token::Struct(vec![ + Token::U16(10), + Token::Struct(vec![ + Token::Bool(true), + Token::Array(vec![Token::U8(1), Token::U8(2)]), + ]), + ])]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0xea, 0x0a, 0xfd, 0x23]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + println!("Encoded ABI for ({fn_signature}): {encoded:#0x?}"); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn encode_comprehensive_function() -> Result<()> { + // let json_abi = + // r#" + // [ + // { + // "type": "contract", + // "inputs": [ + // { + // "name": "arg", + // "type": "Foo" + // }, + // { + // "name": "arg2", + // "type": "u8[2]" + // }, + // { + // "name": "arg3", + // "type": "b256" + // }, + // { + // "name": "arg", + // "type": "str[23]" + // } + // ], + // "name": "long_function", + // "outputs": [] + // } + // ] + // "#; + + // struct Foo { + // x: u16, + // y: Bar, + // } + // + // struct Bar { + // a: bool, + // b: u8[2], + // } + + let fn_signature = "long_function(Foo,u8[2],b256,str[23])"; + + let foo = Token::Struct(vec![ + Token::U16(10), + Token::Struct(vec![ + Token::Bool(true), + Token::Array(vec![Token::U8(1), Token::U8(2)]), + ]), + ]); + + let u8_arr = Token::Array(vec![Token::U8(1), Token::U8(2)]); + + let mut hasher = Sha256::new(); + hasher.update("test string".as_bytes()); + + let b256 = Token::B256(hasher.finalize().into()); + + let s = Token::StringArray(StaticStringToken::new( + "This is a full sentence".into(), + Some(23), + )); + + let args: Vec = vec![foo, u8_arr, b256, s]; + + let expected_encoded_abi = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, // foo.x == 10u16 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // foo.y.a == true + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // foo.b.0 == 1u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // foo.b.1 == 2u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // u8[2].0 == 1u8 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // u8[2].0 == 2u8 + 0xd5, 0x57, 0x9c, 0x46, 0xdf, 0xcc, 0x7f, 0x18, // b256 + 0x20, 0x70, 0x13, 0xe6, 0x5b, 0x44, 0xe4, 0xcb, // b256 + 0x4e, 0x2c, 0x22, 0x98, 0xf4, 0xac, 0x45, 0x7b, // b256 + 0xa8, 0xf8, 0x27, 0x43, 0xf3, 0x1e, 0x93, 0xb, // b256 + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, // str[23] + 0x61, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x73, // str[23] + 0x65, 0x6e, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x0, // str[23] + ]; + + let expected_function_selector = [0x0, 0x0, 0x0, 0x0, 0x10, 0x93, 0xb2, 0x12]; + + let encoded_function_selector = first_four_bytes_of_sha256_hash(fn_signature); + + let encoded = ABIEncoder::encode(&args)?.resolve(0); + + assert_eq!(hex::encode(expected_encoded_abi), hex::encode(encoded)); + assert_eq!(encoded_function_selector, expected_function_selector); + Ok(()) + } + + #[test] + fn enums_with_only_unit_variants_are_encoded_in_one_word() -> Result<()> { + let expected = [0, 0, 0, 0, 0, 0, 0, 1]; + + let types = vec![ParamType::Unit, ParamType::Unit]; + let enum_selector = Box::new((1, Token::Unit, EnumVariants::new(types)?)); + + let actual = ABIEncoder::encode(&[Token::Enum(enum_selector)])?.resolve(0); + + assert_eq!(actual, expected); + Ok(()) + } + + #[test] + fn units_in_composite_types_are_encoded_in_one_word() -> Result<()> { + let expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5]; + + let actual = + ABIEncoder::encode(&[Token::Struct(vec![Token::Unit, Token::U32(5)])])?.resolve(0); + + assert_eq!(actual, expected); + Ok(()) + } + + #[test] + fn enums_with_units_are_correctly_padded() -> Result<()> { + let discriminant = vec![0, 0, 0, 0, 0, 0, 0, 1]; + let padding = vec![0; 32]; + let expected: Vec = [discriminant, padding].into_iter().flatten().collect(); + + let types = vec![ParamType::B256, ParamType::Unit]; + let enum_selector = Box::new((1, Token::Unit, EnumVariants::new(types)?)); + + let actual = ABIEncoder::encode(&[Token::Enum(enum_selector)])?.resolve(0); + + assert_eq!(actual, expected); + Ok(()) + } + + #[test] + fn vector_has_ptr_cap_len_and_then_data() -> Result<()> { + // arrange + let offset: u8 = 150; + let token = Token::Vector(vec![Token::U64(5)]); + + // act + let result = ABIEncoder::encode(&[token])?.resolve(offset as u64); + + // assert + let ptr = [0, 0, 0, 0, 0, 0, 0, 3 * WORD_SIZE as u8 + offset]; + let cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let len = [0, 0, 0, 0, 0, 0, 0, 1]; + let data = [0, 0, 0, 0, 0, 0, 0, 5]; + + let expected = chain!(ptr, cap, len, data).collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn data_from_two_vectors_aggregated_at_the_end() -> Result<()> { + // arrange + let offset: u8 = 40; + let vec_1 = Token::Vector(vec![Token::U64(5)]); + let vec_2 = Token::Vector(vec![Token::U64(6)]); + + // act + let result = ABIEncoder::encode(&[vec_1, vec_2])?.resolve(offset as u64); + + // assert + let vec1_data_offset = 6 * WORD_SIZE as u8 + offset; + let vec1_ptr = [0, 0, 0, 0, 0, 0, 0, vec1_data_offset]; + let vec1_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_len = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_data = [0, 0, 0, 0, 0, 0, 0, 5]; + + let vec2_data_offset = vec1_data_offset + vec1_data.len() as u8; + let vec2_ptr = [0, 0, 0, 0, 0, 0, 0, vec2_data_offset]; + let vec2_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec2_len = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec2_data = [0, 0, 0, 0, 0, 0, 0, 6]; + + let expected = chain!( + vec1_ptr, vec1_cap, vec1_len, vec2_ptr, vec2_cap, vec2_len, vec1_data, vec2_data, + ) + .collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn a_vec_in_an_enum() -> Result<()> { + // arrange + let offset = 40; + let types = vec![ParamType::B256, ParamType::Vector(Box::new(ParamType::U64))]; + let variants = EnumVariants::new(types)?; + let selector = (1, Token::Vector(vec![Token::U64(5)]), variants); + let token = Token::Enum(Box::new(selector)); + + // act + let result = ABIEncoder::encode(&[token])?.resolve(offset as u64); + + // assert + let discriminant = vec![0, 0, 0, 0, 0, 0, 0, 1]; + + const PADDING: usize = std::mem::size_of::<[u8; 32]>() - VEC_METADATA_SIZE; + + let vec1_ptr = ((DISCRIMINANT_SIZE + PADDING + VEC_METADATA_SIZE + offset) as u64) + .to_be_bytes() + .to_vec(); + let vec1_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_len = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_data = [0, 0, 0, 0, 0, 0, 0, 5]; + + let expected = chain!( + discriminant, + vec![0; PADDING], + vec1_ptr, + vec1_cap, + vec1_len, + vec1_data + ) + .collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn an_enum_in_a_vec() -> Result<()> { + // arrange + let offset = 40; + let types = vec![ParamType::B256, ParamType::U8]; + let variants = EnumVariants::new(types)?; + let selector = (1, Token::U8(8), variants); + let enum_token = Token::Enum(Box::new(selector)); + + let vec_token = Token::Vector(vec![enum_token]); + + // act + let result = ABIEncoder::encode(&[vec_token])?.resolve(offset as u64); + + // assert + const PADDING: usize = std::mem::size_of::<[u8; 32]>() - WORD_SIZE; + + let vec1_ptr = ((VEC_METADATA_SIZE + offset) as u64).to_be_bytes().to_vec(); + let vec1_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_len = [0, 0, 0, 0, 0, 0, 0, 1]; + let discriminant = 1u64.to_be_bytes(); + let vec1_data = chain!(discriminant, [0; PADDING], 8u64.to_be_bytes()).collect::>(); + + let expected = chain!(vec1_ptr, vec1_cap, vec1_len, vec1_data).collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn a_vec_in_a_struct() -> Result<()> { + // arrange + let offset = 40; + let token = Token::Struct(vec![Token::Vector(vec![Token::U64(5)]), Token::U8(9)]); + + // act + let result = ABIEncoder::encode(&[token])?.resolve(offset as u64); + + // assert + let vec1_ptr = ((VEC_METADATA_SIZE + WORD_SIZE + offset) as u64) + .to_be_bytes() + .to_vec(); + let vec1_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_len = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_data = [0, 0, 0, 0, 0, 0, 0, 5]; + + let expected = chain!( + vec1_ptr, + vec1_cap, + vec1_len, + [0, 0, 0, 0, 0, 0, 0, 9], + vec1_data + ) + .collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn a_vec_in_a_vec() -> Result<()> { + // arrange + let offset = 40; + let token = Token::Vector(vec![Token::Vector(vec![Token::U8(5), Token::U8(6)])]); + + // act + let result = ABIEncoder::encode(&[token])?.resolve(offset as u64); + + // assert + let vec1_data_offset = (VEC_METADATA_SIZE + offset) as u64; + let vec1_ptr = vec1_data_offset.to_be_bytes().to_vec(); + let vec1_cap = [0, 0, 0, 0, 0, 0, 0, 1]; + let vec1_len = [0, 0, 0, 0, 0, 0, 0, 1]; + + let vec2_ptr = (vec1_data_offset + VEC_METADATA_SIZE as u64) + .to_be_bytes() + .to_vec(); + let vec2_cap = [0, 0, 0, 0, 0, 0, 0, 2]; + let vec2_len = [0, 0, 0, 0, 0, 0, 0, 2]; + let vec2_data = [0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6]; + + let vec1_data = chain!(vec2_ptr, vec2_cap, vec2_len, vec2_data).collect::>(); + + let expected = chain!(vec1_ptr, vec1_cap, vec1_len, vec1_data).collect::>(); + + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn encoding_bytes() -> Result<()> { + // arrange + let token = Token::Bytes(vec![1, 2, 3]); + let offset = 40; + + // act + let encoded_bytes = ABIEncoder::encode(&[token])?.resolve(offset); + + // assert + let ptr = [0, 0, 0, 0, 0, 0, 0, 64]; + let cap = [0, 0, 0, 0, 0, 0, 0, 8]; + let len = [0, 0, 0, 0, 0, 0, 0, 3]; + let data = [1, 2, 3, 0, 0, 0, 0, 0]; + + let expected_encoded_bytes = [ptr, cap, len, data].concat(); + + assert_eq!(expected_encoded_bytes, encoded_bytes); + + Ok(()) + } + + #[test] + fn encoding_raw_slices() -> Result<()> { + // arrange + let token = Token::RawSlice(vec![1, 2, 3]); + let offset = 40; + + // act + let encoded_bytes = ABIEncoder::encode(&[token])?.resolve(offset); + + // assert + let ptr = vec![0, 0, 0, 0, 0, 0, 0, 56]; + let len = vec![0, 0, 0, 0, 0, 0, 0, 24]; + let data = [ + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 2], + [0, 0, 0, 0, 0, 0, 0, 3], + ] + .concat(); + + let expected_encoded_bytes = [ptr, len, data].concat(); + + assert_eq!(expected_encoded_bytes, encoded_bytes); + + Ok(()) + } + + #[test] + fn encoding_std_string() -> Result<()> { + // arrange + let string = String::from("This "); + let token = Token::String(string); + let offset = 40; + + // act + let encoded_std_string = ABIEncoder::encode(&[token])?.resolve(offset); + + // assert + let ptr = [0, 0, 0, 0, 0, 0, 0, 64]; + let cap = [0, 0, 0, 0, 0, 0, 0, 8]; + let len = [0, 0, 0, 0, 0, 0, 0, 5]; + let data = [0x54, 0x68, 0x69, 0x73, 0x20, 0, 0, 0]; + + let expected_encoded_std_string = [ptr, cap, len, data].concat(); + + assert_eq!(expected_encoded_std_string, encoded_std_string); + + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/function_selector.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/function_selector.rs new file mode 100644 index 00000000..2e04683e --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/codec/function_selector.rs @@ -0,0 +1,264 @@ +use sha2::{Digest, Sha256}; + +use crate::types::{param_types::ParamType, ByteArray}; + +/// Given a function name and its inputs will return a ByteArray representing +/// the function selector as specified in the Fuel specs. +pub fn resolve_fn_selector(name: &str, inputs: &[ParamType]) -> ByteArray { + let fn_signature = resolve_fn_signature(name, inputs); + + first_four_bytes_of_sha256_hash(&fn_signature) +} + +fn resolve_fn_signature(name: &str, inputs: &[ParamType]) -> String { + let fn_args = resolve_args(inputs); + + format!("{name}({fn_args})") +} + +fn resolve_args(arg: &[ParamType]) -> String { + arg.iter().map(resolve_arg).collect::>().join(",") +} + +fn resolve_arg(arg: &ParamType) -> String { + match &arg { + ParamType::U8 => "u8".to_owned(), + ParamType::U16 => "u16".to_owned(), + ParamType::U32 => "u32".to_owned(), + ParamType::U64 => "u64".to_owned(), + ParamType::U128 => "s(u64,u64)".to_owned(), + ParamType::U256 => "s(u64,u64,u64,u64)".to_owned(), + ParamType::Bool => "bool".to_owned(), + ParamType::B256 => "b256".to_owned(), + ParamType::Unit => "()".to_owned(), + ParamType::StringSlice => "str".to_owned(), + ParamType::StringArray(len) => { + format!("str[{len}]") + } + ParamType::Array(internal_type, len) => { + let inner = resolve_arg(internal_type); + format!("a[{inner};{len}]") + } + ParamType::Struct { + fields, generics, .. + } => { + let gen_params = resolve_args(generics); + let field_params = resolve_args(fields); + let gen_params = if !gen_params.is_empty() { + format!("<{gen_params}>") + } else { + gen_params + }; + format!("s{gen_params}({field_params})") + } + ParamType::Enum { + variants: fields, + generics, + .. + } => { + let gen_params = resolve_args(generics); + let field_params = resolve_args(fields.param_types()); + let gen_params = if !gen_params.is_empty() { + format!("<{gen_params}>") + } else { + gen_params + }; + format!("e{gen_params}({field_params})") + } + ParamType::Tuple(inner) => { + let inner = resolve_args(inner); + format!("({inner})") + } + ParamType::Vector(el_type) => { + let inner = resolve_arg(el_type); + format!("s<{inner}>(s<{inner}>(rawptr,u64),u64)") + } + ParamType::RawSlice => "rawslice".to_string(), + ParamType::Bytes => "s(s(rawptr,u64),u64)".to_string(), + ParamType::String => "s(s(s(rawptr,u64),u64))".to_string(), + } +} + +/// Hashes an encoded function selector using SHA256 and returns the first 4 bytes. +/// The function selector has to have been already encoded following the ABI specs defined +/// [here](https://github.com/FuelLabs/fuel-specs/blob/1be31f70c757d8390f74b9e1b3beb096620553eb/specs/protocol/abi.md) +pub(crate) fn first_four_bytes_of_sha256_hash(string: &str) -> ByteArray { + let string_as_bytes = string.as_bytes(); + let mut hasher = Sha256::new(); + hasher.update(string_as_bytes); + let result = hasher.finalize(); + let mut output = ByteArray::default(); + output[4..].copy_from_slice(&result[..4]); + output +} + +#[macro_export] +macro_rules! fn_selector { + ( $fn_name: ident ( $($fn_arg: ty),* ) ) => { + ::fuels::core::codec::resolve_fn_selector( + stringify!($fn_name), + &[$( <$fn_arg as ::fuels::core::traits::Parameterize>::param_type() ),*] + ) + .to_vec() + } +} + +pub use fn_selector; + +#[macro_export] +macro_rules! calldata { + ( $($arg: expr),* ) => { + ::fuels::core::codec::ABIEncoder::encode(&[$(::fuels::core::traits::Tokenizable::into_token($arg)),*]) + .map(|ub| ub.resolve(0)) + } +} + +pub use calldata; + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::enum_variants::EnumVariants; + + #[test] + fn handles_primitive_types() { + let check_selector_for_type = |primitive_type: ParamType, expected_selector: &str| { + let selector = resolve_fn_signature("some_fun", &[primitive_type]); + + assert_eq!(selector, format!("some_fun({expected_selector})")); + }; + + for (param_type, expected_signature) in [ + (ParamType::U8, "u8"), + (ParamType::U16, "u16"), + (ParamType::U32, "u32"), + (ParamType::U64, "u64"), + (ParamType::Bool, "bool"), + (ParamType::B256, "b256"), + (ParamType::Unit, "()"), + (ParamType::StringArray(15), "str[15]"), + (ParamType::StringSlice, "str"), + ] { + check_selector_for_type(param_type, expected_signature); + } + } + + #[test] + fn handles_std_strings() { + let inputs = [ParamType::String]; + + let signature = resolve_fn_signature("some_fn", &inputs); + + assert_eq!(signature, "some_fn(s(s(s(rawptr,u64),u64)))"); + } + + #[test] + fn handles_arrays() { + let inputs = [ParamType::Array(Box::new(ParamType::U8), 1)]; + + let signature = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(signature, format!("some_fun(a[u8;1])")); + } + + #[test] + fn handles_tuples() { + let inputs = [ParamType::Tuple(vec![ParamType::U8, ParamType::U8])]; + + let selector = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(selector, format!("some_fun((u8,u8))")); + } + + #[test] + fn handles_structs() { + let fields = vec![ParamType::U64, ParamType::U32]; + let generics = vec![ParamType::U32]; + let inputs = [ParamType::Struct { fields, generics }]; + + let selector = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(selector, format!("some_fun(s(u64,u32))")); + } + + #[test] + fn handles_vectors() { + let inputs = [ParamType::Vector(Box::new(ParamType::U32))]; + + let selector = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(selector, "some_fun(s(s(rawptr,u64),u64))") + } + + #[test] + fn handles_bytes() { + let inputs = [ParamType::Bytes]; + + let selector = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(selector, "some_fun(s(s(rawptr,u64),u64))") + } + + #[test] + fn handles_enums() { + let types = vec![ParamType::U64, ParamType::U32]; + let variants = EnumVariants::new(types).unwrap(); + let generics = vec![ParamType::U32]; + let inputs = [ParamType::Enum { variants, generics }]; + + let selector = resolve_fn_signature("some_fun", &inputs); + + assert_eq!(selector, format!("some_fun(e(u64,u32))")); + } + + #[test] + fn ultimate_test() { + let fields = vec![ParamType::Struct { + fields: vec![ParamType::StringArray(2)], + generics: vec![ParamType::StringArray(2)], + }]; + let struct_a = ParamType::Struct { + fields, + generics: vec![ParamType::StringArray(2)], + }; + + let fields = vec![ParamType::Array(Box::new(struct_a.clone()), 2)]; + let struct_b = ParamType::Struct { + fields, + generics: vec![struct_a], + }; + + let fields = vec![ParamType::Tuple(vec![struct_b.clone(), struct_b.clone()])]; + let struct_c = ParamType::Struct { + fields, + generics: vec![struct_b], + }; + + let types = vec![ParamType::U64, struct_c.clone()]; + let fields = vec![ + ParamType::Tuple(vec![ + ParamType::Array(Box::new(ParamType::B256), 2), + ParamType::StringArray(2), + ]), + ParamType::Tuple(vec![ + ParamType::Array( + Box::new(ParamType::Enum { + variants: EnumVariants::new(types).unwrap(), + generics: vec![struct_c], + }), + 1, + ), + ParamType::U32, + ]), + ]; + + let inputs = [ParamType::Struct { + fields, + generics: vec![ParamType::StringArray(2), ParamType::B256], + }]; + + let selector = resolve_fn_signature("complex_test", &inputs); + + assert_eq!(selector, "complex_test(s((a[b256;2],str[2]),(a[e(s(str[2]))>(a[s(s(str[2]));2])>((s(s(str[2]))>(a[s(s(str[2]));2]),s(s(str[2]))>(a[s(s(str[2]));2])))>(u64,s(s(str[2]))>(a[s(s(str[2]));2])>((s(s(str[2]))>(a[s(s(str[2]));2]),s(s(str[2]))>(a[s(s(str[2]));2]))));1],u32)))"); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/lib.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/lib.rs new file mode 100644 index 00000000..4fe809b0 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/lib.rs @@ -0,0 +1,24 @@ +pub mod codec; +pub mod traits; +pub mod types; +mod utils; + +pub use utils::*; + +#[derive(Debug, Clone, Default)] +pub struct Configurables { + offsets_with_data: Vec<(u64, Vec)>, +} + +impl Configurables { + pub fn new(offsets_with_data: Vec<(u64, Vec)>) -> Self { + Self { offsets_with_data } + } + + pub fn update_constants_in(&self, binary: &mut [u8]) { + for (offset, data) in &self.offsets_with_data { + let offset = *offset as usize; + binary[offset..offset + data.len()].copy_from_slice(data) + } + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/traits.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits.rs new file mode 100644 index 00000000..fa9fe98b --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits.rs @@ -0,0 +1,5 @@ +mod parameterize; +mod tokenizable; + +pub use parameterize::*; +pub use tokenizable::*; diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/parameterize.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/parameterize.rs new file mode 100644 index 00000000..447101a7 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/parameterize.rs @@ -0,0 +1,222 @@ +use fuel_types::{Address, AssetId, ContractId}; + +use crate::types::{ + enum_variants::EnumVariants, param_types::ParamType, AsciiString, Bits256, Bytes, RawSlice, + SizedAsciiString, +}; + +/// `abigen` requires `Parameterized` to construct nested types. It is also used by `try_from_bytes` +/// to facilitate the instantiation of custom types from bytes. +pub trait Parameterize { + fn param_type() -> ParamType; +} + +impl Parameterize for Bits256 { + fn param_type() -> ParamType { + ParamType::B256 + } +} + +impl Parameterize for RawSlice { + fn param_type() -> ParamType { + ParamType::RawSlice + } +} + +impl Parameterize for [T; SIZE] { + fn param_type() -> ParamType { + ParamType::Array(Box::new(T::param_type()), SIZE) + } +} + +impl Parameterize for Vec { + fn param_type() -> ParamType { + ParamType::Vector(Box::new(T::param_type())) + } +} + +impl Parameterize for Bytes { + fn param_type() -> ParamType { + ParamType::Bytes + } +} + +impl Parameterize for String { + fn param_type() -> ParamType { + ParamType::String + } +} + +impl Parameterize for Address { + fn param_type() -> ParamType { + ParamType::Struct { + fields: vec![ParamType::B256], + generics: vec![], + } + } +} + +impl Parameterize for ContractId { + fn param_type() -> ParamType { + ParamType::Struct { + fields: vec![ParamType::B256], + generics: vec![], + } + } +} + +impl Parameterize for AssetId { + fn param_type() -> ParamType { + ParamType::Struct { + fields: vec![ParamType::B256], + generics: vec![], + } + } +} + +impl Parameterize for () { + fn param_type() -> ParamType { + ParamType::Unit + } +} + +impl Parameterize for bool { + fn param_type() -> ParamType { + ParamType::Bool + } +} + +impl Parameterize for u8 { + fn param_type() -> ParamType { + ParamType::U8 + } +} + +impl Parameterize for u16 { + fn param_type() -> ParamType { + ParamType::U16 + } +} + +impl Parameterize for u32 { + fn param_type() -> ParamType { + ParamType::U32 + } +} + +impl Parameterize for u64 { + fn param_type() -> ParamType { + ParamType::U64 + } +} + +impl Parameterize for u128 { + fn param_type() -> ParamType { + ParamType::U128 + } +} + +impl Parameterize for Option +where + T: Parameterize, +{ + fn param_type() -> ParamType { + let param_types = vec![ParamType::Unit, T::param_type()]; + let variants = EnumVariants::new(param_types) + .expect("should never happen as we provided valid Option param types"); + ParamType::Enum { + variants, + generics: vec![T::param_type()], + } + } +} + +impl Parameterize for Result +where + T: Parameterize, + E: Parameterize, +{ + fn param_type() -> ParamType { + let param_types = vec![T::param_type(), E::param_type()]; + let variants = EnumVariants::new(param_types.clone()) + .expect("should never happen as we provided valid Result param types"); + ParamType::Enum { + variants, + generics: param_types, + } + } +} + +impl Parameterize for SizedAsciiString { + fn param_type() -> ParamType { + ParamType::StringArray(LEN) + } +} + +impl Parameterize for AsciiString { + fn param_type() -> ParamType { + ParamType::StringSlice + } +} + +// Here we implement `Parameterize` for a given tuple of a given length. +// This is done this way because we can't use `impl Parameterize for (T,)`. +// So we implement `Parameterize` for each tuple length, covering +// a reasonable range of tuple lengths. +macro_rules! impl_parameterize_tuples { + ($num: expr, $( $ty: ident : $no: tt, )+) => { + impl<$($ty, )+> Parameterize for ($($ty,)+) where + $( + $ty: Parameterize, + )+ + { + fn param_type() -> ParamType { + ParamType::Tuple(vec![ + $( $ty::param_type(), )+ + ]) + } + + } + } +} + +// And where we actually implement the `Parameterize` for tuples +// from size 1 to size 16. +impl_parameterize_tuples!(1, A:0, ); +impl_parameterize_tuples!(2, A:0, B:1, ); +impl_parameterize_tuples!(3, A:0, B:1, C:2, ); +impl_parameterize_tuples!(4, A:0, B:1, C:2, D:3, ); +impl_parameterize_tuples!(5, A:0, B:1, C:2, D:3, E:4, ); +impl_parameterize_tuples!(6, A:0, B:1, C:2, D:3, E:4, F:5, ); +impl_parameterize_tuples!(7, A:0, B:1, C:2, D:3, E:4, F:5, G:6, ); +impl_parameterize_tuples!(8, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, ); +impl_parameterize_tuples!(9, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, ); +impl_parameterize_tuples!(10, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, ); +impl_parameterize_tuples!(11, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, ); +impl_parameterize_tuples!(12, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, ); +impl_parameterize_tuples!(13, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, ); +impl_parameterize_tuples!(14, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, ); +impl_parameterize_tuples!(15, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, ); +impl_parameterize_tuples!(16, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, ); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sized_ascii_string_is_parameterized_correctly() { + let param_type = SizedAsciiString::<3>::param_type(); + + assert!(matches!(param_type, ParamType::StringArray(3))); + } + + #[test] + fn test_param_type_b256() { + assert_eq!(Bits256::param_type(), ParamType::B256); + } + + #[test] + fn test_param_type_raw_slice() { + assert_eq!(RawSlice::param_type(), ParamType::RawSlice); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/tokenizable.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/tokenizable.rs new file mode 100644 index 00000000..2413089f --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/traits/tokenizable.rs @@ -0,0 +1,623 @@ +use fuel_types::{Address, AssetId, ContractId}; + +use crate::{ + traits::Parameterize, + types::{ + errors::{error, Error, Result}, + param_types::ParamType, + AsciiString, Bits256, Bytes, RawSlice, SizedAsciiString, StaticStringToken, Token, + }, +}; + +pub trait Tokenizable { + /// Converts a `Token` into expected type. + fn from_token(token: Token) -> Result + where + Self: Sized; + /// Converts a specified type back into token. + fn into_token(self) -> Token; +} + +impl Tokenizable for Token { + fn from_token(token: Token) -> Result { + Ok(token) + } + fn into_token(self) -> Token { + self + } +} + +impl Tokenizable for Bits256 { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::B256(data) => Ok(Bits256(data)), + _ => Err(error!( + InvalidData, + "Bits256 cannot be constructed from token {token}" + )), + } + } + + fn into_token(self) -> Token { + Token::B256(self.0) + } +} + +impl Tokenizable for Vec { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + if let Token::Vector(tokens) = token { + tokens.into_iter().map(Tokenizable::from_token).collect() + } else { + Err(error!( + InvalidData, + "Vec::from_token must only be given a Token::Vector. Got: {token}" + )) + } + } + + fn into_token(self) -> Token { + let tokens = self.into_iter().map(Tokenizable::into_token).collect(); + Token::Vector(tokens) + } +} + +impl Tokenizable for bool { + fn from_token(token: Token) -> Result { + match token { + Token::Bool(data) => Ok(data), + other => Err(error!( + InstantiationError, + "Expected `bool`, got {:?}", other + )), + } + } + fn into_token(self) -> Token { + Token::Bool(self) + } +} + +impl Tokenizable for () { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::Unit => Ok(()), + other => Err(error!( + InstantiationError, + "Expected `Unit`, got {:?}", other + )), + } + } + + fn into_token(self) -> Token { + Token::Unit + } +} + +impl Tokenizable for u8 { + fn from_token(token: Token) -> Result { + match token { + Token::U8(data) => Ok(data), + other => Err(error!(InstantiationError, "Expected `u8`, got {:?}", other)), + } + } + fn into_token(self) -> Token { + Token::U8(self) + } +} + +impl Tokenizable for u16 { + fn from_token(token: Token) -> Result { + match token { + Token::U16(data) => Ok(data), + other => Err(error!( + InstantiationError, + "Expected `u16`, got {:?}", other + )), + } + } + fn into_token(self) -> Token { + Token::U16(self) + } +} + +impl Tokenizable for u32 { + fn from_token(token: Token) -> Result { + match token { + Token::U32(data) => Ok(data), + other => Err(error!( + InstantiationError, + "Expected `u32`, got {:?}", other + )), + } + } + fn into_token(self) -> Token { + Token::U32(self) + } +} + +impl Tokenizable for u64 { + fn from_token(token: Token) -> Result { + match token { + Token::U64(data) => Ok(data), + other => Err(error!( + InstantiationError, + "Expected `u64`, got {:?}", other + )), + } + } + fn into_token(self) -> Token { + Token::U64(self) + } +} + +impl Tokenizable for u128 { + fn from_token(token: Token) -> Result { + match token { + Token::U128(data) => Ok(data), + other => Err(error!( + InstantiationError, + "Expected `u128`, got {:?}", other + )), + } + } + fn into_token(self) -> Token { + Token::U128(self) + } +} + +impl Tokenizable for RawSlice { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::RawSlice(contents) => Ok(Self(contents)), + _ => Err(error!(InvalidData, + "RawSlice::from_token expected a token of the variant Token::RawSlice, got: {token}" + )), + } + } + + fn into_token(self) -> Token { + Token::RawSlice(Vec::from(self)) + } +} + +impl Tokenizable for Bytes { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::Bytes(contents) => Ok(Self(contents)), + _ => Err(error!( + InvalidData, + "Bytes::from_token expected a token of the variant Token::Bytes, got: {token}" + )), + } + } + + fn into_token(self) -> Token { + Token::Bytes(Vec::from(self)) + } +} + +impl Tokenizable for String { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::String(string) => Ok(string), + _ => Err(error!( + InvalidData, + "String::from_token expected a token of the variant Token::String, got: {token}" + )), + } + } + + fn into_token(self) -> Token { + Token::String(self) + } +} + +// Here we implement `Tokenizable` for a given tuple of a given length. +// This is done this way because we can't use `impl Tokenizable for (T,)`. +// So we implement `Tokenizable` for each tuple length, covering +// a reasonable range of tuple lengths. +macro_rules! impl_tokenizable_tuples { + ($num: expr, $( $ty: ident : $no: tt, )+) => { + impl<$($ty, )+> Tokenizable for ($($ty,)+) where + $( + $ty: Tokenizable, + )+ + { + fn from_token(token: Token) -> Result { + match token { + Token::Tuple(tokens) => { + let mut it = tokens.into_iter(); + let mut next_token = move || { + it.next().ok_or_else(|| { + error!(InstantiationError,"Ran out of tokens before tuple could be constructed") + }) + }; + Ok(($( + $ty::from_token(next_token()?)?, + )+)) + }, + other => Err(error!(InstantiationError, + "Expected `Tuple`, got {:?}", + other + )), + } + } + + fn into_token(self) -> Token { + Token::Tuple(vec![ + $( self.$no.into_token(), )+ + ]) + } + } + + } +} + +// And where we actually implement the `Tokenizable` for tuples +// from size 1 to size 16. +impl_tokenizable_tuples!(1, A:0, ); +impl_tokenizable_tuples!(2, A:0, B:1, ); +impl_tokenizable_tuples!(3, A:0, B:1, C:2, ); +impl_tokenizable_tuples!(4, A:0, B:1, C:2, D:3, ); +impl_tokenizable_tuples!(5, A:0, B:1, C:2, D:3, E:4, ); +impl_tokenizable_tuples!(6, A:0, B:1, C:2, D:3, E:4, F:5, ); +impl_tokenizable_tuples!(7, A:0, B:1, C:2, D:3, E:4, F:5, G:6, ); +impl_tokenizable_tuples!(8, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, ); +impl_tokenizable_tuples!(9, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, ); +impl_tokenizable_tuples!(10, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, ); +impl_tokenizable_tuples!(11, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, ); +impl_tokenizable_tuples!(12, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, ); +impl_tokenizable_tuples!(13, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, ); +impl_tokenizable_tuples!(14, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, ); +impl_tokenizable_tuples!(15, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, ); +impl_tokenizable_tuples!(16, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, ); + +impl Tokenizable for ContractId { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + if let Token::Struct(tokens) = token { + if let [Token::B256(data)] = tokens.as_slice() { + Ok(ContractId::from(*data)) + } else { + Err(error!( + InstantiationError, + "ContractId expected one `Token::B256`, got {tokens:?}" + )) + } + } else { + Err(error!( + InstantiationError, + "Address expected `Token::Struct` got {token:?}" + )) + } + } + + fn into_token(self) -> Token { + let underlying_data: &[u8; 32] = &self; + Token::Struct(vec![Bits256(*underlying_data).into_token()]) + } +} + +impl Tokenizable for Address { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + if let Token::Struct(tokens) = token { + if let [Token::B256(data)] = tokens.as_slice() { + Ok(Address::from(*data)) + } else { + Err(error!( + InstantiationError, + "Address expected one `Token::B256`, got {tokens:?}" + )) + } + } else { + Err(error!( + InstantiationError, + "Address expected `Token::Struct` got {token:?}" + )) + } + } + + fn into_token(self) -> Token { + let underlying_data: &[u8; 32] = &self; + + Token::Struct(vec![Bits256(*underlying_data).into_token()]) + } +} + +impl Tokenizable for AssetId { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + if let Token::Struct(tokens) = token { + if let [Token::B256(data)] = tokens.as_slice() { + Ok(AssetId::from(*data)) + } else { + Err(error!( + InstantiationError, + "AssetId expected one `Token::B256`, got {tokens:?}" + )) + } + } else { + Err(error!( + InstantiationError, + "AssetId expected `Token::Struct` got {token:?}" + )) + } + } + + fn into_token(self) -> Token { + let underlying_data: &[u8; 32] = &self; + Token::Struct(vec![Bits256(*underlying_data).into_token()]) + } +} + +impl Tokenizable for Option +where + T: Tokenizable + Parameterize, +{ + fn from_token(token: Token) -> Result { + if let Token::Enum(enum_selector) = token { + match *enum_selector { + (0u8, _, _) => Ok(None), + (1u8, token, _) => Ok(Option::::Some(T::from_token(token)?)), + (_, _, _) => Err(error!( + InstantiationError, + "Could not construct Option from enum_selector. Received: {:?}", enum_selector + )), + } + } else { + Err(error!( + InstantiationError, + "Could not construct Option from token. Received: {token:?}" + )) + } + } + fn into_token(self) -> Token { + let (dis, tok) = match self { + None => (0u8, Token::Unit), + Some(value) => (1u8, value.into_token()), + }; + if let ParamType::Enum { variants, .. } = Self::param_type() { + let selector = (dis, tok, variants); + Token::Enum(Box::new(selector)) + } else { + panic!("should never happen as Option::param_type() returns valid Enum variants"); + } + } +} + +impl Tokenizable for std::result::Result +where + T: Tokenizable + Parameterize, + E: Tokenizable + Parameterize, +{ + fn from_token(token: Token) -> Result { + if let Token::Enum(enum_selector) = token { + match *enum_selector { + (0u8, token, _) => Ok(std::result::Result::::Ok(T::from_token(token)?)), + (1u8, token, _) => Ok(std::result::Result::::Err(E::from_token(token)?)), + (_, _, _) => Err(error!( + InstantiationError, + "Could not construct Result from enum_selector. Received: {:?}", enum_selector + )), + } + } else { + Err(error!( + InstantiationError, + "Could not construct Result from token. Received: {token:?}" + )) + } + } + fn into_token(self) -> Token { + let (dis, tok) = match self { + Ok(value) => (0u8, value.into_token()), + Err(value) => (1u8, value.into_token()), + }; + if let ParamType::Enum { variants, .. } = Self::param_type() { + let selector = (dis, tok, variants); + Token::Enum(Box::new(selector)) + } else { + panic!("should never happen as Result::param_type() returns valid Enum variants"); + } + } +} + +impl Tokenizable for [T; SIZE] { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + let gen_error = |reason| { + error!( + InvalidData, + "While constructing an array of size {SIZE}: {reason}" + ) + }; + + match token { + Token::Array(elements) => { + let len = elements.len(); + if len != SIZE { + return Err(gen_error(format!( + "Was given a Token::Array with wrong number of elements: {len}" + ))); + } + + let detokenized = elements + .into_iter() + .map(Tokenizable::from_token) + .collect::>>() + .map_err(|err| { + gen_error(format!(", not all elements could be detokenized: {err}")) + })?; + + Ok(detokenized.try_into().unwrap_or_else(|_| { + panic!("This should never fail since we're checking the length beforehand.") + })) + } + _ => Err(gen_error(format!("Expected a Token::Array, got {token}"))), + } + } + + fn into_token(self) -> Token { + Token::Array(self.map(Tokenizable::into_token).to_vec()) + } +} + +impl Tokenizable for SizedAsciiString { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::StringArray(contents) => { + let expected_len = contents.get_encodable_str()?.len() ; + if expected_len!= LEN { + return Err(error!(InvalidData,"SizedAsciiString<{LEN}>::from_token got a Token::StringArray whose expected length({}) is != {LEN}", expected_len)) + } + Self::new(contents.try_into()?) + }, + _ => { + Err(error!(InvalidData,"SizedAsciiString<{LEN}>::from_token expected a token of the variant Token::StringArray, got: {token}")) + } + } + } + + fn into_token(self) -> Token { + Token::StringArray(StaticStringToken::new(self.into(), Some(LEN))) + } +} + +impl Tokenizable for AsciiString { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::StringSlice(contents) => { + Self::new(contents.try_into()?) + }, + _ => { + Err(error!(InvalidData,"AsciiString::from_token expected a token of the variant Token::StringSlice, got: {token}")) + } + } + } + + fn into_token(self) -> Token { + Token::StringSlice(StaticStringToken::new(self.into(), None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_token_b256() -> Result<()> { + let data = [1u8; 32]; + let token = Token::B256(data); + + let bits256 = Bits256::from_token(token)?; + + assert_eq!(bits256.0, data); + + Ok(()) + } + + #[test] + fn test_into_token_b256() { + let bytes = [1u8; 32]; + let bits256 = Bits256(bytes); + + let token = bits256.into_token(); + + assert_eq!(token, Token::B256(bytes)); + } + + #[test] + fn test_from_token_raw_slice() -> Result<()> { + let data = vec![42; 11]; + let token = Token::RawSlice(data.clone()); + + let slice = RawSlice::from_token(token)?; + + assert_eq!(slice, data); + + Ok(()) + } + + #[test] + fn test_into_token_raw_slice() { + let data = vec![13; 32]; + let raw_slice_token = Token::RawSlice(data.clone()); + + let token = raw_slice_token.into_token(); + + assert_eq!(token, Token::RawSlice(data)); + } + + #[test] + fn sized_ascii_string_is_tokenized_correctly() -> Result<()> { + let sut = SizedAsciiString::<3>::new("abc".to_string())?; + + let token = sut.into_token(); + + match token { + Token::StringArray(string_token) => { + let contents = string_token.get_encodable_str()?; + assert_eq!(contents, "abc"); + } + _ => { + panic!("Not tokenized correctly! Should have gotten a Token::String") + } + } + + Ok(()) + } + + #[test] + fn sized_ascii_string_is_detokenized_correctly() -> Result<()> { + let token = Token::StringArray(StaticStringToken::new("abc".to_string(), Some(3))); + + let sized_ascii_string = + SizedAsciiString::<3>::from_token(token).expect("Should have succeeded"); + + assert_eq!(sized_ascii_string, "abc"); + + Ok(()) + } + + #[test] + fn test_into_token_std_string() -> Result<()> { + let expected = String::from("hello"); + let token = Token::String(expected.clone()); + let detokenized = String::from_token(token.into_token())?; + + assert_eq!(detokenized, expected); + + Ok(()) + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types.rs new file mode 100644 index 00000000..d423909f --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types.rs @@ -0,0 +1,139 @@ +use std::fmt; + +pub use fuel_tx::{Address, AssetId, ContractId, TxPointer, UtxoId}; +use fuel_types::bytes::padded_len; +pub use fuel_types::{ChainId, MessageId, Nonce}; + +pub use crate::types::{core::*, wrappers::*}; +use crate::types::{ + enum_variants::EnumVariants, + errors::{error, Error, Result}, +}; + +pub mod bech32; +mod core; +pub mod enum_variants; +pub mod errors; +pub mod param_types; +pub mod transaction_builders; +pub mod unresolved_bytes; +mod wrappers; + +pub type ByteArray = [u8; 8]; +pub type Selector = ByteArray; +pub type EnumSelector = (u8, Token, EnumVariants); + +#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] +pub struct StaticStringToken { + data: String, + expected_len: Option, +} + +impl StaticStringToken { + pub fn new(data: String, expected_len: Option) -> Self { + StaticStringToken { data, expected_len } + } + + fn validate(&self) -> Result<()> { + if !self.data.is_ascii() { + return Err(error!( + InvalidData, + "String data can only have ascii values" + )); + } + + if let Some(expected_len) = self.expected_len { + if self.data.len() != expected_len { + return Err(error!( + InvalidData, + "String data has len {}, but the expected len is {}", + self.data.len(), + expected_len + )); + } + } + + Ok(()) + } + + pub fn get_encodable_str(&self) -> Result<&str> { + self.validate()?; + Ok(self.data.as_str()) + } +} + +impl TryFrom for String { + type Error = Error; + fn try_from(string_token: StaticStringToken) -> Result { + string_token.validate()?; + Ok(string_token.data) + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum Token { + // Used for unit type variants in Enum. An "empty" enum is not represented as Enum, + // because this way we can have both unit and non-unit type variants. + Unit, + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(u128), + U256(U256), + Bool(bool), + B256([u8; 32]), + Array(Vec), + Vector(Vec), + StringSlice(StaticStringToken), + StringArray(StaticStringToken), + Struct(Vec), + Enum(Box), + Tuple(Vec), + RawSlice(Vec), + Bytes(Vec), + String(String), +} + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Default for Token { + fn default() -> Self { + Token::U8(0) + } +} + +/// Converts a u8 to a right aligned array of 8 bytes. +pub fn pad_u8(value: u8) -> ByteArray { + let mut padded = ByteArray::default(); + padded[7] = value; + padded +} + +/// Converts a u16 to a right aligned array of 8 bytes. +pub fn pad_u16(value: u16) -> ByteArray { + let mut padded = ByteArray::default(); + padded[6..].copy_from_slice(&value.to_be_bytes()); + padded +} + +/// Converts a u32 to a right aligned array of 8 bytes. +pub fn pad_u32(value: u32) -> ByteArray { + let mut padded = [0u8; 8]; + padded[4..].copy_from_slice(&value.to_be_bytes()); + padded +} + +pub fn pad_string(s: &str) -> Vec { + let pad = padded_len(s.as_bytes()) - s.len(); + + let mut padded = s.as_bytes().to_owned(); + + padded.extend_from_slice(&vec![0; pad]); + + padded +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/bech32.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/bech32.rs new file mode 100644 index 00000000..2d7fab01 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/bech32.rs @@ -0,0 +1,240 @@ +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use bech32::{FromBase32, ToBase32, Variant::Bech32m}; +use fuel_tx::{Address, Bytes32, ContractId, ContractIdExt}; +use fuel_types::AssetId; + +use crate::types::{ + errors::{Error, Result}, + Bits256, +}; + +// Fuel Network human-readable part for bech32 encoding +pub const FUEL_BECH32_HRP: &str = "fuel"; + +/// Generate type represented in the Bech32 format, +/// consisting of a human-readable part (hrp) and a hash (e.g. pubkey-, contract hash) +macro_rules! bech32type { + ($i:ident) => { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct $i { + pub hrp: String, + pub hash: Bytes32, + } + + impl $i { + pub fn new>(hrp: &str, hash: T) -> Self { + Self { + hrp: hrp.to_string(), + hash: Bytes32::from(hash.into()), + } + } + + pub fn hash(&self) -> Bytes32 { + self.hash + } + + pub fn hrp(&self) -> &str { + &self.hrp + } + } + + impl Default for $i { + fn default() -> $i { + Self { + hrp: FUEL_BECH32_HRP.to_string(), + hash: Bytes32::new([0u8; 32]), + } + } + } + + impl FromStr for $i { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (hrp, pubkey_hash_base32, _) = bech32::decode(s)?; + + let pubkey_hash: [u8; Address::LEN] = Vec::::from_base32(&pubkey_hash_base32)? + .as_slice() + .try_into()?; + + Ok(Self { + hrp, + hash: Bytes32::new(pubkey_hash), + }) + } + } + + impl Display for $i { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let data_base32 = self.hash.to_base32(); + let encoding = bech32::encode(&self.hrp, &data_base32, Bech32m).unwrap(); + + write!(f, "{}", encoding) + } + } + }; +} + +bech32type!(Bech32Address); +bech32type!(Bech32ContractId); + +// Bech32Address - Address conversion +impl From<&Bech32Address> for Bech32Address { + fn from(data: &Bech32Address) -> Bech32Address { + data.clone() + } +} +impl From<&Bech32Address> for Address { + fn from(data: &Bech32Address) -> Address { + Address::new(*data.hash) + } +} +impl From for Address { + fn from(data: Bech32Address) -> Address { + Address::new(*data.hash) + } +} +impl From
for Bech32Address { + fn from(address: Address) -> Self { + Self { + hrp: FUEL_BECH32_HRP.to_string(), + hash: Bytes32::new(*address), + } + } +} + +// Bech32ContractId - ContractId conversion +impl From<&Bech32ContractId> for Bech32ContractId { + fn from(data: &Bech32ContractId) -> Bech32ContractId { + data.clone() + } +} +impl From<&Bech32ContractId> for ContractId { + fn from(data: &Bech32ContractId) -> ContractId { + ContractId::new(*data.hash) + } +} +impl From for ContractId { + fn from(data: Bech32ContractId) -> ContractId { + ContractId::new(*data.hash) + } +} +impl From for Bech32ContractId { + fn from(contract_id: ContractId) -> Self { + Self { + hrp: FUEL_BECH32_HRP.to_string(), + hash: Bytes32::new(*contract_id), + } + } +} + +impl Bech32ContractId { + /// Creates an `AssetId` from the `Bech32ContractId` and `sub_id`. + pub fn asset_id(&self, sub_id: &Bits256) -> AssetId { + let sub_id = Bytes32::from(sub_id.0); + ContractId::from(self).asset_id(&sub_id) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_new() { + let pubkey_hash = [ + 107, 50, 223, 89, 84, 225, 186, 222, 175, 254, 253, 44, 15, 197, 229, 148, 220, 255, + 55, 19, 170, 227, 221, 24, 183, 217, 102, 98, 75, 1, 0, 39, + ]; + + { + // Create from Bytes32 + let bech32_addr = &Bech32Address::new(FUEL_BECH32_HRP, Bytes32::new(pubkey_hash)); + let bech32_cid = &Bech32ContractId::new(FUEL_BECH32_HRP, Bytes32::new(pubkey_hash)); + + assert_eq!(*bech32_addr.hash(), pubkey_hash); + assert_eq!(*bech32_cid.hash(), pubkey_hash); + } + + { + // Create from ContractId + let bech32_addr = &Bech32Address::new(FUEL_BECH32_HRP, ContractId::new(pubkey_hash)); + let bech32_cid = &Bech32ContractId::new(FUEL_BECH32_HRP, ContractId::new(pubkey_hash)); + + assert_eq!(*bech32_addr.hash(), pubkey_hash); + assert_eq!(*bech32_cid.hash(), pubkey_hash); + } + + { + // Create from Address + let bech32_addr = &Bech32Address::new(FUEL_BECH32_HRP, Address::new(pubkey_hash)); + let bech32_cid = &Bech32ContractId::new(FUEL_BECH32_HRP, Address::new(pubkey_hash)); + + assert_eq!(*bech32_addr.hash(), pubkey_hash); + assert_eq!(*bech32_cid.hash(), pubkey_hash); + } + } + + #[test] + fn test_from_str() { + let pubkey_hashes = [ + [ + 107, 50, 223, 89, 84, 225, 186, 222, 175, 254, 253, 44, 15, 197, 229, 148, 220, + 255, 55, 19, 170, 227, 221, 24, 183, 217, 102, 98, 75, 1, 0, 39, + ], + [ + 49, 83, 18, 64, 150, 242, 119, 146, 83, 184, 84, 96, 160, 212, 110, 69, 81, 34, + 101, 86, 182, 99, 62, 68, 44, 28, 40, 26, 131, 21, 221, 64, + ], + [ + 48, 101, 49, 52, 48, 102, 48, 55, 48, 100, 49, 97, 102, 117, 51, 57, 49, 50, 48, + 54, 48, 98, 48, 100, 48, 56, 49, 53, 48, 52, 49, 52, + ], + ]; + let bech32m_encodings = [ + "fuel1dved7k25uxadatl7l5kql309jnw07dcn4t3a6x9hm9nxyjcpqqns50p7n2", + "fuel1x9f3ysyk7fmey5ac23s2p4rwg4gjye2kke3nu3pvrs5p4qc4m4qqwx56k3", + "fuel1xpjnzdpsvccrwvryx9skvafn8ycnyvpkxp3rqeps8qcn2vp5xy6qu7yyz7", + ]; + + for (b32m_e, pbkh) in bech32m_encodings.iter().zip(pubkey_hashes) { + let bech32_contract_id = &Bech32ContractId::from_str(b32m_e).unwrap(); + assert_eq!(*bech32_contract_id.hash(), pbkh); + } + + for (b32m_e, pbkh) in bech32m_encodings.iter().zip(pubkey_hashes) { + let bech32_contract_id = &Bech32Address::from_str(b32m_e).unwrap(); + assert_eq!(*bech32_contract_id.hash(), pbkh); + } + } + + #[test] + fn test_from_invalid_bech32_string() { + { + let expected = [ + Error::from(bech32::Error::InvalidChecksum), + Error::from(bech32::Error::InvalidChar('b')), + Error::from(bech32::Error::MissingSeparator), + ]; + let invalid_bech32 = [ + "fuel1x9f3ysyk7fmey5ac23s2p4rwg4gjye2kke3nu3pvrs5p4qc4m4qqwx32k3", + "fuel1xpjnzdpsvccrwvryx9skvafn8ycnyvpkxp3rqeps8qcn2vp5xy6qu7yyb7", + "fuelldved7k25uxadatl7l5kql309jnw07dcn4t3a6x9hm9nxyjcpqqns50p7n2", + ]; + + for (b32m_e, e) in invalid_bech32.iter().zip(expected.iter()) { + let result = &Bech32ContractId::from_str(b32m_e).expect_err("should error"); + assert_eq!(result.to_string(), e.to_string()); + } + + for (b32m_e, e) in invalid_bech32.iter().zip(expected) { + let result = &Bech32Address::from_str(b32m_e).expect_err("should error"); + assert_eq!(result.to_string(), e.to_string()); + } + } + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core.rs new file mode 100644 index 00000000..55cd9399 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core.rs @@ -0,0 +1,13 @@ +pub use bits::*; +pub use bytes::*; +pub use identity::*; +pub use raw_slice::*; +pub use sized_ascii_string::*; +pub use u256::*; + +mod bits; +mod bytes; +mod identity; +mod raw_slice; +mod sized_ascii_string; +mod u256; diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bits.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bits.rs new file mode 100644 index 00000000..8234232b --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bits.rs @@ -0,0 +1,165 @@ +use fuel_types::AssetId; +use fuels_macros::{Parameterize, Tokenizable, TryFrom}; + +use crate::types::errors::{error, Error, Result}; + +// A simple wrapper around [u8; 32] representing the `b256` type. Exists +// mainly so that we may differentiate `Parameterize` and `Tokenizable` +// implementations from what otherwise is just an array of 32 u8's. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Bits256(pub [u8; 32]); + +impl Bits256 { + /// Returns `Self` with zeroes inside. + pub fn zeroed() -> Self { + Self([0; 32]) + } + + /// Create a new `Bits256` from a string representation of a hex. + /// Accepts both `0x` prefixed and non-prefixed hex strings. + pub fn from_hex_str(hex: &str) -> Result { + let hex = if let Some(stripped_hex) = hex.strip_prefix("0x") { + stripped_hex + } else { + hex + }; + + let mut bytes = [0u8; 32]; + hex::decode_to_slice(hex, &mut bytes as &mut [u8]).map_err(|e| { + error!( + InvalidData, + "Could not convert hex str '{hex}' to Bits256! {e}" + ) + })?; + Ok(Bits256(bytes)) + } +} + +impl From for Bits256 { + fn from(value: AssetId) -> Self { + Self(value.into()) + } +} + +// A simple wrapper around [Bits256; 2] representing the `B512` type. +#[derive(Debug, PartialEq, Eq, Copy, Clone, Parameterize, Tokenizable, TryFrom)] +#[FuelsCorePath = "crate"] +#[FuelsTypesPath = "crate::types"] +// ANCHOR: b512 +pub struct B512 { + pub bytes: [Bits256; 2], +} +// ANCHOR_END: b512 + +impl From<(Bits256, Bits256)> for B512 { + fn from(bits_tuple: (Bits256, Bits256)) -> Self { + B512 { + bytes: [bits_tuple.0, bits_tuple.1], + } + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Parameterize, Tokenizable, TryFrom)] +#[FuelsCorePath = "crate"] +#[FuelsTypesPath = "crate::types"] +// ANCHOR: evm_address +pub struct EvmAddress { + // An evm address is only 20 bytes, the first 12 bytes should be set to 0 + value: Bits256, +} +// ANCHOR_END: evm_address +impl EvmAddress { + fn new(b256: Bits256) -> Self { + Self { + value: Bits256(Self::clear_12_bytes(b256.0)), + } + } + + pub fn value(&self) -> Bits256 { + self.value + } + + // sets the leftmost 12 bytes to zero + fn clear_12_bytes(bytes: [u8; 32]) -> [u8; 32] { + let mut bytes = bytes; + bytes[..12].copy_from_slice(&[0u8; 12]); + + bytes + } +} + +impl From for EvmAddress { + fn from(b256: Bits256) -> Self { + EvmAddress::new(b256) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::{Parameterize, Tokenizable}, + types::{param_types::ParamType, Token}, + }; + + #[test] + fn from_hex_str_b256() -> Result<()> { + // ANCHOR: from_hex_str + let hex_str = "0101010101010101010101010101010101010101010101010101010101010101"; + + let bits256 = Bits256::from_hex_str(hex_str)?; + + assert_eq!(bits256.0, [1u8; 32]); + + // With the `0x0` prefix + let hex_str = "0x0101010101010101010101010101010101010101010101010101010101010101"; + + let bits256 = Bits256::from_hex_str(hex_str)?; + + assert_eq!(bits256.0, [1u8; 32]); + // ANCHOR_END: from_hex_str + + Ok(()) + } + + #[test] + fn test_param_type_evm_addr() { + assert_eq!( + EvmAddress::param_type(), + ParamType::Struct { + fields: vec![ParamType::B256], + generics: vec![] + } + ); + } + + #[test] + fn evm_address_clears_first_12_bytes() -> Result<()> { + let data = [1u8; 32]; + let address = EvmAddress::new(Bits256(data)); + + let expected_data = Bits256([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]); + + assert_eq!(address.value(), expected_data); + + Ok(()) + } + + #[test] + fn test_into_token_evm_addr() { + let bits = [1u8; 32]; + let evm_address = EvmAddress::from(Bits256(bits)); + + let token = evm_address.into_token(); + + let expected_data = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, + ]; + + assert_eq!(token, Token::Struct(vec![Token::B256(expected_data)])); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bytes.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bytes.rs new file mode 100644 index 00000000..10f0cb9a --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/bytes.rs @@ -0,0 +1,20 @@ +#[derive(Debug, PartialEq, Clone, Eq)] +pub struct Bytes(pub Vec); + +impl From for Vec { + fn from(raw_slice: Bytes) -> Vec { + raw_slice.0 + } +} + +impl PartialEq> for Bytes { + fn eq(&self, other: &Vec) -> bool { + self.0 == *other + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &Bytes) -> bool { + *self == other.0 + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/identity.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/identity.rs new file mode 100644 index 00000000..3df23840 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/identity.rs @@ -0,0 +1,28 @@ +use fuel_tx::{Address, ContractId}; +use fuels_macros::{Parameterize, Tokenizable, TryFrom}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Parameterize, Tokenizable, TryFrom, Serialize, Deserialize, +)] +#[FuelsCorePath = "crate"] +#[FuelsTypesPath = "crate::types"] +pub enum Identity { + Address(Address), + ContractId(ContractId), +} + +impl Default for Identity { + fn default() -> Self { + Self::Address(Address::default()) + } +} + +impl AsRef<[u8]> for Identity { + fn as_ref(&self) -> &[u8] { + match self { + Identity::Address(address) => address.as_ref(), + Identity::ContractId(contract_id) => contract_id.as_ref(), + } + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/raw_slice.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/raw_slice.rs new file mode 100644 index 00000000..d4e0b0cd --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/raw_slice.rs @@ -0,0 +1,22 @@ +#[derive(Debug, PartialEq, Clone, Eq)] +// `RawSlice` is a mapping of the contract type "untyped raw slice" -- currently the only way of +// returning dynamically sized data from a script. +pub struct RawSlice(pub Vec); + +impl From for Vec { + fn from(raw_slice: RawSlice) -> Vec { + raw_slice.0 + } +} + +impl PartialEq> for RawSlice { + fn eq(&self, other: &Vec) -> bool { + self.0 == *other + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &RawSlice) -> bool { + *self == other.0 + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/sized_ascii_string.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/sized_ascii_string.rs new file mode 100644 index 00000000..1eccc985 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/sized_ascii_string.rs @@ -0,0 +1,302 @@ +use std::fmt::{Debug, Display, Formatter}; + +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)] +pub struct AsciiString { + data: String, +} + +impl AsciiString { + pub fn new(data: String) -> Result { + if !data.is_ascii() { + return Err(error!(InvalidData, + "AsciiString must be constructed from a string containing only ascii encodable characters. Got: {data}" + )); + } + Ok(Self { data }) + } + + pub fn to_trimmed_str(&self) -> &str { + self.data.trim() + } + pub fn to_left_trimmed_str(&self) -> &str { + self.data.trim_start() + } + pub fn to_right_trimmed_str(&self) -> &str { + self.data.trim_end() + } +} + +impl TryFrom<&str> for AsciiString { + type Error = Error; + + fn try_from(value: &str) -> Result { + Self::new(value.to_owned()) + } +} + +impl TryFrom for AsciiString { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl From for String { + fn from(ascii_str: AsciiString) -> Self { + ascii_str.data + } +} + +impl Display for AsciiString { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.data) + } +} + +impl PartialEq<&str> for AsciiString { + fn eq(&self, other: &&str) -> bool { + self.data == *other + } +} +impl PartialEq for &str { + fn eq(&self, other: &AsciiString) -> bool { + *self == other.data + } +} + +// To be used when interacting with contracts which have strings in their ABI. +// The length of a string is part of its type -- i.e. str[2] is a +// different type from str[3]. The FuelVM strings only support ascii characters. +#[derive(Debug, PartialEq, Clone, Eq, Hash, Default)] +pub struct SizedAsciiString { + data: String, +} + +impl SizedAsciiString { + pub fn new(data: String) -> Result { + if !data.is_ascii() { + return Err(error!(InvalidData, + "SizedAsciiString must be constructed from a string containing only ascii encodable characters. Got: {data}" + )); + } + if data.len() != LEN { + return Err(error!(InvalidData, + "SizedAsciiString<{LEN}> can only be constructed from a String of length {LEN}. Got: {data}" + )); + } + Ok(Self { data }) + } + + pub fn to_trimmed_str(&self) -> &str { + self.data.trim() + } + pub fn to_left_trimmed_str(&self) -> &str { + self.data.trim_start() + } + pub fn to_right_trimmed_str(&self) -> &str { + self.data.trim_end() + } + + /// Pad `data` string with whitespace characters on the right to fit into the `SizedAsciiString` + pub fn new_with_right_whitespace_padding(data: String) -> Result { + if data.len() > LEN { + return Err(error!( + InvalidData, + "SizedAsciiString<{LEN}> cannot be constructed from a string of size {}", + data.len() + )); + } + + Ok(Self { + data: format!("{:LEN$}", data), + }) + } +} + +impl TryFrom<&str> for SizedAsciiString { + type Error = Error; + + fn try_from(value: &str) -> Result { + Self::new(value.to_owned()) + } +} + +impl TryFrom for SizedAsciiString { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl From> for String { + fn from(sized_ascii_str: SizedAsciiString) -> Self { + sized_ascii_str.data + } +} + +impl Display for SizedAsciiString { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.data) + } +} + +impl PartialEq<&str> for SizedAsciiString { + fn eq(&self, other: &&str) -> bool { + self.data == *other + } +} + +impl PartialEq> for &str { + fn eq(&self, other: &SizedAsciiString) -> bool { + *self == other.data + } +} + +impl Serialize for SizedAsciiString { + fn serialize( + &self, + serializer: S, + ) -> core::result::Result { + self.data.serialize(serializer) + } +} + +impl<'de, const LEN: usize> Deserialize<'de> for SizedAsciiString { + fn deserialize>( + deserializer: D, + ) -> core::result::Result { + let data = String::deserialize(deserializer)?; + Self::new(data).map_err(serde::de::Error::custom) + } +} + +impl AsRef<[u8]> for SizedAsciiString { + fn as_ref(&self) -> &[u8] { + self.data.as_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_ascii_of_correct_length() { + // ANCHOR: string_simple_example + let ascii_data = "abc".to_string(); + + SizedAsciiString::<3>::new(ascii_data) + .expect("Should have succeeded since we gave ascii data of correct length!"); + // ANCHOR_END: string_simple_example + } + + #[test] + fn refuses_non_ascii() { + let ascii_data = "ab©".to_string(); + + let err = SizedAsciiString::<3>::new(ascii_data) + .expect_err("Should not have succeeded since we gave non ascii data"); + + let expected_reason = "SizedAsciiString must be constructed from a string containing only ascii encodable characters. Got: "; + assert!(matches!(err, Error::InvalidData(reason) if reason.starts_with(expected_reason))); + } + + #[test] + fn refuses_invalid_len() { + let ascii_data = "abcd".to_string(); + + let err = SizedAsciiString::<3>::new(ascii_data) + .expect_err("Should not have succeeded since we gave data of wrong length"); + + let expected_reason = + "SizedAsciiString<3> can only be constructed from a String of length 3. Got: abcd"; + assert!(matches!(err, Error::InvalidData(reason) if reason.starts_with(expected_reason))); + } + + // ANCHOR: conversion + #[test] + fn can_be_constructed_from_str_ref() { + let _: SizedAsciiString<3> = "abc".try_into().expect("Should have succeeded"); + } + + #[test] + fn can_be_constructed_from_string() { + let _: SizedAsciiString<3> = "abc".to_string().try_into().expect("Should have succeeded"); + } + + #[test] + fn can_be_converted_into_string() { + let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap(); + + let str: String = sized_str.into(); + + assert_eq!(str, "abc"); + } + // ANCHOR_END: conversion + + #[test] + fn can_be_printed() { + let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap(); + + assert_eq!(sized_str.to_string(), "abc"); + } + + #[test] + fn can_be_compared_w_str_ref() { + let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap(); + + assert_eq!(sized_str, "abc"); + // and vice-versa + assert_eq!("abc", sized_str); + } + + #[test] + fn trim() -> Result<()> { + // Using single whitespaces + let untrimmed = SizedAsciiString::<9>::new(" est abc ".to_string())?; + assert_eq!("est abc ", untrimmed.to_left_trimmed_str()); + assert_eq!(" est abc", untrimmed.to_right_trimmed_str()); + assert_eq!("est abc", untrimmed.to_trimmed_str()); + + let padded = // adds 6 whitespaces + SizedAsciiString::<12>::new_with_right_whitespace_padding("victor".to_string())?; + assert_eq!("victor ", padded); + + Ok(()) + } + + #[test] + fn test_can_serialize_sized_ascii() { + let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap(); + + let serialized = serde_json::to_string(&sized_str).unwrap(); + assert_eq!(serialized, "\"abc\""); + } + + #[test] + fn test_can_deserialize_sized_ascii() { + let serialized = "\"abc\""; + + let deserialized: SizedAsciiString<3> = serde_json::from_str(serialized).unwrap(); + assert_eq!( + deserialized, + SizedAsciiString::<3>::new("abc".to_string()).unwrap() + ); + } + + #[test] + fn test_can_convert_sized_ascii_to_bytes() { + let sized_str = SizedAsciiString::<3>::new("abc".to_string()).unwrap(); + + let bytes: &[u8] = sized_str.as_ref(); + assert_eq!(bytes, &[97, 98, 99]); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/u256.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/u256.rs new file mode 100644 index 00000000..b948002d --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/core/u256.rs @@ -0,0 +1,76 @@ +#![allow(clippy::assign_op_pattern)] + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use uint::construct_uint; + +use crate::{ + traits::{Parameterize, Tokenizable}, + types::{ + errors::{error, Error, Result as FuelsResult}, + param_types::ParamType, + Token, + }, +}; + +construct_uint! { + /// 256-bit unsigned integer. + pub struct U256(4); +} + +impl Parameterize for U256 { + fn param_type() -> ParamType { + ParamType::U256 + } +} + +impl Tokenizable for U256 { + fn from_token(token: Token) -> FuelsResult + where + Self: Sized, + { + match token { + Token::U256(data) => Ok(data), + _ => Err(error!( + InvalidData, + "U256 cannot be constructed from token {token}" + )), + } + } + + fn into_token(self) -> Token { + Token::U256(self) + } +} + +impl Serialize for U256 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for U256 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + U256::from_dec_str(Deserialize::deserialize(deserializer)?).map_err(de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use crate::types::U256; + + #[test] + fn u256_serialize_deserialize() { + let num = U256::from(123); + let serialized: String = serde_json::to_string(&num).unwrap(); + assert_eq!(serialized, "\"123\""); + + let deserialized_num: U256 = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized_num, num); + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/enum_variants.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/enum_variants.rs new file mode 100644 index 00000000..ef532bd8 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/enum_variants.rs @@ -0,0 +1,66 @@ +use crate::{ + constants::{ENUM_DISCRIMINANT_WORD_WIDTH, WORD_SIZE}, + types::{ + errors::{error, Error, Result}, + param_types::ParamType, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EnumVariants { + param_types: Vec, +} + +impl EnumVariants { + pub fn new(param_types: Vec) -> Result { + if !param_types.is_empty() { + Ok(EnumVariants { param_types }) + } else { + Err(error!(InvalidData, "Enum variants can not be empty!")) + } + } + + pub fn param_types(&self) -> &[ParamType] { + &self.param_types + } + + pub fn param_type_of_variant(&self, discriminant: u8) -> Result<&ParamType> { + self.param_types.get(discriminant as usize).ok_or_else(|| { + error!( + InvalidData, + "Discriminant '{discriminant}' doesn't point to any variant: {:?}", + self.param_types() + ) + }) + } + + pub fn only_units_inside(&self) -> bool { + self.param_types + .iter() + .all(|param_type| *param_type == ParamType::Unit) + } + + /// Calculates how many WORDs are needed to encode an enum. + pub fn compute_encoding_width_of_enum(&self) -> usize { + if self.only_units_inside() { + return ENUM_DISCRIMINANT_WORD_WIDTH; + } + self.param_types() + .iter() + .map(|p| p.compute_encoding_width()) + .max() + .map(|width| width + ENUM_DISCRIMINANT_WORD_WIDTH) + .expect( + "Will never panic because EnumVariants must have at least one variant inside it!", + ) + } + + /// Determines the padding needed for the provided enum variant (based on the width of the + /// biggest variant) and returns it. + pub fn compute_padding_amount(&self, variant_param_type: &ParamType) -> usize { + let biggest_variant_width = + self.compute_encoding_width_of_enum() - ENUM_DISCRIMINANT_WORD_WIDTH; + let variant_width = variant_param_type.compute_encoding_width(); + (biggest_variant_width - variant_width) * WORD_SIZE + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/errors.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/errors.rs new file mode 100644 index 00000000..e6e5c6f1 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/errors.rs @@ -0,0 +1,66 @@ +use std::{array::TryFromSliceError, str::Utf8Error}; + +use fuel_tx::{CheckError, Receipt}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Invalid data: {0}")] + InvalidData(String), + #[error("Serialization error: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), + #[error("Invalid type: {0}")] + InvalidType(String), + #[error("Utf8 error: {0}")] + Utf8Error(#[from] Utf8Error), + #[error("Instantiation error: {0}")] + InstantiationError(String), + #[error("Infrastructure error: {0}")] + InfrastructureError(String), + #[error("Account error: {0}")] + AccountError(String), + #[error("Wallet error: {0}")] + WalletError(String), + #[error("Provider error: {0}")] + ProviderError(String), + #[error("Validation error: {0}")] + ValidationError(#[from] CheckError), + #[error("Tried to forward assets to a contract method that is not payable.")] + AssetsForwardedToNonPayableMethod, + #[error("Revert transaction error: {reason},\n receipts: {receipts:?}")] + RevertTransactionError { + reason: String, + revert_id: u64, + receipts: Vec, + }, + #[error("Transaction build error: {0}")] + TransactionBuildError(String), +} + +pub type Result = std::result::Result; + +/// This macro can only be used for `Error` variants that have a `String` field. +/// Those are: `InvalidData`, `InvalidType`, `InfrastructureError`, +/// `InstantiationError`, `WalletError`, `ProviderError`, `TransactionBuildError` +#[macro_export] +macro_rules! error { + ($err_variant:ident, $fmt_str: literal $(,$arg: expr)*) => { + Error::$err_variant(format!($fmt_str,$($arg),*)) + } +} +pub use error; + +macro_rules! impl_error_from { + ($err_variant:ident, $err_type:ty ) => { + impl From<$err_type> for Error { + fn from(err: $err_type) -> Error { + Error::$err_variant(err.to_string()) + } + } + }; +} + +impl_error_from!(InvalidData, bech32::Error); +impl_error_from!(InvalidData, TryFromSliceError); diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/param_types.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/param_types.rs new file mode 100644 index 00000000..efb20c42 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/param_types.rs @@ -0,0 +1,1507 @@ +use std::{collections::HashMap, iter::zip}; + +use fuel_abi_types::{ + abi::program::{TypeApplication, TypeDeclaration}, + utils::{extract_array_len, extract_generic_name, extract_str_len, has_tuple_format}, +}; +use itertools::chain; + +use crate::{ + constants::WORD_SIZE, + types::{ + enum_variants::EnumVariants, + errors::{error, Error, Result}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ParamType { + U8, + U16, + U32, + U64, + U128, + 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), + StringSlice, + StringArray(usize), + Struct { + fields: Vec, + generics: Vec, + }, + Enum { + variants: EnumVariants, + generics: Vec, + }, + Tuple(Vec), + RawSlice, + Bytes, + String, +} + +pub enum ReturnLocation { + Return, + ReturnData, +} + +impl ParamType { + // Depending on the type, the returned value will be stored + // either in `Return` or `ReturnData`. + pub fn get_return_location(&self) -> ReturnLocation { + match self { + Self::Unit | Self::U8 | Self::U16 | Self::U32 | Self::U64 | Self::Bool => { + ReturnLocation::Return + } + + _ => ReturnLocation::ReturnData, + } + } + + /// Given a [ParamType], return the number of elements of that [ParamType] that can fit in + /// `available_bytes`: it is the length of the corresponding heap type. + pub fn calculate_num_of_elements( + param_type: &ParamType, + available_bytes: usize, + ) -> Result { + let memory_size = param_type.compute_encoding_width() * WORD_SIZE; + let remainder = available_bytes % memory_size; + if remainder != 0 { + return Err(error!( + InvalidData, + "{remainder} extra bytes detected while decoding heap type" + )); + } + Ok(available_bytes / memory_size) + } + + pub fn contains_nested_heap_types(&self) -> bool { + match &self { + ParamType::Vector(param_type) => param_type.uses_heap_types(), + ParamType::Bytes => false, + // Here, we return false because even though the `Token::String` type has an underlying + // `Bytes` type nested, it is an exception that will be generalized as part of + // https://github.com/FuelLabs/fuels-rs/discussions/944 + ParamType::String => false, + _ => self.uses_heap_types(), + } + } + + fn uses_heap_types(&self) -> bool { + match &self { + ParamType::Vector(..) | ParamType::Bytes | ParamType::String => true, + ParamType::Array(param_type, ..) => param_type.uses_heap_types(), + ParamType::Tuple(param_types, ..) => Self::any_nested_heap_types(param_types), + ParamType::Enum { + generics, variants, .. + } => { + let variants_types = variants.param_types(); + Self::any_nested_heap_types(chain!(generics, variants_types)) + } + ParamType::Struct { + fields, generics, .. + } => Self::any_nested_heap_types(chain!(fields, generics)), + _ => false, + } + } + + fn any_nested_heap_types<'a>(param_types: impl IntoIterator) -> bool { + param_types + .into_iter() + .any(|param_type| param_type.uses_heap_types()) + } + + pub fn is_vm_heap_type(&self) -> bool { + matches!( + self, + ParamType::Vector(..) | ParamType::Bytes | ParamType::String + ) + } + + /// Compute the inner memory size of a containing heap type (`Bytes` or `Vec`s). + pub fn heap_inner_element_size(&self) -> Option { + match &self { + ParamType::Vector(inner_param_type) => { + Some(inner_param_type.compute_encoding_width() * WORD_SIZE) + } + // `Bytes` type is byte-packed in the VM, so it's the size of an u8 + ParamType::Bytes | ParamType::String => Some(std::mem::size_of::()), + _ => None, + } + } + + /// Calculates the number of `WORD`s the VM expects this parameter to be encoded in. + pub fn compute_encoding_width(&self) -> usize { + const fn count_words(bytes: usize) -> usize { + let q = bytes / WORD_SIZE; + let r = bytes % WORD_SIZE; + match r == 0 { + true => q, + false => q + 1, + } + } + + match &self { + ParamType::Unit + | ParamType::U8 + | ParamType::U16 + | ParamType::U32 + | ParamType::U64 + | ParamType::Bool => 1, + ParamType::U128 | ParamType::RawSlice | ParamType::StringSlice => 2, + ParamType::Vector(_) | ParamType::Bytes | ParamType::String => 3, + ParamType::U256 | ParamType::B256 => 4, + ParamType::Array(param, count) => param.compute_encoding_width() * count, + ParamType::StringArray(len) => count_words(*len), + ParamType::Struct { fields, .. } => fields + .iter() + .map(|param_type| param_type.compute_encoding_width()) + .sum(), + ParamType::Enum { variants, .. } => variants.compute_encoding_width_of_enum(), + ParamType::Tuple(params) => params.iter().map(|p| p.compute_encoding_width()).sum(), + } + } + + /// For when you need to convert a ABI JSON's TypeApplication into a ParamType. + /// + /// # Arguments + /// + /// * `type_application`: The TypeApplication you wish to convert into a ParamType + /// * `type_lookup`: A HashMap of TypeDeclarations mentioned in the + /// TypeApplication where the type id is the key. + pub fn try_from_type_application( + type_application: &TypeApplication, + type_lookup: &HashMap, + ) -> Result { + Type::try_from(type_application, type_lookup)?.try_into() + } +} + +#[derive(Debug, Clone)] +struct Type { + type_field: String, + generic_params: Vec, + components: Vec, +} + +impl Type { + /// Will recursively drill down the given generic parameters until all types are + /// resolved. + /// + /// # Arguments + /// + /// * `type_application`: the type we wish to resolve + /// * `types`: all types used in the function call + pub fn try_from( + type_application: &TypeApplication, + type_lookup: &HashMap, + ) -> Result { + Self::resolve(type_application, type_lookup, &[]) + } + + fn resolve( + type_application: &TypeApplication, + type_lookup: &HashMap, + parent_generic_params: &[(usize, Type)], + ) -> Result { + let type_declaration = type_lookup.get(&type_application.type_id).ok_or_else(|| { + error!( + InvalidData, + "type id {} not found in type lookup", type_application.type_id + ) + })?; + + if extract_generic_name(&type_declaration.type_field).is_some() { + let (_, generic_type) = parent_generic_params + .iter() + .find(|(id, _)| *id == type_application.type_id) + .ok_or_else(|| { + error!( + InvalidData, + "type id {} not found in parent's generic parameters", + type_application.type_id + ) + })?; + + return Ok(generic_type.clone()); + } + + // Figure out what does the current type do with the inherited generic + // parameters and reestablish the mapping since the current type might have + // renamed the inherited generic parameters. + let generic_params_lookup = Self::determine_generics_for_type( + type_application, + type_lookup, + type_declaration, + parent_generic_params, + )?; + + // Resolve the enclosed components (if any) with the newly resolved generic + // parameters. + let components = type_declaration + .components + .iter() + .flatten() + .map(|component| Self::resolve(component, type_lookup, &generic_params_lookup)) + .collect::>>()?; + + Ok(Type { + type_field: type_declaration.type_field.clone(), + components, + generic_params: generic_params_lookup + .into_iter() + .map(|(_, ty)| ty) + .collect(), + }) + } + + /// For the given type generates generic_type_id -> Type mapping describing to + /// which types generic parameters should be resolved. + /// + /// # Arguments + /// + /// * `type_application`: The type on which the generic parameters are defined. + /// * `types`: All types used. + /// * `parent_generic_params`: The generic parameters as inherited from the + /// enclosing type (a struct/enum/array etc.). + fn determine_generics_for_type( + type_application: &TypeApplication, + type_lookup: &HashMap, + type_declaration: &TypeDeclaration, + parent_generic_params: &[(usize, Type)], + ) -> Result> { + match &type_declaration.type_parameters { + // The presence of type_parameters indicates that the current type + // (a struct or an enum) defines some generic parameters (i.e. SomeStruct). + Some(params) if !params.is_empty() => { + // Determine what Types the generics will resolve to. + let generic_params_from_current_type = type_application + .type_arguments + .iter() + .flatten() + .map(|ty| Self::resolve(ty, type_lookup, parent_generic_params)) + .collect::>>()?; + + let generics_to_use = if !generic_params_from_current_type.is_empty() { + generic_params_from_current_type + } else { + // Types such as arrays and enums inherit and forward their + // generic parameters, without declaring their own. + parent_generic_params + .iter() + .map(|(_, ty)| ty) + .cloned() + .collect() + }; + + // All inherited but unused generic types are dropped. The rest are + // re-mapped to new type_ids since child types are free to rename + // the generic parameters as they see fit -- i.e. + // struct ParentStruct{ + // b: ChildStruct + // } + // struct ChildStruct { + // c: K + // } + + Ok(zip(params.clone(), generics_to_use).collect()) + } + _ => Ok(parent_generic_params.to_vec()), + } + } +} + +impl TryFrom for ParamType { + type Error = Error; + + fn try_from(value: Type) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&Type> for ParamType { + type Error = Error; + + fn try_from(the_type: &Type) -> Result { + let matched_param_type = [ + try_primitive, + try_array, + try_str_array, + try_str_slice, + try_tuple, + try_vector, + try_bytes, + try_std_string, + try_raw_slice, + try_enum, + try_u128, + try_u256, + try_struct, + ] + .into_iter() + .map(|fun| fun(the_type)) + .flat_map(|result| result.ok().flatten()) + .next(); + + matched_param_type.map(Ok).unwrap_or_else(|| { + Err(error!( + InvalidType, + "Type {} couldn't be converted into a ParamType", the_type.type_field + )) + }) + } +} + +fn convert_into_param_types(coll: &[Type]) -> Result> { + coll.iter().map(ParamType::try_from).collect() +} + +fn try_struct(the_type: &Type) -> Result> { + let result = if has_struct_format(&the_type.type_field) { + let generics = param_types(&the_type.generic_params)?; + + let fields = convert_into_param_types(&the_type.components)?; + Some(ParamType::Struct { fields, generics }) + } else { + None + }; + + Ok(result) +} + +fn has_struct_format(field: &str) -> bool { + field.starts_with("struct ") +} + +fn try_vector(the_type: &Type) -> Result> { + if !["struct std::vec::Vec", "struct Vec"].contains(&the_type.type_field.as_str()) { + return Ok(None); + } + + if the_type.generic_params.len() != 1 { + return Err(error!( + InvalidType, + "Vec must have exactly one generic argument for its type. Found: {:?}", + the_type.generic_params + )); + } + + let vec_elem_type = convert_into_param_types(&the_type.generic_params)?.remove(0); + + Ok(Some(ParamType::Vector(Box::new(vec_elem_type)))) +} + +fn try_u128(the_type: &Type) -> Result> { + Ok(["struct std::u128::U128", "struct U128"] + .contains(&the_type.type_field.as_str()) + .then_some(ParamType::U128)) +} + +fn try_u256(the_type: &Type) -> Result> { + Ok(["struct std::u256::U256", "struct U256"] + .contains(&the_type.type_field.as_str()) + .then_some(ParamType::U256)) +} + +fn try_bytes(the_type: &Type) -> Result> { + Ok(["struct std::bytes::Bytes", "struct Bytes"] + .contains(&the_type.type_field.as_str()) + .then_some(ParamType::Bytes)) +} + +fn try_std_string(the_type: &Type) -> Result> { + Ok(["struct std::string::String", "struct String"] + .contains(&the_type.type_field.as_str()) + .then_some(ParamType::String)) +} + +fn try_raw_slice(the_type: &Type) -> Result> { + Ok((the_type.type_field == "raw untyped slice").then_some(ParamType::RawSlice)) +} + +fn try_enum(the_type: &Type) -> Result> { + let field = &the_type.type_field; + let result = if field.starts_with("enum ") { + let generics = param_types(&the_type.generic_params)?; + + let components = convert_into_param_types(&the_type.components)?; + let variants = EnumVariants::new(components)?; + + Some(ParamType::Enum { variants, generics }) + } else { + None + }; + + Ok(result) +} + +fn try_tuple(the_type: &Type) -> Result> { + let result = if has_tuple_format(&the_type.type_field) { + let tuple_elements = param_types(&the_type.components)?; + Some(ParamType::Tuple(tuple_elements)) + } else { + None + }; + + Ok(result) +} + +fn param_types(coll: &[Type]) -> Result> { + coll.iter().map(|e| e.try_into()).collect() +} + +fn try_str_array(the_type: &Type) -> Result> { + Ok(extract_str_len(&the_type.type_field).map(ParamType::StringArray)) +} + +fn try_str_slice(the_type: &Type) -> Result> { + Ok(if the_type.type_field == "str" { + Some(ParamType::StringSlice) + } else { + None + }) +} + +fn try_array(the_type: &Type) -> Result> { + if let Some(len) = extract_array_len(&the_type.type_field) { + return match the_type.components.as_slice() { + [single_type] => { + let array_type = single_type.try_into()?; + Ok(Some(ParamType::Array(Box::new(array_type), len))) + } + _ => Err(error!( + InvalidType, + "An array must have elements of exactly one type. Array types: {:?}", + the_type.components + )), + }; + } + Ok(None) +} + +fn try_primitive(the_type: &Type) -> Result> { + let result = match the_type.type_field.as_str() { + "bool" => Some(ParamType::Bool), + "u8" => Some(ParamType::U8), + "u16" => Some(ParamType::U16), + "u32" => Some(ParamType::U32), + "u64" => Some(ParamType::U64), + "b256" => Some(ParamType::B256), + "()" => Some(ParamType::Unit), + "str" => Some(ParamType::StringSlice), + _ => None, + }; + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::param_types::ParamType; + + const WIDTH_OF_B256: usize = 4; + const WIDTH_OF_U32: usize = 1; + const WIDTH_OF_BOOL: usize = 1; + + #[test] + fn array_size_dependent_on_num_of_elements() { + const NUM_ELEMENTS: usize = 11; + let param = ParamType::Array(Box::new(ParamType::B256), NUM_ELEMENTS); + + let width = param.compute_encoding_width(); + + let expected = NUM_ELEMENTS * WIDTH_OF_B256; + assert_eq!(expected, width); + } + + #[test] + fn string_size_dependent_on_num_of_elements() { + const NUM_ASCII_CHARS: usize = 9; + let param = ParamType::StringArray(NUM_ASCII_CHARS); + + let width = param.compute_encoding_width(); + + // 2 WORDS or 16 B are enough to fit 9 ascii chars + assert_eq!(2, width); + } + + #[test] + fn structs_are_just_all_elements_combined() { + let inner_struct = ParamType::Struct { + fields: vec![ParamType::U32, ParamType::U32], + generics: vec![], + }; + + let a_struct = ParamType::Struct { + fields: vec![ParamType::B256, ParamType::Bool, inner_struct], + generics: vec![], + }; + + let width = a_struct.compute_encoding_width(); + + const INNER_STRUCT_WIDTH: usize = WIDTH_OF_U32 * 2; + const EXPECTED_WIDTH: usize = WIDTH_OF_B256 + WIDTH_OF_BOOL + INNER_STRUCT_WIDTH; + assert_eq!(EXPECTED_WIDTH, width); + } + + #[test] + fn enums_are_as_big_as_their_biggest_variant_plus_a_word() -> Result<()> { + let fields = vec![ParamType::B256]; + let inner_struct = ParamType::Struct { + fields, + generics: vec![], + }; + let types = vec![ParamType::U32, inner_struct]; + let param = ParamType::Enum { + variants: EnumVariants::new(types)?, + generics: vec![], + }; + + let width = param.compute_encoding_width(); + + const INNER_STRUCT_SIZE: usize = WIDTH_OF_B256; + const EXPECTED_WIDTH: usize = INNER_STRUCT_SIZE + 1; + assert_eq!(EXPECTED_WIDTH, width); + Ok(()) + } + + #[test] + fn tuples_are_just_all_elements_combined() { + let inner_tuple = ParamType::Tuple(vec![ParamType::B256]); + let param = ParamType::Tuple(vec![ParamType::U32, inner_tuple]); + + let width = param.compute_encoding_width(); + + const INNER_TUPLE_WIDTH: usize = WIDTH_OF_B256; + const EXPECTED_WIDTH: usize = WIDTH_OF_U32 + INNER_TUPLE_WIDTH; + assert_eq!(EXPECTED_WIDTH, width); + } + + #[test] + fn handles_simple_types() -> Result<()> { + let parse_param_type = |type_field: &str| { + let type_application = TypeApplication { + name: "".to_string(), + type_id: 0, + type_arguments: None, + }; + + let declarations = [TypeDeclaration { + type_id: 0, + type_field: type_field.to_string(), + components: None, + type_parameters: None, + }]; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + ParamType::try_from_type_application(&type_application, &type_lookup) + }; + + assert_eq!(parse_param_type("u8")?, ParamType::U8); + assert_eq!(parse_param_type("u16")?, ParamType::U16); + assert_eq!(parse_param_type("u32")?, ParamType::U32); + assert_eq!(parse_param_type("u64")?, ParamType::U64); + assert_eq!(parse_param_type("bool")?, ParamType::Bool); + assert_eq!(parse_param_type("b256")?, ParamType::B256); + assert_eq!(parse_param_type("()")?, ParamType::Unit); + assert_eq!(parse_param_type("str[21]")?, ParamType::StringArray(21)); + assert_eq!(parse_param_type("str")?, ParamType::StringSlice); + + Ok(()) + } + + #[test] + fn handles_arrays() -> Result<()> { + // given + let type_application = TypeApplication { + name: "".to_string(), + type_id: 0, + type_arguments: None, + }; + + let declarations = [ + TypeDeclaration { + type_id: 0, + type_field: "[_; 10]".to_string(), + components: Some(vec![TypeApplication { + name: "__array_element".to_string(), + type_id: 1, + type_arguments: None, + }]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 1, + type_field: "u8".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + assert_eq!(result, ParamType::Array(Box::new(ParamType::U8), 10)); + + Ok(()) + } + + #[test] + fn handles_vectors() -> Result<()> { + // given + let declarations = [ + TypeDeclaration { + type_id: 1, + type_field: "generic T".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 2, + type_field: "raw untyped ptr".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 3, + type_field: "struct std::vec::RawVec".to_string(), + components: Some(vec![ + TypeApplication { + name: "ptr".to_string(), + type_id: 2, + type_arguments: None, + }, + TypeApplication { + name: "cap".to_string(), + type_id: 5, + type_arguments: None, + }, + ]), + type_parameters: Some(vec![1]), + }, + TypeDeclaration { + type_id: 4, + type_field: "struct std::vec::Vec".to_string(), + components: Some(vec![ + TypeApplication { + name: "buf".to_string(), + type_id: 3, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 1, + type_arguments: None, + }]), + }, + TypeApplication { + name: "len".to_string(), + type_id: 5, + type_arguments: None, + }, + ]), + type_parameters: Some(vec![1]), + }, + TypeDeclaration { + type_id: 5, + type_field: "u64".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 6, + type_field: "u8".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_application = TypeApplication { + name: "arg".to_string(), + type_id: 4, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 6, + type_arguments: None, + }]), + }; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + assert_eq!(result, ParamType::Vector(Box::new(ParamType::U8))); + + Ok(()) + } + + #[test] + fn handles_structs() -> Result<()> { + // given + let declarations = [ + TypeDeclaration { + type_id: 1, + type_field: "generic T".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 2, + type_field: "struct SomeStruct".to_string(), + components: Some(vec![TypeApplication { + name: "field".to_string(), + type_id: 1, + type_arguments: None, + }]), + type_parameters: Some(vec![1]), + }, + TypeDeclaration { + type_id: 3, + type_field: "u8".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_application = TypeApplication { + name: "arg".to_string(), + type_id: 2, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 3, + type_arguments: None, + }]), + }; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + assert_eq!( + result, + ParamType::Struct { + fields: vec![ParamType::U8], + generics: vec![ParamType::U8] + } + ); + + Ok(()) + } + + #[test] + fn handles_enums() -> Result<()> { + // given + let declarations = [ + TypeDeclaration { + type_id: 1, + type_field: "generic T".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 2, + type_field: "enum SomeEnum".to_string(), + components: Some(vec![TypeApplication { + name: "variant".to_string(), + type_id: 1, + type_arguments: None, + }]), + type_parameters: Some(vec![1]), + }, + TypeDeclaration { + type_id: 3, + type_field: "u8".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_application = TypeApplication { + name: "arg".to_string(), + type_id: 2, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 3, + type_arguments: None, + }]), + }; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + assert_eq!( + result, + ParamType::Enum { + variants: EnumVariants::new(vec![ParamType::U8])?, + generics: vec![ParamType::U8] + } + ); + + Ok(()) + } + + #[test] + fn handles_tuples() -> Result<()> { + // given + let declarations = [ + TypeDeclaration { + type_id: 1, + type_field: "(_, _)".to_string(), + components: Some(vec![ + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 3, + type_arguments: None, + }, + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 2, + type_arguments: None, + }, + ]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 2, + type_field: "str[15]".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 3, + type_field: "u8".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_application = TypeApplication { + name: "arg".to_string(), + type_id: 1, + type_arguments: None, + }; + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + assert_eq!( + result, + ParamType::Tuple(vec![ParamType::U8, ParamType::StringArray(15)]) + ); + + Ok(()) + } + + #[test] + fn ultimate_example() -> Result<()> { + // given + let declarations = [ + TypeDeclaration { + type_id: 1, + type_field: "(_, _)".to_string(), + components: Some(vec![ + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 11, + type_arguments: None, + }, + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 11, + type_arguments: None, + }, + ]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 2, + type_field: "(_, _)".to_string(), + components: Some(vec![ + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 4, + type_arguments: None, + }, + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 24, + type_arguments: None, + }, + ]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 3, + type_field: "(_, _)".to_string(), + components: Some(vec![ + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 5, + type_arguments: None, + }, + TypeApplication { + name: "__tuple_element".to_string(), + type_id: 13, + type_arguments: None, + }, + ]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 4, + type_field: "[_; 1]".to_string(), + components: Some(vec![TypeApplication { + name: "__array_element".to_string(), + type_id: 8, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 22, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 21, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 18, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 13, + type_arguments: None, + }]), + }]), + }]), + }]), + }]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 5, + type_field: "[_; 2]".to_string(), + components: Some(vec![TypeApplication { + name: "__array_element".to_string(), + type_id: 14, + type_arguments: None, + }]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 6, + type_field: "[_; 2]".to_string(), + components: Some(vec![TypeApplication { + name: "__array_element".to_string(), + type_id: 10, + type_arguments: None, + }]), + type_parameters: None, + }, + TypeDeclaration { + type_id: 7, + type_field: "b256".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 8, + type_field: "enum EnumWGeneric".to_string(), + components: Some(vec![ + TypeApplication { + name: "a".to_string(), + type_id: 25, + type_arguments: None, + }, + TypeApplication { + name: "b".to_string(), + type_id: 12, + type_arguments: None, + }, + ]), + type_parameters: Some(vec![12]), + }, + TypeDeclaration { + type_id: 9, + type_field: "generic K".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 10, + type_field: "generic L".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 11, + type_field: "generic M".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 12, + type_field: "generic N".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 13, + type_field: "generic T".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 14, + type_field: "generic U".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 15, + type_field: "raw untyped ptr".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 16, + type_field: "str[2]".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 17, + type_field: "struct MegaExample".to_string(), + components: Some(vec![ + TypeApplication { + name: "a".to_string(), + type_id: 3, + type_arguments: None, + }, + TypeApplication { + name: "b".to_string(), + type_id: 23, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 2, + type_arguments: None, + }]), + }, + ]), + type_parameters: Some(vec![13, 14]), + }, + TypeDeclaration { + type_id: 18, + type_field: "struct PassTheGenericOn".to_string(), + components: Some(vec![TypeApplication { + name: "one".to_string(), + type_id: 20, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 9, + type_arguments: None, + }]), + }]), + type_parameters: Some(vec![9]), + }, + TypeDeclaration { + type_id: 19, + type_field: "struct std::vec::RawVec".to_string(), + components: Some(vec![ + TypeApplication { + name: "ptr".to_string(), + type_id: 15, + type_arguments: None, + }, + TypeApplication { + name: "cap".to_string(), + type_id: 25, + type_arguments: None, + }, + ]), + type_parameters: Some(vec![13]), + }, + TypeDeclaration { + type_id: 20, + type_field: "struct SimpleGeneric".to_string(), + components: Some(vec![TypeApplication { + name: "single_generic_param".to_string(), + type_id: 13, + type_arguments: None, + }]), + type_parameters: Some(vec![13]), + }, + TypeDeclaration { + type_id: 21, + type_field: "struct StructWArrayGeneric".to_string(), + components: Some(vec![TypeApplication { + name: "a".to_string(), + type_id: 6, + type_arguments: None, + }]), + type_parameters: Some(vec![10]), + }, + TypeDeclaration { + type_id: 22, + type_field: "struct StructWTupleGeneric".to_string(), + components: Some(vec![TypeApplication { + name: "a".to_string(), + type_id: 1, + type_arguments: None, + }]), + type_parameters: Some(vec![11]), + }, + TypeDeclaration { + type_id: 23, + type_field: "struct std::vec::Vec".to_string(), + components: Some(vec![ + TypeApplication { + name: "buf".to_string(), + type_id: 19, + type_arguments: Some(vec![TypeApplication { + name: "".to_string(), + type_id: 13, + type_arguments: None, + }]), + }, + TypeApplication { + name: "len".to_string(), + type_id: 25, + type_arguments: None, + }, + ]), + type_parameters: Some(vec![13]), + }, + TypeDeclaration { + type_id: 24, + type_field: "u32".to_string(), + components: None, + type_parameters: None, + }, + TypeDeclaration { + type_id: 25, + type_field: "u64".to_string(), + components: None, + type_parameters: None, + }, + ]; + + let type_lookup = declarations + .into_iter() + .map(|decl| (decl.type_id, decl)) + .collect::>(); + + let type_application = TypeApplication { + name: "arg1".to_string(), + type_id: 17, + type_arguments: Some(vec![ + TypeApplication { + name: "".to_string(), + type_id: 16, + type_arguments: None, + }, + TypeApplication { + name: "".to_string(), + type_id: 7, + type_arguments: None, + }, + ]), + }; + + // when + let result = ParamType::try_from_type_application(&type_application, &type_lookup)?; + + // then + let expected_param_type = { + let fields = vec![ParamType::Struct { + fields: vec![ParamType::StringArray(2)], + generics: vec![ParamType::StringArray(2)], + }]; + let pass_the_generic_on = ParamType::Struct { + fields, + generics: vec![ParamType::StringArray(2)], + }; + + let fields = vec![ParamType::Array(Box::from(pass_the_generic_on.clone()), 2)]; + let struct_w_array_generic = ParamType::Struct { + fields, + generics: vec![pass_the_generic_on], + }; + + let fields = vec![ParamType::Tuple(vec![ + struct_w_array_generic.clone(), + struct_w_array_generic.clone(), + ])]; + let struct_w_tuple_generic = ParamType::Struct { + fields, + generics: vec![struct_w_array_generic], + }; + + let types = vec![ParamType::U64, struct_w_tuple_generic.clone()]; + let fields = vec![ + ParamType::Tuple(vec![ + ParamType::Array(Box::from(ParamType::B256), 2), + ParamType::StringArray(2), + ]), + ParamType::Vector(Box::from(ParamType::Tuple(vec![ + ParamType::Array( + Box::from(ParamType::Enum { + variants: EnumVariants::new(types).unwrap(), + generics: vec![struct_w_tuple_generic], + }), + 1, + ), + ParamType::U32, + ]))), + ]; + ParamType::Struct { + fields, + generics: vec![ParamType::StringArray(2), ParamType::B256], + } + }; + + assert_eq!(result, expected_param_type); + + Ok(()) + } + + #[test] + fn contains_nested_heap_types_false_on_simple_types() -> Result<()> { + // Simple types cannot have nested heap types + assert!(!ParamType::Unit.contains_nested_heap_types()); + assert!(!ParamType::U8.contains_nested_heap_types()); + assert!(!ParamType::U16.contains_nested_heap_types()); + assert!(!ParamType::U32.contains_nested_heap_types()); + assert!(!ParamType::U64.contains_nested_heap_types()); + assert!(!ParamType::Bool.contains_nested_heap_types()); + assert!(!ParamType::B256.contains_nested_heap_types()); + assert!(!ParamType::StringArray(10).contains_nested_heap_types()); + assert!(!ParamType::RawSlice.contains_nested_heap_types()); + assert!(!ParamType::Bytes.contains_nested_heap_types()); + assert!(!ParamType::String.contains_nested_heap_types()); + Ok(()) + } + + #[test] + fn test_complex_types_for_nested_heap_types_containing_vectors() -> Result<()> { + let base_vector = ParamType::Vector(Box::from(ParamType::U8)); + let param_types_no_nested_vec = vec![ParamType::U64, ParamType::U32]; + let param_types_nested_vec = vec![ParamType::Unit, ParamType::Bool, base_vector.clone()]; + + let is_nested = |param_type: ParamType| assert!(param_type.contains_nested_heap_types()); + let not_nested = |param_type: ParamType| assert!(!param_type.contains_nested_heap_types()); + + not_nested(base_vector.clone()); + is_nested(ParamType::Vector(Box::from(base_vector.clone()))); + + not_nested(ParamType::Array(Box::from(ParamType::U8), 10)); + is_nested(ParamType::Array(Box::from(base_vector), 10)); + + not_nested(ParamType::Tuple(param_types_no_nested_vec.clone())); + is_nested(ParamType::Tuple(param_types_nested_vec.clone())); + + not_nested(ParamType::Struct { + generics: param_types_no_nested_vec.clone(), + fields: param_types_no_nested_vec.clone(), + }); + is_nested(ParamType::Struct { + generics: param_types_nested_vec.clone(), + fields: param_types_no_nested_vec.clone(), + }); + is_nested(ParamType::Struct { + generics: param_types_no_nested_vec.clone(), + fields: param_types_nested_vec.clone(), + }); + + not_nested(ParamType::Enum { + variants: EnumVariants::new(param_types_no_nested_vec.clone())?, + generics: param_types_no_nested_vec.clone(), + }); + is_nested(ParamType::Enum { + variants: EnumVariants::new(param_types_nested_vec.clone())?, + generics: param_types_no_nested_vec.clone(), + }); + is_nested(ParamType::Enum { + variants: EnumVariants::new(param_types_no_nested_vec)?, + generics: param_types_nested_vec, + }); + Ok(()) + } + + #[test] + fn test_complex_types_for_nested_heap_types_containing_bytes() -> Result<()> { + let base_bytes = ParamType::Bytes; + let param_types_no_nested_bytes = vec![ParamType::U64, ParamType::U32]; + let param_types_nested_bytes = vec![ParamType::Unit, ParamType::Bool, base_bytes.clone()]; + + let is_nested = |param_type: ParamType| assert!(param_type.contains_nested_heap_types()); + let not_nested = |param_type: ParamType| assert!(!param_type.contains_nested_heap_types()); + + not_nested(base_bytes.clone()); + is_nested(ParamType::Vector(Box::from(base_bytes.clone()))); + + not_nested(ParamType::Array(Box::from(ParamType::U8), 10)); + is_nested(ParamType::Array(Box::from(base_bytes), 10)); + + not_nested(ParamType::Tuple(param_types_no_nested_bytes.clone())); + is_nested(ParamType::Tuple(param_types_nested_bytes.clone())); + + let not_nested_struct = ParamType::Struct { + generics: param_types_no_nested_bytes.clone(), + fields: param_types_no_nested_bytes.clone(), + }; + not_nested(not_nested_struct); + + let nested_struct = ParamType::Struct { + generics: param_types_nested_bytes.clone(), + fields: param_types_no_nested_bytes.clone(), + }; + is_nested(nested_struct); + + let nested_struct = ParamType::Struct { + generics: param_types_no_nested_bytes.clone(), + fields: param_types_nested_bytes.clone(), + }; + is_nested(nested_struct); + + let not_nested_enum = ParamType::Enum { + variants: EnumVariants::new(param_types_no_nested_bytes.clone())?, + generics: param_types_no_nested_bytes.clone(), + }; + not_nested(not_nested_enum); + + let nested_enum = ParamType::Enum { + variants: EnumVariants::new(param_types_nested_bytes.clone())?, + generics: param_types_no_nested_bytes.clone(), + }; + is_nested(nested_enum); + + let nested_enum = ParamType::Enum { + variants: EnumVariants::new(param_types_no_nested_bytes)?, + generics: param_types_nested_bytes, + }; + is_nested(nested_enum); + + Ok(()) + } + + #[test] + fn try_vector_is_type_path_backward_compatible() { + // TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. + let the_type = given_generic_type_with_path("Vec"); + + let param_type = try_vector(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::Vector(Box::new(ParamType::U8))); + } + + #[test] + fn try_vector_correctly_resolves_param_type() { + let the_type = given_generic_type_with_path("std::vec::Vec"); + + let param_type = try_vector(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::Vector(Box::new(ParamType::U8))); + } + + #[test] + fn try_bytes_is_type_path_backward_compatible() { + // TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. + let the_type = given_type_with_path("Bytes"); + + let param_type = try_bytes(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::Bytes); + } + + #[test] + fn try_bytes_correctly_resolves_param_type() { + let the_type = given_type_with_path("std::bytes::Bytes"); + + let param_type = try_bytes(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::Bytes); + } + + #[test] + fn try_raw_slice_correctly_resolves_param_type() { + let the_type = Type { + type_field: "raw untyped slice".to_string(), + generic_params: vec![], + components: vec![], + }; + + let param_type = try_raw_slice(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::RawSlice); + } + + #[test] + fn try_std_string_correctly_resolves_param_type() { + let the_type = given_type_with_path("std::string::String"); + + let param_type = try_std_string(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::String); + } + + #[test] + fn try_std_string_is_type_path_backward_compatible() { + // TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. + let the_type = given_type_with_path("String"); + + let param_type = try_std_string(&the_type).unwrap().unwrap(); + + assert_eq!(param_type, ParamType::String); + } + + fn given_type_with_path(path: &str) -> Type { + Type { + type_field: format!("struct {path}"), + generic_params: vec![], + components: vec![], + } + } + + fn given_generic_type_with_path(path: &str) -> Type { + Type { + type_field: format!("struct {path}"), + generic_params: vec![Type { + type_field: "u8".to_string(), + generic_params: vec![], + components: vec![], + }], + components: vec![], + } + } +} diff --git a/docs/beta-4/fuels-rs/packages/fuels-core/src/types/transaction_builders.rs b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/transaction_builders.rs new file mode 100644 index 00000000..6e761d35 --- /dev/null +++ b/docs/beta-4/fuels-rs/packages/fuels-core/src/types/transaction_builders.rs @@ -0,0 +1,740 @@ +#![cfg(feature = "std")] + +use std::collections::HashMap; + +use fuel_asm::{op, GTFArgs, RegId}; +use fuel_crypto::{Message as CryptoMessage, SecretKey, Signature}; +use fuel_tx::{ + 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, MemLayout, Salt}; +use fuel_vm::{checked_transaction::EstimatePredicates, gas::GasCosts}; + +use super::unresolved_bytes::UnresolvedBytes; +use crate::{ + constants::{BASE_ASSET_ID, WORD_SIZE}, + offsets, + types::{ + bech32::Bech32Address, + coin::Coin, + coin_type::CoinType, + errors::{error, Error, Result}, + input::Input, + message::Message, + transaction::{CreateTransaction, ScriptTransaction, Transaction, TxParameters}, + Address, AssetId, ContractId, + }, +}; + +#[derive(Debug, Clone, Default)] +struct UnresolvedSignatures { + addr_idx_offset_map: HashMap, + secret_keys: Vec, +} + +pub trait TransactionBuilder: Send { + type TxType: Transaction; + + fn build(self) -> Result; + fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); + 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; + fn with_tx_params(self, tx_params: TxParameters) -> Self; + 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; + fn outputs_mut(&mut self) -> &mut Vec; + fn witnesses(&self) -> &Vec; + fn witnesses_mut(&mut self) -> &mut Vec; +} + +macro_rules! impl_tx_trait { + ($ty: ty, $tx_ty: ident) => { + impl TransactionBuilder for $ty { + 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) + } 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()) + }; + + let num_witnesses = self.num_witnesses()?; + let mut tx = + self.resolve_fuel_tx(base_offset, num_witnesses, &consensus_parameters)?; + + tx.precompute(&consensus_parameters.chain_id)?; + + if uses_predicates { + estimate_predicates(&mut tx, &consensus_parameters)?; + }; + + Ok($tx_ty { tx }) + } + + 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); + } + + 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 { + self.maturity = maturity.into(); + self + } + + fn with_gas_price(mut self, gas_price: u64) -> Self { + self.gas_price = gas_price; + self + } + + fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = 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_inputs(mut self, inputs: Vec) -> Self { + self.inputs = inputs; + self + } + + fn with_outputs(mut self, outputs: Vec) -> Self { + self.outputs = outputs; + self + } + + fn with_witnesses(mut self, witnesses: Vec) -> Self { + self.witnesses = witnesses; + 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() + } + + fn inputs_mut(&mut self) -> &mut Vec { + &mut self.inputs + } + + fn outputs(&self) -> &Vec { + self.outputs.as_ref() + } + + fn outputs_mut(&mut self) -> &mut Vec { + &mut self.outputs + } + + fn witnesses(&self) -> &Vec { + self.witnesses.as_ref() + } + + fn witnesses_mut(&mut self) -> &mut Vec { + &mut self.witnesses + } + } + + impl $ty { + fn is_using_predicates(&self) -> bool { + self.inputs() + .iter() + .any(|input| matches!(input, Input::ResourcePredicate { .. })) + } + + fn num_witnesses(&self) -> Result { + 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")); + } + + Ok(num_witnesses as u8) + } + } + }; +} + +#[derive(Debug, Clone, Default)] +pub struct ScriptTransactionBuilder { + pub gas_price: u64, + pub gas_limit: u64, + pub maturity: u32, + pub script: Vec, + pub script_data: Vec, + pub inputs: Vec, + pub outputs: Vec, + pub witnesses: Vec, + pub(crate) consensus_parameters: Option, + unresolved_signatures: UnresolvedSignatures, +} + +#[derive(Debug, Clone, Default)] +pub struct CreateTransactionBuilder { + pub gas_price: u64, + pub gas_limit: u64, + pub maturity: u32, + pub bytecode_length: u64, + pub bytecode_witness_index: u8, + pub storage_slots: Vec, + pub inputs: Vec, + pub outputs: Vec, + pub witnesses: Vec, + pub salt: Salt, + pub(crate) consensus_parameters: Option, + unresolved_signatures: UnresolvedSignatures, +} + +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