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