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 self.add_witnesses(&mut tb)?;
264 self.adjust_for_fee(&mut tb, balance.into())
265 .await
266 .context("failed to adjust inputs to cover for missing base asset")?;
267
268 let tx = tb.build(provider).await?;
269
270 let consensus_parameters = provider.consensus_parameters().await?;
271 let tx_id = tx.id(consensus_parameters.chain_id());
272
273 let tx_status = provider.send_transaction_and_await_commit(tx).await?;
274
275 Ok(TxResponse {
276 tx_status: tx_status.take_success_checked(None)?,
277 tx_id,
278 })
279 }
280
281 async fn withdraw_to_base_layer(
285 &self,
286 to: Address,
287 amount: u64,
288 tx_policies: TxPolicies,
289 ) -> Result<WithdrawToBaseResponse> {
290 let provider = self.try_provider()?;
291 let consensus_parameters = provider.consensus_parameters().await?;
292
293 let inputs = self
294 .get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), amount.into(), None)
295 .await?;
296
297 let mut tb = ScriptTransactionBuilder::prepare_message_to_output(
298 to,
299 amount,
300 inputs,
301 tx_policies,
302 *consensus_parameters.base_asset_id(),
303 );
304
305 self.add_witnesses(&mut tb)?;
306 self.adjust_for_fee(&mut tb, amount.into())
307 .await
308 .context("failed to adjust inputs to cover for missing base asset")?;
309
310 let tx = tb.build(provider).await?;
311 let tx_id = tx.id(consensus_parameters.chain_id());
312
313 let tx_status = provider.send_transaction_and_await_commit(tx).await?;
314 let success = tx_status.take_success_checked(None)?;
315
316 let nonce = extract_message_nonce(&success.receipts)
317 .expect("MessageId could not be retrieved from tx receipts.");
318
319 Ok(WithdrawToBaseResponse {
320 tx_status: success,
321 tx_id,
322 nonce,
323 })
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use std::str::FromStr;
330
331 use fuel_crypto::{Message, SecretKey, Signature};
332 use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction};
333 use fuels_core::{
334 traits::Signer,
335 types::{DryRun, DryRunner, transaction::Transaction},
336 };
337
338 use super::*;
339 use crate::signers::private_key::PrivateKeySigner;
340
341 #[derive(Default)]
342 struct MockDryRunner {
343 c_param: ConsensusParameters,
344 }
345
346 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
347 impl DryRunner for MockDryRunner {
348 async fn dry_run(&self, _: FuelTransaction) -> Result<DryRun> {
349 Ok(DryRun {
350 succeeded: true,
351 script_gas: 0,
352 variable_outputs: 0,
353 })
354 }
355
356 async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
357 Ok(self.c_param.clone())
358 }
359
360 async fn estimate_gas_price(&self, _block_header: u32) -> Result<u64> {
361 Ok(0)
362 }
363
364 async fn estimate_predicates(
365 &self,
366 _: &FuelTransaction,
367 _: Option<u32>,
368 ) -> Result<FuelTransaction> {
369 unimplemented!()
370 }
371 }
372
373 #[tokio::test]
374 async fn sign_tx_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
375 let secret = SecretKey::from_str(
377 "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
378 )?;
379 let signer = PrivateKeySigner::new(secret);
380
381 let mut tb = {
383 let input_coin = Input::ResourceSigned {
384 resource: CoinType::Coin(Coin {
385 amount: 10000000,
386 owner: signer.address(),
387 ..Default::default()
388 }),
389 };
390
391 let output_coin = Output::coin(
392 Address::from_str(
393 "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
394 )?,
395 1,
396 Default::default(),
397 );
398 let change = Output::change(signer.address(), 0, Default::default());
399
400 ScriptTransactionBuilder::prepare_transfer(
401 vec![input_coin],
402 vec![output_coin, change],
403 Default::default(),
404 )
405 };
406
407 tb.add_signer(signer.clone())?;
409 let tx = tb.build(MockDryRunner::default()).await?; let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
415 let tx_signature = Signature::from_bytes(bytes);
416
417 let message = Message::from_bytes(*tx.id(0.into()));
419 let signature = signer.sign(message).await?;
420
421 assert_eq!(signature, tx_signature);
423
424 assert_eq!(
426 signature,
427 Signature::from_str(
428 "faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f"
429 )?
430 );
431
432 let recovered_address = signature.recover(&message)?;
434
435 assert_eq!(*signer.address(), *recovered_address.hash());
436
437 signature.verify(&recovered_address, &message)?;
439
440 Ok(())
441 }
442}