Skip to content
1 change: 1 addition & 0 deletions .changes/added/2912.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the `allow_partial` parameter to the `coinsToSpend` query. The default value of this parameters is `false` to preserve the old behavior. If set to `true`, the query returns available coins instead of failing when the requested amount is unavailable.
4 changes: 4 additions & 0 deletions crates/client/assets/schema.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,10 @@ input SpendQueryElementInput {
The maximum number of currencies for selection.
"""
max: U16
"""
If true, returns available coins instead of failing when the requested amount is unavailable.
"""
allowPartial: Boolean
}

type SqueezedOutStatus {
Expand Down
125 changes: 102 additions & 23 deletions crates/fuel-core/src/coins_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub async fn largest_first(
let target = query.asset.target;
let max = query.asset.max;
let asset_id = query.asset.id;
let allow_partial = query.asset.allow_partial;
let mut inputs: Vec<CoinType> = query.coins().try_collect().await?;
inputs.sort_by_key(|coin| Reverse(coin.amount()));

Expand All @@ -150,13 +151,17 @@ pub async fn largest_first(
break
}

// Error if we can't fit more coins
if coins.len() >= max as usize {
return Err(CoinsQueryError::InsufficientCoinsForTheMax {
asset_id,
collected_amount,
max,
})
if allow_partial {
return Ok(coins)
} else {
// Error if we can't fit more coins
return Err(CoinsQueryError::InsufficientCoinsForTheMax {
asset_id,
collected_amount,
max,
})
}
}

// Add to list
Expand All @@ -165,11 +170,15 @@ pub async fn largest_first(
}

if collected_amount < target {
return Err(CoinsQueryError::InsufficientCoinsForTheMax {
asset_id,
collected_amount,
max,
})
if allow_partial {
return Ok(coins);
} else {
return Err(CoinsQueryError::InsufficientCoinsForTheMax {
asset_id,
collected_amount,
max,
})
}
}

Ok(coins)
Expand Down Expand Up @@ -419,6 +428,7 @@ impl From<anyhow::Error> for CoinsQueryError {
}

#[allow(clippy::arithmetic_side_effects)]
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use crate::{
Expand Down Expand Up @@ -579,7 +589,7 @@ mod tests {
// Query some targets, including higher than the owner's balance
for target in 0..20 {
let coins = query(
&[AssetSpendTarget::new(asset_id, target, u16::MAX)],
&[AssetSpendTarget::new(asset_id, target, u16::MAX, false)],
&owner,
base_asset_id,
&db.service_database(),
Expand Down Expand Up @@ -640,7 +650,7 @@ mod tests {

// Query with too small max_inputs
let coins = query(
&[AssetSpendTarget::new(asset_id, 6, 1)],
&[AssetSpendTarget::new(asset_id, 6, 1, false)],
&owner,
base_asset_id,
&db.service_database(),
Expand Down Expand Up @@ -681,8 +691,8 @@ mod tests {
) {
let coins = query(
&[
AssetSpendTarget::new(asset_ids[0], 3, u16::MAX),
AssetSpendTarget::new(asset_ids[1], 6, u16::MAX),
AssetSpendTarget::new(asset_ids[0], 3, u16::MAX, false),
AssetSpendTarget::new(asset_ids[1], 6, u16::MAX, false),
],
&owner,
base_asset_id,
Expand All @@ -709,6 +719,71 @@ mod tests {
let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
multiple_assets_helper(owner, &asset_ids, &base_asset_id, db).await;
}

mod allow_partial {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also would be nice to add tests for the case when indexation is available

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the tests

use crate::{
coins_query::tests::{
largest_first::query,
setup_coins,
},
query::asset_query::AssetSpendTarget,
};

#[tokio::test]
async fn largest_first__error_when_not_enough_coins_and_allow_partial_false()
{
// Given
let (owner, asset_ids, base_asset_id, db) = setup_coins();
let asset_id = asset_ids[0];
let target = 20_000_000;
let allow_partial = false;

// When
let coins = query(
&[AssetSpendTarget::new(
asset_id,
target,
u16::MAX,
allow_partial,
)],
&owner,
&base_asset_id,
&db.service_database(),
)
.await;

// Then
assert!(coins.is_err());
}

#[tokio::test]
async fn largest_first__ok_when_not_enough_coins_and_allow_partial_true() {
// Given
let (owner, asset_ids, base_asset_id, db) = setup_coins();
let asset_id = asset_ids[0];
let target = 20_000_000;
let allow_partial = true;

// When
let coins = query(
&[AssetSpendTarget::new(
asset_id,
target,
u16::MAX,
allow_partial,
)],
&owner,
&base_asset_id,
&db.service_database(),
)
.await
.expect("should return coins");

// Then
let coins: Vec<_> = coins[0].iter().map(|(_, amount)| *amount).collect();
assert_eq!(coins, vec![5, 4, 3, 2, 1]);
}
}
}

mod random_improve {
Expand Down Expand Up @@ -764,7 +839,7 @@ mod tests {
// Query some amounts, including higher than the owner's balance
for amount in 0..20 {
let coins = query(
vec![AssetSpendTarget::new(asset_id, amount, u16::MAX)],
vec![AssetSpendTarget::new(asset_id, amount, u16::MAX, false)],
owner,
asset_ids,
base_asset_id,
Expand Down Expand Up @@ -816,8 +891,9 @@ mod tests {
// Query with too small max_inputs
let coins = query(
vec![AssetSpendTarget::new(
asset_id, 6, // target
1, // max
asset_id, 6, // target
1, // max
false, // allow_partial
)],
owner,
asset_ids,
Expand Down Expand Up @@ -863,13 +939,15 @@ mod tests {
vec![
AssetSpendTarget::new(
asset_ids[0],
3, // target
3, // max
3, // target
3, // max
false, // allow_partial
),
AssetSpendTarget::new(
asset_ids[1],
6, // target
3, // max
6, // target
3, // max
false, // allow_partial
),
],
owner,
Expand Down Expand Up @@ -969,7 +1047,7 @@ mod tests {
owner,
base_asset_id,
asset_ids,
vec![AssetSpendTarget::new(asset_id, amount, u16::MAX)],
vec![AssetSpendTarget::new(asset_id, amount, u16::MAX, false)],
excluded_ids.clone(),
)
.await;
Expand Down Expand Up @@ -1501,6 +1579,7 @@ mod tests {
id: asset_ids[0],
target: target_amount,
max: max_coins,
allow_partial: false,
}],
Cow::Owned(Exclude::default()),
base_asset_id,
Expand Down
2 changes: 1 addition & 1 deletion crates/fuel-core/src/query/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl ReadView {
} else {
AssetQuery::new(
&owner,
&AssetSpendTarget::new(asset_id, u128::MAX, u16::MAX),
&AssetSpendTarget::new(asset_id, u128::MAX, u16::MAX, false),
&base_asset_id,
None,
self,
Expand Down
10 changes: 8 additions & 2 deletions crates/fuel-core/src/query/balance/asset_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ pub struct AssetSpendTarget {
pub id: AssetId,
pub target: u128,
pub max: u16,
pub allow_partial: bool,
}

impl AssetSpendTarget {
pub fn new(id: AssetId, target: u128, max: u16) -> Self {
Self { id, target, max }
pub fn new(id: AssetId, target: u128, max: u16, allow_partial: bool) -> Self {
Self {
id,
target,
max,
allow_partial,
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/fuel-core/src/schema/coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ pub struct SpendQueryElementInput {
pub amount: U128,
/// The maximum number of currencies for selection.
pub max: Option<U16>,
/// If true, returns available coins instead of failing when the requested amount is unavailable.
pub allow_partial: Option<bool>,
}

#[derive(async_graphql::InputObject)]
Expand Down Expand Up @@ -379,6 +381,7 @@ async fn coins_to_spend_without_cache(
e.asset_id.0,
e.amount.0,
e.max.map(|max| max.0).unwrap_or(max_input).min(max_input),
e.allow_partial.unwrap_or(false),
)
})
.collect_vec();
Expand Down
5 changes: 4 additions & 1 deletion crates/fuel-core/src/schema/tx/assemble_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl<'a> AssembleArguments<'a> {
asset_id: AssetId,
amount: u64,
remaining_input_slots: u16,
allow_partial: bool,
) -> anyhow::Result<Vec<CoinType>> {
if amount == 0 {
return Ok(Vec::new());
Expand All @@ -109,6 +110,7 @@ impl<'a> AssembleArguments<'a> {
asset_id: asset_id.into(),
amount: (amount as u128).into(),
max: None,
allow_partial: Some(allow_partial),
};

let result = self
Expand Down Expand Up @@ -424,7 +426,7 @@ where

let selected_coins = self
.arguments
.coins(owner, asset_id, amount, remaining_input_slots)
.coins(owner, asset_id, amount, remaining_input_slots, false)
.await?;

for coin in selected_coins
Expand Down Expand Up @@ -958,6 +960,7 @@ where
base_asset_id,
how_much_to_add,
remaining_input_slots,
true,
)
.await?;

Expand Down
Loading