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        let consensus_parameters = provider.consensus_parameters().await?;
264        let used_base_amount = if asset_id == *consensus_parameters.base_asset_id() {
265            balance
266        } else {
267            0
268        };
269
270        self.add_witnesses(&mut tb)?;
271        self.adjust_for_fee(&mut tb, used_base_amount.into())
272            .await
273            .context("failed to adjust inputs to cover for missing base asset")?;
274
275        let tx = tb.build(provider).await?;
276
277        let consensus_parameters = provider.consensus_parameters().await?;
278        let tx_id = tx.id(consensus_parameters.chain_id());
279
280        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
281
282        Ok(TxResponse {
283            tx_status: tx_status.take_success_checked(None)?,
284            tx_id,
285        })
286    }
287
288    /// Withdraws an amount of the base asset to
289    /// an address on the base chain.
290    /// Returns the transaction ID, message ID and the list of receipts.
291    async fn withdraw_to_base_layer(
292        &self,
293        to: Address,
294        amount: u64,
295        tx_policies: TxPolicies,
296    ) -> Result<WithdrawToBaseResponse> {
297        let provider = self.try_provider()?;
298        let consensus_parameters = provider.consensus_parameters().await?;
299
300        let inputs = self
301            .get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), amount.into(), None)
302            .await?;
303
304        let mut tb = ScriptTransactionBuilder::prepare_message_to_output(
305            to,
306            amount,
307            inputs,
308            tx_policies,
309            *consensus_parameters.base_asset_id(),
310        );
311
312        self.add_witnesses(&mut tb)?;
313        self.adjust_for_fee(&mut tb, amount.into())
314            .await
315            .context("failed to adjust inputs to cover for missing base asset")?;
316
317        let tx = tb.build(provider).await?;
318        let tx_id = tx.id(consensus_parameters.chain_id());
319
320        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
321        let success = tx_status.take_success_checked(None)?;
322
323        let nonce = extract_message_nonce(&success.receipts)
324            .expect("MessageId could not be retrieved from tx receipts.");
325
326        Ok(WithdrawToBaseResponse {
327            tx_status: success,
328            tx_id,
329            nonce,
330        })
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use std::str::FromStr;
337
338    use fuel_crypto::{Message, SecretKey, Signature};
339    use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction};
340    use fuels_core::{
341        traits::Signer,
342        types::{DryRun, DryRunner, transaction::Transaction},
343    };
344
345    use super::*;
346    use crate::signers::private_key::PrivateKeySigner;
347
348    #[derive(Default)]
349    struct MockDryRunner {
350        c_param: ConsensusParameters,
351    }
352
353    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
354    impl DryRunner for MockDryRunner {
355        async fn dry_run(&self, _: FuelTransaction) -> Result<DryRun> {
356            Ok(DryRun {
357                succeeded: true,
358                script_gas: 0,
359                variable_outputs: 0,
360            })
361        }
362
363        async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
364            Ok(self.c_param.clone())
365        }
366
367        async fn estimate_gas_price(&self, _block_header: u32) -> Result<u64> {
368            Ok(0)
369        }
370
371        async fn estimate_predicates(
372            &self,
373            _: &FuelTransaction,
374            _: Option<u32>,
375        ) -> Result<FuelTransaction> {
376            unimplemented!()
377        }
378    }
379
380    #[tokio::test]
381    async fn sign_tx_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
382        // ANCHOR: sign_tb
383        let secret = SecretKey::from_str(
384            "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
385        )?;
386        let signer = PrivateKeySigner::new(secret);
387
388        // Set up a transaction
389        let mut tb = {
390            let input_coin = Input::ResourceSigned {
391                resource: CoinType::Coin(Coin {
392                    amount: 10000000,
393                    owner: signer.address(),
394                    ..Default::default()
395                }),
396            };
397
398            let output_coin = Output::coin(
399                Address::from_str(
400                    "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
401                )?,
402                1,
403                Default::default(),
404            );
405            let change = Output::change(signer.address(), 0, Default::default());
406
407            ScriptTransactionBuilder::prepare_transfer(
408                vec![input_coin],
409                vec![output_coin, change],
410                Default::default(),
411            )
412        };
413
414        // Add `Signer` to the transaction builder
415        tb.add_signer(signer.clone())?;
416        // ANCHOR_END: sign_tb
417
418        let tx = tb.build(MockDryRunner::default()).await?; // Resolve signatures and add corresponding witness indexes
419
420        // Extract the signature from the tx witnesses
421        let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
422        let tx_signature = Signature::from_bytes(bytes);
423
424        // Sign the transaction manually
425        let message = Message::from_bytes(*tx.id(0.into()));
426        let signature = signer.sign(message).await?;
427
428        // Check if the signatures are the same
429        assert_eq!(signature, tx_signature);
430
431        // Check if the signature is what we expect it to be
432        assert_eq!(
433            signature,
434            Signature::from_str(
435                "faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f"
436            )?
437        );
438
439        // Recover the address that signed the transaction
440        let recovered_address = signature.recover(&message)?;
441
442        assert_eq!(*signer.address(), *recovered_address.hash());
443
444        // Verify signature
445        signature.verify(&recovered_address, &message)?;
446
447        Ok(())
448    }
449}