forc-client 0.71.3

A `forc` plugin for interacting with a Fuel node.
use crate::{
    constants::DEFAULT_PRIVATE_KEY,
    util::{account::ForcClientAccount, aws::AwsSigner, target::Target},
};
use anyhow::Result;
use dialoguer::{theme::ColorfulTheme, Confirm, Password, Select};
use forc_tracing::{println_action_green, println_warning};
use forc_wallet::{
    account::{derive_secret_key, new_at_index_cli},
    balance::{collect_accounts_with_verification, AccountBalances, AccountVerification},
    import::{import_wallet_cli, Import},
    new::{new_wallet_cli, New},
    utils::default_wallet_path,
};
use fuel_crypto::SecretKey;
use fuel_tx::{AssetId, ContractId};
use fuels::{
    macros::abigen, programs::responses::CallResponse, types::checksum_address::checksum_encode,
};
use fuels_accounts::{
    provider::Provider,
    signers::private_key::PrivateKeySigner,
    wallet::{Unlocked, Wallet},
    ViewOnlyAccount,
};

use std::{collections::BTreeMap, path::Path, str::FromStr};

use super::aws::{AwsClient, AwsConfig};

type AccountsMap = BTreeMap<usize, fuel_tx::Address>;

#[derive(PartialEq, Eq)]
pub enum SignerSelectionMode {
    /// Holds the password of forc-wallet instance.
    ForcWallet(String),
    /// Holds ARN of the AWS signer.
    AwsSigner(String),
    Manual,
}

fn ask_user_yes_no_question(question: &str) -> Result<bool> {
    let answer = Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt(question)
        .default(false)
        .show_default(false)
        .interact()?;
    Ok(answer)
}

fn ask_user_with_options(question: &str, options: &[&str], default: usize) -> Result<usize> {
    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt(question)
        .items(options)
        .default(default)
        .interact()?;
    Ok(selection)
}

async fn collect_user_accounts(
    wallet_path: &Path,
    password: &str,
    node_url: &str,
) -> Result<AccountsMap> {
    let verification = AccountVerification::Yes(password.to_string());
    let node_url = reqwest::Url::parse(node_url)
        .map_err(|e| anyhow::anyhow!("Failed to parse node URL: {}", e))?;
    let accounts = collect_accounts_with_verification(wallet_path, verification, &node_url)
        .await
        .map_err(|e| {
            if e.to_string().contains("Mac Mismatch") {
                anyhow::anyhow!("Failed to access forc-wallet vault. Please check your password")
            } else {
                e
            }
        })?;
    let accounts = accounts
        .into_iter()
        .map(|(index, address)| {
            let bytes: [u8; fuel_tx::Address::LEN] = address.into();
            (index, fuel_tx::Address::from(bytes))
        })
        .collect();
    Ok(accounts)
}

pub(crate) fn prompt_forc_wallet_password() -> Result<String> {
    let password = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("Wallet password")
        .allow_empty_password(true)
        .interact()?;

    Ok(password)
}

pub(crate) async fn check_and_create_wallet_at_default_path(wallet_path: &Path) -> Result<()> {
    if !wallet_path.exists() {
        let question =
            format!("Could not find a wallet at {wallet_path:?}, please select an option: ");
        let wallet_options = ask_user_with_options(
            &question,
            &["Create new wallet", "Import existing wallet"],
            0,
        )?;
        let ctx = forc_wallet::CliContext {
            wallet_path: wallet_path.to_path_buf(),
            node_url: forc_wallet::network::DEFAULT.parse().unwrap(),
        };
        match wallet_options {
            0 => {
                new_wallet_cli(&ctx, New { force: false, cache_accounts: None }).await?;
                println!("Wallet created successfully.");
            }
            1 => {
                import_wallet_cli(&ctx, Import { force: false, cache_accounts: None }).await?;
                println!("Wallet imported successfully.");
            },
            _ => anyhow::bail!("Refused to create or import a new wallet. If you don't want to use forc-wallet, you can sign this transaction manually with --manual-signing flag."),
        }
        // Derive first account for the fresh wallet we created.
        new_at_index_cli(&ctx, 0).await?;
        println!("Account derived successfully.");
    }
    Ok(())
}

pub(crate) fn secret_key_from_forc_wallet(
    wallet_path: &Path,
    account_index: usize,
    password: &str,
) -> Result<SecretKey> {
    let secret_key = derive_secret_key(wallet_path, account_index, password).map_err(|e| {
        if e.to_string().contains("Mac Mismatch") {
            anyhow::anyhow!("Failed to access forc-wallet vault. Please check your password")
        } else {
            e
        }
    })?;
    SecretKey::try_from(secret_key.as_ref())
        .map_err(|e| anyhow::anyhow!("Failed to convert secret key: {e}"))
}

pub(crate) fn select_manual_secret_key(
    default_signer: bool,
    signing_key: Option<SecretKey>,
) -> Option<SecretKey> {
    match (default_signer, signing_key) {
        // Note: unwrap is safe here as we already know that 'DEFAULT_PRIVATE_KEY' is a valid private key.
        (true, None) => Some(SecretKey::from_str(DEFAULT_PRIVATE_KEY).unwrap()),
        (true, Some(signing_key)) => {
            println_warning("Signing key is provided while requesting to sign with a default signer. Using signing key");
            Some(signing_key)
        }
        (false, None) => None,
        (false, Some(signing_key)) => Some(signing_key),
    }
}

/// Collect and return balances of each account in the accounts map.
async fn collect_account_balances(
    accounts_map: &AccountsMap,
    provider: &Provider,
) -> Result<AccountBalances> {
    let accounts: Vec<_> = accounts_map
        .values()
        .map(|addr| Wallet::new_locked(*addr, provider.clone()))
        .collect();

    futures::future::try_join_all(accounts.iter().map(|acc| acc.get_balances()))
        .await
        .map_err(|e| anyhow::anyhow!("{e}"))
}

/// Format collected account balances for each asset type, including only the balance of the base asset that can be used to pay gas.
pub fn format_base_asset_account_balances(
    accounts_map: &AccountsMap,
    account_balances: &AccountBalances,
    base_asset_id: &AssetId,
) -> Result<Vec<String>> {
    accounts_map
        .iter()
        .zip(account_balances)
        .map(|((ix, address), balance)| {
            let base_asset_amount = balance
                .get(&base_asset_id.to_string())
                .copied()
                .unwrap_or(0);
            let raw_addr = format!("0x{address}");
            let checksum_addr = checksum_encode(&raw_addr)?;
            let eth_amount = base_asset_amount as f64 / 1_000_000_000.0;
            Ok(format!("[{ix}] {checksum_addr} - {eth_amount} ETH"))
        })
        .collect::<Result<Vec<_>>>()
}

// TODO: Simplify the function signature once https://github.com/FuelLabs/sway/issues/6071 is closed.
pub(crate) async fn select_account(
    wallet_mode: &SignerSelectionMode,
    default_sign: bool,
    signing_key: Option<SecretKey>,
    provider: &Provider,
    tx_count: usize,
) -> Result<ForcClientAccount> {
    let chain_info = provider.chain_info().await?;
    match wallet_mode {
        SignerSelectionMode::ForcWallet(password) => {
            let wallet_path = default_wallet_path();
            let accounts = collect_user_accounts(&wallet_path, password, provider.url()).await?;
            let account_balances = collect_account_balances(&accounts, provider).await?;

            let total_balance = account_balances
                .iter()
                .flat_map(|account| account.values())
                .sum::<u128>();
            if total_balance == 0 {
                let first_account = accounts
                    .get(&0)
                    .ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))?;
                let target = Target::from_str(&chain_info.name).unwrap_or_default();
                let message = if let Some(faucet_url) = target.faucet_url() {
                    format!(
                        "Your wallet does not have any funds to pay for the transaction.\
                        \n\nIf you are interacting with a testnet, consider using the faucet.\
                        \n-> {target} network faucet: {faucet_url}/?address={first_account}\
                        \nIf you are interacting with a local node, consider providing a chainConfig which funds your account."
                    )
                } else {
                    "Your wallet does not have any funds to pay for the transaction.".to_string()
                };
                anyhow::bail!(message)
            }

            // TODO: Do this via forc-wallet once the functionality is exposed.
            // TODO: calculate the number of transactions to sign and ask the user to confirm.
            let question = format!(
                "Do you agree to sign {tx_count} transaction{}?",
                if tx_count > 1 { "s" } else { "" }
            );
            let accepted = ask_user_yes_no_question(&question)?;
            if !accepted {
                anyhow::bail!("User refused to sign");
            }

            let wallet = select_local_wallet_account(password, provider).await?;
            Ok(ForcClientAccount::Wallet(wallet))
        }
        SignerSelectionMode::Manual => {
            let secret_key = select_manual_secret_key(default_sign, signing_key)
                .ok_or_else(|| anyhow::anyhow!("missing manual secret key"))?;
            let signer = PrivateKeySigner::new(secret_key);
            let wallet = Wallet::new(signer, provider.clone());
            Ok(ForcClientAccount::Wallet(wallet))
        }
        SignerSelectionMode::AwsSigner(arn) => {
            let aws_config = AwsConfig::from_env().await;
            let aws_client = AwsClient::new(aws_config);
            let aws_signer = AwsSigner::new(aws_client, arn.clone(), provider.clone()).await?;

            let account = ForcClientAccount::KmsSigner(aws_signer);
            Ok(account)
        }
    }
}

pub(crate) async fn select_local_wallet_account(
    password: &str,
    provider: &Provider,
) -> Result<Wallet<Unlocked<PrivateKeySigner>>> {
    let wallet_path = default_wallet_path();
    let accounts = collect_user_accounts(&wallet_path, password, provider.url()).await?;
    let account_balances = collect_account_balances(&accounts, provider).await?;
    let consensus_parameters = provider.consensus_parameters().await?;
    let base_asset_id = consensus_parameters.base_asset_id();
    let selections =
        format_base_asset_account_balances(&accounts, &account_balances, base_asset_id)?;

    let mut account_index;
    loop {
        account_index = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("Wallet account")
            .max_length(5)
            .items(&selections[..])
            .default(0)
            .interact()?;

        if accounts.contains_key(&account_index) {
            break;
        }
        let options: Vec<String> = accounts
            .keys()
            .map(|key| {
                let raw_addr = format!("0x{key}");
                let checksum_addr = checksum_encode(&raw_addr)?;
                Ok(checksum_addr)
            })
            .collect::<Result<Vec<_>>>()?;
        println_warning(&format!(
            "\"{}\" is not a valid account.\nPlease choose a valid option from {}",
            account_index,
            options.join(","),
        ));
    }

    let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, password)?;
    let signer = PrivateKeySigner::new(secret_key);
    let wallet = Wallet::new(signer, provider.clone());
    Ok(wallet)
}

pub async fn update_proxy_contract_target(
    account: &ForcClientAccount,
    proxy_contract_id: ContractId,
    new_target: ContractId,
) -> Result<CallResponse<()>> {
    abigen!(Contract(name = "ProxyContract", abi = "{\"programType\":\"contract\",\"specVersion\":\"1.1\",\"encodingVersion\":\"1\",\"concreteTypes\":[{\"type\":\"()\",\"concreteTypeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"type\":\"enum standards::src5::AccessError\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\",\"metadataTypeId\":1},{\"type\":\"enum standards::src5::State\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"metadataTypeId\":2},{\"type\":\"enum std::option::Option<struct std::contract_id::ContractId>\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"metadataTypeId\":4,\"typeArguments\":[\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\",\"metadataTypeId\":5},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\",\"metadataTypeId\":6},{\"type\":\"str\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"},{\"type\":\"struct std::contract_id::ContractId\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\",\"metadataTypeId\":9},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\",\"metadataTypeId\":10},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\",\"metadataTypeId\":11}],\"metadataTypes\":[{\"type\":\"b256\",\"metadataTypeId\":0},{\"type\":\"enum standards::src5::AccessError\",\"metadataTypeId\":1,\"components\":[{\"name\":\"NotOwner\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum standards::src5::State\",\"metadataTypeId\":2,\"components\":[{\"name\":\"Uninitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Initialized\",\"typeId\":3},{\"name\":\"Revoked\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum std::identity::Identity\",\"metadataTypeId\":3,\"components\":[{\"name\":\"Address\",\"typeId\":8},{\"name\":\"ContractId\",\"typeId\":9}]},{\"type\":\"enum std::option::Option\",\"metadataTypeId\":4,\"components\":[{\"name\":\"None\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Some\",\"typeId\":7}],\"typeParameters\":[7]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"metadataTypeId\":5,\"components\":[{\"name\":\"CannotReinitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"metadataTypeId\":6,\"components\":[{\"name\":\"CannotUninitialize\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"generic T\",\"metadataTypeId\":7},{\"type\":\"struct std::address::Address\",\"metadataTypeId\":8,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct std::contract_id::ContractId\",\"metadataTypeId\":9,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"metadataTypeId\":10,\"components\":[{\"name\":\"new_proxy_owner\",\"typeId\":2}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"metadataTypeId\":11,\"components\":[{\"name\":\"new_target\",\"typeId\":9}]}],\"functions\":[{\"inputs\":[],\"name\":\"proxy_target\",\"output\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [Option<ContractId>] - The new proxy contract to which all fallback calls will be passed or `None`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[{\"name\":\"new_target\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"}],\"name\":\"set_proxy_target\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Change the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called by the `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When not called by `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Write: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\",\"write\"]}]},{\"inputs\":[],\"name\":\"proxy_owner\",\"output\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the owner of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [State] - Represents the state of ownership for this contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[],\"name\":\"initialize_proxy\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Initializes the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This then allows methods that write to storage to be called.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called once.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When `storage::SRC14.proxy_owner` is not [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `2`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]},{\"inputs\":[{\"name\":\"new_proxy_owner\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\"}],\"name\":\"set_proxy_owner\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Changes proxy ownership to the passed State.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can be used to transfer ownership between Identities or to revoke ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_proxy_owner`: [State] - The new state of the proxy ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the sender is not the current proxy owner.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the new state of the proxy ownership is [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]}],\"loggedTypes\":[{\"logId\":\"4571204900286667806\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\"},{\"logId\":\"2151606668983994881\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\"},{\"logId\":\"2161305517876418151\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\"},{\"logId\":\"4354576968059844266\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\"},{\"logId\":\"10870989709723147660\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\"},{\"logId\":\"10098701174489624218\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"}],\"messagesTypes\":[],\"configurables\":[{\"name\":\"INITIAL_TARGET\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"offset\":13368},{\"name\":\"INITIAL_OWNER\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"offset\":13320}]}",));

    let proxy_contract = ProxyContract::new(proxy_contract_id, account.clone());

    let result = proxy_contract
        .methods()
        .set_proxy_target(new_target)
        .call()
        .await?;
    println_action_green(
        "Updated",
        &format!("proxy contract target to 0x{new_target}"),
    );
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::{BTreeMap, HashMap};

    #[test]
    fn test_format_base_asset_account_balances() {
        let mut accounts_map: AccountsMap = BTreeMap::new();

        let address1 = fuel_tx::Address::from_str(
            "7bbd8a4ea06e94461b959ab18d35802bbac3cf47e2bf29195f7db2ce41630cd7",
        )
        .expect("address1");

        let address2 = fuel_tx::Address::from_str(
            "99bd8a4ea06e94461b959ab18d35802bbac3cf47e2bf29195f7db2ce41630cd7",
        )
        .expect("address2");

        let base_asset_id = AssetId::zeroed();

        accounts_map.insert(0, address1);
        accounts_map.insert(1, address2);

        let mut account_balances: AccountBalances = Vec::new();
        let mut balance1 = HashMap::new();
        balance1.insert(base_asset_id.to_string(), 1_500_000_000);
        balance1.insert("other_asset".to_string(), 2_000_000_000);
        account_balances.push(balance1);

        let mut balance2 = HashMap::new();
        balance2.insert("other_asset".to_string(), 3_000_000_000);
        account_balances.push(balance2);

        let address1_expected =
            "0x7bBD8a4ea06E94461b959aB18d35802BbAC3cf47e2bF29195F7db2CE41630CD7";
        let address2_expected =
            "0x99Bd8a4eA06E94461b959AB18d35802bBaC3Cf47E2Bf29195f7DB2cE41630cD7";
        let expected = vec![
            format!("[0] {address1_expected} - 1.5 ETH"),
            format!("[1] {address2_expected} - 0 ETH"),
        ];

        let result =
            format_base_asset_account_balances(&accounts_map, &account_balances, &base_asset_id)
                .unwrap();
        assert_eq!(result, expected);
    }
}