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
85pub 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 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 pub fn assets(&self) -> &Vec<AssetSpendTarget> {
112 &self.query_per_asset
113 }
114
115 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 pub fn exclude(&self) -> &Exclude {
133 self.exclude.as_ref()
134 }
135
136 pub fn owner(&self) -> &Address {
138 &self.owner
139 }
140}
141
142pub 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 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 return Err(CoinsQueryError::MaxCoinsReached {
171 owner: *owner,
172 asset_id,
173 collected_amount,
174 max,
175 });
176 }
177 }
178
179 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
199pub 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 let upper_target = target.saturating_mul(2);
219
220 for coin in inputs {
221 if collected_amount >= target {
223 if collected_amount >= u64::MAX as u128
225 || coin.amount() as u128 > upper_target
226 {
227 break;
228 }
229
230 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 collected_amount = collected_amount.saturating_add(coin.amount() as u128);
244 coins.push(coin);
245 }
246
247 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 const TOTAL_AMOUNT_ADJUSTMENT_FACTOR: u128 = 2;
278
279 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 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 (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 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 let coins = coins.map(|coins| {
627 coins[0]
628 .iter()
629 .map(|(id, amount)| {
630 assert_eq!(id, &asset_id);
632
633 *amount
634 })
635 .collect::<Vec<u64>>()
636 });
637
638 match target {
639 0 => {
641 assert_matches!(coins, Ok(coins) if coins.is_empty())
642 }
643 1..=5 => {
645 assert_matches!(coins, Ok(coins) if coins == vec![5])
646 }
647 6..=9 => {
649 assert_matches!(coins, Ok(coins) if coins == vec![5, 4])
650 }
651 10..=12 => {
653 assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3])
654 }
655 13..=14 => {
657 assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2])
658 }
659 15 => {
661 assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2, 1])
662 }
663 _ => {
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 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 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 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 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 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 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 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 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 assert!(coins.is_err());
779 }
780
781 #[tokio::test]
782 async fn query__ok_when_not_enough_coins_and_allow_partial_true() {
783 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 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 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 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 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 let coins = coins.map(|coins| {
874 coins
875 .into_iter()
876 .map(|(id, amount)| {
877 assert_eq!(id, asset_id);
879
880 amount
881 })
882 .collect::<Vec<u64>>()
883 });
884
885 match amount {
886 0 => assert_matches!(coins, Ok(coins) if coins.is_empty()),
888 1..=7 => {
890 assert_matches!(coins, Ok(coins) if coins.iter().sum::<u64>() as u128 >= amount)
892 }
895 8..=15 => {
897 assert_matches!(coins, Ok(coins) if coins == vec![5, 4, 3, 2, 1])
898 }
899 _ => {
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 let coins = query(
915 vec![AssetSpendTarget::new(
916 asset_id, 6, 1, false, )],
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 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 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 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 let coins = query(
958 vec![
959 AssetSpendTarget::new(
960 asset_ids[0],
961 3, 3, false, ),
965 AssetSpendTarget::new(
966 asset_ids[1],
967 6, 3, false, ),
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 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 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 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 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 let coins = coins.map(|coins| {
1076 coins
1077 .into_iter()
1078 .map(|(id, amount)| {
1079 assert_eq!(id, asset_id);
1081 amount
1082 })
1083 .collect::<Vec<u64>>()
1084 });
1085
1086 match amount {
1087 0 => assert_matches!(coins, Ok(coins) if coins.is_empty()),
1089 1..=4 => {
1091 assert_matches!(coins, Ok(coins) if coins.iter().sum::<u64>() as u128 >= amount)
1093 }
1096 5..=10 => {
1098 assert_matches!(coins, Ok(coins) if coins == vec![4, 3, 2, 1])
1099 }
1100 _ => {
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 let (owner, asset_ids, base_asset_id, db) = setup_coins();
1119
1120 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 let (owner, base_asset_id, db) = setup_messages();
1136
1137 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 let (owner, asset_ids, base_asset_id, db) = setup_coins_and_messages();
1154
1155 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 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 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 assert_eq!(result.0, 1 + 2 + 3); assert_eq!(result.1.len(), 3);
1251 }
1252
1253 #[tokio::test]
1254 async fn select_coins_until_respects_excluded_ids() {
1255 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 let utxo_id = utxo_ids[1];
1266 let exclude = Exclude::new(vec![CoinId::Utxo(utxo_id)]);
1267
1268 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 assert_eq!(result.0, 1 + 3 + 4 + 5); assert_eq!(result.1.len(), 4);
1281 }
1282
1283 #[tokio::test]
1284 async fn select_coins_until_respects_predicate() {
1285 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 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 assert_eq!(result.0, 1 + 2 + 3 + 4); assert_eq!(result.1.len(), 4);
1313 }
1314
1315 #[tokio::test]
1316 async fn already_selected_big_coins_are_never_reselected_as_dust() {
1317 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 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 let expected = vec![100, 100, 4];
1362 let actual: Vec<_> = results.drain(..3).collect();
1363 assert_eq!(expected, actual);
1364
1365 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 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 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 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 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 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 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 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 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 assert_eq!(result, Ok(Vec::new()));
1497 }
1498
1499 #[tokio::test]
1500 async fn select_coins_to_spend_should_bail_on_incorrect_total() {
1501 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 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 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 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; 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 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 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; 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 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 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 assert!(result.is_err());
1674 }
1675
1676 #[tokio::test]
1677 async fn query__ok_when_not_enough_coins_and_allow_partial_true() {
1678 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 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 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 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 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}