fuel_core/
coins_query.rs

1use crate::{
2    fuel_core_graphql_api::{
3        database::ReadView,
4        storage::coins::CoinsToSpendIndexKey,
5    },
6    graphql_api::ports::CoinsToSpendIndexIter,
7    query::asset_query::{
8        AssetQuery,
9        AssetSpendTarget,
10        Exclude,
11    },
12};
13use fuel_core_services::yield_stream::StreamYieldExt;
14use fuel_core_storage::{
15    Error as StorageError,
16    Result as StorageResult,
17};
18use fuel_core_types::{
19    entities::coins::CoinType,
20    fuel_types::{
21        Address,
22        AssetId,
23    },
24};
25use futures::{
26    Stream,
27    StreamExt,
28    TryStreamExt,
29};
30use rand::prelude::*;
31use std::{
32    borrow::Cow,
33    cmp::Reverse,
34};
35use thiserror::Error;
36
37#[derive(Debug, Error)]
38pub enum CoinsQueryError {
39    #[error("store error occurred: {0}")]
40    StorageError(StorageError),
41    #[error(
42        "the target cannot be met due to insufficient coins available for {asset_id}. Collected: {collected_amount}. Owner: {owner}."
43    )]
44    InsufficientCoins {
45        owner: Address,
46        asset_id: AssetId,
47        collected_amount: u128,
48    },
49    #[error(
50        "the target for {asset_id} cannot be met due to exceeding the {max} coin limit. Collected: {collected_amount}. Owner: {owner}."
51    )]
52    MaxCoinsReached {
53        owner: Address,
54        asset_id: AssetId,
55        collected_amount: u128,
56        max: u16,
57    },
58    #[error("the query contains duplicate assets")]
59    DuplicateAssets(AssetId),
60    #[error("too many excluded ids: provided ({provided}) is > than allowed ({allowed})")]
61    TooManyExcludedId { provided: usize, allowed: u16 },
62    #[error(
63        "the query requires more coins than the max allowed coins: required ({required}) > max ({max})"
64    )]
65    TooManyCoinsSelected { required: usize, max: u16 },
66    #[error("coins to spend index entry contains wrong coin foreign key")]
67    IncorrectCoinForeignKeyInIndex,
68    #[error("coins to spend index entry contains wrong message foreign key")]
69    IncorrectMessageForeignKeyInIndex,
70    #[error("error while processing the query: {0}")]
71    UnexpectedInternalState(&'static str),
72    #[error("coins to spend index contains incorrect key")]
73    IncorrectCoinsToSpendIndexKey,
74    #[error("unknown error: {0}")]
75    Other(anyhow::Error),
76}
77
78#[cfg(test)]
79impl PartialEq for CoinsQueryError {
80    fn eq(&self, other: &Self) -> bool {
81        format!("{self:?}") == format!("{other:?}")
82    }
83}
84
85/// The prepared spend queries.
86pub struct SpendQuery<'a> {
87    owner: Address,
88    query_per_asset: Vec<AssetSpendTarget>,
89    exclude: Cow<'a, Exclude>,
90    base_asset_id: AssetId,
91}
92
93impl<'s> SpendQuery<'s> {
94    // TODO: Check that number of `queries` is not too high(to prevent attacks).
95    //  https://github.com/FuelLabs/fuel-core/issues/588#issuecomment-1240074551
96    pub fn new(
97        owner: Address,
98        query_per_asset: &[AssetSpendTarget],
99        exclude: Cow<'s, Exclude>,
100        base_asset_id: AssetId,
101    ) -> Result<Self, CoinsQueryError> {
102        Ok(Self {
103            owner,
104            query_per_asset: query_per_asset.to_vec(),
105            exclude,
106            base_asset_id,
107        })
108    }
109
110    /// Return `Asset`s.
111    pub fn assets(&self) -> &Vec<AssetSpendTarget> {
112        &self.query_per_asset
113    }
114
115    /// Return [`AssetQuery`]s.
116    pub fn asset_queries<'a>(&'a self, db: &'a ReadView) -> Vec<AssetQuery<'a>> {
117        self.query_per_asset
118            .iter()
119            .map(|asset| {
120                AssetQuery::new(
121                    &self.owner,
122                    asset,
123                    &self.base_asset_id,
124                    Some(self.exclude.as_ref()),
125                    db,
126                )
127            })
128            .collect()
129    }
130
131    /// Returns exclude that contains information about excluded ids.
132    pub fn exclude(&self) -> &Exclude {
133        self.exclude.as_ref()
134    }
135
136    /// Returns the owner of the query.
137    pub fn owner(&self) -> &Address {
138        &self.owner
139    }
140}
141
142/// Returns the biggest inputs of the `owner` to satisfy the required `target` of the asset. The
143/// number of inputs for each asset can't exceed `max_inputs`, otherwise throw an error that query
144/// can't be satisfied.
145pub async fn largest_first(
146    query: AssetQuery<'_>,
147) -> Result<Vec<CoinType>, CoinsQueryError> {
148    let owner = query.owner;
149    let target = query.asset.target;
150    let max = query.asset.max;
151    let asset_id = query.asset.id;
152    let allow_partial = query.asset.allow_partial;
153    let mut inputs: Vec<CoinType> = query.coins().try_collect().await?;
154    inputs.sort_by_key(|coin| Reverse(coin.amount()));
155
156    let mut collected_amount = 0u128;
157    let mut coins = vec![];
158
159    for coin in inputs {
160        // Break if we don't need any more coins
161        if collected_amount >= target {
162            break;
163        }
164
165        if coins.len() >= max as usize {
166            if allow_partial {
167                return Ok(coins);
168            } else {
169                // Error if we can't fit more coins
170                return Err(CoinsQueryError::MaxCoinsReached {
171                    owner: *owner,
172                    asset_id,
173                    collected_amount,
174                    max,
175                });
176            }
177        }
178
179        // Add to list
180        collected_amount = collected_amount.saturating_add(coin.amount() as u128);
181        coins.push(coin);
182    }
183
184    if collected_amount < target {
185        if allow_partial && collected_amount > 0 {
186            return Ok(coins);
187        } else {
188            return Err(CoinsQueryError::InsufficientCoins {
189                owner: *owner,
190                asset_id,
191                collected_amount,
192            });
193        }
194    }
195
196    Ok(coins)
197}
198
199// An implementation of the method described on: https://iohk.io/en/blog/posts/2018/07/03/self-organisation-in-coin-selection/
200pub async fn random_improve(
201    db: &ReadView,
202    spend_query: &SpendQuery<'_>,
203) -> Result<Vec<Vec<CoinType>>, CoinsQueryError> {
204    let mut coins_per_asset = vec![];
205
206    for query in spend_query.asset_queries(db) {
207        let target = query.asset.target;
208        let max = query.asset.max;
209
210        let mut inputs: Vec<_> = query.clone().coins().try_collect().await?;
211        inputs.shuffle(&mut thread_rng());
212        inputs.truncate(max as usize);
213
214        let mut collected_amount = 0;
215        let mut coins = vec![];
216
217        // Set parameters according to spec
218        let upper_target = target.saturating_mul(2);
219
220        for coin in inputs {
221            // Try to improve the result by adding dust to the result.
222            if collected_amount >= target {
223                // Break if found coin exceeds max `u64` or the upper limit
224                if collected_amount >= u64::MAX as u128
225                    || coin.amount() as u128 > upper_target
226                {
227                    break;
228                }
229
230                // Break if adding doesn't improve the distance
231                let change_amount = collected_amount
232                    .checked_sub(target)
233                    .expect("We checked it above");
234                let distance = target.abs_diff(change_amount);
235                let next_distance =
236                    target.abs_diff(change_amount.saturating_add(coin.amount() as u128));
237                if next_distance >= distance {
238                    break;
239                }
240            }
241
242            // Add to list
243            collected_amount = collected_amount.saturating_add(coin.amount() as u128);
244            coins.push(coin);
245        }
246
247        // Fallback to largest_first if we can't fit more coins
248        if collected_amount < target {
249            coins = largest_first(query).await?;
250        }
251
252        coins_per_asset.push(coins);
253    }
254
255    Ok(coins_per_asset)
256}
257
258pub async fn select_coins_to_spend(
259    CoinsToSpendIndexIter {
260        big_coins_iter,
261        dust_coins_iter,
262    }: CoinsToSpendIndexIter<'_>,
263    asset: AssetSpendTarget,
264    exclude: &Exclude,
265    batch_size: usize,
266    owner: Address,
267) -> Result<Vec<CoinsToSpendIndexKey>, CoinsQueryError> {
268    let asset_id = asset.id;
269    let total = asset.target;
270    let max = asset.max;
271    let allow_partial = asset.allow_partial;
272
273    // We aim to reduce dust creation by targeting twice the required amount for selection,
274    // inspired by the random-improve approach. This increases the likelihood of generating
275    // useful change outputs for future transactions, minimizing unusable dust outputs.
276    // See also "let upper_target = target.saturating_mul(2);" in "fn random_improve()".
277    const TOTAL_AMOUNT_ADJUSTMENT_FACTOR: u128 = 2;
278
279    // After selecting large coins that cover at least twice the required amount,
280    // we include a limited number of small (dust) coins. The maximum number of dust coins
281    // is determined by the multiplier defined below. Specifically, the number of dust coins
282    // will never exceed FACTOR times the number of large coins selected.
283    //
284    // This limit prevents excessive dust coins from being included in cases where
285    // the query lacks a specified maximum limit (defaulting to 255).
286    //
287    // Example:
288    // - If 3 large coins are selected (and FACTOR is 5), up to 15 dust coins may be included (0..=15).
289    // - Still, if the selected dust can cover the amount of some big coins, the
290    //   latter will be removed from the set
291    const DUST_TO_BIG_COINS_FACTOR: u16 = 5;
292
293    if total == 0 || max == 0 {
294        return Ok(vec![]);
295    }
296
297    let adjusted_total = total.saturating_mul(TOTAL_AMOUNT_ADJUSTMENT_FACTOR);
298
299    let big_coins_stream = futures::stream::iter(big_coins_iter).yield_each(batch_size);
300    let dust_coins_stream = futures::stream::iter(dust_coins_iter).yield_each(batch_size);
301
302    let (selected_big_coins_total, selected_big_coins, has_more_big_coins) =
303        big_coins(big_coins_stream, adjusted_total, max, exclude).await?;
304
305    if selected_big_coins_total == 0
306        || (selected_big_coins_total < total && !allow_partial)
307    {
308        if selected_big_coins.len() >= max as usize && has_more_big_coins {
309            return Err(CoinsQueryError::MaxCoinsReached {
310                owner,
311                asset_id,
312                collected_amount: selected_big_coins_total,
313                max,
314            });
315        }
316
317        return Err(CoinsQueryError::InsufficientCoins {
318            owner,
319            asset_id,
320            collected_amount: selected_big_coins_total,
321        });
322    }
323
324    let Some(last_selected_big_coin) = selected_big_coins.last() else {
325        // Should never happen, because at this stage we know that:
326        // 1) selected_big_coins_total >= total
327        // 2) total > 0
328        // hence: selected_big_coins_total > 0
329        // therefore, at least one coin is selected - if not, it's a bug
330        return Err(CoinsQueryError::UnexpectedInternalState(
331            "at least one coin should be selected",
332        ));
333    };
334
335    let selected_big_coins_len = selected_big_coins.len();
336    let number_of_big_coins: u16 = selected_big_coins_len.try_into().map_err(|_| {
337        CoinsQueryError::TooManyCoinsSelected {
338            required: selected_big_coins_len,
339            max: u16::MAX,
340        }
341    })?;
342
343    let max_dust_count =
344        max_dust_count(max, number_of_big_coins, DUST_TO_BIG_COINS_FACTOR);
345    let (dust_coins_total, selected_dust_coins, _has_more_dust_coins) = dust_coins(
346        dust_coins_stream,
347        last_selected_big_coin,
348        max_dust_count,
349        exclude,
350    )
351    .await?;
352
353    let retained_big_coins_iter =
354        skip_big_coins_up_to_amount(selected_big_coins, dust_coins_total);
355
356    Ok((retained_big_coins_iter.chain(selected_dust_coins)).collect())
357}
358
359async fn big_coins(
360    big_coins_stream: impl Stream<Item = StorageResult<CoinsToSpendIndexKey>> + Unpin,
361    total: u128,
362    max: u16,
363    exclude: &Exclude,
364) -> Result<(u128, Vec<CoinsToSpendIndexKey>, bool), CoinsQueryError> {
365    select_coins_until(big_coins_stream, max, exclude, |_, total_so_far| {
366        total_so_far >= total
367    })
368    .await
369}
370
371async fn dust_coins(
372    dust_coins_stream: impl Stream<Item = StorageResult<CoinsToSpendIndexKey>> + Unpin,
373    last_big_coin: &CoinsToSpendIndexKey,
374    max_dust_count: u16,
375    exclude: &Exclude,
376) -> Result<(u128, Vec<CoinsToSpendIndexKey>, bool), CoinsQueryError> {
377    select_coins_until(dust_coins_stream, max_dust_count, exclude, |coin, _| {
378        coin == last_big_coin
379    })
380    .await
381}
382
383async fn select_coins_until<Pred>(
384    mut coins_stream: impl Stream<Item = StorageResult<CoinsToSpendIndexKey>> + Unpin,
385    max: u16,
386    exclude: &Exclude,
387    predicate: Pred,
388) -> Result<(u128, Vec<CoinsToSpendIndexKey>, bool), CoinsQueryError>
389where
390    Pred: Fn(&CoinsToSpendIndexKey, u128) -> bool,
391{
392    let mut coins_total_value: u128 = 0;
393    let mut coins = Vec::with_capacity(max as usize);
394    let mut has_more_coins = false;
395    while let Some(coin) = coins_stream.next().await {
396        let coin = coin?;
397        if !is_excluded(&coin, exclude) {
398            if coins.len() >= max as usize || predicate(&coin, coins_total_value) {
399                has_more_coins = true;
400                break;
401            }
402            let amount = coin.amount() as u128;
403            coins_total_value = coins_total_value.saturating_add(amount);
404            coins.push(coin);
405        }
406    }
407    Ok((coins_total_value, coins, has_more_coins))
408}
409
410fn is_excluded(key: &CoinsToSpendIndexKey, exclude: &Exclude) -> bool {
411    match key {
412        CoinsToSpendIndexKey::Coin { utxo_id, .. } => exclude.contains_coin(utxo_id),
413        CoinsToSpendIndexKey::Message { nonce, .. } => exclude.contains_message(nonce),
414    }
415}
416
417fn max_dust_count(max: u16, big_coins_len: u16, dust_to_big_coins_factor: u16) -> u16 {
418    let mut rng = rand::thread_rng();
419
420    let max_from_factor = big_coins_len.saturating_mul(dust_to_big_coins_factor);
421    let max_adjusted = max.saturating_sub(big_coins_len);
422    let upper_bound = max_from_factor.min(max_adjusted);
423
424    rng.gen_range(0..=upper_bound)
425}
426
427fn skip_big_coins_up_to_amount(
428    big_coins: impl IntoIterator<Item = CoinsToSpendIndexKey>,
429    skipped_amount: u128,
430) -> impl Iterator<Item = CoinsToSpendIndexKey> {
431    let mut current_dust_coins_value = skipped_amount;
432    big_coins.into_iter().skip_while(move |item| {
433        let item_amount = item.amount() as u128;
434        current_dust_coins_value
435            .checked_sub(item_amount)
436            .map(|new_value| {
437                current_dust_coins_value = new_value;
438                true
439            })
440            .unwrap_or(false)
441    })
442}
443
444impl From<StorageError> for CoinsQueryError {
445    fn from(e: StorageError) -> Self {
446        CoinsQueryError::StorageError(e)
447    }
448}
449
450impl From<anyhow::Error> for CoinsQueryError {
451    fn from(e: anyhow::Error) -> Self {
452        CoinsQueryError::Other(e)
453    }
454}
455
456#[allow(clippy::arithmetic_side_effects)]
457#[allow(non_snake_case)]
458#[cfg(test)]
459mod tests {
460    use crate::{
461        coins_query::{
462            CoinsQueryError,
463            SpendQuery,
464            largest_first,
465            max_dust_count,
466            random_improve,
467        },
468        combined_database::CombinedDatabase,
469        fuel_core_graphql_api::{
470            api_service::ReadDatabase as ServiceDatabase,
471            storage::{
472                coins::{
473                    OwnedCoins,
474                    owner_coin_id_key,
475                },
476                messages::{
477                    OwnedMessageIds,
478                    OwnedMessageKey,
479                },
480            },
481        },
482        query::asset_query::{
483            AssetQuery,
484            AssetSpendTarget,
485            Exclude,
486        },
487    };
488    use assert_matches::assert_matches;
489    use fuel_core_storage::{
490        StorageMutate,
491        iter::IterDirection,
492        tables::{
493            Coins,
494            Messages,
495        },
496    };
497    use fuel_core_types::{
498        blockchain::primitives::DaBlockHeight,
499        entities::{
500            coins::coin::{
501                Coin,
502                CompressedCoin,
503            },
504            relayer::message::{
505                Message,
506                MessageV1,
507            },
508        },
509        fuel_asm::Word,
510        fuel_tx::*,
511    };
512    use futures::TryStreamExt;
513    use itertools::Itertools;
514    use proptest::{
515        prelude::*,
516        proptest,
517    };
518    use rand::{
519        Rng,
520        SeedableRng,
521        rngs::StdRng,
522    };
523    use std::{
524        borrow::Cow,
525        cmp::Reverse,
526    };
527
528    fn setup_coins() -> (Address, [AssetId; 2], AssetId, TestDatabase) {
529        let mut rng = StdRng::seed_from_u64(0xf00df00d);
530        let owner = Address::default();
531        let asset_ids = [rng.r#gen(), rng.r#gen()];
532        let base_asset_id = rng.r#gen();
533        let mut db = TestDatabase::new();
534        (0..5usize).for_each(|i| {
535            db.make_coin(owner, (i + 1) as Word, asset_ids[0]);
536            db.make_coin(owner, (i + 1) as Word, asset_ids[1]);
537        });
538
539        (owner, asset_ids, base_asset_id, db)
540    }
541
542    fn setup_messages() -> (Address, AssetId, TestDatabase) {
543        let mut rng = StdRng::seed_from_u64(0xf00df00d);
544        let owner = Address::default();
545        let base_asset_id = rng.r#gen();
546        let mut db = TestDatabase::new();
547        (0..5usize).for_each(|i| {
548            db.make_message(owner, (i + 1) as Word);
549        });
550
551        (owner, base_asset_id, db)
552    }
553
554    fn setup_coins_and_messages() -> (Address, [AssetId; 2], AssetId, TestDatabase) {
555        let mut rng = StdRng::seed_from_u64(0xf00df00d);
556        let owner = Address::default();
557        let base_asset_id = rng.r#gen();
558        let asset_ids = [base_asset_id, rng.r#gen()];
559        let mut db = TestDatabase::new();
560        // 2 coins and 3 messages
561        (0..2usize).for_each(|i| {
562            db.make_coin(owner, (i + 1) as Word, asset_ids[0]);
563        });
564        (2..5usize).for_each(|i| {
565            db.make_message(owner, (i + 1) as Word);
566        });
567
568        (0..5usize).for_each(|i| {
569            db.make_coin(owner, (i + 1) as Word, asset_ids[1]);
570        });
571
572        (owner, asset_ids, base_asset_id, db)
573    }
574
575    mod largest_first {
576        use super::*;
577
578        async fn query(
579            spend_query: &[AssetSpendTarget],
580            owner: &Address,
581            base_asset_id: &AssetId,
582            db: &ServiceDatabase,
583        ) -> Result<Vec<Vec<(AssetId, Word)>>, CoinsQueryError> {
584            let mut results = vec![];
585
586            for asset in spend_query {
587                let coins = largest_first(AssetQuery::new(
588                    owner,
589                    asset,
590                    base_asset_id,
591                    None,
592                    &db.test_view(),
593                ))
594                .await
595                .map(|coins| {
596                    coins
597                        .iter()
598                        .map(|coin| (*coin.asset_id(base_asset_id), coin.amount()))
599                        .collect()
600                })?;
601                results.push(coins);
602            }
603
604            Ok(results)
605        }
606
607        async fn single_asset_assert(
608            owner: Address,
609            asset_ids: &[AssetId],
610            base_asset_id: &AssetId,
611            db: TestDatabase,
612        ) {
613            let asset_id = asset_ids[0];
614
615            // Query some targets, including higher than the owner's balance
616            for target in 0..20 {
617                let coins = query(
618                    &[AssetSpendTarget::new(asset_id, target, u16::MAX, false)],
619                    &owner,
620                    base_asset_id,
621                    &db.service_database(),
622                )
623                .await;
624
625                // Transform result for convenience
626                let coins = coins.map(|coins| {
627                    coins[0]
628                        .iter()
629                        .map(|(id, amount)| {
630                            // Check the asset ID before we drop it
631                            assert_eq!(id, &asset_id);
632
633                            *amount
634                        })
635                        .collect::<Vec<u64>>()
636                });
637
638                match target {
639                    // This should return nothing
640                    0 => {
641                        assert_matches!(coins, Ok(coins) if coins.is_empty())
642                    }
643                    // This range should return the largest coins
644                    1..=5 => {
645                        assert_matches!(coins, Ok(coins) if coins == vec![5])
646                    }
647                    // This range should return the largest two coins
648                    6..=9 => {
649                        assert_matches!(coins, Ok(coins) if coins == vec![5, 4])
650                    }
651                    // This range should return the largest three coins
652                    10..=12 => {
653                        assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3])
654                    }
655                    // This range should return the largest four coins
656                    13..=14 => {
657                        assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2])
658                    }
659                    // This range should return all coins
660                    15 => {
661                        assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2, 1])
662                    }
663                    // Asking for more than the owner's balance should error
664                    _ => {
665                        assert_matches!(
666                            coins,
667                            Err(CoinsQueryError::InsufficientCoins {
668                                owner: _,
669                                asset_id: _,
670                                collected_amount: 15,
671                            })
672                        )
673                    }
674                };
675            }
676
677            // Query with too small max_inputs
678            let coins = query(
679                &[AssetSpendTarget::new(asset_id, 6, 1, false)],
680                &owner,
681                base_asset_id,
682                &db.service_database(),
683            )
684            .await;
685            assert_matches!(coins, Err(CoinsQueryError::MaxCoinsReached { .. }));
686        }
687
688        #[tokio::test]
689        async fn single_asset_coins() {
690            // Setup for coins
691            let (owner, asset_ids, base_asset_id, db) = setup_coins();
692            single_asset_assert(owner, &asset_ids, &base_asset_id, db).await;
693        }
694
695        #[tokio::test]
696        async fn single_asset_messages() {
697            // Setup for messages
698            let (owner, base_asset_id, db) = setup_messages();
699            single_asset_assert(owner, &[base_asset_id], &base_asset_id, db).await;
700        }
701
702        #[tokio::test]
703        async fn single_asset_coins_and_messages() {
704            // Setup for coins and messages
705            let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
706            single_asset_assert(owner, &asset_ids, &base_asset_id, db).await;
707        }
708
709        async fn multiple_assets_helper(
710            owner: Address,
711            asset_ids: &[AssetId],
712            base_asset_id: &AssetId,
713            db: TestDatabase,
714        ) {
715            let coins = query(
716                &[
717                    AssetSpendTarget::new(asset_ids[0], 3, u16::MAX, false),
718                    AssetSpendTarget::new(asset_ids[1], 6, u16::MAX, false),
719                ],
720                &owner,
721                base_asset_id,
722                &db.service_database(),
723            )
724            .await;
725            let expected = vec![
726                vec![(asset_ids[0], 5)],
727                vec![(asset_ids[1], 5), (asset_ids[1], 4)],
728            ];
729            assert_matches!(coins, Ok(coins) if coins == expected);
730        }
731
732        #[tokio::test]
733        async fn multiple_assets_coins() {
734            // Setup coins
735            let (owner, asset_ids, base_asset_id, db) = setup_coins();
736            multiple_assets_helper(owner, &asset_ids, &base_asset_id, db).await;
737        }
738
739        #[tokio::test]
740        async fn multiple_assets_coins_and_messages() {
741            // Setup coins and messages
742            let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
743            multiple_assets_helper(owner, &asset_ids, &base_asset_id, db).await;
744        }
745
746        mod allow_partial {
747            use crate::{
748                coins_query::tests::{
749                    largest_first::query,
750                    setup_coins,
751                },
752                query::asset_query::AssetSpendTarget,
753            };
754
755            #[tokio::test]
756            async fn query__error_when_not_enough_coins_and_allow_partial_false() {
757                // Given
758                let (owner, asset_ids, base_asset_id, db) = setup_coins();
759                let asset_id = asset_ids[0];
760                let target = 20_000_000;
761                let allow_partial = false;
762
763                // When
764                let coins = query(
765                    &[AssetSpendTarget::new(
766                        asset_id,
767                        target,
768                        u16::MAX,
769                        allow_partial,
770                    )],
771                    &owner,
772                    &base_asset_id,
773                    &db.service_database(),
774                )
775                .await;
776
777                // Then
778                assert!(coins.is_err());
779            }
780
781            #[tokio::test]
782            async fn query__ok_when_not_enough_coins_and_allow_partial_true() {
783                // Given
784                let (owner, asset_ids, base_asset_id, db) = setup_coins();
785                let asset_id = asset_ids[0];
786                let target = 20_000_000;
787                let allow_partial = true;
788
789                // When
790                let coins = query(
791                    &[AssetSpendTarget::new(
792                        asset_id,
793                        target,
794                        u16::MAX,
795                        allow_partial,
796                    )],
797                    &owner,
798                    &base_asset_id,
799                    &db.service_database(),
800                )
801                .await
802                .expect("should return coins");
803
804                // Then
805                let coins: Vec<_> = coins[0].iter().map(|(_, amount)| *amount).collect();
806                assert_eq!(coins, vec![5, 4, 3, 2, 1]);
807            }
808        }
809    }
810
811    mod random_improve {
812        use super::*;
813        use crate::query::asset_query::Exclude;
814        use std::borrow::Cow;
815
816        async fn query(
817            query_per_asset: Vec<AssetSpendTarget>,
818            owner: Address,
819            asset_ids: &[AssetId],
820            base_asset_id: AssetId,
821            db: &ServiceDatabase,
822        ) -> Result<Vec<(AssetId, u64)>, CoinsQueryError> {
823            let coins = random_improve(
824                &db.test_view(),
825                &SpendQuery::new(
826                    owner,
827                    &query_per_asset,
828                    Cow::Owned(Exclude::default()),
829                    base_asset_id,
830                )?,
831            )
832            .await;
833
834            // Transform result for convenience
835            coins.map(|coins| {
836                coins
837                    .into_iter()
838                    .flat_map(|coins| {
839                        coins
840                            .into_iter()
841                            .map(|coin| (*coin.asset_id(&base_asset_id), coin.amount()))
842                            .sorted_by_key(|(asset_id, amount)| {
843                                (
844                                    asset_ids.iter().position(|c| c == asset_id).unwrap(),
845                                    Reverse(*amount),
846                                )
847                            })
848                    })
849                    .collect()
850            })
851        }
852
853        async fn single_asset_assert(
854            owner: Address,
855            asset_ids: &[AssetId],
856            base_asset_id: AssetId,
857            db: TestDatabase,
858        ) {
859            let asset_id = asset_ids[0];
860
861            // Query some amounts, including higher than the owner's balance
862            for amount in 0..20 {
863                let coins = query(
864                    vec![AssetSpendTarget::new(asset_id, amount, u16::MAX, false)],
865                    owner,
866                    asset_ids,
867                    base_asset_id,
868                    &db.service_database(),
869                )
870                .await;
871
872                // Transform result for convenience
873                let coins = coins.map(|coins| {
874                    coins
875                        .into_iter()
876                        .map(|(id, amount)| {
877                            // Check the asset ID before we drop it
878                            assert_eq!(id, asset_id);
879
880                            amount
881                        })
882                        .collect::<Vec<u64>>()
883                });
884
885                match amount {
886                    // This should return nothing
887                    0 => assert_matches!(coins, Ok(coins) if coins.is_empty()),
888                    // This range should...
889                    1..=7 => {
890                        // ...satisfy the amount
891                        assert_matches!(coins, Ok(coins) if coins.iter().sum::<u64>() as u128 >= amount)
892                        // ...and add more for dust management
893                        // TODO: Implement the test
894                    }
895                    // This range should return all coins
896                    8..=15 => {
897                        assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2, 1])
898                    }
899                    // Asking for more than the owner's balance should error
900                    _ => {
901                        assert_matches!(
902                            coins,
903                            Err(CoinsQueryError::InsufficientCoins {
904                                owner: _,
905                                asset_id: _,
906                                collected_amount: 15,
907                            })
908                        )
909                    }
910                };
911            }
912
913            // Query with too small max_inputs
914            let coins = query(
915                vec![AssetSpendTarget::new(
916                    asset_id, 6,     // target
917                    1,     // max
918                    false, // allow_partial
919                )],
920                owner,
921                asset_ids,
922                base_asset_id,
923                &db.service_database(),
924            )
925            .await;
926            assert_matches!(coins, Err(CoinsQueryError::MaxCoinsReached { .. }));
927        }
928
929        #[tokio::test]
930        async fn single_asset_coins() {
931            // Setup for coins
932            let (owner, asset_ids, base_asset_id, db) = setup_coins();
933            single_asset_assert(owner, &asset_ids, base_asset_id, db).await;
934        }
935
936        #[tokio::test]
937        async fn single_asset_messages() {
938            // Setup for messages
939            let (owner, base_asset_id, db) = setup_messages();
940            single_asset_assert(owner, &[base_asset_id], base_asset_id, db).await;
941        }
942
943        #[tokio::test]
944        async fn single_asset_coins_and_messages() {
945            // Setup for coins and messages
946            let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
947            single_asset_assert(owner, &asset_ids, base_asset_id, db).await;
948        }
949
950        async fn multiple_assets_assert(
951            owner: Address,
952            asset_ids: &[AssetId],
953            base_asset_id: AssetId,
954            db: TestDatabase,
955        ) {
956            // Query multiple asset IDs
957            let coins = query(
958                vec![
959                    AssetSpendTarget::new(
960                        asset_ids[0],
961                        3,     // target
962                        3,     // max
963                        false, // allow_partial
964                    ),
965                    AssetSpendTarget::new(
966                        asset_ids[1],
967                        6,     // target
968                        3,     // max
969                        false, // allow_partial
970                    ),
971                ],
972                owner,
973                asset_ids,
974                base_asset_id,
975                &db.service_database(),
976            )
977            .await;
978            assert_matches!(coins, Ok(ref coins) if coins.len() <= 6);
979            let coins = coins.unwrap();
980            assert!(
981                coins
982                    .iter()
983                    .filter(|c| c.0 == asset_ids[0])
984                    .map(|c| c.1)
985                    .sum::<u64>()
986                    >= 3
987            );
988            assert!(
989                coins
990                    .iter()
991                    .filter(|c| c.0 == asset_ids[1])
992                    .map(|c| c.1)
993                    .sum::<u64>()
994                    >= 6
995            );
996        }
997
998        #[tokio::test]
999        async fn multiple_assets_coins() {
1000            // Setup coins
1001            let (owner, asset_ids, base_asset_id, db) = setup_coins();
1002            multiple_assets_assert(owner, &asset_ids, base_asset_id, db).await;
1003        }
1004
1005        #[tokio::test]
1006        async fn multiple_assets_coins_and_messages() {
1007            // Setup coins and messages
1008            let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
1009            multiple_assets_assert(owner, &asset_ids, base_asset_id, db).await;
1010        }
1011    }
1012
1013    mod exclusion {
1014        use super::*;
1015        use crate::query::asset_query::Exclude;
1016        use fuel_core_types::entities::coins::CoinId;
1017        use std::borrow::Cow;
1018
1019        async fn query(
1020            db: &ServiceDatabase,
1021            owner: Address,
1022            base_asset_id: AssetId,
1023            asset_ids: &[AssetId],
1024            query_per_asset: Vec<AssetSpendTarget>,
1025            excluded_ids: Vec<CoinId>,
1026        ) -> Result<Vec<(AssetId, u64)>, CoinsQueryError> {
1027            let spend_query = SpendQuery::new(
1028                owner,
1029                &query_per_asset,
1030                Cow::Owned(Exclude::new(excluded_ids)),
1031                base_asset_id,
1032            )?;
1033            let coins = random_improve(&db.test_view(), &spend_query).await;
1034
1035            // Transform result for convenience
1036            coins.map(|coins| {
1037                coins
1038                    .into_iter()
1039                    .flat_map(|coin| {
1040                        coin.into_iter()
1041                            .map(|coin| (*coin.asset_id(&base_asset_id), coin.amount()))
1042                            .sorted_by_key(|(asset_id, amount)| {
1043                                (
1044                                    asset_ids.iter().position(|c| c == asset_id).unwrap(),
1045                                    Reverse(*amount),
1046                                )
1047                            })
1048                    })
1049                    .collect()
1050            })
1051        }
1052
1053        async fn exclusion_assert(
1054            owner: Address,
1055            asset_ids: &[AssetId],
1056            base_asset_id: AssetId,
1057            db: TestDatabase,
1058            excluded_ids: Vec<CoinId>,
1059        ) {
1060            let asset_id = asset_ids[0];
1061
1062            // Query some amounts, including higher than the owner's balance
1063            for amount in 0..20 {
1064                let coins = query(
1065                    &db.service_database(),
1066                    owner,
1067                    base_asset_id,
1068                    asset_ids,
1069                    vec![AssetSpendTarget::new(asset_id, amount, u16::MAX, false)],
1070                    excluded_ids.clone(),
1071                )
1072                .await;
1073
1074                // Transform result for convenience
1075                let coins = coins.map(|coins| {
1076                    coins
1077                        .into_iter()
1078                        .map(|(id, amount)| {
1079                            // Check the asset ID before we drop it
1080                            assert_eq!(id, asset_id);
1081                            amount
1082                        })
1083                        .collect::<Vec<u64>>()
1084                });
1085
1086                match amount {
1087                    // This should return nothing
1088                    0 => assert_matches!(coins, Ok(coins) if coins.is_empty()),
1089                    // This range should...
1090                    1..=4 => {
1091                        // ...satisfy the amount
1092                        assert_matches!(coins, Ok(coins) if coins.iter().sum::<u64>() as u128 >= amount)
1093                        // ...and add more for dust management
1094                        // TODO: Implement the test
1095                    }
1096                    // This range should return all coins
1097                    5..=10 => {
1098                        assert_matches!(coins, Ok(coins) if coins == vec![4, 3, 2, 1])
1099                    }
1100                    // Asking for more than the owner's balance should error
1101                    _ => {
1102                        assert_matches!(
1103                            coins,
1104                            Err(CoinsQueryError::InsufficientCoins {
1105                                owner: _,
1106                                asset_id: _,
1107                                collected_amount: 10,
1108                            })
1109                        )
1110                    }
1111                };
1112            }
1113        }
1114
1115        #[tokio::test]
1116        async fn exclusion_coins() {
1117            // Setup coins
1118            let (owner, asset_ids, base_asset_id, db) = setup_coins();
1119
1120            // Exclude largest coin IDs
1121            let excluded_ids = db
1122                .owned_coins(&owner)
1123                .await
1124                .into_iter()
1125                .filter(|coin| coin.amount == 5)
1126                .map(|coin| CoinId::Utxo(coin.utxo_id))
1127                .collect_vec();
1128
1129            exclusion_assert(owner, &asset_ids, base_asset_id, db, excluded_ids).await;
1130        }
1131
1132        #[tokio::test]
1133        async fn exclusion_messages() {
1134            // Setup messages
1135            let (owner, base_asset_id, db) = setup_messages();
1136
1137            // Exclude largest messages IDs
1138            let excluded_ids = db
1139                .owned_messages(&owner)
1140                .await
1141                .into_iter()
1142                .filter(|message| message.amount() == 5)
1143                .map(|message| CoinId::Message(*message.id()))
1144                .collect_vec();
1145
1146            exclusion_assert(owner, &[base_asset_id], base_asset_id, db, excluded_ids)
1147                .await;
1148        }
1149
1150        #[tokio::test]
1151        async fn exclusion_coins_and_messages() {
1152            // Setup coins and messages
1153            let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
1154
1155            // Exclude largest messages IDs, because coins only 1 and 2
1156            let excluded_ids = db
1157                .owned_messages(&owner)
1158                .await
1159                .into_iter()
1160                .filter(|message| message.amount() == 5)
1161                .map(|message| CoinId::Message(*message.id()))
1162                .collect_vec();
1163
1164            exclusion_assert(owner, &asset_ids, base_asset_id, db, excluded_ids).await;
1165        }
1166    }
1167
1168    mod indexed_coins_to_spend {
1169        use super::*;
1170        use fuel_core_storage::iter::IntoBoxedIter;
1171        use fuel_core_types::{
1172            entities::coins::{
1173                CoinId,
1174                coin::Coin,
1175            },
1176            fuel_tx::{
1177                AssetId,
1178                TxId,
1179                UtxoId,
1180            },
1181        };
1182
1183        use crate::{
1184            coins_query::{
1185                CoinsQueryError,
1186                CoinsToSpendIndexKey,
1187                select_coins_to_spend,
1188                select_coins_until,
1189            },
1190            graphql_api::ports::CoinsToSpendIndexIter,
1191            query::asset_query::Exclude,
1192        };
1193
1194        const BATCH_SIZE: usize = 1;
1195
1196        struct TestCoinSpec {
1197            index_entry: Result<CoinsToSpendIndexKey, fuel_core_storage::Error>,
1198            utxo_id: UtxoId,
1199        }
1200
1201        fn setup_test_coins(coins: impl IntoIterator<Item = u8>) -> Vec<TestCoinSpec> {
1202            coins
1203                .into_iter()
1204                .map(|i| {
1205                    let tx_id: TxId = [i; 32].into();
1206                    let output_index = i as u16;
1207                    let utxo_id = UtxoId::new(tx_id, output_index);
1208
1209                    let coin = Coin {
1210                        utxo_id,
1211                        owner: Default::default(),
1212                        amount: i as u64,
1213                        asset_id: Default::default(),
1214                        tx_pointer: Default::default(),
1215                    };
1216
1217                    TestCoinSpec {
1218                        index_entry: Ok(CoinsToSpendIndexKey::from_coin(&coin)),
1219                        utxo_id,
1220                    }
1221                })
1222                .collect()
1223        }
1224
1225        #[tokio::test]
1226        async fn select_coins_until_respects_max() {
1227            // Given
1228            const MAX: u16 = 3;
1229
1230            let coins = setup_test_coins([1, 2, 3, 4, 5]);
1231            let (coins, _): (Vec<_>, Vec<_>) = coins
1232                .into_iter()
1233                .map(|spec| (spec.index_entry, spec.utxo_id))
1234                .unzip();
1235
1236            let exclude = Exclude::default();
1237
1238            // When
1239            let result = select_coins_until(
1240                futures::stream::iter(coins),
1241                MAX,
1242                &exclude,
1243                |_, _| false,
1244            )
1245            .await
1246            .expect("should select coins");
1247
1248            // Then
1249            assert_eq!(result.0, 1 + 2 + 3); // Limit is set at 3 coins
1250            assert_eq!(result.1.len(), 3);
1251        }
1252
1253        #[tokio::test]
1254        async fn select_coins_until_respects_excluded_ids() {
1255            // Given
1256            const MAX: u16 = u16::MAX;
1257
1258            let coins = setup_test_coins([1, 2, 3, 4, 5]);
1259            let (coins, utxo_ids): (Vec<_>, Vec<_>) = coins
1260                .into_iter()
1261                .map(|spec| (spec.index_entry, spec.utxo_id))
1262                .unzip();
1263
1264            // Exclude coin with amount '2'.
1265            let utxo_id = utxo_ids[1];
1266            let exclude = Exclude::new(vec![CoinId::Utxo(utxo_id)]);
1267
1268            // When
1269            let result = select_coins_until(
1270                futures::stream::iter(coins),
1271                MAX,
1272                &exclude,
1273                |_, _| false,
1274            )
1275            .await
1276            .expect("should select coins");
1277
1278            // Then
1279            assert_eq!(result.0, 1 + 3 + 4 + 5); // '2' is skipped.
1280            assert_eq!(result.1.len(), 4);
1281        }
1282
1283        #[tokio::test]
1284        async fn select_coins_until_respects_predicate() {
1285            // Given
1286            const MAX: u16 = u16::MAX;
1287            const TOTAL: u128 = 7;
1288
1289            let coins = setup_test_coins([1, 2, 3, 4, 5]);
1290            let (coins, _): (Vec<_>, Vec<_>) = coins
1291                .into_iter()
1292                .map(|spec| (spec.index_entry, spec.utxo_id))
1293                .unzip();
1294
1295            let exclude = Exclude::default();
1296
1297            let predicate: fn(&CoinsToSpendIndexKey, u128) -> bool =
1298                |_, total| total > TOTAL;
1299
1300            // When
1301            let result = select_coins_until(
1302                futures::stream::iter(coins),
1303                MAX,
1304                &exclude,
1305                predicate,
1306            )
1307            .await
1308            .expect("should select coins");
1309
1310            // Then
1311            assert_eq!(result.0, 1 + 2 + 3 + 4); // Keep selecting until total is greater than 7.
1312            assert_eq!(result.1.len(), 4);
1313        }
1314
1315        #[tokio::test]
1316        async fn already_selected_big_coins_are_never_reselected_as_dust() {
1317            // Given
1318            const MAX: u16 = u16::MAX;
1319            const TOTAL: u128 = 101;
1320
1321            let test_coins = [100, 100, 4, 3, 2];
1322            let big_coins_iter = setup_test_coins(test_coins)
1323                .into_iter()
1324                .map(|spec| spec.index_entry)
1325                .into_boxed();
1326
1327            let dust_coins_iter = setup_test_coins(test_coins)
1328                .into_iter()
1329                .rev()
1330                .map(|spec| spec.index_entry)
1331                .into_boxed();
1332
1333            let coins_to_spend_iter = CoinsToSpendIndexIter {
1334                big_coins_iter,
1335                dust_coins_iter,
1336            };
1337            let asset_id = AssetId::default();
1338            let exclude = Exclude::default();
1339            let owner = Address::default();
1340            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1341
1342            // When
1343            let result = select_coins_to_spend(
1344                coins_to_spend_iter,
1345                asset_target,
1346                &exclude,
1347                BATCH_SIZE,
1348                owner,
1349            )
1350            .await
1351            .expect("should not error");
1352
1353            let mut results = result
1354                .into_iter()
1355                .map(|key| key.amount())
1356                .collect::<Vec<_>>();
1357
1358            // Then
1359
1360            // Because we select a total of 202 (TOTAL * 2), first 3 coins should always selected (100, 100, 4).
1361            let expected = vec![100, 100, 4];
1362            let actual: Vec<_> = results.drain(..3).collect();
1363            assert_eq!(expected, actual);
1364
1365            // The number of dust coins is selected randomly, so we might have:
1366            // - 0 dust coins
1367            // - 1 dust coin [2]
1368            // - 2 dust coins [2, 3]
1369            // Even though in majority of cases we will have 2 dust coins selected (due to
1370            // MAX being huge), we can't guarantee that, hence we assert against all possible cases.
1371            // The important fact is that neither 100 nor 4 are selected as dust coins.
1372            let expected_1: Vec<u64> = vec![];
1373            let expected_2: Vec<u64> = vec![2];
1374            let expected_3: Vec<u64> = vec![2, 3];
1375            let actual: Vec<_> = results;
1376
1377            assert!(
1378                actual == expected_1 || actual == expected_2 || actual == expected_3,
1379                "Unexpected dust coins: {:?}",
1380                actual,
1381            );
1382        }
1383
1384        #[tokio::test]
1385        async fn selects_double_the_value_of_coins() {
1386            // Given
1387            const MAX: u16 = u16::MAX;
1388            const TOTAL: u128 = 10;
1389
1390            let coins = setup_test_coins([10, 10, 9, 8, 7]);
1391            let (coins, _): (Vec<_>, Vec<_>) = coins
1392                .into_iter()
1393                .map(|spec| (spec.index_entry, spec.utxo_id))
1394                .unzip();
1395
1396            let exclude = Exclude::default();
1397
1398            let coins_to_spend_iter = CoinsToSpendIndexIter {
1399                big_coins_iter: coins.into_iter().into_boxed(),
1400                dust_coins_iter: std::iter::empty().into_boxed(),
1401            };
1402            let asset_id = AssetId::default();
1403            let owner = Address::default();
1404            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1405
1406            // When
1407            let result = select_coins_to_spend(
1408                coins_to_spend_iter,
1409                asset_target,
1410                &exclude,
1411                BATCH_SIZE,
1412                owner,
1413            )
1414            .await
1415            .expect("should not error");
1416
1417            // Then
1418            let results: Vec<_> = result.into_iter().map(|key| key.amount()).collect();
1419            assert_eq!(results, vec![10, 10]);
1420        }
1421
1422        #[tokio::test]
1423        async fn select_coins_to_spend_should_bail_on_storage_error() {
1424            // Given
1425            const MAX: u16 = u16::MAX;
1426            const TOTAL: u128 = 101;
1427
1428            let coins = setup_test_coins([10, 9, 8, 7]);
1429            let (mut coins, _): (Vec<_>, Vec<_>) = coins
1430                .into_iter()
1431                .map(|spec| (spec.index_entry, spec.utxo_id))
1432                .unzip();
1433            let error = fuel_core_storage::Error::NotFound("S1", "S2");
1434
1435            let first_2: Vec<_> = coins.drain(..2).collect();
1436            let last_2: Vec<_> = std::mem::take(&mut coins);
1437
1438            let exclude = Exclude::default();
1439
1440            // Inject an error into the middle of coins.
1441            let coins: Vec<_> = first_2
1442                .into_iter()
1443                .take(2)
1444                .chain(std::iter::once(Err(error)))
1445                .chain(last_2)
1446                .collect();
1447            let coins_to_spend_iter = CoinsToSpendIndexIter {
1448                big_coins_iter: coins.into_iter().into_boxed(),
1449                dust_coins_iter: std::iter::empty().into_boxed(),
1450            };
1451            let asset_id = AssetId::default();
1452            let owner = Address::default();
1453            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1454
1455            // When
1456            let result = select_coins_to_spend(
1457                coins_to_spend_iter,
1458                asset_target,
1459                &exclude,
1460                BATCH_SIZE,
1461                owner,
1462            )
1463            .await;
1464
1465            // Then
1466            assert!(matches!(result, Err(actual_error)
1467                if CoinsQueryError::StorageError(fuel_core_storage::Error::NotFound("S1", "S2")) == actual_error));
1468        }
1469
1470        #[tokio::test]
1471        async fn select_coins_to_spend_should_bail_on_incorrect_max() {
1472            // Given
1473            const MAX: u16 = 0;
1474            const TOTAL: u128 = 101;
1475
1476            let exclude = Exclude::default();
1477
1478            let coins_to_spend_iter = CoinsToSpendIndexIter {
1479                big_coins_iter: std::iter::empty().into_boxed(),
1480                dust_coins_iter: std::iter::empty().into_boxed(),
1481            };
1482            let asset_id = AssetId::default();
1483            let owner = Address::default();
1484            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1485
1486            let result = select_coins_to_spend(
1487                coins_to_spend_iter,
1488                asset_target,
1489                &exclude,
1490                BATCH_SIZE,
1491                owner,
1492            )
1493            .await;
1494
1495            // Then
1496            assert_eq!(result, Ok(Vec::new()));
1497        }
1498
1499        #[tokio::test]
1500        async fn select_coins_to_spend_should_bail_on_incorrect_total() {
1501            // Given
1502            const MAX: u16 = 101;
1503            const TOTAL: u128 = 0;
1504
1505            let exclude = Exclude::default();
1506
1507            let coins_to_spend_iter = CoinsToSpendIndexIter {
1508                big_coins_iter: std::iter::empty().into_boxed(),
1509                dust_coins_iter: std::iter::empty().into_boxed(),
1510            };
1511            let asset_id = AssetId::default();
1512            let owner = Address::default();
1513            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1514
1515            let result = select_coins_to_spend(
1516                coins_to_spend_iter,
1517                asset_target,
1518                &exclude,
1519                BATCH_SIZE,
1520                owner,
1521            )
1522            .await;
1523
1524            // Then
1525            assert_eq!(result, Ok(Vec::new()));
1526        }
1527
1528        #[tokio::test]
1529        async fn select_coins_to_spend__fails_if_coins_exceed_max_inputs() {
1530            // Given
1531            const MAX: u16 = 3;
1532            const TOTAL: u128 = 2137;
1533
1534            let coins = setup_test_coins([10, 9, 8, 7]);
1535            let (coins, _): (Vec<_>, Vec<_>) = coins
1536                .into_iter()
1537                .map(|spec| (spec.index_entry, spec.utxo_id))
1538                .unzip();
1539
1540            let exclude = Exclude::default();
1541
1542            let coins_to_spend_iter = CoinsToSpendIndexIter {
1543                big_coins_iter: coins.into_iter().into_boxed(),
1544                dust_coins_iter: std::iter::empty().into_boxed(),
1545            };
1546            let asset_id = AssetId::default();
1547            let owner = Address::default();
1548            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1549
1550            // When
1551            let result = select_coins_to_spend(
1552                coins_to_spend_iter,
1553                asset_target,
1554                &exclude,
1555                BATCH_SIZE,
1556                owner,
1557            )
1558            .await;
1559
1560            const EXPECTED_COLLECTED_AMOUNT: u128 = 10 + 9 + 8; // Because MAX == 3
1561
1562            // Then
1563            assert_matches!(
1564                result,
1565                Err(CoinsQueryError::MaxCoinsReached {
1566                    owner: _,
1567                    asset_id: _,
1568                    collected_amount: EXPECTED_COLLECTED_AMOUNT,
1569                    max: MAX,
1570                })
1571            );
1572        }
1573
1574        #[tokio::test]
1575        async fn select_coins_to_spend__fails_if_insufficient_funds() {
1576            // Given
1577            const MAX: u16 = 3;
1578            const TOTAL: u128 = 28;
1579
1580            let coins = setup_test_coins([10, 9, 8]);
1581            let (coins, _): (Vec<_>, Vec<_>) = coins
1582                .into_iter()
1583                .map(|spec| (spec.index_entry, spec.utxo_id))
1584                .unzip();
1585
1586            let exclude = Exclude::default();
1587
1588            let coins_to_spend_iter = CoinsToSpendIndexIter {
1589                big_coins_iter: coins.into_iter().into_boxed(),
1590                dust_coins_iter: std::iter::empty().into_boxed(),
1591            };
1592
1593            let asset_id = AssetId::default();
1594            let owner = Address::default();
1595            let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1596
1597            // When
1598            let result = select_coins_to_spend(
1599                coins_to_spend_iter,
1600                asset_target,
1601                &exclude,
1602                BATCH_SIZE,
1603                owner,
1604            )
1605            .await;
1606
1607            const EXPECTED_COLLECTED_AMOUNT: u128 = 10 + 9 + 8; // Because MAX == 3
1608
1609            // Then
1610            assert_matches!(
1611                result,
1612                Err(CoinsQueryError::InsufficientCoins {
1613                    owner: _,
1614                    asset_id: _,
1615                    collected_amount: EXPECTED_COLLECTED_AMOUNT
1616                })
1617            );
1618        }
1619
1620        mod allow_partial {
1621            use fuel_core_storage::iter::IntoBoxedIter;
1622            use fuel_core_types::{
1623                fuel_tx::AssetId,
1624                fuel_types::Address,
1625            };
1626
1627            use crate::{
1628                coins_query::tests::indexed_coins_to_spend::{
1629                    BATCH_SIZE,
1630                    select_coins_to_spend,
1631                    setup_test_coins,
1632                },
1633                graphql_api::ports::CoinsToSpendIndexIter,
1634                query::asset_query::{
1635                    AssetSpendTarget,
1636                    Exclude,
1637                },
1638            };
1639
1640            #[tokio::test]
1641            async fn query__error_when_not_enough_coins_and_allow_partial_false() {
1642                // Given
1643                const MAX: u16 = 3;
1644                const TOTAL: u128 = 2137;
1645
1646                let coins = setup_test_coins([1, 1]);
1647                let (coins, _): (Vec<_>, Vec<_>) = coins
1648                    .into_iter()
1649                    .map(|spec| (spec.index_entry, spec.utxo_id))
1650                    .unzip();
1651
1652                let exclude = Exclude::default();
1653
1654                let coins_to_spend_iter = CoinsToSpendIndexIter {
1655                    big_coins_iter: coins.into_iter().into_boxed(),
1656                    dust_coins_iter: std::iter::empty().into_boxed(),
1657                };
1658                let asset_id = AssetId::default();
1659                let owner = Address::default();
1660                let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, false);
1661
1662                // When
1663                let result = select_coins_to_spend(
1664                    coins_to_spend_iter,
1665                    asset_target,
1666                    &exclude,
1667                    BATCH_SIZE,
1668                    owner,
1669                )
1670                .await;
1671
1672                // Then
1673                assert!(result.is_err());
1674            }
1675
1676            #[tokio::test]
1677            async fn query__ok_when_not_enough_coins_and_allow_partial_true() {
1678                // Given
1679                const MAX: u16 = 3;
1680                const TOTAL: u128 = 2137;
1681
1682                let coins = setup_test_coins([1, 1]);
1683                let (coins, _): (Vec<_>, Vec<_>) = coins
1684                    .into_iter()
1685                    .map(|spec| (spec.index_entry, spec.utxo_id))
1686                    .unzip();
1687
1688                let exclude = Exclude::default();
1689
1690                let coins_to_spend_iter = CoinsToSpendIndexIter {
1691                    big_coins_iter: coins.into_iter().into_boxed(),
1692                    dust_coins_iter: std::iter::empty().into_boxed(),
1693                };
1694                let asset_id = AssetId::default();
1695                let owner = Address::default();
1696                let asset_target = AssetSpendTarget::new(asset_id, TOTAL, MAX, true);
1697
1698                // When
1699                let result = select_coins_to_spend(
1700                    coins_to_spend_iter,
1701                    asset_target,
1702                    &exclude,
1703                    BATCH_SIZE,
1704                    owner,
1705                )
1706                .await
1707                .expect("should return coins");
1708
1709                // Then
1710                let coins: Vec<_> = result.into_iter().map(|key| key.amount()).collect();
1711                assert_eq!(coins, vec![1, 1]);
1712            }
1713        }
1714    }
1715
1716    #[derive(Clone, Debug)]
1717    struct TestCase {
1718        db_amount: Vec<Word>,
1719        target_amount: u128,
1720        max_coins: u16,
1721    }
1722
1723    pub enum CoinType {
1724        Coin,
1725        Message,
1726    }
1727
1728    async fn test_case_run(
1729        case: TestCase,
1730        coin_type: CoinType,
1731        base_asset_id: AssetId,
1732    ) -> Result<usize, CoinsQueryError> {
1733        let TestCase {
1734            db_amount,
1735            target_amount,
1736            max_coins,
1737        } = case;
1738        let owner = Address::default();
1739        let asset_ids = [base_asset_id];
1740        let mut db = TestDatabase::new();
1741        for amount in db_amount {
1742            match coin_type {
1743                CoinType::Coin => {
1744                    let _ = db.make_coin(owner, amount, asset_ids[0]);
1745                }
1746                CoinType::Message => {
1747                    let _ = db.make_message(owner, amount);
1748                }
1749            };
1750        }
1751
1752        let coins = random_improve(
1753            &db.service_database().test_view(),
1754            &SpendQuery::new(
1755                owner,
1756                &[AssetSpendTarget {
1757                    id: asset_ids[0],
1758                    target: target_amount,
1759                    max: max_coins,
1760                    allow_partial: false,
1761                }],
1762                Cow::Owned(Exclude::default()),
1763                base_asset_id,
1764            )?,
1765        )
1766        .await?;
1767
1768        assert_eq!(coins.len(), 1);
1769        Ok(coins[0].len())
1770    }
1771
1772    #[tokio::test]
1773    async fn insufficient_coins_returns_error() {
1774        let test_case = TestCase {
1775            db_amount: vec![0],
1776            target_amount: u128::MAX,
1777            max_coins: u16::MAX,
1778        };
1779        let mut rng = StdRng::seed_from_u64(0xF00DF00D);
1780        let base_asset_id = rng.r#gen();
1781        let coin_result =
1782            test_case_run(test_case.clone(), CoinType::Coin, base_asset_id).await;
1783        let message_result =
1784            test_case_run(test_case, CoinType::Message, base_asset_id).await;
1785        assert_eq!(coin_result, message_result);
1786        assert_matches!(
1787            coin_result,
1788            Err(CoinsQueryError::InsufficientCoins {
1789                owner: _,
1790                asset_id: _base_asset_id,
1791                collected_amount: 0,
1792            })
1793        )
1794    }
1795
1796    proptest! {
1797        #[test]
1798        fn max_dust_count_respects_limits(
1799            max in 1u16..255,
1800            number_of_big_coins in 1u16..255,
1801            factor in 1u16..10,
1802        ) {
1803            // We're at the stage of the algorithm where we have already selected the big coins and
1804            // we're trying to select the dust coins.
1805            // So we're sure that the following assumptions hold:
1806            // 1. number_of_big_coins <= max - big coin selection algo is capped at 'max'.
1807            // 2. there must be at least one big coin selected, otherwise we'll break
1808            //    with the `InsufficientCoinsForTheMax` error earlier.
1809            prop_assume!(number_of_big_coins <= max && number_of_big_coins >= 1);
1810
1811            let max_dust_count = max_dust_count(max, number_of_big_coins, factor);
1812            prop_assert!(number_of_big_coins + max_dust_count <= max);
1813            prop_assert!(max_dust_count <= number_of_big_coins.saturating_mul(factor));
1814        }
1815    }
1816
1817    #[test_case::test_case(
1818        TestCase {
1819            db_amount: vec![u64::MAX, u64::MAX],
1820            target_amount: u64::MAX as u128,
1821            max_coins: u16::MAX,
1822        }
1823        => Ok(1)
1824        ; "Enough coins in the DB to reach target(u64::MAX) by 1 coin"
1825    )]
1826    #[test_case::test_case(
1827        TestCase {
1828            db_amount: vec![2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, u64::MAX - 1],
1829            target_amount: u64::MAX as u128,
1830            max_coins: 2,
1831        }
1832        => Ok(2)
1833        ; "Enough coins in the DB to reach target(u64::MAX) by 2 coins"
1834    )]
1835    #[tokio::test]
1836    async fn corner_cases(case: TestCase) -> Result<usize, CoinsQueryError> {
1837        let mut rng = StdRng::seed_from_u64(0xF00DF00D);
1838        let base_asset_id = rng.r#gen();
1839        let coin_result =
1840            test_case_run(case.clone(), CoinType::Coin, base_asset_id).await;
1841        let message_result = test_case_run(case, CoinType::Message, base_asset_id).await;
1842        assert_eq!(coin_result, message_result);
1843        coin_result
1844    }
1845
1846    #[tokio::test]
1847    async fn enough_coins_in_the_db_to_reach_target_u64_max_but_limit_is_zero() {
1848        let mut rng = StdRng::seed_from_u64(0xF00DF00D);
1849
1850        let case = TestCase {
1851            db_amount: vec![u64::MAX, u64::MAX],
1852            target_amount: u128::MAX,
1853            max_coins: 0,
1854        };
1855
1856        let base_asset_id = rng.r#gen();
1857        let coin_result =
1858            test_case_run(case.clone(), CoinType::Coin, base_asset_id).await;
1859        let message_result = test_case_run(case, CoinType::Message, base_asset_id).await;
1860        assert_eq!(coin_result, message_result);
1861        assert!(matches!(
1862            coin_result,
1863            Err(CoinsQueryError::MaxCoinsReached { .. })
1864        ));
1865    }
1866
1867    // TODO: Should use any mock database instead of the `fuel_core::CombinedDatabase`.
1868    pub struct TestDatabase {
1869        database: CombinedDatabase,
1870        last_coin_index: u64,
1871        last_message_index: u64,
1872    }
1873
1874    impl TestDatabase {
1875        fn new() -> Self {
1876            Self {
1877                database: Default::default(),
1878                last_coin_index: Default::default(),
1879                last_message_index: Default::default(),
1880            }
1881        }
1882
1883        fn service_database(&self) -> ServiceDatabase {
1884            let on_chain = self.database.on_chain().clone();
1885            let off_chain = self.database.off_chain().clone();
1886            ServiceDatabase::new(100, 0u32.into(), on_chain, off_chain)
1887                .expect("should create service database")
1888        }
1889    }
1890
1891    impl TestDatabase {
1892        pub fn make_coin(
1893            &mut self,
1894            owner: Address,
1895            amount: Word,
1896            asset_id: AssetId,
1897        ) -> Coin {
1898            let index = self.last_coin_index;
1899            self.last_coin_index += 1;
1900
1901            let id = UtxoId::new(Bytes32::from([0u8; 32]), index.try_into().unwrap());
1902            let mut coin = CompressedCoin::default();
1903            coin.set_owner(owner);
1904            coin.set_amount(amount);
1905            coin.set_asset_id(asset_id);
1906
1907            let db = self.database.on_chain_mut();
1908            StorageMutate::<Coins>::insert(db, &id, &coin).unwrap();
1909            let db = self.database.off_chain_mut();
1910            let coin_by_owner = owner_coin_id_key(&owner, &id);
1911            StorageMutate::<OwnedCoins>::insert(db, &coin_by_owner, &()).unwrap();
1912
1913            coin.uncompress(id)
1914        }
1915
1916        pub fn make_message(&mut self, owner: Address, amount: Word) -> Message {
1917            let nonce = self.last_message_index.into();
1918            self.last_message_index += 1;
1919
1920            let message: Message = MessageV1 {
1921                sender: Default::default(),
1922                recipient: owner,
1923                nonce,
1924                amount,
1925                data: vec![],
1926                da_height: DaBlockHeight::from(1u64),
1927            }
1928            .into();
1929
1930            let db = self.database.on_chain_mut();
1931            StorageMutate::<Messages>::insert(db, message.id(), &message).unwrap();
1932            let db = self.database.off_chain_mut();
1933            let owned_message_key = OwnedMessageKey::new(&owner, &nonce);
1934            StorageMutate::<OwnedMessageIds>::insert(db, &owned_message_key, &())
1935                .unwrap();
1936
1937            message
1938        }
1939
1940        pub async fn owned_coins(&self, owner: &Address) -> Vec<Coin> {
1941            let query = self.service_database();
1942            let query = query.test_view();
1943            query
1944                .owned_coins(owner, None, IterDirection::Forward)
1945                .try_collect()
1946                .await
1947                .unwrap()
1948        }
1949
1950        pub async fn owned_messages(&self, owner: &Address) -> Vec<Message> {
1951            let query = self.service_database();
1952            let query = query.test_view();
1953            query
1954                .owned_messages(owner, None, IterDirection::Forward)
1955                .try_collect()
1956                .await
1957                .unwrap()
1958        }
1959    }
1960}