1use std::{collections::HashMap, fmt, ops, path::Path};
2
3use async_trait::async_trait;
4use elliptic_curve::rand_core;
5use eth_keystore::KeystoreError;
6use fuel_core_client::client::{PaginatedResult, PaginationRequest};
7use fuel_crypto::{Message, PublicKey, SecretKey, Signature};
8use fuel_tx::{AssetId, Bytes32, ContractId, Input, Output, Receipt, TxPointer, UtxoId, Witness};
9use fuel_types::MessageId;
10use fuels_core::{
11 abi_encoder::UnresolvedBytes,
12 offsets::{base_offset, coin_predicate_data_offset, message_predicate_data_offset},
13};
14use fuels_types::{
15 bech32::{Bech32Address, Bech32ContractId, FUEL_BECH32_HRP},
16 coin::Coin,
17 constants::BASE_ASSET_ID,
18 errors::{error, Error, Result},
19 message::Message as InputMessage,
20 resource::Resource,
21 transaction::{ScriptTransaction, Transaction, TxParameters},
22 transaction_response::TransactionResponse,
23};
24use rand::{CryptoRng, Rng};
25use thiserror::Error;
26
27use crate::{
28 provider::{Provider, ResourceFilter},
29 Signer,
30};
31
32pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'";
33
34type WalletResult<T> = std::result::Result<T, WalletError>;
35
36#[derive(Clone)]
44pub struct Wallet {
45 pub(crate) address: Bech32Address,
48 pub(crate) provider: Option<Provider>,
49}
50
51#[derive(Clone, Debug)]
55pub struct WalletUnlocked {
56 wallet: Wallet,
57 pub(crate) private_key: SecretKey,
58}
59
60#[derive(Error, Debug)]
61pub enum WalletError {
63 #[error(transparent)]
65 Hex(#[from] hex::FromHexError),
66 #[error("Failed to parse slice")]
68 Parsing(#[from] std::array::TryFromSliceError),
69 #[error("No provider was setup: make sure to set_provider in your wallet!")]
70 NoProvider,
71 #[error(transparent)]
73 KeystoreError(#[from] KeystoreError),
74 #[error(transparent)]
75 FuelCrypto(#[from] fuel_crypto::Error),
76}
77
78impl From<WalletError> for Error {
79 fn from(e: WalletError) -> Self {
80 Error::WalletError(e.to_string())
81 }
82}
83
84impl Wallet {
85 pub fn from_address(address: Bech32Address, provider: Option<Provider>) -> Self {
87 Self { address, provider }
88 }
89
90 pub fn get_provider(&self) -> WalletResult<&Provider> {
91 self.provider.as_ref().ok_or(WalletError::NoProvider)
92 }
93
94 pub fn set_provider(&mut self, provider: Provider) {
95 self.provider = Some(provider)
96 }
97
98 pub fn address(&self) -> &Bech32Address {
99 &self.address
100 }
101
102 pub async fn get_transactions(
103 &self,
104 request: PaginationRequest<String>,
105 ) -> Result<PaginatedResult<TransactionResponse, String>> {
106 Ok(self
107 .get_provider()?
108 .get_transactions_by_owner(&self.address, request)
109 .await?)
110 }
111
112 pub async fn get_asset_inputs_for_amount(
117 &self,
118 asset_id: AssetId,
119 amount: u64,
120 witness_index: u8,
121 ) -> Result<Vec<Input>> {
122 let filter = ResourceFilter {
123 from: self.address().clone(),
124 asset_id,
125 amount,
126 ..Default::default()
127 };
128 self.get_provider()?
129 .get_asset_inputs(filter, witness_index)
130 .await
131 }
132
133 pub fn get_asset_outputs_for_amount(
135 &self,
136 to: &Bech32Address,
137 asset_id: AssetId,
138 amount: u64,
139 ) -> Vec<Output> {
140 vec![
141 Output::coin(to.into(), amount, asset_id),
142 Output::change((&self.address).into(), 0, asset_id),
145 ]
146 }
147
148 pub async fn get_coins(&self, asset_id: AssetId) -> Result<Vec<Coin>> {
150 Ok(self
151 .get_provider()?
152 .get_coins(&self.address, asset_id)
153 .await?)
154 }
155
156 pub async fn get_spendable_resources(
160 &self,
161 asset_id: AssetId,
162 amount: u64,
163 ) -> Result<Vec<Resource>> {
164 let filter = ResourceFilter {
165 from: self.address().clone(),
166 asset_id,
167 amount,
168 ..Default::default()
169 };
170 self.get_provider()?
171 .get_spendable_resources(filter)
172 .await
173 .map_err(Into::into)
174 }
175
176 pub async fn get_asset_balance(&self, asset_id: &AssetId) -> Result<u64> {
180 self.get_provider()?
181 .get_asset_balance(&self.address, *asset_id)
182 .await
183 .map_err(Into::into)
184 }
185
186 pub async fn get_balances(&self) -> Result<HashMap<String, u64>> {
190 self.get_provider()?
191 .get_balances(&self.address)
192 .await
193 .map_err(Into::into)
194 }
195
196 pub async fn get_messages(&self) -> Result<Vec<InputMessage>> {
197 Ok(self.get_provider()?.get_messages(&self.address).await?)
198 }
199
200 pub fn unlock(self, private_key: SecretKey) -> WalletUnlocked {
205 WalletUnlocked {
206 wallet: self,
207 private_key,
208 }
209 }
210}
211
212impl WalletUnlocked {
213 pub fn lock(self) -> Wallet {
215 self.wallet
216 }
217
218 pub fn set_provider(&mut self, provider: Provider) {
223 self.wallet.set_provider(provider)
224 }
225
226 pub fn new_random(provider: Option<Provider>) -> Self {
228 let mut rng = rand::thread_rng();
229 let private_key = SecretKey::random(&mut rng);
230 Self::new_from_private_key(private_key, provider)
231 }
232
233 pub fn new_from_private_key(private_key: SecretKey, provider: Option<Provider>) -> Self {
235 let public = PublicKey::from(&private_key);
236 let hashed = public.hash();
237 let address = Bech32Address::new(FUEL_BECH32_HRP, hashed);
238 Wallet::from_address(address, provider).unlock(private_key)
239 }
240
241 pub fn new_from_mnemonic_phrase(
244 phrase: &str,
245 provider: Option<Provider>,
246 ) -> WalletResult<Self> {
247 let path = format!("{DEFAULT_DERIVATION_PATH_PREFIX}/0'/0/0");
248 Self::new_from_mnemonic_phrase_with_path(phrase, provider, &path)
249 }
250
251 pub fn new_from_mnemonic_phrase_with_path(
254 phrase: &str,
255 provider: Option<Provider>,
256 path: &str,
257 ) -> WalletResult<Self> {
258 let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, path)?;
259
260 Ok(Self::new_from_private_key(secret_key, provider))
261 }
262
263 pub fn new_from_keystore<P, R, S>(
265 dir: P,
266 rng: &mut R,
267 password: S,
268 provider: Option<Provider>,
269 ) -> WalletResult<(Self, String)>
270 where
271 P: AsRef<Path>,
272 R: Rng + CryptoRng + rand_core::CryptoRng,
273 S: AsRef<[u8]>,
274 {
275 let (secret, uuid) = eth_keystore::new(dir, rng, password)?;
276
277 let secret_key = unsafe { SecretKey::from_slice_unchecked(&secret) };
278
279 let wallet = Self::new_from_private_key(secret_key, provider);
280
281 Ok((wallet, uuid))
282 }
283
284 pub fn encrypt<P, S>(&self, dir: P, password: S) -> WalletResult<String>
287 where
288 P: AsRef<Path>,
289 S: AsRef<[u8]>,
290 {
291 let mut rng = rand::thread_rng();
292
293 Ok(eth_keystore::encrypt_key(
294 dir,
295 &mut rng,
296 *self.private_key,
297 password,
298 )?)
299 }
300
301 pub fn load_keystore<P, S>(
303 keypath: P,
304 password: S,
305 provider: Option<Provider>,
306 ) -> WalletResult<Self>
307 where
308 P: AsRef<Path>,
309 S: AsRef<[u8]>,
310 {
311 let secret = eth_keystore::decrypt_key(keypath, password)?;
312 let secret_key = unsafe { SecretKey::from_slice_unchecked(&secret) };
313 Ok(Self::new_from_private_key(secret_key, provider))
314 }
315
316 pub async fn add_fee_resources(
324 &self,
325 tx: &mut impl Transaction,
326 previous_base_amount: u64,
327 witness_index: u8,
328 ) -> Result<()> {
329 let consensus_parameters = self
330 .get_provider()?
331 .chain_info()
332 .await?
333 .consensus_parameters;
334 let transaction_fee = tx
335 .fee_checked_from_tx(&consensus_parameters)
336 .expect("Error calculating TransactionFee");
337
338 let (base_asset_inputs, remaining_inputs): (Vec<_>, Vec<_>) =
339 tx.inputs().iter().cloned().partition(|input| {
340 matches!(input, Input::MessageSigned { .. })
341 || matches!(input, Input::CoinSigned { asset_id, .. } if asset_id == &BASE_ASSET_ID)
342 });
343
344 let base_inputs_sum: u64 = base_asset_inputs
345 .iter()
346 .map(|input| input.amount().unwrap())
347 .sum();
348 if base_inputs_sum < previous_base_amount {
350 return Err(error!(
351 WalletError,
352 "The provided base asset amount is less than the present input coins"
353 ));
354 }
355
356 let mut new_base_amount = transaction_fee.total() + previous_base_amount;
357 let is_consuming_utxos = tx
361 .inputs()
362 .iter()
363 .any(|input| !matches!(input, Input::Contract { .. }));
364 const MIN_AMOUNT: u64 = 1;
365 if !is_consuming_utxos && new_base_amount == 0 {
366 new_base_amount = MIN_AMOUNT;
367 }
368
369 let new_base_inputs = self
370 .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount, witness_index)
371 .await?;
372 let adjusted_inputs: Vec<_> = remaining_inputs
373 .into_iter()
374 .chain(new_base_inputs.into_iter())
375 .collect();
376 *tx.inputs_mut() = adjusted_inputs;
377
378 let is_base_change_present = tx.outputs().iter().any(|output| {
379 matches!(output, Output::Change { asset_id, .. } if asset_id == &BASE_ASSET_ID)
380 });
381 if !is_base_change_present && new_base_amount != 0 {
383 tx.outputs_mut()
384 .push(Output::change(self.address().into(), 0, BASE_ASSET_ID));
385 }
386
387 Ok(())
388 }
389
390 pub async fn transfer(
394 &self,
395 to: &Bech32Address,
396 amount: u64,
397 asset_id: AssetId,
398 tx_parameters: TxParameters,
399 ) -> Result<(String, Vec<Receipt>)> {
400 let inputs = self
401 .get_asset_inputs_for_amount(asset_id, amount, 0)
402 .await?;
403 let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
404
405 let mut tx = ScriptTransaction::new(inputs, outputs, tx_parameters);
406
407 if asset_id == AssetId::default() {
409 self.add_fee_resources(&mut tx, amount, 0).await?;
410 } else {
411 self.add_fee_resources(&mut tx, 0, 0).await?;
412 };
413 self.sign_transaction(&mut tx).await?;
414
415 let tx_id = tx.id().to_string();
416 let receipts = self.get_provider()?.send_transaction(&tx).await?;
417
418 Ok((tx_id, receipts))
419 }
420
421 pub async fn withdraw_to_base_layer(
425 &self,
426 to: &Bech32Address,
427 amount: u64,
428 tx_parameters: TxParameters,
429 ) -> Result<(String, String, Vec<Receipt>)> {
430 let inputs = self
431 .get_asset_inputs_for_amount(BASE_ASSET_ID, amount, 0)
432 .await?;
433
434 let mut tx =
435 ScriptTransaction::build_message_to_output_tx(to.into(), amount, inputs, tx_parameters);
436
437 self.add_fee_resources(&mut tx, amount, 0).await?;
438 self.sign_transaction(&mut tx).await?;
439
440 let tx_id = tx.id().to_string();
441 let receipts = self.get_provider()?.send_transaction(&tx).await?;
442
443 let message_id = WalletUnlocked::extract_message_id(&receipts)
444 .expect("MessageId could not be retrieved from tx receipts.");
445
446 Ok((tx_id, message_id.to_string(), receipts))
447 }
448
449 fn extract_message_id(receipts: &[Receipt]) -> Option<&MessageId> {
450 receipts
451 .iter()
452 .find(|r| matches!(r, Receipt::MessageOut { .. }))
453 .and_then(|m| m.message_id())
454 }
455
456 #[allow(clippy::too_many_arguments)]
457 pub async fn spend_predicate(
458 &self,
459 predicate_address: &Bech32Address,
460 code: Vec<u8>,
461 amount: u64,
462 asset_id: AssetId,
463 to: &Bech32Address,
464 predicate_data: UnresolvedBytes,
465 tx_parameters: TxParameters,
466 ) -> Result<Vec<Receipt>> {
467 let provider = self.get_provider()?;
468
469 let filter = ResourceFilter {
470 from: predicate_address.clone(),
471 amount,
472 ..Default::default()
473 };
474 let spendable_predicate_resources = provider.get_spendable_resources(filter).await?;
475
476 let input_amount: u64 = spendable_predicate_resources
479 .iter()
480 .map(|resource| resource.amount())
481 .sum();
482
483 let mut offset = base_offset(&provider.consensus_parameters().await?);
486 let inputs = spendable_predicate_resources
487 .into_iter()
488 .map(|resource| match resource {
489 Resource::Coin(coin) => {
490 offset += coin_predicate_data_offset(code.len());
491
492 let data = predicate_data.clone().resolve(offset as u64);
493 offset += data.len();
494
495 self.create_coin_predicate(coin, asset_id, code.clone(), data)
496 }
497 Resource::Message(message) => {
498 offset += message_predicate_data_offset(message.data.len(), code.len());
499
500 let data = predicate_data.clone().resolve(offset as u64);
501 offset += data.len();
502
503 self.create_message_predicate(message, code.clone(), data)
504 }
505 })
506 .collect::<Vec<_>>();
507
508 let outputs = vec![
509 Output::coin(to.into(), amount, asset_id),
510 Output::coin(predicate_address.into(), input_amount - amount, asset_id),
511 ];
512
513 let mut tx = ScriptTransaction::new(inputs, outputs, tx_parameters);
514 self.add_fee_resources(&mut tx, 0, 0).await?;
516 self.sign_transaction(&mut tx).await?;
517
518 provider.send_transaction(&tx).await
519 }
520
521 fn create_coin_predicate(
522 &self,
523 coin: Coin,
524 asset_id: AssetId,
525 code: Vec<u8>,
526 predicate_data: Vec<u8>,
527 ) -> Input {
528 Input::coin_predicate(
529 coin.utxo_id,
530 coin.owner.into(),
531 coin.amount,
532 asset_id,
533 TxPointer::default(),
534 0,
535 code,
536 predicate_data,
537 )
538 }
539
540 fn create_message_predicate(
541 &self,
542 message: InputMessage,
543 code: Vec<u8>,
544 predicate_data: Vec<u8>,
545 ) -> Input {
546 Input::message_predicate(
547 message.message_id(),
548 message.sender.into(),
549 message.recipient.into(),
550 message.amount,
551 message.nonce,
552 message.data,
553 code,
554 predicate_data,
555 )
556 }
557
558 pub async fn receive_from_predicate(
559 &self,
560 predicate_address: &Bech32Address,
561 predicate_code: Vec<u8>,
562 amount: u64,
563 asset_id: AssetId,
564 predicate_data: UnresolvedBytes,
565 tx_parameters: TxParameters,
566 ) -> Result<Vec<Receipt>> {
567 self.spend_predicate(
568 predicate_address,
569 predicate_code,
570 amount,
571 asset_id,
572 self.address(),
573 predicate_data,
574 tx_parameters,
575 )
576 .await
577 }
578
579 pub async fn force_transfer_to_contract(
589 &self,
590 to: &Bech32ContractId,
591 balance: u64,
592 asset_id: AssetId,
593 tx_parameters: TxParameters,
594 ) -> Result<(String, Vec<Receipt>)> {
595 let zeroes = Bytes32::zeroed();
596 let plain_contract_id: ContractId = to.into();
597
598 let mut inputs = vec![Input::contract(
599 UtxoId::new(zeroes, 0),
600 zeroes,
601 zeroes,
602 TxPointer::default(),
603 plain_contract_id,
604 )];
605 inputs.extend(
606 self.get_asset_inputs_for_amount(asset_id, balance, 0)
607 .await?,
608 );
609
610 let outputs = vec![
611 Output::contract(0, zeroes, zeroes),
612 Output::change((&self.address).into(), 0, asset_id),
613 ];
614
615 let mut tx = ScriptTransaction::build_contract_transfer_tx(
617 plain_contract_id,
618 balance,
619 asset_id,
620 inputs,
621 outputs,
622 tx_parameters,
623 );
624 let base_amount = if asset_id == AssetId::default() {
626 balance
627 } else {
628 0
629 };
630 self.add_fee_resources(&mut tx, base_amount, 0).await?;
631 self.sign_transaction(&mut tx).await?;
632
633 let tx_id = tx.id();
634 let receipts = self.get_provider()?.send_transaction(&tx).await?;
635
636 Ok((tx_id.to_string(), receipts))
637 }
638}
639
640#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
641#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
642impl Signer for WalletUnlocked {
643 type Error = WalletError;
644
645 async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
646 &self,
647 message: S,
648 ) -> WalletResult<Signature> {
649 let message = Message::new(message);
650 let sig = Signature::sign(&self.private_key, &message);
651 Ok(sig)
652 }
653
654 async fn sign_transaction<T: Transaction + Send>(&self, tx: &mut T) -> WalletResult<Signature> {
655 let id = tx.id();
656
657 let message = unsafe { Message::from_bytes_unchecked(*id) };
663 let sig = Signature::sign(&self.private_key, &message);
664
665 let witness = vec![Witness::from(sig.as_ref())];
666
667 let witnesses: &mut Vec<Witness> = tx.witnesses_mut();
668
669 match witnesses.len() {
670 0 => *witnesses = witness,
671 _ => {
672 witnesses.extend(witness);
673 }
674 }
675
676 Ok(sig)
677 }
678
679 fn address(&self) -> &Bech32Address {
680 &self.address
681 }
682}
683
684impl fmt::Debug for Wallet {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 f.debug_struct("Wallet")
687 .field("address", &self.address)
688 .finish()
689 }
690}
691
692impl ops::Deref for WalletUnlocked {
693 type Target = Wallet;
694 fn deref(&self) -> &Self::Target {
695 &self.wallet
696 }
697}
698
699pub fn generate_mnemonic_phrase<R: Rng>(rng: &mut R, count: usize) -> WalletResult<String> {
702 Ok(fuel_crypto::FuelMnemonic::generate_mnemonic_phrase(
703 rng, count,
704 )?)
705}
706
707#[cfg(test)]
708mod tests {
709 use tempfile::tempdir;
710
711 use super::*;
712
713 #[tokio::test]
714 async fn encrypted_json_keystore() -> Result<()> {
715 let dir = tempdir()?;
716 let mut rng = rand::thread_rng();
717
718 let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?;
720
721 let message = "Hello there!";
723 let signature = wallet.sign_message(message).await?;
724
725 let path = Path::new(dir.path()).join(uuid);
727 let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?;
728
729 let signature2 = recovered_wallet.sign_message(message).await?;
731 assert_eq!(signature, signature2);
732
733 assert!(std::fs::remove_file(&path).is_ok());
735 Ok(())
736 }
737
738 #[tokio::test]
739 async fn mnemonic_generation() -> Result<()> {
740 let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 12)?;
741
742 let _wallet = WalletUnlocked::new_from_mnemonic_phrase(&mnemonic, None)?;
743 Ok(())
744 }
745
746 #[tokio::test]
747 async fn wallet_from_mnemonic_phrase() -> Result<()> {
748 let phrase =
749 "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
750
751 let wallet =
753 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
754
755 let expected_plain_address =
756 "df9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185";
757 let expected_address = "fuel1m7wsumrvtaw6d6pwtcd809627ejzhk69pggvg0cvdyg2yynqqxzseuzply";
758
759 assert_eq!(wallet.address().hash().to_string(), expected_plain_address);
760 assert_eq!(wallet.address().to_string(), expected_address);
761
762 let wallet2 =
764 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/1'/0/0")?;
765
766 let expected_second_plain_address =
767 "261191b0164a24fd0fd51566ec5e5b0b9ba8fb2d42dc9cf7dbbd6f23d2742759";
768 let expected_second_address =
769 "fuel1ycgervqkfgj06r74z4nwchjmpwd637edgtwfea7mh4hj85n5yavszjk4cc";
770
771 assert_eq!(
772 wallet2.address().hash().to_string(),
773 expected_second_plain_address
774 );
775 assert_eq!(wallet2.address().to_string(), expected_second_address);
776
777 Ok(())
778 }
779
780 #[tokio::test]
781 async fn encrypt_and_store_wallet_from_mnemonic() -> Result<()> {
782 let dir = tempdir()?;
783
784 let phrase =
785 "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
786
787 let wallet =
789 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
790
791 let uuid = wallet.encrypt(&dir, "password")?;
792
793 let path = Path::new(dir.path()).join(uuid);
794
795 let recovered_wallet = WalletUnlocked::load_keystore(&path, "password", None)?;
796
797 assert_eq!(wallet.address(), recovered_wallet.address());
798
799 assert!(std::fs::remove_file(&path).is_ok());
801 Ok(())
802 }
803}