diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 04c5db7a36..a981aa27e5 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -14,7 +14,7 @@ use iota_sdk::{ address::{AccountAddress, Bech32Address, ToBech32Ext}, mana::ManaAllotment, output::{ - feature::{BlockIssuerKeySource, MetadataFeature}, + feature::{BlockIssuerKeySource, Ed25519PublicKeyHashBlockIssuerKey, MetadataFeature}, unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder, DelegationId, FoundryId, NativeToken, NativeTokensBuilder, NftId, Output, OutputId, TokenId, @@ -26,8 +26,8 @@ use iota_sdk::{ utils::ConvertTo, wallet::{ types::OutputData, BeginStakingParams, ConsolidationParams, CreateDelegationParams, CreateNativeTokenParams, - MintNftParams, OutputsToClaim, ReturnStrategy, SendManaParams, SendNativeTokenParams, SendNftParams, - SendParams, SyncOptions, Wallet, WalletError, + MintNftParams, ModifyAccountBlockIssuerKey, OutputsToClaim, ReturnStrategy, SendManaParams, + SendNativeTokenParams, SendNftParams, SendParams, SyncOptions, Wallet, WalletError, }, U256, }; @@ -196,6 +196,22 @@ pub enum WalletCommand { }, /// Lists the implicit accounts of the wallet. ImplicitAccounts, + /// Adds a block issuer key to an account. + AddBlockIssuerKey { + /// The account to which the key should be added. + account_id: AccountId, + /// The hex-encoded public key to add. + // TODO: Use the actual type somehow? + block_issuer_key: String, + }, + /// Removes a block issuer key from an account. + RemoveBlockIssuerKey { + /// The account from which the key should be removed. + account_id: AccountId, + /// The hex-encoded public key to remove. + // TODO: Use the actual type somehow? + block_issuer_key: String, + }, /// Mint additional native tokens. MintNativeToken { /// Token ID to be minted, e.g. 0x087d205988b733d97fb145ae340e27a8b19554d1ceee64574d7e5ff66c45f69e7a0100000000. @@ -924,6 +940,46 @@ pub async fn implicit_accounts_command(wallet: &Wallet) -> Result<(), Error> { Ok(()) } +// `add-block-issuer-key` command +pub async fn add_block_issuer_key(wallet: &Wallet, account_id: AccountId, issuer_key: &str) -> Result<(), Error> { + let issuer_key: [u8; Ed25519PublicKeyHashBlockIssuerKey::LENGTH] = prefix_hex::decode(issuer_key)?; + let params = ModifyAccountBlockIssuerKey { + account_id, + keys_to_add: vec![Ed25519PublicKeyHashBlockIssuerKey::new(issuer_key).into()], + keys_to_remove: vec![], + }; + + let transaction = wallet.modify_account_output_block_issuer_keys(params, None).await?; + + println_log_info!( + "Block issuer key adding transaction sent:\n{:?}\n{:?}", + transaction.transaction_id, + transaction.block_id + ); + + Ok(()) +} + +// `remove-block-issuer-key` command +pub async fn remove_block_issuer_key(wallet: &Wallet, account_id: AccountId, issuer_key: &str) -> Result<(), Error> { + let issuer_key: [u8; Ed25519PublicKeyHashBlockIssuerKey::LENGTH] = prefix_hex::decode(issuer_key)?; + let params = ModifyAccountBlockIssuerKey { + account_id, + keys_to_add: vec![], + keys_to_remove: vec![Ed25519PublicKeyHashBlockIssuerKey::new(issuer_key).into()], + }; + + let transaction = wallet.modify_account_output_block_issuer_keys(params, None).await?; + + println_log_info!( + "Block issuer key removing transaction sent:\n{:?}\n{:?}", + transaction.transaction_id, + transaction.block_id + ); + + Ok(()) +} + // `melt-native-token` command pub async fn melt_native_token_command(wallet: &Wallet, token_id: TokenId, amount: U256) -> Result<(), Error> { let transaction = wallet.melt_native_token(token_id, amount, None).await?; @@ -1580,6 +1636,20 @@ pub async fn prompt_internal( implicit_account_transition_command(wallet, output_id).await } WalletCommand::ImplicitAccounts => implicit_accounts_command(wallet).await, + WalletCommand::AddBlockIssuerKey { + account_id, + block_issuer_key, + } => { + ensure_password(wallet).await?; + add_block_issuer_key(wallet, account_id, &block_issuer_key).await + } + WalletCommand::RemoveBlockIssuerKey { + account_id, + block_issuer_key, + } => { + ensure_password(wallet).await?; + remove_block_issuer_key(wallet, account_id, &block_issuer_key).await + } WalletCommand::MeltNativeToken { token_id, amount } => { ensure_password(wallet).await?; melt_native_token_command(wallet, token_id, amount).await diff --git a/sdk/src/client/api/block_builder/transaction_builder/error.rs b/sdk/src/client/api/block_builder/transaction_builder/error.rs index 8c3f50e910..22f76bd746 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/error.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/error.rs @@ -82,6 +82,8 @@ pub enum TransactionBuilderError { NoAvailableInputsProvided, #[error("account {0} is not staking")] NotStaking(AccountId), + #[error("account {0} has no block issuer feature")] + MissingBlockIssuerFeature(AccountId), /// Required input is not available. #[error("required input {0} is not available")] RequiredInputIsNotAvailable(OutputId), diff --git a/sdk/src/client/api/block_builder/transaction_builder/mod.rs b/sdk/src/client/api/block_builder/transaction_builder/mod.rs index 3e538a0960..78d32a3d13 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/mod.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/mod.rs @@ -321,6 +321,13 @@ impl TransactionBuilder { // Gets requirements from burn. self.burn_requirements()?; + // Add requirements from transitions. + if let Some(transitions) = &self.transitions { + for account_id in transitions.accounts().keys() { + self.requirements.push(Requirement::Account(*account_id)); + } + } + Ok(()) } @@ -331,7 +338,10 @@ impl TransactionBuilder { // If burn or mana allotments are provided, outputs will be added later, in the other cases it will just // create remainder outputs. if !self.provided_outputs.is_empty() - || (self.burn.is_none() && self.mana_allotments.is_empty() && self.required_inputs.is_empty()) + || (self.burn.is_none() + && self.mana_allotments.is_empty() + && self.required_inputs.is_empty() + && self.transitions.is_none()) { return Err(TransactionBuilderError::InvalidOutputCount(self.provided_outputs.len())); } diff --git a/sdk/src/client/api/block_builder/transaction_builder/transition.rs b/sdk/src/client/api/block_builder/transaction_builder/transition.rs index 8137dabb62..f7cb4eef02 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/transition.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/transition.rs @@ -69,6 +69,8 @@ impl TransactionBuilder { .cloned() .collect::>(); + let mut new_amount = None; + if let Some(change) = self.transitions.as_ref().and_then(|t| t.accounts.get(&account_id)) { match change { AccountChange::BeginStaking { @@ -83,6 +85,7 @@ impl TransactionBuilder { self.protocol_parameters .past_bounded_slot(self.latest_slot_commitment_id), ); + new_amount = Some(*staked_amount); features.push( StakingFeature::new( *staked_amount, @@ -101,6 +104,7 @@ impl TransactionBuilder { .protocol_parameters .future_bounded_epoch(self.latest_slot_commitment_id); let staking_feature = feature.as_staking(); + new_amount = Some(staking_feature.staked_amount()); // Just extend the end epoch if it's still possible if future_bounded_epoch <= staking_feature.end_epoch() { *feature = StakingFeature::new( @@ -140,15 +144,36 @@ impl TransactionBuilder { } features.retain(|f| !f.is_staking()); } + AccountChange::ModifyBlockIssuerKeys { + keys_to_add, + keys_to_remove, + } => { + if let Some(feature) = features.iter_mut().find(|f| f.is_block_issuer()) { + let block_issuer_feature = feature.as_block_issuer(); + let updated_keys = block_issuer_feature + .block_issuer_keys() + .iter() + .filter(|k| !keys_to_remove.contains(k)) + .chain(keys_to_add) + .cloned() + .collect::>(); + *feature = BlockIssuerFeature::new(block_issuer_feature.expiry_slot(), updated_keys)?.into(); + } else { + return Err(TransactionBuilderError::MissingBlockIssuerFeature(account_id)); + } + } } } let mut builder = AccountOutputBuilder::from(input) - .with_minimum_amount(self.protocol_parameters.storage_score_parameters()) .with_mana(0) .with_account_id(account_id) .with_foundry_counter(u32::max(highest_foundry_serial_number, input.foundry_counter())) .with_features(features); + match new_amount { + Some(amount) => builder = builder.with_amount(amount), + None => builder = builder.with_minimum_amount(self.protocol_parameters.storage_score_parameters()), + } // Block issuers cannot move their mana elsewhere. if input.is_block_issuer() { @@ -308,6 +333,12 @@ pub enum AccountChange { additional_epochs: u32, }, EndStaking, + ModifyBlockIssuerKeys { + /// The keys that will be added. + keys_to_add: Vec, + /// The keys that will be removed. + keys_to_remove: Vec, + }, } /// A type to specify intended transitions. diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 8b704305ab..746f18c39c 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -51,6 +51,7 @@ pub use self::{ }, transaction::{ high_level::{ + account_block_issuer_keys::ModifyAccountBlockIssuerKey, create_account::CreateAccountParams, delegation::create::{ CreateDelegationParams, CreateDelegationTransaction, PreparedCreateDelegationTransaction, diff --git a/sdk/src/wallet/operations/transaction/high_level/account_block_issuer_keys.rs b/sdk/src/wallet/operations/transaction/high_level/account_block_issuer_keys.rs new file mode 100644 index 0000000000..e36eaa4bb7 --- /dev/null +++ b/sdk/src/wallet/operations/transaction/high_level/account_block_issuer_keys.rs @@ -0,0 +1,77 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::{ + client::{ + api::{ + transaction_builder::{transition::AccountChange, Transitions}, + PreparedTransactionData, + }, + secret::SecretManage, + ClientError, + }, + types::block::output::{feature::BlockIssuerKey, AccountId}, + wallet::{operations::transaction::TransactionOptions, types::TransactionWithMetadata, Wallet, WalletError}, +}; + +/// Params for `modify_account_output_block_issuer_keys()` +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModifyAccountBlockIssuerKey { + pub account_id: AccountId, + /// The keys that will be added. + pub keys_to_add: Vec, + /// The keys that will be removed. + pub keys_to_remove: Vec, +} + +impl Wallet +where + WalletError: From, + ClientError: From, +{ + pub async fn modify_account_output_block_issuer_keys( + &self, + params: ModifyAccountBlockIssuerKey, + options: impl Into> + Send, + ) -> Result { + let options = options.into(); + let prepared_transaction = self + .prepare_modify_account_output_block_issuer_keys(params, options.clone()) + .await?; + + self.sign_and_submit_transaction(prepared_transaction, options).await + } + + /// Prepares the transaction for [Wallet::create_account_output()]. + pub async fn prepare_modify_account_output_block_issuer_keys( + &self, + params: ModifyAccountBlockIssuerKey, + options: impl Into> + Send, + ) -> Result { + log::debug!("[TRANSACTION] prepare_modify_account_output_block_issuer_keys"); + + let change = AccountChange::ModifyBlockIssuerKeys { + keys_to_add: params.keys_to_add, + keys_to_remove: params.keys_to_remove, + }; + + let account_id = params.account_id; + + let mut options = options.into(); + if let Some(options) = options.as_mut() { + if let Some(transitions) = options.transitions.take() { + options.transitions = Some(transitions.add_account(account_id, change)); + } + } else { + options.replace(TransactionOptions { + transitions: Some(Transitions::new().add_account(account_id, change)), + ..Default::default() + }); + } + + self.prepare_send_outputs(None, options).await + } +} diff --git a/sdk/src/wallet/operations/transaction/high_level/create_account.rs b/sdk/src/wallet/operations/transaction/high_level/create_account.rs index 9974fa7ac9..6bf95e24c6 100644 --- a/sdk/src/wallet/operations/transaction/high_level/create_account.rs +++ b/sdk/src/wallet/operations/transaction/high_level/create_account.rs @@ -18,7 +18,7 @@ use crate::{ }, }; -/// Params `create_account_output()` +/// Params for `create_account_output()` #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAccountParams { diff --git a/sdk/src/wallet/operations/transaction/high_level/mod.rs b/sdk/src/wallet/operations/transaction/high_level/mod.rs index 0312a91efe..5f0189596d 100644 --- a/sdk/src/wallet/operations/transaction/high_level/mod.rs +++ b/sdk/src/wallet/operations/transaction/high_level/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod account_block_issuer_keys; pub(crate) mod allot_mana; pub(crate) mod burning_melting; pub(crate) mod create_account;