1use crate::format::Table;
2use crate::sign;
3use crate::utils::{
4 display_string_discreetly, get_derivation_path, load_wallet, user_fuel_wallets_accounts_dir,
5};
6use anyhow::{anyhow, bail, Context, Result};
7use clap::{Args, Subcommand};
8use eth_keystore::EthKeystore;
9use forc_tracing::println_warning;
10use fuels::accounts::provider::Provider;
11use fuels::accounts::signers::private_key::PrivateKeySigner;
12use fuels::accounts::wallet::Unlocked;
13use fuels::accounts::ViewOnlyAccount;
14use fuels::crypto::{PublicKey, SecretKey};
15use fuels::types::checksum_address::{checksum_encode, is_checksum_valid};
16use fuels::types::transaction::TxPolicies;
17use fuels::types::{Address, AssetId};
18use fuels::{
19 accounts::wallet::Wallet,
20 types::bech32::{Bech32Address, FUEL_BECH32_HRP},
21};
22use std::ops::Range;
23use std::{
24 collections::BTreeMap,
25 fmt, fs,
26 path::{Path, PathBuf},
27 str::FromStr,
28};
29use url::Url;
30
31type WalletUnlocked<S> = Wallet<Unlocked<S>>;
32
33#[derive(Debug, Args)]
34pub struct Accounts {
35 #[clap(flatten)]
36 unverified: UnverifiedOpt,
37 #[clap(long)]
41 as_bech32: bool,
42}
43
44#[derive(Debug, Args)]
45pub struct Account {
46 index: Option<usize>,
50 #[clap(flatten)]
51 unverified: UnverifiedOpt,
52 #[clap(subcommand)]
53 cmd: Option<Command>,
54}
55
56#[derive(Debug, Args)]
57pub(crate) struct Fmt {
58 #[clap(long)]
62 as_hex: bool,
63}
64
65#[derive(Debug, Subcommand)]
66pub(crate) enum Command {
67 New,
76 #[clap(subcommand)]
78 Sign(sign::Data),
79 PrivateKey,
84 PublicKey(Fmt),
87 Balance(Balance),
89 Transfer(Transfer),
91}
92
93#[derive(Debug, Args)]
94pub(crate) struct Balance {
95 #[clap(flatten)]
96 pub(crate) unverified: UnverifiedOpt,
97}
98
99#[derive(Debug, Args)]
100pub(crate) struct Transfer {
101 #[clap(long)]
103 to: To,
104 #[clap(long)]
106 amount: u64,
107 #[clap(long)]
109 asset_id: AssetId,
110 #[clap(long)]
111 gas_price: Option<u64>,
112 #[clap(long)]
113 gas_limit: Option<u64>,
114 #[clap(long)]
115 maturity: Option<u64>,
116}
117
118#[derive(Debug, Args)]
119pub(crate) struct UnverifiedOpt {
120 #[clap(long = "unverified")]
126 pub(crate) unverified: bool,
127}
128
129#[derive(Debug, Clone)]
130enum To {
131 Bech32Address(Bech32Address),
132 HexAddress(Address),
133}
134
135impl FromStr for To {
136 type Err = String;
137
138 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
139 if let Ok(bech32_address) = Bech32Address::from_str(s) {
140 return Ok(Self::Bech32Address(bech32_address));
141 } else if let Ok(hex_address) = Address::from_str(s) {
142 if !is_checksum_valid(s) {
143 return Err(format!(
144 "Checksum is not valid for address `{}`, the address might not be an account.",
145 s
146 ));
147 }
148 return Ok(Self::HexAddress(hex_address));
149 }
150
151 Err(format!(
152 "Invalid address '{}': address must either be in bech32 or hex",
153 s
154 ))
155 }
156}
157
158impl fmt::Display for To {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 match self {
161 To::Bech32Address(bech32_addr) => write!(f, "{bech32_addr}"),
162 To::HexAddress(hex_addr) => {
163 let hex_addr = checksum_encode(&format!("0x{hex_addr}")).unwrap();
167 write!(f, "{hex_addr}")
168 }
169 }
170 }
171}
172
173type AccountAddresses = BTreeMap<usize, Address>;
175
176pub async fn cli(ctx: &crate::CliContext, account: Account) -> Result<()> {
177 match (account.index, account.cmd) {
178 (None, Some(Command::New)) => new_cli(ctx).await?,
179 (Some(acc_ix), Some(Command::New)) => new_at_index_cli(ctx, acc_ix).await?,
180 (Some(acc_ix), None) => print_address(ctx, acc_ix, account.unverified.unverified).await?,
181 (Some(acc_ix), Some(Command::Sign(sign_cmd))) => {
182 sign::wallet_account_cli(ctx, acc_ix, sign_cmd)?
183 }
184 (Some(acc_ix), Some(Command::PrivateKey)) => private_key_cli(ctx, acc_ix)?,
185 (Some(acc_ix), Some(Command::PublicKey(format))) => match format.as_hex {
186 true => hex_address_cli(ctx, acc_ix)?,
187 false => public_key_cli(ctx, acc_ix)?,
188 },
189
190 (Some(acc_ix), Some(Command::Balance(balance))) => {
191 account_balance_cli(ctx, acc_ix, &balance).await?
192 }
193 (Some(acc_ix), Some(Command::Transfer(transfer))) => {
194 transfer_cli(ctx, acc_ix, transfer).await?
195 }
196 (None, Some(cmd)) => print_subcmd_index_warning(&cmd),
197 (None, None) => print_subcmd_help(),
198 }
199 Ok(())
200}
201
202pub(crate) async fn account_balance_cli(
203 ctx: &crate::CliContext,
204 acc_ix: usize,
205 balance: &Balance,
206) -> Result<()> {
207 let wallet = load_wallet(&ctx.wallet_path)?;
208 let provider = Provider::connect(&ctx.node_url).await?;
209 let mut cached_addrs = read_cached_addresses(&wallet.crypto.ciphertext)?;
210 let cached_addr = cached_addrs
211 .remove(&acc_ix)
212 .ok_or_else(|| anyhow!("No cached address for account {acc_ix}"))?;
213
214 let account = if balance.unverified.unverified {
215 let cached_addr = Bech32Address::from(cached_addr);
216 Wallet::new_locked(cached_addr, provider)
217 } else {
218 let prompt = format!("Please enter your wallet password to verify account {acc_ix}: ");
219 let password = rpassword::prompt_password(prompt)?;
220 let account = derive_account_unlocked(&ctx.wallet_path, acc_ix, &password, &provider)?;
221 let cached_addr = Bech32Address::from(cached_addr);
222 verify_address_and_update_cache(acc_ix, &account, &cached_addr, &wallet.crypto.ciphertext)?;
223 account.lock()
224 };
225 println!("Connecting to {}", &ctx.node_url);
226 println!("Fetching the balance of the following account:",);
227 let account_adr = checksum_encode(&format!("0x{}", account.address()))?;
228 println!(" {acc_ix:>3}: {}", account_adr);
229 let account_balance: BTreeMap<_, _> = account.get_balances().await?.into_iter().collect();
230 println!("\nAccount {acc_ix}:");
231 if account_balance.is_empty() {
232 print_balance_empty(&ctx.node_url);
233 } else {
234 print_balance(&account_balance);
235 }
236 Ok(())
237}
238
239pub(crate) fn verify_address_and_update_cache(
243 acc_ix: usize,
244 account: &Wallet,
245 expected_addr: &Bech32Address,
246 wallet_ciphertext: &[u8],
247) -> Result<bool> {
248 let addr = account.address();
249 if addr == expected_addr {
250 return Ok(true);
251 }
252 println_warning(&format!(
253 "Cached address for account {} differs from derived address.\n\
254{:>2}Cached: {}
255{:>2}Derived: {}
256{:>2}Updating cache with newly derived address.",
257 acc_ix, "", expected_addr, "", addr, "",
258 ));
259 cache_address(wallet_ciphertext, acc_ix, addr)?;
260 Ok(false)
261}
262
263pub(crate) fn print_balance_empty(node_url: &Url) {
264 let testnet_url = crate::network::TESTNET.parse::<Url>().unwrap();
265
266 let faucet_url = match node_url.host_str() {
267 host if host == testnet_url.host_str() => crate::network::TESTNET_FAUCET,
268 _ => return println!(" Account empty."),
269 };
270 if node_url
271 .host_str()
272 .is_some_and(|a| a == crate::network::MAINNET)
273 {
274 println!(" Account empty.");
275 } else {
276 println!(
277 " Account empty. Visit the faucet to acquire some test funds: {}",
278 faucet_url
279 );
280 }
281}
282
283pub(crate) fn print_balance(balance: &BTreeMap<String, u128>) {
284 let mut table = Table::default();
285 table.add_header("Asset ID");
286 table.add_header("Amount");
287
288 for (asset_id, amount) in balance {
289 table
290 .add_row(vec![asset_id.to_owned(), amount.to_string()])
291 .expect("add_row");
292 }
293 println!("{}", table);
294}
295
296pub async fn print_accounts_cli(ctx: &crate::CliContext, accounts: Accounts) -> Result<()> {
298 let wallet = load_wallet(&ctx.wallet_path)?;
299 let addresses = read_cached_addresses(&wallet.crypto.ciphertext)?;
300 if accounts.unverified.unverified {
301 println!("Account addresses (unverified, printed from cache):");
302 addresses
303 .iter()
304 .for_each(|(ix, addr)| match accounts.as_bech32 {
305 false => {
306 println!("[{ix}] {addr}")
307 }
308 true => {
309 let bytes_addr: Bech32Address = Bech32Address::from(*addr);
310 println!("[{ix}] {bytes_addr}");
311 }
312 });
313 } else {
314 let prompt = "Please enter your wallet password to verify cached accounts: ";
315 let password = rpassword::prompt_password(prompt)?;
316 let provider = Provider::connect(&ctx.node_url).await?;
317 for &ix in addresses.keys() {
318 let account = derive_account_unlocked(&ctx.wallet_path, ix, &password, &provider)?;
319 let account_addr = account.address();
320 match accounts.as_bech32 {
321 false => {
322 let account_addr: Address = account.address().into();
323 let account_addr = checksum_encode(&format!("0x{account_addr}"))?;
324 println!("[{ix}] {account_addr}")
325 }
326 true => {
327 let bytes_addr: Bech32Address = Bech32Address::from(account_addr);
328 println!("[{ix}] {bytes_addr}");
329 }
330 }
331
332 cache_address(&wallet.crypto.ciphertext, ix, account_addr)?;
333 }
334 }
335 Ok(())
336}
337
338fn print_subcmd_help() {
339 std::process::Command::new("forc-wallet")
345 .args(["account", "--help"])
346 .stdout(std::process::Stdio::inherit())
347 .stderr(std::process::Stdio::inherit())
348 .output()
349 .expect("failed to invoke `forc wallet account --help` command");
350}
351
352fn print_subcmd_index_warning(cmd: &Command) {
353 let cmd_str = match cmd {
354 Command::Sign(_) => "sign",
355 Command::PrivateKey => "private-key",
356 Command::PublicKey(_) => "public-key",
357 Command::Transfer(_) => "transfer",
358 Command::Balance(_) => "balance",
359 Command::New => unreachable!("new is valid without an index"),
360 };
361 eprintln!(
362 "Error: The command `{cmd_str}` requires an account index. \
363 For example: `forc wallet account <INDEX> {cmd_str} ...`\n"
364 );
365 print_subcmd_help();
366}
367
368pub async fn print_address(
370 ctx: &crate::CliContext,
371 account_ix: usize,
372 unverified: bool,
373) -> Result<()> {
374 let wallet = load_wallet(&ctx.wallet_path)?;
375 if unverified {
376 let addresses = read_cached_addresses(&wallet.crypto.ciphertext)?;
377 match addresses.get(&account_ix) {
378 Some(address) => println!("Account {account_ix} address (unverified): {address}"),
379 None => eprintln!("Account {account_ix} is not derived yet!"),
380 }
381 } else {
382 let prompt = format!("Please enter your wallet password to verify account {account_ix}: ");
383 let password = rpassword::prompt_password(prompt)?;
384 let provider = Provider::connect(&ctx.node_url).await?;
385 let account = derive_account_unlocked(&ctx.wallet_path, account_ix, &password, &provider)?;
386 let account_addr = account.address();
387 let checksum_addr = checksum_encode(&format!("0x{}", Address::from(account_addr)))?;
388 println!("Account {account_ix} address: {checksum_addr}");
389 cache_address(&wallet.crypto.ciphertext, account_ix, account_addr)?;
390 }
391 Ok(())
392}
393
394pub fn derive_secret_key(
397 wallet_path: &Path,
398 account_index: usize,
399 password: &str,
400) -> Result<SecretKey> {
401 let phrase_recovered = eth_keystore::decrypt_key(wallet_path, password)?;
402 let phrase = String::from_utf8(phrase_recovered)?;
403 let derive_path = get_derivation_path(account_index);
404 let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(&phrase, &derive_path)?;
405 Ok(secret_key)
406}
407
408fn next_derivation_index(addrs: &AccountAddresses) -> usize {
409 addrs.last_key_value().map(|(&ix, _)| ix + 1).unwrap_or(0)
410}
411
412pub(crate) fn derive_account_unlocked(
414 wallet_path: &Path,
415 account_ix: usize,
416 password: &str,
417 provider: &Provider,
418) -> Result<WalletUnlocked<PrivateKeySigner>> {
419 let secret_key = derive_secret_key(wallet_path, account_ix, password)?;
420 let wallet = WalletUnlocked::new(PrivateKeySigner::new(secret_key), provider.clone());
421 Ok(wallet)
422}
423
424pub async fn derive_and_cache_addresses(
425 ctx: &crate::CliContext,
426 mnemonic: &str,
427 range: Range<usize>,
428) -> anyhow::Result<BTreeMap<usize, Address>> {
429 let wallet = load_wallet(&ctx.wallet_path)?;
430 let provider = Provider::connect(&ctx.node_url).await?;
431 range
432 .into_iter()
433 .map(|acc_ix| {
434 let derive_path = get_derivation_path(acc_ix);
435 let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(mnemonic, &derive_path)?;
436 let account = WalletUnlocked::new(PrivateKeySigner::new(secret_key), provider.clone());
437 cache_address(&wallet.crypto.ciphertext, acc_ix, account.address())?;
438
439 Ok(account.address().to_owned().into())
440 })
441 .collect::<Result<Vec<_>, _>>()
442 .map(|x| x.into_iter().enumerate().collect())
443}
444
445fn new_at_index(
446 keystore: &EthKeystore,
447 wallet_path: &Path,
448 account_ix: usize,
449 provider: &Provider,
450) -> Result<String> {
451 let prompt = format!("Please enter your wallet password to derive account {account_ix}: ");
452 let password = rpassword::prompt_password(prompt)?;
453 let account = derive_account_unlocked(wallet_path, account_ix, &password, provider)?;
454 let account_addr = account.address();
455 cache_address(&keystore.crypto.ciphertext, account_ix, account_addr)?;
456 let checksum_addr = checksum_encode(&Address::from(account_addr).to_string())?;
457 println!("Wallet address: {checksum_addr}");
458 Ok(checksum_addr)
459}
460
461pub async fn new_at_index_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
462 let keystore = load_wallet(&ctx.wallet_path)?;
463 let provider = Provider::connect(&ctx.node_url).await?;
464 new_at_index(&keystore, &ctx.wallet_path, account_ix, &provider)?;
465 Ok(())
466}
467
468pub(crate) async fn new_cli(ctx: &crate::CliContext) -> Result<()> {
469 let keystore = load_wallet(&ctx.wallet_path)?;
470 let addresses = read_cached_addresses(&keystore.crypto.ciphertext)?;
471 let account_ix = next_derivation_index(&addresses);
472 let provider = Provider::connect(&ctx.node_url).await?;
473 new_at_index(&keystore, &ctx.wallet_path, account_ix, &provider)?;
474 Ok(())
475}
476
477pub(crate) fn private_key_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
478 let prompt = format!(
479 "Please enter your wallet password to display account {account_ix}'s private key: "
480 );
481 let password = rpassword::prompt_password(prompt)?;
482 let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
483 let secret_key_string = format!("Secret key for account {account_ix}: {secret_key}\n");
484 display_string_discreetly(&secret_key_string, "### Press any key to complete. ###")?;
485 Ok(())
486}
487
488pub(crate) fn public_key_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
490 let prompt =
491 format!("Please enter your wallet password to display account {account_ix}'s public key: ");
492 let password = rpassword::prompt_password(prompt)?;
493 let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
494 let public_key = PublicKey::from(&secret_key);
495 println!("Public key for account {account_ix}: {public_key}");
496 Ok(())
497}
498
499pub(crate) fn hex_address_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
501 let prompt = format!(
502 "Please enter your wallet password to display account {account_ix}'s plain address: "
503 );
504 let password = rpassword::prompt_password(prompt)?;
505 let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
506 let public_key = PublicKey::from(&secret_key);
507 let hashed = public_key.hash();
508 let bech = Bech32Address::new(FUEL_BECH32_HRP, hashed);
509 let plain_address: Address = bech.into();
510 println!("Plain address for {}: {}", account_ix, plain_address);
511 Ok(())
512}
513
514pub(crate) async fn transfer_cli(
516 ctx: &crate::CliContext,
517 acc_ix: usize,
518 transfer: Transfer,
519) -> Result<()> {
520 use fuels::accounts::Account;
521
522 println!(
523 "Preparing to transfer:\n Amount: {}\n Asset ID: 0x{}\n To: {}\n",
524 transfer.amount, transfer.asset_id, transfer.to
525 );
526 let provider = Provider::connect(&ctx.node_url).await?;
527
528 let to = match transfer.to {
529 To::Bech32Address(bech32_addr) => bech32_addr,
530 To::HexAddress(hex_addr) => {
531 let addr = checksum_encode(&format!("0x{hex_addr}"))?;
536 let to_addr = fuels::types::Bytes32::from_str(&addr).map_err(|e| anyhow!("{e}"))?;
537 if !provider.is_user_account(to_addr).await? {
538 bail!(format!("{addr} is not a user account. Aborting transfer."))
539 }
540 Bech32Address::from(hex_addr)
541 }
542 };
543
544 let prompt = format!(
545 "Please enter your wallet password to unlock account {acc_ix} and to initiate transfer: "
546 );
547 let password = rpassword::prompt_password(prompt)?;
548 let mut account = derive_account_unlocked(&ctx.wallet_path, acc_ix, &password, &provider)?;
549 account.set_provider(provider);
550 println!("Transferring...");
551
552 let tx_response = account
553 .transfer(
554 &to,
555 transfer.amount,
556 transfer.asset_id,
557 TxPolicies::new(
558 transfer.gas_price,
559 None,
560 transfer.maturity,
561 None,
562 None,
563 transfer.gas_limit,
564 ),
565 )
566 .await?;
567
568 let block_explorer_url = match ctx.node_url.host_str() {
569 host if host == crate::network::MAINNET.parse::<Url>().unwrap().host_str() => {
570 crate::explorer::DEFAULT
571 }
572 host if host == crate::network::TESTNET.parse::<Url>().unwrap().host_str() => {
573 crate::explorer::TESTNET
574 }
575 _ => "",
576 };
577
578 let tx_explorer_url = format!("{block_explorer_url}/tx/0x{}", tx_response.tx_id);
579 println!(
580 "\nTransfer complete!\nSummary:\n Transaction ID: 0x{}\n Receipts: {:#?}\n Explorer: {}\n",
581 tx_response.tx_id,
582 tx_response.tx_status.receipts,
583 tx_explorer_url
584 );
585
586 Ok(())
587}
588
589fn address_cache_dir_name(wallet_ciphertext: &[u8]) -> String {
591 use std::hash::{Hash, Hasher};
592 let hasher = &mut std::collections::hash_map::DefaultHasher::default();
593 wallet_ciphertext.iter().for_each(|byte| byte.hash(hasher));
594 let hash = hasher.finish();
595 format!("{hash:x}")
596}
597
598fn address_cache_dir(wallet_ciphertext: &[u8]) -> PathBuf {
600 user_fuel_wallets_accounts_dir().join(address_cache_dir_name(wallet_ciphertext))
601}
602
603fn address_path(wallet_ciphertext: &[u8], account_ix: usize) -> PathBuf {
605 address_cache_dir(wallet_ciphertext).join(format!("{account_ix}"))
606}
607
608pub fn cache_address(
610 wallet_ciphertext: &[u8],
611 account_ix: usize,
612 account_addr: &Bech32Address,
613) -> Result<()> {
614 let path = address_path(wallet_ciphertext, account_ix);
615 if path.exists() && !path.is_file() {
616 bail!("attempting to cache account address to {path:?}, but the path is a directory");
617 }
618 let parent = path
619 .parent()
620 .expect("account address path contained no parent directory");
621 fs::create_dir_all(parent).context("failed to create account address cache directory")?;
622 fs::write(path, account_addr.to_string()).context("failed to cache account address to file")?;
623 Ok(())
624}
625
626pub(crate) fn read_cached_addresses(wallet_ciphertext: &[u8]) -> Result<AccountAddresses> {
628 let wallet_accounts_dir = address_cache_dir(wallet_ciphertext);
629 if !wallet_accounts_dir.exists() {
630 return Ok(Default::default());
631 }
632 fs::read_dir(&wallet_accounts_dir)
633 .context("failed to read account address cache")?
634 .map(|res| {
635 let entry = res.context("failed to read account address cache")?;
636 let path = entry.path();
637 let file_name = path
638 .file_name()
639 .and_then(|os_str| os_str.to_str())
640 .ok_or_else(|| anyhow!("failed to read utf8 file name from {path:?}"))?;
641 let account_ix: usize = file_name
642 .parse()
643 .context("failed to parse account index from file name")?;
644 let account_addr_str = std::fs::read_to_string(&path)
645 .context("failed to read account address from cache")?;
646 let account_addr_bech32: Bech32Address = account_addr_str
647 .parse()
648 .context("failed to parse cached account address as a bech32 address")?;
649 let account_addr: Address = account_addr_bech32.into();
650 Ok((account_ix, account_addr))
651 })
652 .collect()
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::utils::test_utils::{
659 mock_provider, with_tmp_dir_and_wallet, TEST_MNEMONIC, TEST_PASSWORD,
660 };
661 use crate::utils::write_wallet_from_mnemonic_and_password;
662 use fuels::types::Address;
663
664 #[tokio::test]
665 async fn create_new_account() {
666 let mock_provider = mock_provider().await;
667
668 let tmp_dir = tempfile::TempDir::new().unwrap();
669 let wallet_path = tmp_dir.path().join("wallet.json");
670 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
671 .unwrap();
672
673 let wallet = derive_account_unlocked(&wallet_path, 0, TEST_PASSWORD, &mock_provider)
674 .expect("wallet unlocked");
675 let wallet_addr = wallet.address();
676 let wallet_addr_str = wallet_addr.to_string();
677 assert_eq!(
678 wallet_addr_str,
679 "fuel1j9zsg4yt45adrcky3xlr4a5rah5ync5xhms2xjtyfm0teyfx000q94t6el"
680 );
681 let wallet_hash = wallet_addr.hash();
682 assert_eq!(
683 wallet_hash.to_string(),
684 "914504548bad3ad1e2c489be3af683ede849e286bee0a349644edebc91267bde"
685 );
686 }
687
688 #[test]
689 fn derive_account_by_index() {
690 with_tmp_dir_and_wallet(|_dir, wallet_path| {
691 let account_ix = 0;
693 let private_key = derive_secret_key(wallet_path, account_ix, TEST_PASSWORD).unwrap();
694 assert_eq!(
695 private_key.to_string(),
696 "961bf9754dd036dd13b1d543b3c0f74062bc4ac668ea89d38ce8d712c591f5cf"
697 )
698 });
699 }
700 #[test]
701 fn derive_plain_address() {
702 let address = "fuel1j78es08cyyz5n75jugal7p759ccs323etnykzpndsvhzu6399yqqpjmmd2";
703 let bech32 = <fuels::types::bech32::Bech32Address as std::str::FromStr>::from_str(address)
704 .expect("failed to create Bech32 address from string");
705 let plain_address: Address = bech32.into();
706 assert_eq!(
707 <Address as std::str::FromStr>::from_str(
708 "978f983cf8210549fa92e23bff07d42e3108aa395cc961066d832e2e6a252900"
709 )
710 .expect("RIP"),
711 plain_address
712 )
713 }
714}