1use std::{
2 borrow::Cow,
3 collections::HashSet,
4};
5
6use crate::{
7 coins_query::{
8 CoinsQueryError,
9 SpendQuery,
10 random_improve,
11 select_coins_to_spend,
12 },
13 database::database_description::IndexationKind,
14 fuel_core_graphql_api::{
15 IntoApiResult,
16 query_costs,
17 storage::coins::CoinsToSpendIndexKey,
18 },
19 graphql_api::{
20 api_service::ChainInfoProvider,
21 database::ReadView,
22 },
23 query::asset_query::{
24 AssetSpendTarget,
25 Exclude,
26 },
27 schema::{
28 ReadViewProvider,
29 scalars::{
30 Address,
31 AssetId,
32 Nonce,
33 U16,
34 U32,
35 U64,
36 U128,
37 UtxoId,
38 },
39 },
40};
41use async_graphql::{
42 Context,
43 connection::{
44 Connection,
45 EmptyFields,
46 },
47};
48use fuel_core_types::{
49 entities::coins::{
50 self,
51 CoinId,
52 coin::Coin as CoinModel,
53 message_coin::{
54 self,
55 MessageCoin as MessageCoinModel,
56 },
57 },
58 fuel_tx::{
59 self,
60 ConsensusParameters,
61 },
62};
63use itertools::Itertools;
64use tokio_stream::StreamExt;
65
66pub struct Coin(pub(crate) CoinModel);
67
68#[async_graphql::Object]
69impl Coin {
70 async fn utxo_id(&self) -> UtxoId {
71 self.0.utxo_id.into()
72 }
73
74 async fn owner(&self) -> Address {
75 self.0.owner.into()
76 }
77
78 async fn amount(&self) -> U64 {
79 self.0.amount.into()
80 }
81
82 async fn asset_id(&self) -> AssetId {
83 self.0.asset_id.into()
84 }
85
86 async fn block_created(&self) -> U32 {
88 u32::from(self.0.tx_pointer.block_height()).into()
89 }
90
91 async fn tx_created_idx(&self) -> U16 {
93 self.0.tx_pointer.tx_index().into()
94 }
95}
96
97impl From<CoinModel> for Coin {
98 fn from(value: CoinModel) -> Self {
99 Coin(value)
100 }
101}
102
103pub struct MessageCoin(pub(crate) MessageCoinModel);
104
105#[async_graphql::Object]
106impl MessageCoin {
107 async fn sender(&self) -> Address {
108 self.0.sender.into()
109 }
110
111 async fn recipient(&self) -> Address {
112 self.0.recipient.into()
113 }
114
115 async fn nonce(&self) -> Nonce {
116 self.0.nonce.into()
117 }
118
119 async fn amount(&self) -> U64 {
120 self.0.amount.into()
121 }
122
123 #[graphql(complexity = "query_costs().storage_read")]
124 async fn asset_id(&self, ctx: &Context<'_>) -> AssetId {
125 let params = ctx
126 .data_unchecked::<ChainInfoProvider>()
127 .current_consensus_params();
128
129 let base_asset_id = *params.base_asset_id();
130 base_asset_id.into()
131 }
132
133 async fn da_height(&self) -> U64 {
134 self.0.da_height.0.into()
135 }
136}
137
138impl From<MessageCoinModel> for MessageCoin {
139 fn from(value: MessageCoinModel) -> Self {
140 MessageCoin(value)
141 }
142}
143
144#[derive(async_graphql::Union)]
146pub enum CoinType {
147 Coin(Coin),
149 MessageCoin(MessageCoin),
151}
152
153impl CoinType {
154 pub fn amount(&self) -> u64 {
155 match self {
156 CoinType::Coin(coin) => coin.0.amount,
157 CoinType::MessageCoin(coin) => coin.0.amount,
158 }
159 }
160}
161
162impl From<coins::CoinType> for CoinType {
163 fn from(value: coins::CoinType) -> Self {
164 match value {
165 coins::CoinType::Coin(coin) => CoinType::Coin(coin.into()),
166 coins::CoinType::MessageCoin(coin) => CoinType::MessageCoin(coin.into()),
167 }
168 }
169}
170
171#[derive(async_graphql::InputObject)]
172struct CoinFilterInput {
173 owner: Address,
175 asset_id: Option<AssetId>,
177}
178
179#[derive(async_graphql::InputObject)]
180pub struct SpendQueryElementInput {
181 pub asset_id: AssetId,
183 pub amount: U128,
185 pub max: Option<U16>,
187 pub allow_partial: Option<bool>,
189}
190
191#[derive(async_graphql::InputObject)]
192pub struct ExcludeInput {
193 pub utxos: Vec<UtxoId>,
195 pub messages: Vec<Nonce>,
197}
198
199impl From<Option<ExcludeInput>> for Exclude {
200 fn from(value: Option<ExcludeInput>) -> Self {
201 let excluded_ids: Option<Vec<_>> = value.map(|exclude| {
202 let utxos = exclude
203 .utxos
204 .into_iter()
205 .map(|utxo| coins::CoinId::Utxo(utxo.into()));
206 let messages = exclude
207 .messages
208 .into_iter()
209 .map(|message| coins::CoinId::Message(message.into()));
210 utxos.chain(messages).collect()
211 });
212
213 Exclude::new(excluded_ids.unwrap_or_default())
214 }
215}
216
217#[derive(Default)]
218pub struct CoinQuery;
219
220#[async_graphql::Object]
221impl CoinQuery {
222 #[graphql(complexity = "query_costs().storage_read + child_complexity")]
224 async fn coin(
225 &self,
226 ctx: &Context<'_>,
227 #[graphql(desc = "The ID of the coin")] utxo_id: UtxoId,
228 ) -> async_graphql::Result<Option<Coin>> {
229 let query = ctx.read_view()?;
230 query.coin(utxo_id.0).into_api_result()
231 }
232
233 #[graphql(complexity = "{\
235 query_costs().storage_iterator\
236 + (query_costs().storage_read + first.unwrap_or_default() as usize) * child_complexity \
237 + (query_costs().storage_read + last.unwrap_or_default() as usize) * child_complexity\
238 }")]
239 async fn coins(
240 &self,
241 ctx: &Context<'_>,
242 filter: CoinFilterInput,
243 first: Option<i32>,
244 after: Option<String>,
245 last: Option<i32>,
246 before: Option<String>,
247 ) -> async_graphql::Result<Connection<UtxoId, Coin, EmptyFields, EmptyFields>> {
248 let query = ctx.read_view()?;
249 let owner: fuel_tx::Address = filter.owner.into();
250 crate::schema::query_pagination(after, before, first, last, |start, direction| {
251 let coins = query
252 .owned_coins(&owner, (*start).map(Into::into), direction)
253 .filter_map(|result| {
254 if let (Ok(coin), Some(filter_asset_id)) = (&result, &filter.asset_id)
255 && coin.asset_id != filter_asset_id.0
256 {
257 return None;
258 }
259
260 Some(result)
261 })
262 .map(|res| res.map(|coin| (coin.utxo_id.into(), coin.into())));
263
264 Ok(coins)
265 })
266 .await
267 }
268
269 #[graphql(complexity = "query_costs().coins_to_spend")]
280 async fn coins_to_spend(
281 &self,
282 ctx: &Context<'_>,
283 #[graphql(desc = "The `Address` of the coins owner.")] owner: Address,
284 #[graphql(desc = "\
285 The list of requested assets` coins with asset ids, `target` amount the user wants \
286 to reach, and the `max` number of coins in the selection. Several entries with the \
287 same asset id are not allowed. The result can't contain more coins than `max_inputs`.")]
288 mut query_per_asset: Vec<SpendQueryElementInput>,
289 #[graphql(desc = "The excluded coins from the selection.")] excluded_ids: Option<
290 ExcludeInput,
291 >,
292 ) -> async_graphql::Result<Vec<Vec<CoinType>>> {
293 let params = ctx
294 .data_unchecked::<ChainInfoProvider>()
295 .current_consensus_params();
296 let max_input = params.tx_params().max_inputs();
297
298 let excluded_id_count = excluded_ids.as_ref().map_or(0, |exclude| {
299 exclude.utxos.len().saturating_add(exclude.messages.len())
300 });
301 if excluded_id_count > max_input as usize {
302 return Err(CoinsQueryError::TooManyExcludedId {
303 provided: excluded_id_count,
304 allowed: max_input,
305 }
306 .into());
307 }
308
309 let exclude: Exclude = excluded_ids.into();
310
311 let mut duplicate_checker = HashSet::with_capacity(query_per_asset.len());
312 for query in &query_per_asset {
313 let asset_id: fuel_tx::AssetId = query.asset_id.into();
314 if !duplicate_checker.insert(asset_id) {
315 return Err(CoinsQueryError::DuplicateAssets(asset_id).into());
316 }
317 }
318
319 let owner: fuel_tx::Address = owner.0;
320
321 query_per_asset.truncate(max_input as usize);
328
329 let read_view = ctx.read_view()?;
330 let result = read_view
331 .coins_to_spend(owner, &query_per_asset, &exclude, ¶ms, max_input)
332 .await?;
333
334 Ok(result)
335 }
336}
337
338impl ReadView {
339 pub async fn coins_to_spend(
340 &self,
341 owner: fuel_tx::Address,
342 query_per_asset: &[SpendQueryElementInput],
343 excluded: &Exclude,
344 params: &ConsensusParameters,
345 max_input: u16,
346 ) -> Result<Vec<Vec<CoinType>>, CoinsQueryError> {
347 let indexation_available = self
348 .indexation_flags
349 .contains(&IndexationKind::CoinsToSpend);
350 if indexation_available {
351 coins_to_spend_with_cache(owner, query_per_asset, excluded, max_input, self)
352 .await
353 } else {
354 let base_asset_id = params.base_asset_id();
355 coins_to_spend_without_cache(
356 owner,
357 query_per_asset,
358 excluded,
359 max_input,
360 base_asset_id,
361 self,
362 )
363 .await
364 }
365 }
366}
367
368async fn coins_to_spend_without_cache(
369 owner: fuel_tx::Address,
370 query_per_asset: &[SpendQueryElementInput],
371 exclude: &Exclude,
372 max_input: u16,
373 base_asset_id: &fuel_tx::AssetId,
374 db: &ReadView,
375) -> Result<Vec<Vec<CoinType>>, CoinsQueryError> {
376 let query_per_asset = query_per_asset
377 .iter()
378 .map(|e| {
379 AssetSpendTarget::new(
380 e.asset_id.0,
381 e.amount.0,
382 e.max.map(|max| max.0).unwrap_or(max_input).min(max_input),
383 e.allow_partial.unwrap_or(false),
384 )
385 })
386 .collect_vec();
387
388 let spend_query = SpendQuery::new(
389 owner,
390 &query_per_asset,
391 Cow::Borrowed(exclude),
392 *base_asset_id,
393 )?;
394
395 let all_coins = random_improve(db, &spend_query)
396 .await?
397 .into_iter()
398 .map(|coins| {
399 coins
400 .into_iter()
401 .map(|coin| match coin {
402 coins::CoinType::Coin(coin) => CoinType::Coin(coin.into()),
403 coins::CoinType::MessageCoin(coin) => {
404 CoinType::MessageCoin(coin.into())
405 }
406 })
407 .collect_vec()
408 })
409 .collect();
410
411 Ok(all_coins)
412}
413
414async fn coins_to_spend_with_cache(
415 owner: fuel_tx::Address,
416 query_per_asset: &[SpendQueryElementInput],
417 excluded: &Exclude,
418 max_input: u16,
419 db: &ReadView,
420) -> Result<Vec<Vec<CoinType>>, CoinsQueryError> {
421 let mut all_coins = Vec::with_capacity(query_per_asset.len());
422
423 for element in query_per_asset {
424 let asset_id = element.asset_id.0;
425 let target = element.amount.0;
426 let max = element
427 .max
428 .map(|max| max.0)
429 .unwrap_or(max_input)
430 .min(max_input);
431 let allow_partial = element.allow_partial.unwrap_or(false);
432
433 let asset_target = AssetSpendTarget::new(asset_id, target, max, allow_partial);
434
435 let selected_coins = select_coins_to_spend(
436 db.off_chain.coins_to_spend_index(&owner, &asset_id),
437 asset_target,
438 excluded,
439 db.batch_size,
440 owner,
441 )
442 .await?;
443
444 let mut coins_per_asset = Vec::with_capacity(selected_coins.len());
445 for coin_or_message_id in into_coin_id(&selected_coins) {
446 let coin_type = match coin_or_message_id {
447 coins::CoinId::Utxo(utxo_id) => {
448 db.coin(utxo_id).map(|coin| CoinType::Coin(coin.into()))?
449 }
450 coins::CoinId::Message(nonce) => {
451 let message = db.message(&nonce)?;
452 let message_coin: message_coin::MessageCoin = message.try_into()?;
453 CoinType::MessageCoin(message_coin.into())
454 }
455 };
456
457 coins_per_asset.push(coin_type);
458 }
459
460 all_coins.push(coins_per_asset);
461 }
462 Ok(all_coins)
463}
464
465fn into_coin_id(selected: &[CoinsToSpendIndexKey]) -> Vec<CoinId> {
466 let mut coins = Vec::with_capacity(selected.len());
467 for coin in selected {
468 let coin = match coin {
469 CoinsToSpendIndexKey::Coin { utxo_id, .. } => CoinId::Utxo(*utxo_id),
470 CoinsToSpendIndexKey::Message { nonce, .. } => CoinId::Message(*nonce),
471 };
472 coins.push(coin);
473 }
474 coins
475}