1use fuel_core_types::{
2 fuel_tx::UtxoId,
3 fuel_types::{
4 Address,
5 AssetId,
6 },
7};
8use fuels::types::{
9 coin::Coin,
10 coin_type::CoinType,
11 input::Input,
12};
13use std::{
14 collections::{
15 BTreeSet,
16 HashMap,
17 HashSet,
18 hash_map::Entry,
19 },
20 sync::Arc,
21};
22
23pub struct CoinsResult {
24 pub known_coins: Vec<FuelTxCoin>,
25 pub unknown_coins: HashSet<UtxoId>,
26}
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30enum SelectionOrder {
31 Fifo,
33 Largest,
35}
36
37#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
38pub struct FuelTxCoin {
39 pub amount: u64,
40 pub asset_id: AssetId,
41 pub utxo_id: UtxoId,
42 pub owner: Address,
43}
44
45impl From<Coin> for FuelTxCoin {
46 fn from(value: Coin) -> Self {
47 Self {
48 amount: value.amount,
49 asset_id: value.asset_id,
50 utxo_id: value.utxo_id,
51 owner: value.owner,
52 }
53 }
54}
55
56impl From<FuelTxCoin> for Coin {
57 fn from(value: FuelTxCoin) -> Self {
58 Self {
59 amount: value.amount,
60 asset_id: value.asset_id,
61 utxo_id: value.utxo_id,
62 owner: value.owner,
63 }
64 }
65}
66
67impl TryFrom<&fuel_core_types::fuel_tx::Input> for FuelTxCoin {
68 type Error = anyhow::Error;
69
70 fn try_from(input: &fuel_core_types::fuel_tx::Input) -> Result<Self, Self::Error> {
71 if let fuel_core_types::fuel_tx::Input::CoinSigned(coin) = input {
72 return Ok(FuelTxCoin {
73 utxo_id: coin.utxo_id,
74 owner: coin.owner,
75 amount: coin.amount,
76 asset_id: coin.asset_id,
77 });
78 }
79 anyhow::bail!("Invalid input type")
80 }
81}
82
83impl From<FuelTxCoin> for Input {
84 fn from(value: FuelTxCoin) -> Self {
85 Input::resource_signed(CoinType::Coin(value.into()))
86 }
87}
88
89impl Ord for FuelTxCoin {
90 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
91 self.amount
92 .cmp(&other.amount)
93 .then_with(|| self.asset_id.cmp(&other.asset_id))
94 .then_with(|| self.utxo_id.cmp(&other.utxo_id))
95 .then_with(|| self.owner.cmp(&other.owner))
96 }
97}
98
99impl PartialOrd for FuelTxCoin {
100 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105#[derive(Copy, Clone, Debug, PartialEq, Eq)]
115struct OrderedCoin {
116 seq: u64,
117 coin: FuelTxCoin,
118}
119
120impl Ord for OrderedCoin {
121 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
122 self.seq
123 .cmp(&other.seq)
124 .then_with(|| self.coin.amount.cmp(&other.coin.amount))
125 .then_with(|| self.coin.utxo_id.cmp(&other.coin.utxo_id))
126 }
127}
128
129impl PartialOrd for OrderedCoin {
130 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
131 Some(self.cmp(other))
132 }
133}
134
135pub trait UtxoProvider: Send + 'static {
139 fn balance_of(&self, owner: Address, asset_id: AssetId) -> u128;
140 fn len_per_address(&self, asset_id: AssetId, address: Address) -> usize;
141 fn guaranteed_extract_coins(
142 &mut self,
143 owner: Address,
144 asset_id: AssetId,
145 amount: u128,
146 max_coins: usize,
147 ) -> anyhow::Result<Vec<FuelTxCoin>>;
148 fn load_from_coins_vec(&mut self, coins: Vec<FuelTxCoin>);
149 fn number_of_coins_with_amount_greater_or_equal(
150 &self,
151 owner: Address,
152 asset_id: AssetId,
153 amount: u128,
154 ) -> (u128, usize);
155 fn extract_largest_coins(
156 &mut self,
157 owner: Address,
158 asset_id: AssetId,
159 max_value: u128,
160 ) -> Vec<FuelTxCoin>;
161 fn coin_count(&self) -> usize;
162 fn utxo_ids(&self) -> Vec<UtxoId>;
163 fn remove_coin(&mut self, utxo_id: &UtxoId) -> bool;
164 fn total_balance(&self, asset_id: &AssetId) -> u128;
165}
166
167pub type SharedUtxoManager = Arc<tokio::sync::Mutex<dyn UtxoProvider>>;
168
169pub struct UtxoManager {
172 account_utxos: HashMap<(Address, AssetId), BTreeSet<OrderedCoin>>,
173 coins: HashMap<UtxoId, OrderedCoin>,
174 next_seq: u64,
176}
177
178impl Default for UtxoManager {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184impl UtxoManager {
185 pub fn new() -> Self {
186 Self {
187 account_utxos: HashMap::new(),
188 coins: HashMap::new(),
189 next_seq: 0,
190 }
191 }
192
193 pub fn len_per_address(&self, asset_id: AssetId, address: Address) -> usize {
194 self.account_utxos
195 .get(&(address, asset_id))
196 .map_or(0, |utxos| utxos.len())
197 }
198
199 pub fn new_from_coins<I>(coins: I) -> Self
200 where
201 I: Iterator<Item = FuelTxCoin>,
202 {
203 let mut _self = Self::new();
204
205 _self.load_from_coins(coins);
206
207 _self
208 }
209
210 pub fn load_from_coins<I>(&mut self, coins: I)
211 where
212 I: Iterator<Item = FuelTxCoin>,
213 {
214 let seq = self.next_seq;
218 let mut used = false;
219
220 for coin in coins {
221 if coin.amount == 0 {
222 continue;
223 }
224
225 if self.coins.contains_key(&coin.utxo_id) {
228 continue;
229 }
230
231 let ordered = OrderedCoin { seq, coin };
232 let key = (coin.owner, coin.asset_id);
233 self.account_utxos.entry(key).or_default().insert(ordered);
234 self.coins.insert(coin.utxo_id, ordered);
235 used = true;
236 }
237
238 if used {
239 self.next_seq += 1;
240 }
241 }
242
243 fn extract_utxos(&mut self, utxos: &[UtxoId]) -> anyhow::Result<Vec<FuelTxCoin>> {
244 let mut coins = vec![];
245
246 for utxo_id in utxos {
247 let ordered = self.coins.remove(utxo_id).ok_or_else(|| {
248 anyhow::anyhow!("UTXO {utxo_id} not found in the UTXO manager")
249 })?;
250
251 let key = (ordered.coin.owner, ordered.coin.asset_id);
252 let account = self.account_utxos.entry(key);
253
254 match account {
255 Entry::Occupied(mut occupied) => {
256 occupied.get_mut().remove(&ordered);
257
258 if occupied.get().is_empty() {
259 occupied.remove();
260 }
261
262 coins.push(ordered.coin);
263 }
264 Entry::Vacant(_) => {}
265 }
266 }
267
268 Ok(coins)
269 }
270
271 fn select_within_cap(
276 &self,
277 owner: Address,
278 asset_id: AssetId,
279 amount: u128,
280 max_coins: usize,
281 order: SelectionOrder,
282 ) -> Option<Vec<UtxoId>> {
283 let coins = self.account_utxos.get(&(owner, asset_id))?;
284
285 let ordered: Vec<&OrderedCoin> = match order {
288 SelectionOrder::Fifo => coins.iter().collect(),
289 SelectionOrder::Largest => {
290 let mut by_amount: Vec<&OrderedCoin> = coins.iter().collect();
291 by_amount.sort_by(|a, b| {
292 b.coin
293 .amount
294 .cmp(&a.coin.amount)
295 .then_with(|| a.coin.utxo_id.cmp(&b.coin.utxo_id))
296 });
297 by_amount
298 }
299 };
300
301 let mut total = 0u128;
302 let mut selected = Vec::new();
303 for oc in ordered {
304 if total >= amount || selected.len() >= max_coins {
305 break;
306 }
307 selected.push(oc.coin.utxo_id);
308 total += oc.coin.amount as u128;
309 }
310
311 (total >= amount).then_some(selected)
312 }
313
314 pub fn guaranteed_extract_coins(
321 &mut self,
322 owner: Address,
323 asset_id: AssetId,
324 amount: u128,
325 max_coins: usize,
326 ) -> anyhow::Result<Vec<FuelTxCoin>> {
327 let utxos = self
328 .select_within_cap(owner, asset_id, amount, max_coins, SelectionOrder::Fifo)
329 .or_else(|| {
330 self.select_within_cap(
331 owner,
332 asset_id,
333 amount,
334 max_coins,
335 SelectionOrder::Largest,
336 )
337 })
338 .ok_or_else(|| {
339 anyhow::anyhow!(
340 "Not enough UTXOs found for the given {owner} and \
341 {asset_id} to cover {amount} within {max_coins} coins."
342 )
343 })?;
344
345 self.extract_utxos(&utxos)
346 }
347
348 pub fn number_of_coins_with_amount_greater_or_equal(
349 &self,
350 owner: Address,
351 asset_id: AssetId,
352 amount: u128,
353 ) -> (u128, usize) {
354 self.account_utxos
355 .get(&(owner, asset_id))
356 .map_or((0, 0), |coins| {
357 let mut count = 0;
358 let mut total_balance = 0;
359
360 for ordered in coins.iter() {
361 if ordered.coin.amount as u128 >= amount {
362 count += 1;
363 total_balance += ordered.coin.amount as u128;
364 }
365 }
366
367 (total_balance, count)
368 })
369 }
370
371 pub fn balance_of(&self, owner: Address, asset_id: AssetId) -> u128 {
372 self.account_utxos
373 .get(&(owner, asset_id))
374 .map_or(0, |coins| {
375 coins
376 .iter()
377 .map(|ordered| ordered.coin.amount as u128)
378 .sum()
379 })
380 }
381
382 pub fn coins(&self) -> HashMap<UtxoId, FuelTxCoin> {
384 self.coins
385 .iter()
386 .map(|(utxo_id, ordered)| (*utxo_id, ordered.coin))
387 .collect()
388 }
389
390 pub fn coin_count(&self) -> usize {
392 self.coins.len()
393 }
394
395 pub fn utxo_ids(&self) -> Vec<UtxoId> {
397 self.coins.keys().copied().collect()
398 }
399
400 pub fn contains(&self, utxo_id: &UtxoId) -> bool {
402 self.coins.contains_key(utxo_id)
403 }
404
405 pub fn total_balance(&self, asset_id: &AssetId) -> u128 {
407 self.coins
408 .values()
409 .filter(|ordered| &ordered.coin.asset_id == asset_id)
410 .map(|ordered| ordered.coin.amount as u128)
411 .sum()
412 }
413
414 pub fn remove_coin(&mut self, utxo_id: &UtxoId) -> bool {
416 if let Some(ordered) = self.coins.remove(utxo_id) {
417 let key = (ordered.coin.owner, ordered.coin.asset_id);
418 if let Entry::Occupied(mut entry) = self.account_utxos.entry(key) {
419 entry.get_mut().remove(&ordered);
420 if entry.get().is_empty() {
421 entry.remove();
422 }
423 }
424 true
425 } else {
426 false
427 }
428 }
429
430 pub fn extract_largest_coins(
436 &mut self,
437 owner: Address,
438 asset_id: AssetId,
439 max_value: u128,
440 ) -> Vec<FuelTxCoin> {
441 let utxo_ids: Vec<UtxoId> = {
442 let Some(coins) = self.account_utxos.get(&(owner, asset_id)) else {
443 return vec![];
444 };
445 let mut by_amount: Vec<&OrderedCoin> = coins.iter().collect();
446 by_amount.sort_by(|a, b| {
448 b.coin
449 .amount
450 .cmp(&a.coin.amount)
451 .then_with(|| a.coin.utxo_id.cmp(&b.coin.utxo_id))
452 });
453 let mut total = 0u128;
454 by_amount
455 .into_iter()
456 .take_while(|ordered| {
457 if total >= max_value {
458 return false;
459 }
460 total += ordered.coin.amount as u128;
461 true
462 })
463 .map(|ordered| ordered.coin.utxo_id)
464 .collect()
465 };
466 self.extract_utxos(&utxo_ids).unwrap_or_default()
467 }
468}
469
470impl UtxoProvider for UtxoManager {
471 fn balance_of(&self, owner: Address, asset_id: AssetId) -> u128 {
472 UtxoManager::balance_of(self, owner, asset_id)
473 }
474
475 fn len_per_address(&self, asset_id: AssetId, address: Address) -> usize {
476 UtxoManager::len_per_address(self, asset_id, address)
477 }
478
479 fn guaranteed_extract_coins(
480 &mut self,
481 owner: Address,
482 asset_id: AssetId,
483 amount: u128,
484 max_coins: usize,
485 ) -> anyhow::Result<Vec<FuelTxCoin>> {
486 UtxoManager::guaranteed_extract_coins(self, owner, asset_id, amount, max_coins)
487 }
488
489 fn load_from_coins_vec(&mut self, coins: Vec<FuelTxCoin>) {
490 self.load_from_coins(coins.into_iter());
491 }
492
493 fn number_of_coins_with_amount_greater_or_equal(
494 &self,
495 owner: Address,
496 asset_id: AssetId,
497 amount: u128,
498 ) -> (u128, usize) {
499 UtxoManager::number_of_coins_with_amount_greater_or_equal(
500 self, owner, asset_id, amount,
501 )
502 }
503
504 fn extract_largest_coins(
505 &mut self,
506 owner: Address,
507 asset_id: AssetId,
508 max_value: u128,
509 ) -> Vec<FuelTxCoin> {
510 UtxoManager::extract_largest_coins(self, owner, asset_id, max_value)
511 }
512
513 fn coin_count(&self) -> usize {
514 UtxoManager::coin_count(self)
515 }
516
517 fn utxo_ids(&self) -> Vec<UtxoId> {
518 UtxoManager::utxo_ids(self)
519 }
520
521 fn remove_coin(&mut self, utxo_id: &UtxoId) -> bool {
522 UtxoManager::remove_coin(self, utxo_id)
523 }
524
525 fn total_balance(&self, asset_id: &AssetId) -> u128 {
526 UtxoManager::total_balance(self, asset_id)
527 }
528}
529
530#[cfg(test)]
531#[allow(non_snake_case)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn guaranteed_extract_coins__returns_coins_in_ascending_order_by_amount__when_coins_inserted_in_random_order()
537 {
538 let owner = Address::from([1u8; 32]);
540 let asset_id = AssetId::from([2u8; 32]);
541
542 let coin1 = FuelTxCoin {
543 amount: 100,
544 asset_id,
545 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([1u8; 32]), 0),
546 owner,
547 };
548 let coin2 = FuelTxCoin {
549 amount: 50,
550 asset_id,
551 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([2u8; 32]), 0),
552 owner,
553 };
554 let coin3 = FuelTxCoin {
555 amount: 200,
556 asset_id,
557 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([3u8; 32]), 0),
558 owner,
559 };
560 let coin4 = FuelTxCoin {
561 amount: 75,
562 asset_id,
563 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([4u8; 32]), 0),
564 owner,
565 };
566
567 let mut manager = UtxoManager::new();
568 manager.load_from_coins(vec![coin1, coin2, coin3, coin4].into_iter());
569
570 let total_amount = 100 + 50 + 200 + 75;
572 let extracted = manager
573 .guaranteed_extract_coins(owner, asset_id, total_amount, usize::MAX)
574 .unwrap();
575
576 assert_eq!(extracted.len(), 4);
578 assert_eq!(extracted[0].amount, 50); assert_eq!(extracted[1].amount, 75); assert_eq!(extracted[2].amount, 100); assert_eq!(extracted[3].amount, 200); assert_eq!(manager.balance_of(owner, asset_id), 0);
583 }
584
585 #[test]
586 fn extract_largest_coins__takes_largest_first_up_to_max_value() {
587 let owner = Address::from([1u8; 32]);
588 let asset_id = AssetId::from([2u8; 32]);
589
590 let coins: Vec<FuelTxCoin> = (1..=5)
591 .map(|i| FuelTxCoin {
592 amount: i * 100, asset_id,
594 utxo_id: UtxoId::new(
595 fuel_core_types::fuel_tx::TxId::from([i as u8; 32]),
596 0,
597 ),
598 owner,
599 })
600 .collect();
601 let mut manager = UtxoManager::new();
602 manager.load_from_coins(coins.into_iter());
603
604 let extracted = manager.extract_largest_coins(owner, asset_id, 600);
611 let amounts: Vec<u64> = extracted.iter().map(|c| c.amount).collect();
612 assert_eq!(amounts.len(), 2);
614 assert!(amounts.contains(&500));
615 assert!(amounts.contains(&400));
616 assert_eq!(manager.balance_of(owner, asset_id), 600);
618 }
619
620 fn coin(amount: u64, tag: u8, owner: Address, asset_id: AssetId) -> FuelTxCoin {
621 FuelTxCoin {
622 amount,
623 asset_id,
624 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([tag; 32]), 0),
625 owner,
626 }
627 }
628
629 #[test]
630 fn guaranteed_extract_coins__spends_oldest_batch_first__even_when_newer_coin_is_smaller()
631 {
632 let owner = Address::from([1u8; 32]);
634 let asset_id = AssetId::from([2u8; 32]);
635 let old_large = coin(1_000, 1, owner, asset_id);
636 let new_small = coin(10, 2, owner, asset_id);
637
638 let mut manager = UtxoManager::new();
639 manager.load_from_coins(vec![old_large].into_iter()); manager.load_from_coins(vec![new_small].into_iter()); let extracted = manager
644 .guaranteed_extract_coins(owner, asset_id, 5, usize::MAX)
645 .unwrap();
646
647 assert_eq!(extracted.len(), 1);
649 assert_eq!(extracted[0].utxo_id, old_large.utxo_id);
650 assert_eq!(manager.balance_of(owner, asset_id), 10);
651 }
652
653 #[test]
654 fn guaranteed_extract_coins__prefers_old_small_coin_over_new_large_coin() {
655 let owner = Address::from([1u8; 32]);
658 let asset_id = AssetId::from([2u8; 32]);
659 let old_small = coin(100, 1, owner, asset_id);
660 let new_large = coin(900, 2, owner, asset_id);
661
662 let mut manager = UtxoManager::new();
663 manager.load_from_coins(vec![old_small].into_iter()); manager.load_from_coins(vec![new_large].into_iter()); let extracted = manager
668 .guaranteed_extract_coins(owner, asset_id, 100, usize::MAX)
669 .unwrap();
670
671 assert_eq!(extracted.len(), 1);
673 assert_eq!(extracted[0].utxo_id, old_small.utxo_id);
674 }
675
676 #[test]
677 fn load_from_coins__breaks_ties_within_a_batch_by_amount() {
678 let owner = Address::from([1u8; 32]);
680 let asset_id = AssetId::from([2u8; 32]);
681 let big = coin(300, 1, owner, asset_id);
682 let small = coin(100, 2, owner, asset_id);
683 let mid = coin(200, 3, owner, asset_id);
684
685 let mut manager = UtxoManager::new();
686 manager.load_from_coins(vec![big, small, mid].into_iter());
687
688 let extracted = manager
690 .guaranteed_extract_coins(owner, asset_id, 50, usize::MAX)
691 .unwrap();
692
693 assert_eq!(extracted.len(), 1);
695 assert_eq!(extracted[0].utxo_id, small.utxo_id);
696 }
697
698 #[test]
699 fn load_from_coins__re_adding_existing_coin_keeps_its_queue_position() {
700 let owner = Address::from([1u8; 32]);
702 let asset_id = AssetId::from([2u8; 32]);
703 let old = coin(100, 1, owner, asset_id);
704 let new = coin(100, 2, owner, asset_id);
705
706 let mut manager = UtxoManager::new();
707 manager.load_from_coins(vec![old].into_iter()); manager.load_from_coins(vec![new].into_iter()); manager.load_from_coins(vec![old].into_iter()); assert_eq!(manager.coin_count(), 2);
712
713 let extracted = manager
715 .guaranteed_extract_coins(owner, asset_id, 100, usize::MAX)
716 .unwrap();
717
718 assert_eq!(extracted.len(), 1);
720 assert_eq!(extracted[0].utxo_id, old.utxo_id);
721 }
722
723 #[test]
724 fn extract_largest_coins__still_takes_largest_first__across_batches() {
725 let owner = Address::from([1u8; 32]);
727 let asset_id = AssetId::from([2u8; 32]);
728 let old_small = coin(100, 1, owner, asset_id);
729 let new_large = coin(900, 2, owner, asset_id);
730
731 let mut manager = UtxoManager::new();
732 manager.load_from_coins(vec![old_small].into_iter()); manager.load_from_coins(vec![new_large].into_iter()); let extracted = manager.extract_largest_coins(owner, asset_id, 500);
737 assert_eq!(extracted.len(), 1);
738 assert_eq!(extracted[0].utxo_id, new_large.utxo_id);
739 }
740
741 #[test]
742 fn guaranteed_extract_coins__never_returns_more_than_max_coins() {
743 let owner = Address::from([1u8; 32]);
745 let asset_id = AssetId::from([2u8; 32]);
746 let coins: Vec<FuelTxCoin> =
747 (1..=5).map(|i| coin(100, i, owner, asset_id)).collect();
748
749 let mut manager = UtxoManager::new();
750 manager.load_from_coins(coins.into_iter());
751
752 let result = manager.guaranteed_extract_coins(owner, asset_id, 400, 2);
754
755 assert!(result.is_err());
757 assert_eq!(manager.coin_count(), 5);
759 }
760
761 #[test]
762 fn guaranteed_extract_coins__falls_back_to_largest_when_fifo_cannot_meet_target_within_cap()
763 {
764 let owner = Address::from([1u8; 32]);
766 let asset_id = AssetId::from([2u8; 32]);
767 let dust: Vec<FuelTxCoin> =
768 (1..=5).map(|i| coin(10, i, owner, asset_id)).collect();
769 let big = coin(1_000, 100, owner, asset_id);
770
771 let mut manager = UtxoManager::new();
772 manager.load_from_coins(dust.into_iter()); manager.load_from_coins(vec![big].into_iter()); let extracted = manager
778 .guaranteed_extract_coins(owner, asset_id, 500, 2)
779 .unwrap();
780
781 assert_eq!(extracted.len(), 1);
783 assert_eq!(extracted[0].utxo_id, big.utxo_id);
784 }
785
786 #[test]
787 fn guaranteed_extract_coins__errors_when_neither_strategy_can_cover_target() {
788 let owner = Address::from([1u8; 32]);
790 let asset_id = AssetId::from([2u8; 32]);
791 let small = coin(100, 1, owner, asset_id);
792
793 let mut manager = UtxoManager::new();
794 manager.load_from_coins(vec![small].into_iter());
795
796 let result = manager.guaranteed_extract_coins(owner, asset_id, 1_000, usize::MAX);
798 assert!(result.is_err());
799 assert_eq!(manager.coin_count(), 1);
800 }
801
802 #[test]
803 fn extract_largest_coins__returns_empty_when_no_coins() {
804 let owner = Address::from([1u8; 32]);
805 let asset_id = AssetId::from([2u8; 32]);
806 let mut manager = UtxoManager::new();
807 let extracted = manager.extract_largest_coins(owner, asset_id, 1000);
808 assert!(extracted.is_empty());
809 }
810
811 #[test]
812 fn extract_largest_coins__extracts_single_coin_when_only_one() {
813 let owner = Address::from([1u8; 32]);
814 let asset_id = AssetId::from([2u8; 32]);
815 let coin = FuelTxCoin {
816 amount: 1_000_000,
817 asset_id,
818 utxo_id: UtxoId::new(fuel_core_types::fuel_tx::TxId::from([1u8; 32]), 0),
819 owner,
820 };
821 let mut manager = UtxoManager::new();
822 manager.load_from_coins(vec![coin].into_iter());
823
824 let extracted = manager.extract_largest_coins(owner, asset_id, 500_000);
825 assert_eq!(extracted.len(), 1);
826 assert_eq!(extracted[0].amount, 1_000_000);
827 assert_eq!(manager.balance_of(owner, asset_id), 0);
828 }
829}