fuels_accounts/
account.rs

1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest};
5use fuel_tx::{Output, TxId, TxPointer, UtxoId};
6use fuels_core::types::{
7    Address, AssetId, Bytes32, ContractId, Nonce,
8    coin::Coin,
9    coin_type::CoinType,
10    coin_type_id::CoinTypeId,
11    errors::{Context, Result},
12    input::Input,
13    message::Message,
14    transaction::{Transaction, TxPolicies},
15    transaction_builders::{BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder},
16    transaction_response::TransactionResponse,
17    tx_response::TxResponse,
18    tx_status::Success,
19};
20
21use crate::{
22    accounts_utils::{
23        add_base_change_if_needed, available_base_assets_and_amount, calculate_missing_base_amount,
24        extract_message_nonce, split_into_utxo_ids_and_nonces,
25    },
26    provider::{Provider, ResourceFilter},
27};
28
29#[derive(Clone, Debug)]
30pub struct WithdrawToBaseResponse {
31    pub tx_status: Success,
32    pub tx_id: TxId,
33    pub nonce: Nonce,
34}
35
36#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
37pub trait ViewOnlyAccount: Send + Sync {
38    fn address(&self) -> Address;
39
40    fn try_provider(&self) -> Result<&Provider>;
41
42    async fn get_transactions(
43        &self,
44        request: PaginationRequest<String>,
45    ) -> Result<PaginatedResult<TransactionResponse, String>> {
46        Ok(self
47            .try_provider()?
48            .get_transactions_by_owner(&self.address(), request)
49            .await?)
50    }
51
52    /// Gets all unspent coins of asset `asset_id` owned by the account.
53    async fn get_coins(&self, asset_id: AssetId) -> Result<Vec<Coin>> {
54        Ok(self
55            .try_provider()?
56            .get_coins(&self.address(), asset_id)
57            .await?)
58    }
59
60    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
61    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
62    /// of the UTXOs.
63    async fn get_asset_balance(&self, asset_id: &AssetId) -> Result<u128> {
64        self.try_provider()?
65            .get_asset_balance(&self.address(), asset_id)
66            .await
67    }
68
69    /// Gets all unspent messages owned by the account.
70    async fn get_messages(&self) -> Result<Vec<Message>> {
71        Ok(self.try_provider()?.get_messages(&self.address()).await?)
72    }
73
74    /// Get all the spendable balances of all assets for the account. This is different from getting
75    /// the coins because we are only returning the sum of UTXOs coins amount and not the UTXOs
76    /// coins themselves.
77    async fn get_balances(&self) -> Result<HashMap<String, u128>> {
78        self.try_provider()?.get_balances(&self.address()).await
79    }
80
81    /// Get some spendable resources (coins and messages) of asset `asset_id` owned by the account
82    /// that add up at least to amount `amount`. The returned coins (UTXOs) are actual coins that
83    /// can be spent. The number of UXTOs is optimized to prevent dust accumulation.
84    async fn get_spendable_resources(
85        &self,
86        asset_id: AssetId,
87        amount: u128,
88        excluded_coins: Option<Vec<CoinTypeId>>,
89    ) -> Result<Vec<CoinType>> {
90        let (excluded_utxos, excluded_message_nonces) =
91            split_into_utxo_ids_and_nonces(excluded_coins);
92
93        let filter = ResourceFilter {
94            from: self.address(),
95            asset_id: Some(asset_id),
96            amount,
97            excluded_utxos,
98            excluded_message_nonces,
99        };
100
101        self.try_provider()?.get_spendable_resources(filter).await
102    }
103
104    /// Returns a vector containing the output coin and change output given an asset and amount
105    fn get_asset_outputs_for_amount(
106        &self,
107        to: Address,
108        asset_id: AssetId,
109        amount: u64,
110    ) -> Vec<Output> {
111        vec![
112            Output::coin(to, amount, asset_id),
113            // Note that the change will be computed by the node.
114            // Here we only have to tell the node who will own the change and its asset ID.
115            Output::change(self.address(), 0, asset_id),
116        ]
117    }
118
119    /// Returns a vector consisting of `Input::Coin`s and `Input::Message`s for the given
120    /// asset ID and amount.
121    async fn get_asset_inputs_for_amount(
122        &self,
123        asset_id: AssetId,
124        amount: u128,
125        excluded_coins: Option<Vec<CoinTypeId>>,
126    ) -> Result<Vec<Input>>;
127
128    /// Add base asset inputs to the transaction to cover the estimated fee
129    /// and add a change output for the base asset if needed.
130    /// Requires contract inputs to be at the start of the transactions inputs vec
131    /// so that their indexes are retained
132    async fn adjust_for_fee<Tb: TransactionBuilder + Sync>(
133        &self,
134        tb: &mut Tb,
135        used_base_amount: u128,
136    ) -> Result<()> {
137        let provider = self.try_provider()?;
138        let consensus_parameters = provider.consensus_parameters().await?;
139        let base_asset_id = consensus_parameters.base_asset_id();
140        let (base_assets, base_amount) = available_base_assets_and_amount(tb, base_asset_id);
141        let missing_base_amount =
142            calculate_missing_base_amount(tb, base_amount, used_base_amount, provider).await?;
143
144        if missing_base_amount > 0 {
145            let new_base_inputs = self
146                .get_asset_inputs_for_amount(
147                    *consensus_parameters.base_asset_id(),
148                    missing_base_amount,
149                    Some(base_assets),
150                )
151                .await
152                .with_context(|| {
153                    format!("failed to get base asset ({base_asset_id}) inputs with amount: `{missing_base_amount}`")
154                })?;
155
156            tb.inputs_mut().extend(new_base_inputs);
157        };
158
159        add_base_change_if_needed(tb, self.address(), *consensus_parameters.base_asset_id());
160
161        Ok(())
162    }
163}
164
165#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
166pub trait Account: ViewOnlyAccount {
167    // Add signatures to the builder if the underlying account is a wallet
168    fn add_witnesses<Tb: TransactionBuilder>(&self, _tb: &mut Tb) -> Result<()> {
169        Ok(())
170    }
171
172    /// Transfer funds from this account to another `Address`.
173    /// Fails if amount for asset ID is larger than address's spendable coins.
174    /// Returns the transaction ID that was sent and the list of receipts.
175    async fn transfer(
176        &self,
177        to: Address,
178        amount: u64,
179        asset_id: AssetId,
180        tx_policies: TxPolicies,
181    ) -> Result<TxResponse> {
182        let provider = self.try_provider()?;
183
184        let inputs = self
185            .get_asset_inputs_for_amount(asset_id, amount.into(), None)
186            .await?;
187        let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
188
189        let mut tx_builder =
190            ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_policies);
191
192        self.add_witnesses(&mut tx_builder)?;
193
194        let consensus_parameters = provider.consensus_parameters().await?;
195        let used_base_amount = if asset_id == *consensus_parameters.base_asset_id() {
196            amount.into()
197        } else {
198            0
199        };
200        self.adjust_for_fee(&mut tx_builder, used_base_amount)
201            .await
202            .context("failed to adjust inputs to cover for missing base asset")?;
203
204        let tx = tx_builder.build(provider).await?;
205        let tx_id = tx.id(consensus_parameters.chain_id());
206
207        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
208
209        Ok(TxResponse {
210            tx_status: tx_status.take_success_checked(None)?,
211            tx_id,
212        })
213    }
214
215    /// Unconditionally transfers `balance` of type `asset_id` to
216    /// the contract at `to`.
217    /// Fails if balance for `asset_id` is larger than this account's spendable balance.
218    /// Returns the corresponding transaction ID and the list of receipts.
219    ///
220    /// CAUTION !!!
221    ///
222    /// This will transfer coins to a contract, possibly leading
223    /// to the PERMANENT LOSS OF COINS if not used with care.
224    async fn force_transfer_to_contract(
225        &self,
226        to: ContractId,
227        balance: u64,
228        asset_id: AssetId,
229        tx_policies: TxPolicies,
230    ) -> Result<TxResponse> {
231        let provider = self.try_provider()?;
232
233        let zeroes = Bytes32::zeroed();
234
235        let mut inputs = vec![Input::contract(
236            UtxoId::new(zeroes, 0),
237            zeroes,
238            zeroes,
239            TxPointer::default(),
240            to,
241        )];
242
243        inputs.extend(
244            self.get_asset_inputs_for_amount(asset_id, balance.into(), None)
245                .await?,
246        );
247
248        let outputs = vec![
249            Output::contract(0, zeroes, zeroes),
250            Output::change(self.address(), 0, asset_id),
251        ];
252
253        // Build transaction and sign it
254        let mut tb = ScriptTransactionBuilder::prepare_contract_transfer(
255            to,
256            balance,
257            asset_id,
258            inputs,
259            outputs,
260            tx_policies,
261        );
262
263        self.add_witnesses(&mut tb)?;
264        self.adjust_for_fee(&mut tb, balance.into())
265            .await
266            .context("failed to adjust inputs to cover for missing base asset")?;
267
268        let tx = tb.build(provider).await?;
269
270        let consensus_parameters = provider.consensus_parameters().await?;
271        let tx_id = tx.id(consensus_parameters.chain_id());
272
273        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
274
275        Ok(TxResponse {
276            tx_status: tx_status.take_success_checked(None)?,
277            tx_id,
278        })
279    }
280
281    /// Withdraws an amount of the base asset to
282    /// an address on the base chain.
283    /// Returns the transaction ID, message ID and the list of receipts.
284    async fn withdraw_to_base_layer(
285        &self,
286        to: Address,
287        amount: u64,
288        tx_policies: TxPolicies,
289    ) -> Result<WithdrawToBaseResponse> {
290        let provider = self.try_provider()?;
291        let consensus_parameters = provider.consensus_parameters().await?;
292
293        let inputs = self
294            .get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), amount.into(), None)
295            .await?;
296
297        let mut tb = ScriptTransactionBuilder::prepare_message_to_output(
298            to,
299            amount,
300            inputs,
301            tx_policies,
302            *consensus_parameters.base_asset_id(),
303        );
304
305        self.add_witnesses(&mut tb)?;
306        self.adjust_for_fee(&mut tb, amount.into())
307            .await
308            .context("failed to adjust inputs to cover for missing base asset")?;
309
310        let tx = tb.build(provider).await?;
311        let tx_id = tx.id(consensus_parameters.chain_id());
312
313        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
314        let success = tx_status.take_success_checked(None)?;
315
316        let nonce = extract_message_nonce(&success.receipts)
317            .expect("MessageId could not be retrieved from tx receipts.");
318
319        Ok(WithdrawToBaseResponse {
320            tx_status: success,
321            tx_id,
322            nonce,
323        })
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use std::str::FromStr;
330
331    use fuel_crypto::{Message, SecretKey, Signature};
332    use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction};
333    use fuels_core::{
334        traits::Signer,
335        types::{DryRun, DryRunner, transaction::Transaction},
336    };
337
338    use super::*;
339    use crate::signers::private_key::PrivateKeySigner;
340
341    #[derive(Default)]
342    struct MockDryRunner {
343        c_param: ConsensusParameters,
344    }
345
346    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
347    impl DryRunner for MockDryRunner {
348        async fn dry_run(&self, _: FuelTransaction) -> Result<DryRun> {
349            Ok(DryRun {
350                succeeded: true,
351                script_gas: 0,
352                variable_outputs: 0,
353            })
354        }
355
356        async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
357            Ok(self.c_param.clone())
358        }
359
360        async fn estimate_gas_price(&self, _block_header: u32) -> Result<u64> {
361            Ok(0)
362        }
363
364        async fn estimate_predicates(
365            &self,
366            _: &FuelTransaction,
367            _: Option<u32>,
368        ) -> Result<FuelTransaction> {
369            unimplemented!()
370        }
371    }
372
373    #[tokio::test]
374    async fn sign_tx_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
375        // ANCHOR: sign_tb
376        let secret = SecretKey::from_str(
377            "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
378        )?;
379        let signer = PrivateKeySigner::new(secret);
380
381        // Set up a transaction
382        let mut tb = {
383            let input_coin = Input::ResourceSigned {
384                resource: CoinType::Coin(Coin {
385                    amount: 10000000,
386                    owner: signer.address(),
387                    ..Default::default()
388                }),
389            };
390
391            let output_coin = Output::coin(
392                Address::from_str(
393                    "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
394                )?,
395                1,
396                Default::default(),
397            );
398            let change = Output::change(signer.address(), 0, Default::default());
399
400            ScriptTransactionBuilder::prepare_transfer(
401                vec![input_coin],
402                vec![output_coin, change],
403                Default::default(),
404            )
405        };
406
407        // Add `Signer` to the transaction builder
408        tb.add_signer(signer.clone())?;
409        // ANCHOR_END: sign_tb
410
411        let tx = tb.build(MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes
412
413        // Extract the signature from the tx witnesses
414        let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
415        let tx_signature = Signature::from_bytes(bytes);
416
417        // Sign the transaction manually
418        let message = Message::from_bytes(*tx.id(0.into()));
419        let signature = signer.sign(message).await?;
420
421        // Check if the signatures are the same
422        assert_eq!(signature, tx_signature);
423
424        // Check if the signature is what we expect it to be
425        assert_eq!(
426            signature,
427            Signature::from_str(
428                "faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f"
429            )?
430        );
431
432        // Recover the address that signed the transaction
433        let recovered_address = signature.recover(&message)?;
434
435        assert_eq!(*signer.address(), *recovered_address.hash());
436
437        // Verify signature
438        signature.verify(&recovered_address, &message)?;
439
440        Ok(())
441    }
442}