1use std::{collections::HashMap, io};
2
3use chrono::{DateTime, Duration, Utc};
4#[cfg(feature = "fuel-core")]
5use fuel_core::service::{Config, FuelService};
6use fuel_core_client::client::{
7 schema::{
8 balance::Balance, block::TimeParameters as FuelTimeParameters, contract::ContractBalance,
9 },
10 types::TransactionStatus,
11 FuelClient, PageDirection, PaginatedResult, PaginationRequest,
12};
13use fuel_tx::{AssetId, ConsensusParameters, Input, Receipt, TxPointer, UtxoId};
14use fuel_types::MessageId;
15use fuel_vm::state::ProgramState;
16use fuels_types::{
17 bech32::{Bech32Address, Bech32ContractId},
18 block::Block,
19 chain_info::ChainInfo,
20 coin::Coin,
21 constants::{BASE_ASSET_ID, DEFAULT_GAS_ESTIMATION_TOLERANCE, MAX_GAS_PER_TX},
22 errors::{error, Error, Result},
23 message::Message,
24 message_proof::MessageProof,
25 node_info::NodeInfo,
26 resource::Resource,
27 transaction::Transaction,
28 transaction_response::TransactionResponse,
29};
30use itertools::Itertools;
31use tai64::Tai64;
32use thiserror::Error;
33
34type ProviderResult<T> = std::result::Result<T, ProviderError>;
35
36#[derive(Debug)]
37pub struct TransactionCost {
38 pub min_gas_price: u64,
39 pub gas_price: u64,
40 pub gas_used: u64,
41 pub metered_bytes_size: u64,
42 pub total_fee: u64,
43}
44
45#[derive(Debug)]
46pub struct TimeParameters {
48 pub start_time: DateTime<Utc>,
50 pub block_time_interval: Duration,
52}
53impl From<TimeParameters> for FuelTimeParameters {
56 fn from(time: TimeParameters) -> Self {
57 Self {
58 start_time: Tai64::from_unix(time.start_time.timestamp()).0.into(),
59 block_time_interval: (time.block_time_interval.num_seconds() as u64).into(),
60 }
61 }
62}
63
64pub(crate) struct ResourceQueries {
65 utxos: Vec<String>,
66 messages: Vec<String>,
67 asset_id: String,
68 amount: u64,
69}
70
71impl ResourceQueries {
72 pub fn new(
73 utxo_ids: Vec<UtxoId>,
74 message_ids: Vec<MessageId>,
75 asset_id: AssetId,
76 amount: u64,
77 ) -> Self {
78 let utxos = utxo_ids
79 .iter()
80 .map(|utxo_id| format!("{utxo_id:#x}"))
81 .collect::<Vec<_>>();
82
83 let messages = message_ids
84 .iter()
85 .map(|msg_id| format!("{msg_id:#x}"))
86 .collect::<Vec<_>>();
87
88 Self {
89 utxos,
90 messages,
91 asset_id: format!("{asset_id:#x}"),
92 amount,
93 }
94 }
95
96 pub fn exclusion_query(&self) -> Option<(Vec<&str>, Vec<&str>)> {
97 if self.utxos.is_empty() && self.messages.is_empty() {
98 return None;
99 }
100
101 let utxos_as_str = self.utxos.iter().map(AsRef::as_ref).collect::<Vec<_>>();
102
103 let msg_ids_as_str = self.messages.iter().map(AsRef::as_ref).collect::<Vec<_>>();
104
105 Some((utxos_as_str, msg_ids_as_str))
106 }
107
108 pub fn spend_query(&self) -> Vec<(&str, u64, Option<u64>)> {
109 vec![(self.asset_id.as_str(), self.amount, None)]
110 }
111}
112
113pub struct ResourceFilter {
115 pub from: Bech32Address,
116 pub asset_id: AssetId,
117 pub amount: u64,
118 pub excluded_utxos: Vec<UtxoId>,
119 pub excluded_message_ids: Vec<MessageId>,
120}
121impl ResourceFilter {
124 pub fn owner(&self) -> String {
125 self.from.hash().to_string()
126 }
127
128 pub(crate) fn resource_queries(&self) -> ResourceQueries {
129 ResourceQueries::new(
130 self.excluded_utxos.clone(),
131 self.excluded_message_ids.clone(),
132 self.asset_id,
133 self.amount,
134 )
135 }
136}
137
138impl Default for ResourceFilter {
139 fn default() -> Self {
140 Self {
141 from: Default::default(),
142 asset_id: BASE_ASSET_ID,
143 amount: Default::default(),
144 excluded_utxos: Default::default(),
145 excluded_message_ids: Default::default(),
146 }
147 }
148}
149
150#[derive(Debug, Error)]
151pub enum ProviderError {
152 #[error(transparent)]
154 ClientRequestError(#[from] io::Error),
155}
156
157impl From<ProviderError> for Error {
158 fn from(e: ProviderError) -> Self {
159 Error::ProviderError(e.to_string())
160 }
161}
162
163#[derive(Debug, Clone)]
167pub struct Provider {
168 pub client: FuelClient,
169}
170
171impl Provider {
172 pub fn new(client: FuelClient) -> Self {
173 Self { client }
174 }
175
176 pub async fn send_transaction<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
178 let tolerance = 0.0;
179 let TransactionCost {
180 gas_used,
181 min_gas_price,
182 ..
183 } = self.estimate_transaction_cost(tx, Some(tolerance)).await?;
184
185 if gas_used > tx.gas_limit() {
186 return Err(error!(
187 ProviderError,
188 "gas_limit({}) is lower than the estimated gas_used({})",
189 tx.gas_limit(),
190 gas_used
191 ));
192 } else if min_gas_price > tx.gas_price() {
193 return Err(error!(
194 ProviderError,
195 "gas_price({}) is lower than the required min_gas_price({})",
196 tx.gas_price(),
197 min_gas_price
198 ));
199 }
200
201 let chain_info = self.chain_info().await?;
202 tx.check_without_signatures(
203 chain_info.latest_block.header.height,
204 &chain_info.consensus_parameters,
205 )?;
206
207 let (status, receipts) = self.submit_with_feedback(tx.clone()).await?;
208 Self::if_failure_generate_error(&status, &receipts)?;
209
210 Ok(receipts)
211 }
212
213 fn if_failure_generate_error(status: &TransactionStatus, receipts: &[Receipt]) -> Result<()> {
214 if let TransactionStatus::Failure {
215 reason,
216 program_state,
217 ..
218 } = status
219 {
220 let revert_id = program_state
221 .and_then(|state| match state {
222 ProgramState::Revert(revert_id) => Some(revert_id),
223 _ => None,
224 })
225 .expect("Transaction failed without a `revert_id`");
226
227 return Err(Error::RevertTransactionError {
228 reason: reason.to_string(),
229 revert_id,
230 receipts: receipts.to_owned(),
231 });
232 }
233
234 Ok(())
235 }
236
237 async fn submit_with_feedback(
238 &self,
239 tx: impl Transaction,
240 ) -> ProviderResult<(TransactionStatus, Vec<Receipt>)> {
241 let tx_id = tx.id().to_string();
242 let status = self.client.submit_and_await_commit(&tx.into()).await?;
243 let receipts = self.client.receipts(&tx_id).await?;
244
245 Ok((status, receipts))
246 }
247
248 #[cfg(feature = "fuel-core")]
249 pub async fn launch(config: Config) -> Result<FuelClient> {
251 let srv = FuelService::new_node(config).await.unwrap();
252 Ok(FuelClient::from(srv.bound_address))
253 }
254
255 pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
257 let client = FuelClient::new(url).map_err(|err| error!(InfrastructureError, "{err}"))?;
258 Ok(Provider::new(client))
259 }
260
261 pub async fn chain_info(&self) -> ProviderResult<ChainInfo> {
262 Ok(self.client.chain_info().await?.into())
263 }
264
265 pub async fn consensus_parameters(&self) -> ProviderResult<ConsensusParameters> {
266 Ok(self.client.chain_info().await?.consensus_parameters.into())
267 }
268
269 pub async fn node_info(&self) -> ProviderResult<NodeInfo> {
270 Ok(self.client.node_info().await?.into())
271 }
272
273 pub async fn dry_run<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
274 let receipts = self.client.dry_run(&tx.clone().into()).await?;
275
276 Ok(receipts)
277 }
278
279 pub async fn dry_run_no_validation<T: Transaction + Clone>(
280 &self,
281 tx: &T,
282 ) -> Result<Vec<Receipt>> {
283 let receipts = self
284 .client
285 .dry_run_opt(&tx.clone().into(), Some(false))
286 .await?;
287
288 Ok(receipts)
289 }
290
291 pub async fn get_coins(
293 &self,
294 from: &Bech32Address,
295 asset_id: AssetId,
296 ) -> ProviderResult<Vec<Coin>> {
297 let mut coins: Vec<Coin> = vec![];
298
299 let mut cursor = None;
300
301 loop {
302 let res = self
303 .client
304 .coins(
305 &from.hash().to_string(),
306 Some(&asset_id.to_string()),
307 PaginationRequest {
308 cursor: cursor.clone(),
309 results: 100,
310 direction: PageDirection::Forward,
311 },
312 )
313 .await?;
314
315 if res.results.is_empty() {
316 break;
317 }
318 coins.extend(res.results.into_iter().map(Into::into));
319 cursor = res.cursor;
320 }
321
322 Ok(coins)
323 }
324
325 pub async fn get_spendable_resources(
329 &self,
330 filter: ResourceFilter,
331 ) -> ProviderResult<Vec<Resource>> {
332 let queries = filter.resource_queries();
333
334 let res = self
335 .client
336 .resources_to_spend(
337 &filter.owner(),
338 queries.spend_query(),
339 queries.exclusion_query(),
340 )
341 .await?
342 .into_iter()
343 .flatten()
344 .map(|resource| {
345 resource
346 .try_into()
347 .map_err(ProviderError::ClientRequestError)
348 })
349 .try_collect()?;
350
351 Ok(res)
352 }
353
354 pub async fn get_asset_inputs(
359 &self,
360 filter: ResourceFilter,
361 witness_index: u8,
362 ) -> Result<Vec<Input>> {
363 let asset_id = filter.asset_id;
364 Ok(self
365 .get_spendable_resources(filter)
366 .await?
367 .iter()
368 .map(|resource| match resource {
369 Resource::Coin(coin) => self.create_coin_input(coin, asset_id, witness_index),
370 Resource::Message(message) => self.create_message_input(message, witness_index),
371 })
372 .collect::<Vec<Input>>())
373 }
374
375 fn create_coin_input(&self, coin: &Coin, asset_id: AssetId, witness_index: u8) -> Input {
376 Input::coin_signed(
377 coin.utxo_id,
378 coin.owner.clone().into(),
379 coin.amount,
380 asset_id,
381 TxPointer::default(),
382 witness_index,
383 0,
384 )
385 }
386
387 fn create_message_input(&self, message: &Message, witness_index: u8) -> Input {
388 Input::message_signed(
389 message.message_id(),
390 message.sender.clone().into(),
391 message.recipient.clone().into(),
392 message.amount,
393 message.nonce,
394 witness_index,
395 message.data.clone(),
396 )
397 }
398
399 pub async fn get_asset_balance(
403 &self,
404 address: &Bech32Address,
405 asset_id: AssetId,
406 ) -> ProviderResult<u64> {
407 self.client
408 .balance(&address.hash().to_string(), Some(&*asset_id.to_string()))
409 .await
410 .map_err(Into::into)
411 }
412
413 pub async fn get_contract_asset_balance(
415 &self,
416 contract_id: &Bech32ContractId,
417 asset_id: AssetId,
418 ) -> ProviderResult<u64> {
419 self.client
420 .contract_balance(&contract_id.hash().to_string(), Some(&asset_id.to_string()))
421 .await
422 .map_err(Into::into)
423 }
424
425 pub async fn get_balances(
429 &self,
430 address: &Bech32Address,
431 ) -> ProviderResult<HashMap<String, u64>> {
432 let pagination = PaginationRequest {
435 cursor: None,
436 results: 9999,
437 direction: PageDirection::Forward,
438 };
439 let balances_vec = self
440 .client
441 .balances(&address.hash().to_string(), pagination)
442 .await?
443 .results;
444 let balances = balances_vec
445 .into_iter()
446 .map(
447 |Balance {
448 owner: _,
449 amount,
450 asset_id,
451 }| (asset_id.to_string(), amount.try_into().unwrap()),
452 )
453 .collect();
454 Ok(balances)
455 }
456
457 pub async fn get_contract_balances(
459 &self,
460 contract_id: &Bech32ContractId,
461 ) -> ProviderResult<HashMap<String, u64>> {
462 let pagination = PaginationRequest {
465 cursor: None,
466 results: 9999,
467 direction: PageDirection::Forward,
468 };
469
470 let balances_vec = self
471 .client
472 .contract_balances(&contract_id.hash().to_string(), pagination)
473 .await?
474 .results;
475 let balances = balances_vec
476 .into_iter()
477 .map(
478 |ContractBalance {
479 contract: _,
480 amount,
481 asset_id,
482 }| (asset_id.to_string(), amount.try_into().unwrap()),
483 )
484 .collect();
485 Ok(balances)
486 }
487
488 pub async fn get_transaction_by_id(
489 &self,
490 tx_id: &str,
491 ) -> ProviderResult<Option<TransactionResponse>> {
492 Ok(self.client.transaction(tx_id).await?.map(Into::into))
493 }
494
495 pub async fn get_transactions(
497 &self,
498 request: PaginationRequest<String>,
499 ) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
500 let pr = self.client.transactions(request).await?;
501
502 Ok(PaginatedResult {
503 cursor: pr.cursor,
504 results: pr.results.into_iter().map(Into::into).collect(),
505 has_next_page: pr.has_next_page,
506 has_previous_page: pr.has_previous_page,
507 })
508 }
509
510 pub async fn get_transactions_by_owner(
512 &self,
513 owner: &Bech32Address,
514 request: PaginationRequest<String>,
515 ) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
516 let pr = self
517 .client
518 .transactions_by_owner(&owner.hash().to_string(), request)
519 .await?;
520
521 Ok(PaginatedResult {
522 cursor: pr.cursor,
523 results: pr.results.into_iter().map(Into::into).collect(),
524 has_next_page: pr.has_next_page,
525 has_previous_page: pr.has_previous_page,
526 })
527 }
528
529 pub async fn latest_block_height(&self) -> ProviderResult<u64> {
530 Ok(self.chain_info().await?.latest_block.header.height)
531 }
532
533 pub async fn latest_block_time(&self) -> ProviderResult<Option<DateTime<Utc>>> {
534 Ok(self.chain_info().await?.latest_block.header.time)
535 }
536
537 pub async fn produce_blocks(
538 &self,
539 amount: u64,
540 time: Option<TimeParameters>,
541 ) -> io::Result<u64> {
542 let fuel_time: Option<FuelTimeParameters> = time.map(|t| t.into());
543 self.client.produce_blocks(amount, fuel_time).await
544 }
545
546 pub async fn block(&self, block_id: &str) -> ProviderResult<Option<Block>> {
548 let block = self.client.block(block_id).await?.map(Into::into);
549 Ok(block)
550 }
551
552 pub async fn get_blocks(
554 &self,
555 request: PaginationRequest<String>,
556 ) -> ProviderResult<PaginatedResult<Block, String>> {
557 let pr = self.client.blocks(request).await?;
558
559 Ok(PaginatedResult {
560 cursor: pr.cursor,
561 results: pr.results.into_iter().map(Into::into).collect(),
562 has_next_page: pr.has_next_page,
563 has_previous_page: pr.has_previous_page,
564 })
565 }
566
567 pub async fn estimate_transaction_cost<T: Transaction + Clone>(
568 &self,
569 tx: &T,
570 tolerance: Option<f64>,
571 ) -> Result<TransactionCost> {
572 let NodeInfo { min_gas_price, .. } = self.node_info().await?;
573
574 let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
575 let dry_run_tx = Self::generate_dry_run_tx(tx);
576 let consensus_parameters = self.chain_info().await?.consensus_parameters;
577 let gas_used = self
578 .get_gas_used_with_tolerance(&dry_run_tx, tolerance)
579 .await?;
580 let gas_price = std::cmp::max(tx.gas_price(), min_gas_price);
581
582 dry_run_tx
584 .with_gas_price(gas_price)
585 .with_gas_limit(gas_used);
586
587 let transaction_fee = tx
588 .fee_checked_from_tx(&consensus_parameters)
589 .expect("Error calculating TransactionFee");
590
591 Ok(TransactionCost {
592 min_gas_price,
593 gas_price,
594 gas_used,
595 metered_bytes_size: tx.metered_bytes_size() as u64,
596 total_fee: transaction_fee.total(),
597 })
598 }
599
600 fn generate_dry_run_tx<T: Transaction + Clone>(tx: &T) -> T {
602 tx.clone().with_gas_limit(MAX_GAS_PER_TX).with_gas_price(0)
604 }
605
606 async fn get_gas_used_with_tolerance<T: Transaction + Clone>(
608 &self,
609 tx: &T,
610 tolerance: f64,
611 ) -> Result<u64> {
612 let gas_used = self.get_gas_used(&self.dry_run_no_validation(tx).await?);
613 Ok((gas_used as f64 * (1.0 + tolerance)) as u64)
614 }
615
616 fn get_gas_used(&self, receipts: &[Receipt]) -> u64 {
617 receipts
618 .iter()
619 .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
620 .map(|script_result| {
621 script_result
622 .gas_used()
623 .expect("could not retrieve gas used from ScriptResult")
624 })
625 .unwrap_or(0)
626 }
627
628 pub async fn get_messages(&self, from: &Bech32Address) -> ProviderResult<Vec<Message>> {
629 let pagination = PaginationRequest {
630 cursor: None,
631 results: 100,
632 direction: PageDirection::Forward,
633 };
634 let res = self
635 .client
636 .messages(Some(&from.hash().to_string()), pagination)
637 .await?
638 .results
639 .into_iter()
640 .map(Into::into)
641 .collect();
642 Ok(res)
643 }
644
645 pub async fn get_message_proof(
646 &self,
647 tx_id: &str,
648 message_id: &str,
649 ) -> ProviderResult<Option<MessageProof>> {
650 let proof = self
651 .client
652 .message_proof(tx_id, message_id)
653 .await?
654 .map(Into::into);
655 Ok(proof)
656 }
657}