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 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 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 async fn get_messages(&self) -> Result<Vec<Message>> {
71 Ok(self.try_provider()?.get_messages(&self.address()).await?)
72 }
73
74 async fn get_balances(&self) -> Result<HashMap<String, u128>> {
78 self.try_provider()?.get_balances(&self.address()).await
79 }
80
81 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 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 Output::change(self.address(), 0, asset_id),
116 ]
117 }
118
119 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 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 fn add_witnesses<Tb: TransactionBuilder>(&self, _tb: &mut Tb) -> Result<()> {
169 Ok(())
170 }
171
172 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 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 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 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 let secret = SecretKey::from_str(
384 "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
385 )?;
386 let signer = PrivateKeySigner::new(secret);
387
388 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 tb.add_signer(signer.clone())?;
416 let tx = tb.build(MockDryRunner::default()).await?; let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
422 let tx_signature = Signature::from_bytes(bytes);
423
424 let message = Message::from_bytes(*tx.id(0.into()));
426 let signature = signer.sign(message).await?;
427
428 assert_eq!(signature, tx_signature);
430
431 assert_eq!(
433 signature,
434 Signature::from_str(
435 "faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f"
436 )?
437 );
438
439 let recovered_address = signature.recover(&message)?;
441
442 assert_eq!(*signer.address(), *recovered_address.hash());
443
444 signature.verify(&recovered_address, &message)?;
446
447 Ok(())
448 }
449}