diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d2d43f4e8..920fe55659 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,7 @@ jobs: args: --all-targets --features "default fuel-core-lib test-type-paths" download_sway_artifacts: sway-examples-w-type-paths - cargo_command: nextest - args: run --all-targets --features "default fuel-core-lib test-type-paths" --workspace + args: run --all-targets --features "default fuel-core-lib test-type-paths coin-cache" --workspace download_sway_artifacts: sway-examples-w-type-paths install_fuel_core: true - cargo_command: nextest diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 4dd05ef7f8..0440742faf 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -31,7 +31,9 @@ zeroize = { workspace = true, features = ["derive"] } [dev-dependencies] tempfile = { workspace = true } +tokio = { workspace = true, features = ["test-util"]} [features] default = ["std"] +coin-cache = ["tokio?/time"] std = ["fuels-core/std", "dep:tokio", "fuel-core-client/default", "dep:eth-keystore"] diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index c2d2234eac..14edb72ee5 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -23,7 +23,7 @@ use fuels_core::{ }; use crate::{ - accounts_utils::extract_message_id, + accounts_utils::{adjust_inputs_outputs, calculate_missing_base_amount, extract_message_id}, provider::{Provider, ResourceFilter}, }; @@ -117,26 +117,6 @@ pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone { .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)] @@ -166,11 +146,50 @@ pub trait Account: ViewOnlyAccount { ] } - async fn add_fee_resources( + /// 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, - tb: Tb, - previous_base_amount: u64, - ) -> Result; + 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) + } + + /// Add base asset inputs to the transaction to cover the estimated fee. + /// Requires contract inputs to be at the start of the transactions inputs vec + /// so that their indexes are retained + async fn adjust_for_fee( + &self, + tb: &mut Tb, + used_base_amount: u64, + ) -> Result<()> { + let missing_base_amount = calculate_missing_base_amount(tb, used_base_amount)?; + + if missing_base_amount > 0 { + let new_base_inputs = self + .get_asset_inputs_for_amount(BASE_ASSET_ID, missing_base_amount) + .await?; + + adjust_inputs_outputs(tb, new_base_inputs, self.address()); + }; + + Ok(()) + } + + // Add signatures to the builder if the underlying account is a wallet + fn add_witnessses(&self, _tb: &mut Tb) {} /// Transfer funds from this account to another `Address`. /// Fails if amount for asset ID is larger than address's spendable coins. @@ -186,26 +205,19 @@ pub trait Account: ViewOnlyAccount { let network_info = provider.network_info().await?; let inputs = self.get_asset_inputs_for_amount(asset_id, amount).await?; - let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount); - let tx_builder = ScriptTransactionBuilder::prepare_transfer( + let mut tx_builder = ScriptTransactionBuilder::prepare_transfer( inputs, outputs, tx_parameters, network_info, ); - // 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 - }; + self.add_witnessses(&mut tx_builder); + self.adjust_for_fee(&mut tx_builder, amount).await?; - let tx = self - .add_fee_resources(tx_builder, previous_base_amount) - .await?; + let tx = tx_builder.build()?; let tx_id = provider.send_transaction_and_await_commit(tx).await?; let receipts = provider @@ -254,7 +266,7 @@ pub trait Account: ViewOnlyAccount { ]; // Build transaction and sign it - let tb = ScriptTransactionBuilder::prepare_contract_transfer( + let mut tb = ScriptTransactionBuilder::prepare_contract_transfer( plain_contract_id, balance, asset_id, @@ -264,14 +276,9 @@ pub trait Account: ViewOnlyAccount { network_info, ); - // 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?; + self.add_witnessses(&mut tb); + self.adjust_for_fee(&mut tb, balance).await?; + let tx = tb.build()?; let tx_id = provider.send_transaction_and_await_commit(tx).await?; @@ -299,7 +306,7 @@ pub trait Account: ViewOnlyAccount { .get_asset_inputs_for_amount(BASE_ASSET_ID, amount) .await?; - let tb = ScriptTransactionBuilder::prepare_message_to_output( + let mut tb = ScriptTransactionBuilder::prepare_message_to_output( to.into(), amount, inputs, @@ -307,7 +314,9 @@ pub trait Account: ViewOnlyAccount { network_info, ); - let tx = self.add_fee_resources(tb, amount).await?; + self.add_witnessses(&mut tb); + self.adjust_for_fee(&mut tb, amount).await?; + let tx = tb.build()?; let tx_id = provider.send_transaction_and_await_commit(tx).await?; let receipts = provider diff --git a/packages/fuels-accounts/src/accounts_utils.rs b/packages/fuels-accounts/src/accounts_utils.rs index 6125bf7a81..300e495cb8 100644 --- a/packages/fuels-accounts/src/accounts_utils.rs +++ b/packages/fuels-accounts/src/accounts_utils.rs @@ -6,7 +6,7 @@ use fuels_core::{ bech32::Bech32Address, errors::{error, Error, Result}, input::Input, - transaction_builders::{NetworkInfo, TransactionBuilder}, + transaction_builders::TransactionBuilder, }, }; @@ -14,65 +14,59 @@ pub fn extract_message_id(receipts: &[Receipt]) -> Option { receipts.iter().find_map(|m| m.message_id()) } -pub fn calculate_base_amount_with_fee( +pub fn calculate_missing_base_amount( tb: &impl TransactionBuilder, - network_info: &NetworkInfo, - previous_base_amount: u64, + used_base_amount: u64, ) -> Result { let transaction_fee = tb - .fee_checked_from_tx(network_info)? + .fee_checked_from_tx()? .ok_or(error!(InvalidData, "Error calculating TransactionFee"))?; - let mut new_base_amount = transaction_fee.max_fee() + previous_base_amount; + let available_amount = available_base_amount(tb); - // 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; - } + let total_used = transaction_fee.max_fee() + used_base_amount; + let missing_amount = if total_used > available_amount { + total_used - available_amount + } else if !is_consuming_utxos(tb) { + // A tx needs to have at least 1 spendable input + // Enforce a minimum required amount on the base asset if no other inputs are present + 1 + } else { + 0 + }; - Ok(new_base_amount) + Ok(missing_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() +fn available_base_amount(tb: &impl TransactionBuilder) -> u64 { + tb.inputs() .iter() - .filter(|input| { - input.contains_data() - || !matches!(input , Input::ResourceSigned { resource , .. } - | Input::ResourcePredicate { resource, .. } if resource.asset_id() == BASE_ASSET_ID) + .filter_map(|input| match (input.amount(), input.asset_id()) { + (Some(amount), Some(asset_id)) if asset_id == BASE_ASSET_ID => Some(amount), + _ => None, }) - .cloned() - .chain(new_base_inputs) - .collect(); + .sum() +} - *tb.inputs_mut() = adjusted_inputs +fn is_consuming_utxos(tb: &impl TransactionBuilder) -> bool { + tb.inputs() + .iter() + .any(|input| !matches!(input, Input::Contract { .. })) } -pub fn adjust_outputs( +pub fn adjust_inputs_outputs( tb: &mut impl TransactionBuilder, + new_base_inputs: impl IntoIterator, address: &Bech32Address, - new_base_amount: u64, ) { + tb.inputs_mut().extend(new_base_inputs); + 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 { + if !is_base_change_present { tb.outputs_mut() .push(Output::change(address.into(), 0, BASE_ASSET_ID)); } diff --git a/packages/fuels-accounts/src/coin_cache.rs b/packages/fuels-accounts/src/coin_cache.rs new file mode 100644 index 0000000000..921db892ab --- /dev/null +++ b/packages/fuels-accounts/src/coin_cache.rs @@ -0,0 +1,196 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use tokio::time::{Duration, Instant}; + +use fuel_types::AssetId; +use fuels_core::types::bech32::Bech32Address; +use fuels_core::types::coin_type_id::CoinTypeId; + +type CoinCacheKey = (Bech32Address, AssetId); + +#[derive(Debug)] +pub(crate) struct CoinsCache { + ttl: Duration, + items: HashMap>, +} + +impl Default for CoinsCache { + fn default() -> Self { + Self::new(Duration::from_secs(30)) + } +} + +impl CoinsCache { + pub fn new(ttl: Duration) -> Self { + Self { + ttl, + items: HashMap::default(), + } + } + + pub fn insert_multiple( + &mut self, + coin_ids: impl IntoIterator)>, + ) { + for (key, ids) in coin_ids { + let new_items = ids.into_iter().map(CoinCacheItem::new); + + let items = self.items.entry(key.clone()).or_default(); + items.extend(new_items); + } + } + + pub fn get_active(&mut self, key: &CoinCacheKey) -> HashSet { + self.remove_expired_entries(key); + + self.items + .get(key) + .cloned() + .unwrap_or_default() + .into_iter() + .map(|item| item.id) + .collect() + } + + pub fn remove_items( + &mut self, + inputs: impl IntoIterator)>, + ) { + for (key, ids) in inputs { + for id in ids { + self.remove(&key, id); + } + } + } + + fn remove(&mut self, key: &CoinCacheKey, id: CoinTypeId) { + if let Some(ids) = self.items.get_mut(key) { + let item = CoinCacheItem::new(id); + ids.remove(&item); + } + } + + fn remove_expired_entries(&mut self, key: &CoinCacheKey) { + if let Some(entry) = self.items.get_mut(key) { + entry.retain(|item| item.is_valid(self.ttl)); + } + } +} + +#[derive(Eq, Debug, Clone)] +struct CoinCacheItem { + created_at: Instant, + pub id: CoinTypeId, +} + +impl PartialEq for CoinCacheItem { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Hash for CoinCacheItem { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl CoinCacheItem { + pub fn new(id: CoinTypeId) -> Self { + Self { + created_at: Instant::now(), + id, + } + } + + pub fn is_valid(&self, ttl: Duration) -> bool { + self.created_at + ttl > Instant::now() + } +} + +#[cfg(test)] +mod tests { + use fuel_tx::UtxoId; + use fuel_types::{Bytes32, Nonce}; + + use super::*; + + fn get_items() -> (CoinTypeId, CoinTypeId) { + let utxo_id = UtxoId::new(Bytes32::from([1u8; 32]), 0); + let nonce = Nonce::new([2u8; 32]); + + (CoinTypeId::UtxoId(utxo_id), CoinTypeId::Nonce(nonce)) + } + + #[test] + fn test_insert_and_get_active() { + let mut cache = CoinsCache::new(Duration::from_secs(60)); + + let key: CoinCacheKey = Default::default(); + let (item1, item2) = get_items(); + let items = HashMap::from([(key.clone(), vec![item1.clone(), item2.clone()])]); + + cache.insert_multiple(items); + + let active_coins = cache.get_active(&key); + + assert_eq!(active_coins.len(), 2); + assert!(active_coins.contains(&item1)); + assert!(active_coins.contains(&item2)); + } + + #[tokio::test] + async fn test_insert_and_expire_items() { + let mut cache = CoinsCache::new(Duration::from_secs(10)); + + let key = CoinCacheKey::default(); + let (item1, _) = get_items(); + let items = HashMap::from([(key.clone(), vec![item1.clone()])]); + + cache.insert_multiple(items); + + // Advance time by more than the cache's TTL + tokio::time::pause(); + tokio::time::advance(Duration::from_secs(12)).await; + + let (_, item2) = get_items(); + let items = HashMap::from([(key.clone(), vec![item2.clone()])]); + cache.insert_multiple(items); + + let active_coins = cache.get_active(&key); + + assert_eq!(active_coins.len(), 1); + assert!(!active_coins.contains(&item1)); + assert!(active_coins.contains(&item2)); + } + + #[test] + fn test_get_active_no_items() { + let mut cache = CoinsCache::new(Duration::from_secs(60)); + + let key = Default::default(); + let active_coins = cache.get_active(&key); + + assert!(active_coins.is_empty()); + } + + #[test] + fn test_remove_items() { + let mut cache = CoinsCache::new(Duration::from_secs(60)); + + let key: CoinCacheKey = Default::default(); + let (item1, item2) = get_items(); + + let items_to_insert = [(key.clone(), vec![item1.clone(), item2.clone()])]; + cache.insert_multiple(items_to_insert.iter().cloned()); + + let items_to_remove = [(key.clone(), vec![item1.clone()])]; + cache.remove_items(items_to_remove.iter().cloned()); + + let active_coins = cache.get_active(&key); + + assert_eq!(active_coins.len(), 1); + assert!(!active_coins.contains(&item1)); + assert!(active_coins.contains(&item2)); + } +} diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 8e60fdc80d..94612bf500 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -10,4 +10,7 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; +#[cfg(feature = "coin-cache")] +mod coin_cache; + pub mod predicate; diff --git a/packages/fuels-accounts/src/predicate.rs b/packages/fuels-accounts/src/predicate.rs index 1949384d3b..1ccdea581d 100644 --- a/packages/fuels-accounts/src/predicate.rs +++ b/packages/fuels-accounts/src/predicate.rs @@ -1,21 +1,14 @@ use std::{fmt::Debug, fs}; #[cfg(feature = "std")] -use fuels_core::{ - constants::BASE_ASSET_ID, - types::{input::Input, transaction_builders::TransactionBuilder, AssetId}, -}; +use fuels_core::types::{input::Input, AssetId}; use fuels_core::{ types::{bech32::Bech32Address, errors::Result, unresolved_bytes::UnresolvedBytes}, Configurables, }; #[cfg(feature = "std")] -use crate::{ - accounts_utils::{adjust_inputs, adjust_outputs, calculate_base_amount_with_fee}, - provider::Provider, - Account, AccountError, AccountResult, ViewOnlyAccount, -}; +use crate::{provider::Provider, Account, AccountError, AccountResult, ViewOnlyAccount}; #[derive(Debug, Clone)] pub struct Predicate { @@ -135,30 +128,4 @@ impl Account for Predicate { }) .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 network_info = self.try_provider()?.network_info().await?; - let new_base_amount = - calculate_base_amount_with_fee(&tb, &network_info, 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/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 0b7645337a..629d6a61b9 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -37,6 +37,15 @@ use thiserror::Error; use crate::provider::retryable_client::RetryableClient; +#[cfg(feature = "coin-cache")] +use crate::coin_cache::CoinsCache; +#[cfg(feature = "coin-cache")] +use fuels_core::types::coin_type_id::CoinTypeId; +#[cfg(feature = "coin-cache")] +use std::sync::Arc; +#[cfg(feature = "coin-cache")] +use tokio::sync::Mutex; + type ProviderResult = std::result::Result; #[derive(Debug)] @@ -149,6 +158,8 @@ impl From for Error { pub struct Provider { client: RetryableClient, consensus_parameters: ConsensusParameters, + #[cfg(feature = "coin-cache")] + cache: Arc>, } impl Provider { @@ -158,6 +169,8 @@ impl Provider { Ok(Self { client, consensus_parameters, + #[cfg(feature = "coin-cache")] + cache: Default::default(), }) } @@ -178,6 +191,8 @@ impl Provider { Ok(Self { client, consensus_parameters, + #[cfg(feature = "coin-cache")] + cache: Default::default(), }) } @@ -188,7 +203,17 @@ impl Provider { /// Sends a transaction to the underlying Provider's client. pub async fn send_transaction_and_await_commit(&self, tx: T) -> Result { let tx_id = self.send_transaction(tx.clone()).await?; - self.client.await_transaction_commit(&tx_id).await?; + let _status = self.client.await_transaction_commit(&tx_id).await?; + + #[cfg(feature = "coin-cache")] + { + if matches!( + _status, + TransactionStatus::SqueezedOut { .. } | TransactionStatus::Failure { .. } + ) { + self.cache.lock().await.remove_items(tx.used_coins()) + } + } Ok(tx_id) } @@ -208,7 +233,7 @@ impl Provider { self.validate_transaction(tx.clone()).await?; - Ok(self.client.submit(&tx.into()).await?) + self.submit(tx).await } async fn validate_transaction(&self, tx: T) -> Result<()> { @@ -240,6 +265,20 @@ impl Provider { Ok(()) } + #[cfg(not(feature = "coin-cache"))] + async fn submit(&self, tx: T) -> Result { + Ok(self.client.submit(&tx.into()).await?) + } + + #[cfg(feature = "coin-cache")] + async fn submit(&self, tx: T) -> Result { + let used_utxos = tx.used_coins(); + let tx_id = self.client.submit(&tx.into()).await?; + self.cache.lock().await.insert_multiple(used_utxos); + + Ok(tx_id) + } + pub async fn tx_status(&self, tx_id: &TxId) -> ProviderResult { let fetch_receipts = || async { let receipts = self.client.receipts(tx_id).await?; @@ -397,10 +436,7 @@ impl Provider { 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( + async fn request_coins_to_spend( &self, filter: ResourceFilter, ) -> ProviderResult> { @@ -422,6 +458,59 @@ impl Provider { Ok(res) } + /// 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. + #[cfg(not(feature = "coin-cache"))] + pub async fn get_spendable_resources( + &self, + filter: ResourceFilter, + ) -> ProviderResult> { + self.request_coins_to_spend(filter).await + } + + /// 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. + /// Coins that were recently submitted inside a tx will be ignored from the results. + #[cfg(feature = "coin-cache")] + pub async fn get_spendable_resources( + &self, + mut filter: ResourceFilter, + ) -> ProviderResult> { + self.extend_filter_with_cached(&mut filter).await; + self.request_coins_to_spend(filter).await + } + + #[cfg(feature = "coin-cache")] + async fn extend_filter_with_cached(&self, filter: &mut ResourceFilter) { + let mut cache = self.cache.lock().await; + let used_coins = cache.get_active(&(filter.from.clone(), filter.asset_id)); + + let excluded_utxos = used_coins + .iter() + .filter_map(|coin_id| match coin_id { + CoinTypeId::UtxoId(utxo_id) => Some(utxo_id), + _ => None, + }) + .cloned() + .collect::>(); + + let excluded_message_nonces = used_coins + .iter() + .filter_map(|coin_id| match coin_id { + CoinTypeId::Nonce(nonce) => Some(nonce), + _ => None, + }) + .cloned() + .collect::>(); + + filter.excluded_utxos.extend(excluded_utxos); + filter + .excluded_message_nonces + .extend(excluded_message_nonces); + } + /// 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. diff --git a/packages/fuels-accounts/src/wallet.rs b/packages/fuels-accounts/src/wallet.rs index 11511217aa..1db3135659 100644 --- a/packages/fuels-accounts/src/wallet.rs +++ b/packages/fuels-accounts/src/wallet.rs @@ -4,22 +4,18 @@ 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 fuels_core::types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + AssetId, }; use rand::{CryptoRng, Rng}; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ - accounts_utils::{adjust_inputs, adjust_outputs, calculate_base_amount_with_fee}, provider::{Provider, ProviderError}, Account, AccountError, AccountResult, Signer, ViewOnlyAccount, }; @@ -259,23 +255,8 @@ impl Account for WalletUnlocked { .collect::>()) } - async fn add_fee_resources( - &self, - mut tb: Tb, - previous_base_amount: u64, - ) -> Result { - let network_info = self.try_provider()?.network_info().await?; - self.sign_transaction(&mut tb); - - let new_base_amount = - calculate_base_amount_with_fee(&tb, &network_info, 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() + fn add_witnessses(&self, tb: &mut Tb) { + self.sign_transaction(tb); } } diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index 2b22b41198..190a47493b 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -64,7 +64,7 @@ pub trait TransactionBuilder: Send { fn build(self) -> Result; fn add_unresolved_signature(&mut self, owner: Bech32Address, secret_key: SecretKey); - fn fee_checked_from_tx(&self, network_info: &NetworkInfo) -> Result>; + fn fee_checked_from_tx(&self) -> 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; @@ -78,6 +78,7 @@ pub trait TransactionBuilder: Send { fn outputs_mut(&mut self) -> &mut Vec; fn witnesses(&self) -> &Vec; fn witnesses_mut(&mut self) -> &mut Vec; + fn consensus_parameters(&self) -> ConsensusParameters; } macro_rules! impl_tx_trait { @@ -109,21 +110,17 @@ macro_rules! impl_tx_trait { .insert(owner, index_offset); } - fn fee_checked_from_tx( - &self, - network_info: &NetworkInfo, - ) -> Result> { + fn fee_checked_from_tx(&self) -> Result> { let mut tx = self.clone().build()?; - if tx.is_using_predicates() { tx.estimate_predicates( - &network_info.consensus_parameters, - &network_info.gas_costs, + &self.consensus_parameters(), + &self.network_info.gas_costs, )?; - }; + } Ok(TransactionFee::checked_from_tx( - &network_info.consensus_parameters, + &self.consensus_parameters(), &tx.tx, )) } @@ -188,6 +185,10 @@ macro_rules! impl_tx_trait { fn witnesses_mut(&mut self) -> &mut Vec { &mut self.witnesses } + + fn consensus_parameters(&self) -> ConsensusParameters { + self.network_info.consensus_parameters + } } impl $ty { @@ -292,7 +293,7 @@ impl ScriptTransactionBuilder { } fn base_offset(&self) -> usize { - offsets::base_offset_script(&self.network_info.consensus_parameters) + offsets::base_offset_script(&self.consensus_parameters()) + padded_len_usize(self.script_data.len()) + padded_len_usize(self.script.len()) } @@ -454,7 +455,7 @@ impl CreateTransactionBuilder { } fn base_offset(&self) -> usize { - offsets::base_offset_create(&self.network_info.consensus_parameters) + offsets::base_offset_create(&self.consensus_parameters()) } pub fn with_bytecode_length(mut self, bytecode_length: u64) -> Self { diff --git a/packages/fuels-core/src/types/wrappers.rs b/packages/fuels-core/src/types/wrappers.rs index 4d8d57b318..b0bc9c18b1 100644 --- a/packages/fuels-core/src/types/wrappers.rs +++ b/packages/fuels-core/src/types/wrappers.rs @@ -2,6 +2,7 @@ pub mod block; pub mod chain_info; pub mod coin; pub mod coin_type; +pub mod coin_type_id; pub mod input; pub mod message; pub mod message_proof; diff --git a/packages/fuels-core/src/types/wrappers/coin_type.rs b/packages/fuels-core/src/types/wrappers/coin_type.rs index 201d246764..33dc65d6ee 100644 --- a/packages/fuels-core/src/types/wrappers/coin_type.rs +++ b/packages/fuels-core/src/types/wrappers/coin_type.rs @@ -8,6 +8,8 @@ use crate::{ types::{bech32::Bech32Address, coin::Coin, message::Message}, }; +use super::coin_type_id::CoinTypeId; + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CoinType { Coin(Coin), @@ -30,6 +32,13 @@ impl TryFrom for CoinType { } impl CoinType { + pub fn id(&self) -> CoinTypeId { + match self { + CoinType::Coin(coin) => CoinTypeId::UtxoId(coin.utxo_id), + CoinType::Message(message) => CoinTypeId::Nonce(message.nonce), + } + } + pub fn amount(&self) -> u64 { match self { CoinType::Coin(coin) => coin.amount, diff --git a/packages/fuels-core/src/types/wrappers/coin_type_id.rs b/packages/fuels-core/src/types/wrappers/coin_type_id.rs new file mode 100644 index 0000000000..d8e1879b99 --- /dev/null +++ b/packages/fuels-core/src/types/wrappers/coin_type_id.rs @@ -0,0 +1,8 @@ +use fuel_tx::UtxoId; +use fuel_types::Nonce; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CoinTypeId { + UtxoId(UtxoId), + Nonce(Nonce), +} diff --git a/packages/fuels-core/src/types/wrappers/transaction.rs b/packages/fuels-core/src/types/wrappers/transaction.rs index a50ff9795d..ee08095112 100644 --- a/packages/fuels-core/src/types/wrappers/transaction.rs +++ b/packages/fuels-core/src/types/wrappers/transaction.rs @@ -1,18 +1,26 @@ +use itertools::Itertools; +use std::collections::HashMap; use std::fmt::Debug; use fuel_tx::{ field::{ GasLimit, GasPrice, Inputs, Maturity, Outputs, Script as ScriptField, ScriptData, Witnesses, }, + input::{ + coin::{CoinPredicate, CoinSigned}, + message::{ + MessageCoinPredicate, MessageCoinSigned, MessageDataPredicate, MessageDataSigned, + }, + }, Bytes32, Cacheable, Chargeable, ConsensusParameters, Create, FormatValidityChecks, Input, Output, Salt as FuelSalt, Script, StorageSlot, Transaction as FuelTransaction, TransactionFee, UniqueIdentifier, Witness, }; -use fuel_types::ChainId; +use fuel_types::{AssetId, ChainId}; use fuel_vm::{checked_transaction::EstimatePredicates, prelude::GasCosts}; -use crate::types::Result; +use crate::types::{bech32::Bech32Address, Result}; #[derive(Default, Debug, Copy, Clone)] pub struct TxParameters { @@ -60,6 +68,8 @@ impl TxParameters { use fuel_tx::field::{BytecodeLength, BytecodeWitnessIndex, Salt, StorageSlots}; +use super::coin_type_id::CoinTypeId; + #[derive(Debug, Clone)] pub enum TransactionType { Script(ScriptTransaction), @@ -117,6 +127,8 @@ pub trait Transaction: Into + Clone { /// Append witness and return the corresponding witness index fn append_witness(&mut self, witness: Witness) -> usize; + + fn used_coins(&self) -> HashMap<(Bech32Address, AssetId), Vec>; } impl From for FuelTransaction { @@ -262,6 +274,37 @@ impl Transaction for TransactionType { TransactionType::Create(tx) => tx.append_witness(witness), } } + + fn used_coins(&self) -> HashMap<(Bech32Address, AssetId), Vec> { + match self { + TransactionType::Script(tx) => tx.used_coins(), + TransactionType::Create(tx) => tx.used_coins(), + } + } +} + +fn extract_coin_type_id(input: &Input) -> Option { + if let Some(utxo_id) = input.utxo_id() { + return Some(CoinTypeId::UtxoId(*utxo_id)); + } else if let Some(nonce) = input.nonce() { + return Some(CoinTypeId::Nonce(*nonce)); + } + + None +} + +pub fn extract_owner_or_recipient(input: &Input) -> Option { + let addr = match input { + Input::CoinSigned(CoinSigned { owner, .. }) + | Input::CoinPredicate(CoinPredicate { owner, .. }) => Some(owner), + Input::MessageCoinSigned(MessageCoinSigned { recipient, .. }) + | Input::MessageCoinPredicate(MessageCoinPredicate { recipient, .. }) + | Input::MessageDataSigned(MessageDataSigned { recipient, .. }) + | Input::MessageDataPredicate(MessageDataPredicate { recipient, .. }) => Some(recipient), + Input::Contract(_) => None, + }; + + addr.map(|addr| Bech32Address::from(*addr)) } macro_rules! impl_tx_wrapper { @@ -399,6 +442,22 @@ macro_rules! impl_tx_wrapper { idx } + + fn used_coins(&self) -> HashMap<(Bech32Address, AssetId), Vec> { + self.inputs() + .iter() + .filter_map(|input| match input { + Input::Contract { .. } => None, + _ => { + // not a contract, it's safe to unwrap + let owner = extract_owner_or_recipient(input).unwrap(); + let asset_id = input.asset_id().unwrap().to_owned(); + let id = extract_coin_type_id(input).unwrap(); + Some(((owner, asset_id), id)) + } + }) + .into_group_map() + } } }; } diff --git a/packages/fuels-programs/src/call_utils.rs b/packages/fuels-programs/src/call_utils.rs index de842ea3ec..57030e8ff0 100644 --- a/packages/fuels-programs/src/call_utils.rs +++ b/packages/fuels-programs/src/call_utils.rs @@ -14,7 +14,7 @@ use fuels_core::{ input::Input, param_types::ParamType, transaction::{ScriptTransaction, TxParameters}, - transaction_builders::ScriptTransactionBuilder, + transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, }, }; use itertools::{chain, Itertools}; @@ -129,17 +129,19 @@ pub(crate) async fn build_tx_from_contract_calls( let (inputs, outputs) = get_transaction_inputs_outputs(calls, asset_inputs, account); let network_info = account.try_provider()?.network_info().await?; - let tb = + let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_parameters, network_info) .with_script(script) .with_script_data(script_data.clone()); - let base_asset_amount = required_asset_amounts + let used_base_amount = required_asset_amounts .iter() .find_map(|(asset_id, amount)| (*asset_id == AssetId::default()).then_some(*amount)) .unwrap_or_default(); - account.add_fee_resources(tb, base_asset_amount).await + account.add_witnessses(&mut tb); + account.adjust_for_fee(&mut tb, used_base_amount).await?; + tb.build() } /// Compute the length of the calling scripts for the two types of contract calls: those that return diff --git a/packages/fuels-programs/src/contract.rs b/packages/fuels-programs/src/contract.rs index d42a7a5b34..99589f0f31 100644 --- a/packages/fuels-programs/src/contract.rs +++ b/packages/fuels-programs/src/contract.rs @@ -19,7 +19,7 @@ use fuels_core::{ errors::{error, Error, Result}, param_types::ParamType, transaction::{ScriptTransaction, Transaction, TxParameters}, - transaction_builders::CreateTransactionBuilder, + transaction_builders::{CreateTransactionBuilder, TransactionBuilder}, unresolved_bytes::UnresolvedBytes, Selector, Token, }, @@ -300,7 +300,7 @@ impl Contract { ) -> Result { let network_info = account.try_provider()?.network_info().await?; - let tb = CreateTransactionBuilder::prepare_contract_deployment( + let mut tb = CreateTransactionBuilder::prepare_contract_deployment( self.binary, self.contract_id, self.state_root, @@ -310,10 +310,12 @@ impl Contract { network_info, ); - let tx = account - .add_fee_resources(tb, 0) + account.add_witnessses(&mut tb); + account + .adjust_for_fee(&mut tb, 0) .await .map_err(|err| error!(ProviderError, "{err}"))?; + let tx = tb.build()?; let provider = account .try_provider() @@ -617,7 +619,8 @@ where let tx = self.build_tx().await?; let provider = self.account.try_provider()?; - self.cached_tx_id = Some(tx.id(provider.chain_id())); + let chain_id = provider.chain_id(); + self.cached_tx_id = Some(tx.id(chain_id)); let tx_status = if simulate { provider.checked_dry_run(tx).await? @@ -903,8 +906,9 @@ impl MultiContractCallHandler { ) -> Result> { let tx = self.build_tx().await?; let provider = self.account.try_provider()?; + let chain_id = provider.chain_id(); - self.cached_tx_id = Some(tx.id(provider.chain_id())); + self.cached_tx_id = Some(tx.id(chain_id)); let tx_status = if simulate { provider.checked_dry_run(tx).await? diff --git a/packages/fuels-programs/src/script_calls.rs b/packages/fuels-programs/src/script_calls.rs index 748f38cef2..36c461572f 100644 --- a/packages/fuels-programs/src/script_calls.rs +++ b/packages/fuels-programs/src/script_calls.rs @@ -8,7 +8,6 @@ use fuels_accounts::{ }; use fuels_core::{ codec::{DecoderConfig, LogDecoder}, - constants::BASE_ASSET_ID, offsets::base_offset_script, traits::{Parameterize, Tokenizable}, types::{ @@ -16,7 +15,7 @@ use fuels_core::{ errors::Result, input::Input, transaction::{ScriptTransaction, Transaction, TxParameters}, - transaction_builders::ScriptTransactionBuilder, + transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, unresolved_bytes::UnresolvedBytes, }, }; @@ -207,28 +206,13 @@ where Ok(tb) } - fn calculate_base_asset_sum(&self) -> u64 { - self.script_call - .inputs - .iter() - .map(|input| match input { - Input::ResourceSigned { resource, .. } - | Input::ResourcePredicate { resource, .. } - if resource.asset_id() == BASE_ASSET_ID => - { - resource.amount() - } - _ => 0, - }) - .sum() - } - /// Returns the transaction that executes the script call pub async fn build_tx(&self) -> Result { - let tb = self.prepare_builder().await?; - let base_amount = self.calculate_base_asset_sum(); + let mut tb = self.prepare_builder().await?; + self.account.add_witnessses(&mut tb); + self.account.adjust_for_fee(&mut tb, 0).await?; - self.account.add_fee_resources(tb, base_amount).await + tb.build() } /// Call a script on the node. If `simulate == true`, then the call is done in a @@ -238,6 +222,7 @@ where /// The other field of [`FuelCallResponse`], `receipts`, contains the receipts of the transaction. async fn call_or_simulate(&mut self, simulate: bool) -> Result> { let tx = self.build_tx().await?; + self.cached_tx_id = Some(tx.id(self.provider.chain_id())); let tx_status = if simulate { @@ -290,8 +275,11 @@ where &self, tolerance: Option, ) -> Result { - let tb = self.prepare_builder().await?; - let tx = self.account.add_fee_resources(tb, 0).await?; + let mut tb = self.prepare_builder().await?; + + self.account.add_witnessses(&mut tb); + self.account.adjust_for_fee(&mut tb, 0).await?; + let tx = tb.build()?; let transaction_cost = self .provider diff --git a/packages/fuels-test-helpers/src/accounts.rs b/packages/fuels-test-helpers/src/accounts.rs index ea22c2e9ad..a19f924ad3 100644 --- a/packages/fuels-test-helpers/src/accounts.rs +++ b/packages/fuels-test-helpers/src/accounts.rs @@ -88,7 +88,7 @@ pub async fn launch_custom_provider_and_get_wallets( #[cfg(test)] mod tests { - use fuels_accounts::{fuel_crypto::fuel_types::AssetId, ViewOnlyAccount}; + use fuels_accounts::{fuel_crypto::fuel_types::AssetId, Account, ViewOnlyAccount}; use fuels_core::{ constants::BASE_ASSET_ID, types::{coin_type::CoinType, errors::Result}, diff --git a/packages/fuels/Cargo.toml b/packages/fuels/Cargo.toml index 31317de641..67fbafd515 100644 --- a/packages/fuels/Cargo.toml +++ b/packages/fuels/Cargo.toml @@ -33,10 +33,12 @@ hex = { workspace = true, default-features = false } rand = { workspace = true } sha2 = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["time", "test-util"] } [features] -default = ["std", "fuels-test-helpers?/fuels-accounts"] +default = ["std", "fuels-test-helpers?/fuels-accounts", "coin-cache"] +coin-cache = ["fuels-accounts/coin-cache"] + # The crates enabled via `dep:` below are not currently wasm compatible, as # such they are only available if `std` is enabled. The `dep:` syntax was # used so that we don't get a new feature flag for every optional dependency. diff --git a/packages/fuels/tests/predicates.rs b/packages/fuels/tests/predicates.rs index 379bac35f9..fa72e39ac4 100644 --- a/packages/fuels/tests/predicates.rs +++ b/packages/fuels/tests/predicates.rs @@ -675,7 +675,7 @@ async fn predicate_configurables() -> Result<()> { } #[tokio::test] -async fn predicate_add_fee_persists_message_w_data() -> Result<()> { +async fn predicate_adjust_fee_persists_message_w_data() -> Result<()> { abigen!(Predicate( name = "MyPredicate", abi = "packages/fuels/tests/predicates/basic_predicate/out/debug/basic_predicate-abi.json" @@ -700,13 +700,14 @@ async fn predicate_add_fee_persists_message_w_data() -> Result<()> { predicate.set_provider(provider.clone()); let network_info = provider.network_info().await?; - let tb = ScriptTransactionBuilder::prepare_transfer( + let mut tb = ScriptTransactionBuilder::prepare_transfer( vec![message_input.clone()], vec![], - Default::default(), + TxParameters::default().with_gas_price(1), network_info, ); - let tx = predicate.add_fee_resources(tb, 1000).await?; + predicate.adjust_for_fee(&mut tb, 1000).await?; + let tx = tb.build()?; assert_eq!(tx.inputs().len(), 2); assert_eq!(tx.inputs()[0].message_id().unwrap(), message.message_id()); @@ -754,14 +755,16 @@ async fn predicate_transfer_non_base_asset() -> Result<()> { ]; let network_info = provider.network_info().await?; - let tb = ScriptTransactionBuilder::prepare_transfer( + let mut tb = ScriptTransactionBuilder::prepare_transfer( inputs, outputs, TxParameters::default().with_gas_price(1), network_info, ); - let tx = wallet.add_fee_resources(tb, 0).await?; + wallet.sign_transaction(&mut tb); + wallet.adjust_for_fee(&mut tb, 0).await?; + let tx = tb.build()?; let tx_id = provider.send_transaction_and_await_commit(tx).await?; provider.tx_status(&tx_id).await?.check(None)?; diff --git a/packages/fuels/tests/providers.rs b/packages/fuels/tests/providers.rs index 5645b49e80..4c2a7c76ae 100644 --- a/packages/fuels/tests/providers.rs +++ b/packages/fuels/tests/providers.rs @@ -12,6 +12,7 @@ use fuels::{ tx::Receipt, types::{block::Block, coin_type::CoinType, message::Message}, }; + use fuels_core::types::{ transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}, Bits256, @@ -848,3 +849,138 @@ async fn test_sway_timestamp() -> Result<()> { ); Ok(()) } + +#[cfg(feature = "coin-cache")] +async fn create_transfer( + wallet: &WalletUnlocked, + amount: u64, + to: &Bech32Address, +) -> Result { + let inputs = wallet + .get_asset_inputs_for_amount(BASE_ASSET_ID, amount) + .await?; + let outputs = wallet.get_asset_outputs_for_amount(to, BASE_ASSET_ID, amount); + + let network_info = wallet.try_provider()?.network_info().await?; + + let mut tb = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + TxParameters::default(), + network_info, + ); + wallet.sign_transaction(&mut tb); + wallet.adjust_for_fee(&mut tb, amount).await?; + tb.build() +} + +#[cfg(feature = "coin-cache")] +#[tokio::test] +async fn test_caching() -> Result<()> { + use fuels_core::types::tx_status::TxStatus; + + let provider_config = Config { + manual_blocks_enabled: true, + ..Config::local_node() + }; + let amount = 1000; + let num_coins = 10; + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(1), Some(num_coins), Some(amount)), + Some(provider_config), + None, + ) + .await?; + let wallet_1 = wallets.pop().unwrap(); + let provider = wallet_1.provider().unwrap(); + let wallet_2 = WalletUnlocked::new_random(Some(provider.clone())); + + // Consecutively send transfer txs. Without caching, the txs will + // end up trying to use the same input coins because 'get_spendable_coins()' + // won't filter out recently used coins. + let mut tx_ids = vec![]; + for _ in 0..10 { + let tx = create_transfer(&wallet_1, 100, wallet_2.address()).await?; + let tx_id = provider.send_transaction(tx).await?; + tx_ids.push(tx_id); + } + + provider.produce_blocks(10, None).await?; + + // Confirm all txs are settled + for tx_id in tx_ids { + let status = provider.tx_status(&tx_id).await?; + assert!(matches!(status, TxStatus::Success { .. })); + } + + // Verify the transfers were succesful + assert_eq!(wallet_2.get_asset_balance(&BASE_ASSET_ID).await?, 1000); + + Ok(()) +} + +#[cfg(feature = "coin-cache")] +async fn create_revert_tx(wallet: &WalletUnlocked) -> Result { + use fuel_core_types::fuel_asm::Opcode; + + let amount = 1; + let inputs = wallet + .get_asset_inputs_for_amount(BASE_ASSET_ID, amount) + .await?; + let outputs = + wallet.get_asset_outputs_for_amount(&Bech32Address::default(), BASE_ASSET_ID, amount); + let network_info = wallet.try_provider()?.network_info().await?; + + let mut tb = ScriptTransactionBuilder::prepare_transfer( + inputs, + outputs, + TxParameters::default(), + network_info, + ) + .with_script(vec![Opcode::RVRT.into()]); + + wallet.sign_transaction(&mut tb); + wallet.adjust_for_fee(&mut tb, amount).await?; + tb.build() +} + +#[cfg(feature = "coin-cache")] +#[tokio::test] +async fn test_cache_invalidation_on_await() -> Result<()> { + use fuels_core::types::tx_status::TxStatus; + + let block_time = 1u32; + let provider_config = Config { + manual_blocks_enabled: true, + block_production: Trigger::Interval { + block_time: std::time::Duration::from_secs(block_time.into()), + }, + ..Config::local_node() + }; + + // create wallet with 1 coin so that the cache prevents further + // spending unless the coin is invalidated from the cache + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(1), Some(1), Some(100)), + Some(provider_config), + None, + ) + .await?; + let wallet = wallets.pop().unwrap(); + + let provider = wallet.provider().unwrap(); + let tx = create_revert_tx(&wallet).await?; + + // Pause time so that the cache doesn't invalidate items based on TTL + tokio::time::pause(); + + // tx inputs should be cached and then invalidated due to the tx failing + let tx_id = provider.send_transaction_and_await_commit(tx).await?; + let status = provider.tx_status(&tx_id).await?; + assert!(matches!(status, TxStatus::Revert { .. })); + + let coins = wallet.get_spendable_resources(BASE_ASSET_ID, 1).await?; + assert_eq!(coins.len(), 1); + + Ok(()) +} diff --git a/packages/fuels/tests/wallets.rs b/packages/fuels/tests/wallets.rs index 905d012a35..b312a8b6f1 100644 --- a/packages/fuels/tests/wallets.rs +++ b/packages/fuels/tests/wallets.rs @@ -2,7 +2,7 @@ use std::iter::repeat; use fuel_tx::{input::coin::CoinSigned, Bytes32, Input, Output, TxPointer, UtxoId}; use fuels::{prelude::*, types::transaction_builders::ScriptTransactionBuilder}; -use fuels_accounts::wallet::{Wallet, WalletUnlocked}; +use fuels_accounts::wallet::WalletUnlocked; use fuels_core::types::transaction_builders::TransactionBuilder; use fuels_test_helpers::setup_test_provider; @@ -130,7 +130,7 @@ fn base_asset_wallet_config(num_wallets: u64) -> WalletsConfig { } #[tokio::test] -async fn add_fee_resources_empty_transaction() -> Result<()> { +async fn adjust_fee_empty_transaction() -> Result<()> { let wallet_config = base_asset_wallet_config(1); let wallet = launch_custom_provider_and_get_wallets(wallet_config, None, None) .await? @@ -138,13 +138,16 @@ async fn add_fee_resources_empty_transaction() -> Result<()> { .unwrap(); let network_info = wallet.try_provider()?.network_info().await?; - let tb = ScriptTransactionBuilder::prepare_transfer( + let mut tb = ScriptTransactionBuilder::prepare_transfer( vec![], vec![], TxParameters::default(), network_info, ); - let tx = wallet.add_fee_resources(tb, 0).await?; + + wallet.sign_transaction(&mut tb); + wallet.adjust_for_fee(&mut tb, 0).await?; + let tx = tb.build()?; let zero_utxo_id = UtxoId::new(Bytes32::zeroed(), 0); let mut expected_inputs = vec![Input::coin_signed( @@ -165,7 +168,7 @@ async fn add_fee_resources_empty_transaction() -> Result<()> { } #[tokio::test] -async fn add_fee_resources_to_transfer_with_base_asset() -> Result<()> { +async fn adjust_fee_resources_to_transfer_with_base_asset() -> Result<()> { let wallet_config = base_asset_wallet_config(1); let wallet = launch_custom_provider_and_get_wallets(wallet_config, None, None) .await? @@ -180,13 +183,15 @@ async fn add_fee_resources_to_transfer_with_base_asset() -> Result<()> { wallet.get_asset_outputs_for_amount(&Address::zeroed().into(), BASE_ASSET_ID, base_amount); let network_info = wallet.try_provider()?.network_info().await?; - let tb = ScriptTransactionBuilder::prepare_transfer( + let mut tb = ScriptTransactionBuilder::prepare_transfer( inputs, outputs, TxParameters::default(), network_info, ); - let tx = wallet.add_fee_resources(tb, base_amount).await?; + wallet.sign_transaction(&mut tb); + wallet.adjust_for_fee(&mut tb, base_amount).await?; + let tx = tb.build()?; let zero_utxo_id = UtxoId::new(Bytes32::zeroed(), 0); let mut expected_inputs = repeat(Input::coin_signed( @@ -344,9 +349,9 @@ async fn test_wallet_get_coins() -> Result<()> { Ok(()) } -async fn setup_transfer_test(amount: u64) -> Result<(WalletUnlocked, Wallet)> { +async fn setup_transfer_test(amount: u64) -> Result<(WalletUnlocked, WalletUnlocked)> { let mut wallet_1 = WalletUnlocked::new_random(None); - let mut wallet_2 = WalletUnlocked::new_random(None).lock(); + let mut wallet_2 = WalletUnlocked::new_random(None); let coins = setup_single_asset_coins(wallet_1.address(), BASE_ASSET_ID, 1, amount); @@ -383,7 +388,7 @@ async fn transfer_more_than_owned() -> Result<()> { async fn transfer_coins_of_non_base_asset() -> Result<()> { const AMOUNT: u64 = 10000; let mut wallet_1 = WalletUnlocked::new_random(None); - let mut wallet_2 = WalletUnlocked::new_random(None).lock(); + let mut wallet_2 = WalletUnlocked::new_random(None); let asset_id: AssetId = AssetId::from([1; 32usize]); let mut coins = setup_single_asset_coins(wallet_1.address(), asset_id, 1, AMOUNT);