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