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 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 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 async fn get_messages(&self) -> Result<Vec<Message>> {
63 Ok(self.try_provider()?.get_messages(self.address()).await?)
64 }
65
66 async fn get_balances(&self) -> Result<HashMap<String, u128>> {
70 self.try_provider()?.get_balances(self.address()).await
71 }
72
73 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 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 Output::change(self.address().into(), 0, asset_id),
108 ]
109 }
110
111 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 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 .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 fn add_witnesses<Tb: TransactionBuilder>(&self, _tb: &mut Tb) -> Result<()> {
160 Ok(())
161 }
162
163 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 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 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 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 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 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 assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?);
340
341 let recovered_address = signature.recover(&message)?;
343
344 assert_eq!(wallet.address().hash(), recovered_address.hash());
345
346 signature.verify(&recovered_address, &message)?;
348 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 let secret = SecretKey::from_str(
389 "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
390 )?;
391 let wallet = WalletUnlocked::new_from_private_key(secret, None);
392
393 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 tb.add_signer(wallet.clone())?;
421 let tx = tb.build(MockDryRunner::default()).await?; let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
427 let tx_signature = Signature::from_bytes(bytes);
428
429 let message = Message::from_bytes(*tx.id(0.into()));
431 let signature = wallet.sign(message).await?;
432
433 assert_eq!(signature, tx_signature);
435
436 assert_eq!(signature, Signature::from_str("faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f")?);
438
439 let recovered_address = signature.recover(&message)?;
441
442 assert_eq!(wallet.address().hash(), recovered_address.hash());
443
444 signature.verify(&recovered_address, &message)?;
446
447 Ok(())
448 }
449}