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 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 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 async fn get_messages(&self) -> Result<Vec<Message>> {
72 Ok(self.try_provider()?.get_messages(self.address()).await?)
73 }
74
75 async fn get_balances(&self) -> Result<HashMap<String, u128>> {
79 self.try_provider()?.get_balances(self.address()).await
80 }
81
82 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 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 Output::change(self.address().into(), 0, asset_id),
117 ]
118 }
119
120 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 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 .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 fn add_witnesses<Tb: TransactionBuilder>(&self, _tb: &mut Tb) -> Result<()> {
169 Ok(())
170 }
171
172 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 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 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 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 let secret = SecretKey::from_str(
373 "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
374 )?;
375 let signer = PrivateKeySigner::new(secret);
376
377 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 tb.add_signer(signer.clone())?;
405 let tx = tb.build(MockDryRunner::default()).await?; let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
411 let tx_signature = Signature::from_bytes(bytes);
412
413 let message = Message::from_bytes(*tx.id(0.into()));
415 let signature = signer.sign(message).await?;
416
417 assert_eq!(signature, tx_signature);
419
420 assert_eq!(signature, Signature::from_str("faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f")?);
422
423 let recovered_address = signature.recover(&message)?;
425
426 assert_eq!(signer.address().hash(), recovered_address.hash());
427
428 signature.verify(&recovered_address, &message)?;
430
431 Ok(())
432 }
433}