Skip to content

Commit 24e79f3

Browse files
MujkicAhal3esegfault-magnetiqdecaydigorithm
authored
feat: caching of recently used coins (#1105)
Adds a cache to the `Provider` for recently used coins. The cache is behind an optional feature flag coin-cache. **Other changes:** The strategy for adding fee coins is changed from completly recomputing the base asset input set to just adding an amount of base asset that will cover the fee. --------- Co-authored-by: hal3e <git@hal3e.io> Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> Co-authored-by: iqdecay <victor@berasconsulting.com> Co-authored-by: Rodrigo Araújo <rod.dearaujo@gmail.com>
1 parent 8062ff6 commit 24e79f3

File tree

22 files changed

+679
-220
lines changed

22 files changed

+679
-220
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ jobs:
164164
args: --all-targets --features "default fuel-core-lib test-type-paths"
165165
download_sway_artifacts: sway-examples-w-type-paths
166166
- cargo_command: nextest
167-
args: run --all-targets --features "default fuel-core-lib test-type-paths" --workspace
167+
args: run --all-targets --features "default fuel-core-lib test-type-paths coin-cache" --workspace
168168
download_sway_artifacts: sway-examples-w-type-paths
169169
install_fuel_core: true
170170
- cargo_command: nextest

packages/fuels-accounts/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ zeroize = { workspace = true, features = ["derive"] }
3131

3232
[dev-dependencies]
3333
tempfile = { workspace = true }
34+
tokio = { workspace = true, features = ["test-util"]}
3435

3536
[features]
3637
default = ["std"]
38+
coin-cache = ["tokio?/time"]
3739
std = ["fuels-core/std", "dep:tokio", "fuel-core-client/default", "dep:eth-keystore"]

packages/fuels-accounts/src/account.rs

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use fuels_core::{
2323
};
2424

2525
use crate::{
26-
accounts_utils::extract_message_id,
26+
accounts_utils::{adjust_inputs_outputs, calculate_missing_base_amount, extract_message_id},
2727
provider::{Provider, ResourceFilter},
2828
};
2929

@@ -117,26 +117,6 @@ pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone {
117117
.await
118118
.map_err(Into::into)
119119
}
120-
121-
// /// Get some spendable resources (coins and messages) of asset `asset_id` owned by the account
122-
// /// that add up at least to amount `amount`. The returned coins (UTXOs) are actual coins that
123-
// /// can be spent. The number of UXTOs is optimized to prevent dust accumulation.
124-
async fn get_spendable_resources(
125-
&self,
126-
asset_id: AssetId,
127-
amount: u64,
128-
) -> Result<Vec<CoinType>> {
129-
let filter = ResourceFilter {
130-
from: self.address().clone(),
131-
asset_id,
132-
amount,
133-
..Default::default()
134-
};
135-
self.try_provider()?
136-
.get_spendable_resources(filter)
137-
.await
138-
.map_err(Into::into)
139-
}
140120
}
141121

142122
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -166,11 +146,50 @@ pub trait Account: ViewOnlyAccount {
166146
]
167147
}
168148

169-
async fn add_fee_resources<Tb: TransactionBuilder>(
149+
/// Get some spendable resources (coins and messages) of asset `asset_id` owned by the account
150+
/// that add up at least to amount `amount`. The returned coins (UTXOs) are actual coins that
151+
/// can be spent. The number of UXTOs is optimized to prevent dust accumulation.
152+
async fn get_spendable_resources(
170153
&self,
171-
tb: Tb,
172-
previous_base_amount: u64,
173-
) -> Result<Tb::TxType>;
154+
asset_id: AssetId,
155+
amount: u64,
156+
) -> Result<Vec<CoinType>> {
157+
let filter = ResourceFilter {
158+
from: self.address().clone(),
159+
asset_id,
160+
amount,
161+
..Default::default()
162+
};
163+
164+
self.try_provider()?
165+
.get_spendable_resources(filter)
166+
.await
167+
.map_err(Into::into)
168+
}
169+
170+
/// Add base asset inputs to the transaction to cover the estimated fee.
171+
/// Requires contract inputs to be at the start of the transactions inputs vec
172+
/// so that their indexes are retained
173+
async fn adjust_for_fee<Tb: TransactionBuilder>(
174+
&self,
175+
tb: &mut Tb,
176+
used_base_amount: u64,
177+
) -> Result<()> {
178+
let missing_base_amount = calculate_missing_base_amount(tb, used_base_amount)?;
179+
180+
if missing_base_amount > 0 {
181+
let new_base_inputs = self
182+
.get_asset_inputs_for_amount(BASE_ASSET_ID, missing_base_amount)
183+
.await?;
184+
185+
adjust_inputs_outputs(tb, new_base_inputs, self.address());
186+
};
187+
188+
Ok(())
189+
}
190+
191+
// Add signatures to the builder if the underlying account is a wallet
192+
fn add_witnessses<Tb: TransactionBuilder>(&self, _tb: &mut Tb) {}
174193

175194
/// Transfer funds from this account to another `Address`.
176195
/// Fails if amount for asset ID is larger than address's spendable coins.
@@ -186,26 +205,19 @@ pub trait Account: ViewOnlyAccount {
186205
let network_info = provider.network_info().await?;
187206

188207
let inputs = self.get_asset_inputs_for_amount(asset_id, amount).await?;
189-
190208
let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
191209

192-
let tx_builder = ScriptTransactionBuilder::prepare_transfer(
210+
let mut tx_builder = ScriptTransactionBuilder::prepare_transfer(
193211
inputs,
194212
outputs,
195213
tx_parameters,
196214
network_info,
197215
);
198216

199-
// if we are not transferring the base asset, previous base amount is 0
200-
let previous_base_amount = if asset_id == AssetId::default() {
201-
amount
202-
} else {
203-
0
204-
};
217+
self.add_witnessses(&mut tx_builder);
218+
self.adjust_for_fee(&mut tx_builder, amount).await?;
205219

206-
let tx = self
207-
.add_fee_resources(tx_builder, previous_base_amount)
208-
.await?;
220+
let tx = tx_builder.build()?;
209221
let tx_id = provider.send_transaction_and_await_commit(tx).await?;
210222

211223
let receipts = provider
@@ -254,7 +266,7 @@ pub trait Account: ViewOnlyAccount {
254266
];
255267

256268
// Build transaction and sign it
257-
let tb = ScriptTransactionBuilder::prepare_contract_transfer(
269+
let mut tb = ScriptTransactionBuilder::prepare_contract_transfer(
258270
plain_contract_id,
259271
balance,
260272
asset_id,
@@ -264,14 +276,9 @@ pub trait Account: ViewOnlyAccount {
264276
network_info,
265277
);
266278

267-
// if we are not transferring the base asset, previous base amount is 0
268-
let base_amount = if asset_id == AssetId::default() {
269-
balance
270-
} else {
271-
0
272-
};
273-
274-
let tx = self.add_fee_resources(tb, base_amount).await?;
279+
self.add_witnessses(&mut tb);
280+
self.adjust_for_fee(&mut tb, balance).await?;
281+
let tx = tb.build()?;
275282

276283
let tx_id = provider.send_transaction_and_await_commit(tx).await?;
277284

@@ -299,15 +306,17 @@ pub trait Account: ViewOnlyAccount {
299306
.get_asset_inputs_for_amount(BASE_ASSET_ID, amount)
300307
.await?;
301308

302-
let tb = ScriptTransactionBuilder::prepare_message_to_output(
309+
let mut tb = ScriptTransactionBuilder::prepare_message_to_output(
303310
to.into(),
304311
amount,
305312
inputs,
306313
tx_parameters,
307314
network_info,
308315
);
309316

310-
let tx = self.add_fee_resources(tb, amount).await?;
317+
self.add_witnessses(&mut tb);
318+
self.adjust_for_fee(&mut tb, amount).await?;
319+
let tx = tb.build()?;
311320
let tx_id = provider.send_transaction_and_await_commit(tx).await?;
312321

313322
let receipts = provider

packages/fuels-accounts/src/accounts_utils.rs

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,73 +6,67 @@ use fuels_core::{
66
bech32::Bech32Address,
77
errors::{error, Error, Result},
88
input::Input,
9-
transaction_builders::{NetworkInfo, TransactionBuilder},
9+
transaction_builders::TransactionBuilder,
1010
},
1111
};
1212

1313
pub fn extract_message_id(receipts: &[Receipt]) -> Option<MessageId> {
1414
receipts.iter().find_map(|m| m.message_id())
1515
}
1616

17-
pub fn calculate_base_amount_with_fee(
17+
pub fn calculate_missing_base_amount(
1818
tb: &impl TransactionBuilder,
19-
network_info: &NetworkInfo,
20-
previous_base_amount: u64,
19+
used_base_amount: u64,
2120
) -> Result<u64> {
2221
let transaction_fee = tb
23-
.fee_checked_from_tx(network_info)?
22+
.fee_checked_from_tx()?
2423
.ok_or(error!(InvalidData, "Error calculating TransactionFee"))?;
2524

26-
let mut new_base_amount = transaction_fee.max_fee() + previous_base_amount;
25+
let available_amount = available_base_amount(tb);
2726

28-
// If the tx doesn't consume any UTXOs, attempting to repeat it will lead to an
29-
// error due to non unique tx ids (e.g. repeated contract call with configured gas cost of 0).
30-
// Here we enforce a minimum amount on the base asset to avoid this
31-
let is_consuming_utxos = tb
32-
.inputs()
33-
.iter()
34-
.any(|input| !matches!(input, Input::Contract { .. }));
35-
const MIN_AMOUNT: u64 = 1;
36-
if !is_consuming_utxos && new_base_amount == 0 {
37-
new_base_amount = MIN_AMOUNT;
38-
}
27+
let total_used = transaction_fee.max_fee() + used_base_amount;
28+
let missing_amount = if total_used > available_amount {
29+
total_used - available_amount
30+
} else if !is_consuming_utxos(tb) {
31+
// A tx needs to have at least 1 spendable input
32+
// Enforce a minimum required amount on the base asset if no other inputs are present
33+
1
34+
} else {
35+
0
36+
};
3937

40-
Ok(new_base_amount)
38+
Ok(missing_amount)
4139
}
4240

43-
// Replace the current base asset inputs of a tx builder with the provided ones.
44-
// Only signed resources and coin predicates are replaced, the remaining inputs are kept.
45-
// Messages that contain data are also kept since we don't know who will consume the data.
46-
pub fn adjust_inputs(
47-
tb: &mut impl TransactionBuilder,
48-
new_base_inputs: impl IntoIterator<Item = Input>,
49-
) {
50-
let adjusted_inputs = tb
51-
.inputs()
41+
fn available_base_amount(tb: &impl TransactionBuilder) -> u64 {
42+
tb.inputs()
5243
.iter()
53-
.filter(|input| {
54-
input.contains_data()
55-
|| !matches!(input , Input::ResourceSigned { resource , .. }
56-
| Input::ResourcePredicate { resource, .. } if resource.asset_id() == BASE_ASSET_ID)
44+
.filter_map(|input| match (input.amount(), input.asset_id()) {
45+
(Some(amount), Some(asset_id)) if asset_id == BASE_ASSET_ID => Some(amount),
46+
_ => None,
5747
})
58-
.cloned()
59-
.chain(new_base_inputs)
60-
.collect();
48+
.sum()
49+
}
6150

62-
*tb.inputs_mut() = adjusted_inputs
51+
fn is_consuming_utxos(tb: &impl TransactionBuilder) -> bool {
52+
tb.inputs()
53+
.iter()
54+
.any(|input| !matches!(input, Input::Contract { .. }))
6355
}
6456

65-
pub fn adjust_outputs(
57+
pub fn adjust_inputs_outputs(
6658
tb: &mut impl TransactionBuilder,
59+
new_base_inputs: impl IntoIterator<Item = Input>,
6760
address: &Bech32Address,
68-
new_base_amount: u64,
6961
) {
62+
tb.inputs_mut().extend(new_base_inputs);
63+
7064
let is_base_change_present = tb.outputs().iter().any(|output| {
7165
matches!(output , Output::Change { asset_id , .. }
7266
if asset_id == & BASE_ASSET_ID)
7367
});
7468

75-
if !is_base_change_present && new_base_amount != 0 {
69+
if !is_base_change_present {
7670
tb.outputs_mut()
7771
.push(Output::change(address.into(), 0, BASE_ASSET_ID));
7872
}

0 commit comments

Comments
 (0)