use clap::ValueEnum;
use miden_client::Client;
use miden_client::account::AccountId;
use miden_client::address::{Address, AddressId, AddressInterface, NetworkId, RoutingParameters};
use crate::errors::CliError;
use crate::utils::parse_account_id;
use crate::{Parser, Subcommand, create_dynamic_table};
#[derive(Debug, Clone, ValueEnum)]
pub enum CliAddressInterface {
BasicWallet,
}
impl From<CliAddressInterface> for AddressInterface {
fn from(value: CliAddressInterface) -> Self {
match value {
CliAddressInterface::BasicWallet => AddressInterface::BasicWallet,
}
}
}
#[derive(Debug, Subcommand, Clone)]
pub enum AddressSubCommand {
List { account_id: Option<String> },
Add {
account_id: String,
address: String,
},
Remove {
account_id: String,
address: String,
},
Encode {
account_id: String,
#[arg(value_enum)]
interface: CliAddressInterface,
tag_len: Option<u8>,
},
}
#[derive(Debug, Parser, Clone)]
#[command(about = "Manage account addresses")]
pub struct AddressCmd {
#[clap(subcommand)]
command: Option<AddressSubCommand>,
}
impl AddressCmd {
pub async fn execute<AUTH>(&self, client: Client<AUTH>) -> Result<(), CliError> {
let network_id = client.network_id().await?;
match &self.command {
Some(AddressSubCommand::List { account_id: Some(account_id) }) => {
list_account_addresses(client, account_id, network_id).await?;
},
Some(AddressSubCommand::Add { account_id, address }) => {
add_address(client, account_id.clone(), address.clone(), network_id).await?;
},
Some(AddressSubCommand::Remove { account_id, address }) => {
remove_address(client, account_id.clone(), address.clone(), network_id).await?;
},
Some(AddressSubCommand::Encode { account_id, interface, tag_len }) => {
encode_address(client, account_id, interface.clone(), *tag_len, network_id).await?;
},
_ => {
list_all_addresses(client, network_id).await?;
},
}
Ok(())
}
}
fn print_account_addresses(account_id: &String, addresses: &Vec<Address>, network_id: &NetworkId) {
println!("Addresses for AccountId {account_id}:");
let mut table = create_dynamic_table(&["Address", "Interface"]);
for address in addresses {
let address_bech32 = address.encode(network_id.clone());
let interface = match address.interface() {
Some(interface) => interface.to_string(),
None => "Unspecified".to_string(),
};
table.add_row(vec![address_bech32, interface]);
}
println!("{table}");
}
async fn list_all_addresses<AUTH>(
client: Client<AUTH>,
network_id: NetworkId,
) -> Result<(), CliError> {
println!("Listing addresses for all accounts:\n");
let accounts = client.get_account_headers().await?;
for (acc_header, _) in accounts {
let addresses = client
.account_reader(acc_header.id())
.addresses()
.await
.expect("account is expected to exist if retrieved");
print_account_addresses(&acc_header.id().to_string(), &addresses, &network_id);
println!();
}
Ok(())
}
async fn list_account_addresses<AUTH>(
client: Client<AUTH>,
account_id: &String,
network_id: NetworkId,
) -> Result<(), CliError> {
let id = parse_account_id(&client, account_id).await?;
let addresses = client.account_reader(id).addresses().await.map_err(|_| {
CliError::Input(format!("The account with id `{account_id}` does not exist"))
})?;
print_account_addresses(&id.to_hex(), &addresses, &network_id);
Ok(())
}
async fn add_address<AUTH>(
mut client: Client<AUTH>,
account_id: String,
address: String,
network_id: NetworkId,
) -> Result<(), CliError> {
let account_id = parse_account_id(&client, &account_id).await?;
let address = decode_account_address(&address, account_id, &network_id)?;
let note_tag = address.to_note_tag();
client.add_address(address, account_id).await?;
println!("Address added: Account Id {account_id} - Note tag: {note_tag}");
Ok(())
}
async fn remove_address<AUTH>(
mut client: Client<AUTH>,
account_id: String,
address: String,
network_id: NetworkId,
) -> Result<(), CliError> {
let account_id = parse_account_id(&client, &account_id).await?;
let address = decode_account_address(&address, account_id, &network_id)?;
let note_tag = address.to_note_tag();
println!("removing address - Account Id {account_id} - Note tag: {note_tag}");
client.remove_address(address, account_id).await?;
Ok(())
}
async fn encode_address<AUTH>(
client: Client<AUTH>,
account_id: &str,
interface: CliAddressInterface,
tag_len: Option<u8>,
network_id: NetworkId,
) -> Result<(), CliError> {
let account_id = parse_account_id(&client, account_id).await?;
let interface = interface.into();
let routing_params = match tag_len {
Some(tag_len) => RoutingParameters::new(interface)
.with_note_tag_len(tag_len)
.map_err(|e| CliError::Address(e, String::new()))?,
None => RoutingParameters::new(interface),
};
let address = Address::new(account_id).with_routing_parameters(routing_params);
println!("{}", address.encode(network_id));
Ok(())
}
fn decode_account_address(
encoded: &str,
expected_account_id: AccountId,
expected_network_id: &NetworkId,
) -> Result<Address, CliError> {
let (decoded_network_id, address) =
Address::decode(encoded).map_err(|e| CliError::Address(e, encoded.to_string()))?;
let AddressId::AccountId(address_account_id) = address.id() else {
return Err(CliError::Input(
"Address is not account-ID-based; only account-ID addresses can be tracked".to_string(),
));
};
if address_account_id != expected_account_id {
return Err(CliError::Input(format!(
"Address encodes account ID `{}` which does not match the provided account ID `{}`",
address_account_id.to_hex(),
expected_account_id.to_hex(),
)));
}
if &decoded_network_id != expected_network_id {
return Err(CliError::Input(format!(
"Address network `{decoded_network_id}` does not match configured network `{expected_network_id}`",
)));
}
Ok(address)
}