From 0e2c8ed75b738d347e419160cd862829104c8b46 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 9 Jan 2024 15:36:07 +0100 Subject: [PATCH 01/14] CLI: better UX for accounts and implicit_accounts commands (#1815) --- cli/src/wallet_cli/mod.rs | 45 +++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index dedccc8d8c..73fdfae5bc 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -13,7 +13,7 @@ use iota_sdk::{ types::{ api::plugins::participation::types::ParticipationEventId, block::{ - address::Bech32Address, + address::{Bech32Address, ToBech32Ext}, output::{ unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder, FoundryId, NativeToken, NativeTokensBuilder, NftId, Output, OutputId, TokenId, @@ -314,7 +314,25 @@ impl FromStr for OutputSelector { // `accounts` command pub async fn accounts_command(wallet: &Wallet) -> Result<(), Error> { - print_outputs(wallet.data().await.accounts().cloned().collect(), "Accounts:") + let wallet_data = wallet.data().await; + let accounts = wallet_data.accounts(); + + println_log_info!("Accounts:\n"); + + for account in accounts { + let output_id = account.output_id; + let account_id = account.output.as_account().account_id_non_null(&output_id); + let account_address = account_id.to_bech32(wallet.client().get_bech32_hrp().await?); + + println_log_info!( + "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n", + "Output ID:", + "Account ID:", + "Account Address:" + ); + } + + Ok(()) } // `address` command @@ -612,10 +630,25 @@ pub async fn implicit_account_transition_command( // `implicit-accounts` command pub async fn implicit_accounts_command(wallet: &Wallet) -> Result<(), Error> { - print_outputs( - wallet.data().await.implicit_accounts().cloned().collect(), - "Implicit accounts:", - ) + let wallet_data = wallet.data().await; + let implicit_accounts = wallet_data.implicit_accounts(); + + println_log_info!("Implicit accounts:\n"); + + for implicit_account in implicit_accounts { + let output_id = implicit_account.output_id; + let account_id = AccountId::from(&output_id); + let account_address = account_id.to_bech32(wallet.client().get_bech32_hrp().await?); + + println_log_info!( + "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n", + "Output ID:", + "Account ID:", + "Account Address:" + ); + } + + Ok(()) } // `melt-native-token` command From 4030148546d5b683916d45889668f58fc6a5abe0 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 9 Jan 2024 16:13:10 +0100 Subject: [PATCH 02/14] CLI: add congestion command (#1819) * CLI: add congestion command * Update cli/src/wallet_cli/mod.rs Co-authored-by: DaughterOfMars * fmt --------- Co-authored-by: DaughterOfMars --- cli/src/wallet_cli/completer.rs | 1 + cli/src/wallet_cli/mod.rs | 34 +++++++++++++++++++++++++++++++-- sdk/src/types/api/core.rs | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cli/src/wallet_cli/completer.rs b/cli/src/wallet_cli/completer.rs index af3bc3a843..b3f316db4f 100644 --- a/cli/src/wallet_cli/completer.rs +++ b/cli/src/wallet_cli/completer.rs @@ -17,6 +17,7 @@ const WALLET_COMMANDS: &[&str] = &[ "claim", "claimable-outputs", "clear", + "congestion", "consolidate", "create-alias-output", "create-native-token", diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 73fdfae5bc..96d6386e7c 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -24,8 +24,8 @@ use iota_sdk::{ }, utils::ConvertTo, wallet::{ - types::OutputData, ConsolidationParams, CreateNativeTokenParams, MintNftParams, OutputsToClaim, - SendNativeTokenParams, SendNftParams, SendParams, SyncOptions, TransactionOptions, Wallet, + types::OutputData, ConsolidationParams, CreateNativeTokenParams, Error as WalletError, MintNftParams, + OutputsToClaim, SendNativeTokenParams, SendNftParams, SendParams, SyncOptions, TransactionOptions, Wallet, }, U256, }; @@ -81,6 +81,8 @@ pub enum WalletCommand { }, /// Print details about claimable outputs - if there are any. ClaimableOutputs, + /// Checks if an account is ready to issue a block. + Congestion { account_id: Option }, /// Consolidate all basic outputs into one address. Consolidate, /// Create a new account output. @@ -478,6 +480,33 @@ pub async fn claimable_outputs_command(wallet: &Wallet) -> Result<(), Error> { Ok(()) } +// `congestion` command +pub async fn congestion_command(wallet: &Wallet, account_id: Option) -> Result<(), Error> { + let account_id = { + let wallet_data = wallet.data().await; + account_id + .or_else(|| { + wallet_data + .accounts() + .next() + .map(|o| o.output.as_account().account_id_non_null(&o.output_id)) + }) + .or_else(|| { + wallet_data + .implicit_accounts() + .next() + .map(|o| AccountId::from(&o.output_id)) + }) + .ok_or(WalletError::NoAccountToIssueBlock)? + }; + + let congestion = wallet.client().get_account_congestion(&account_id).await?; + + println_log_info!("{congestion:#?}"); + + Ok(()) +} + // `consolidate` command pub async fn consolidate_command(wallet: &Wallet) -> Result<(), Error> { println_log_info!("Consolidating outputs."); @@ -1151,6 +1180,7 @@ pub async fn prompt_internal( WalletCommand::BurnNft { nft_id } => burn_nft_command(wallet, nft_id).await, WalletCommand::Claim { output_id } => claim_command(wallet, output_id).await, WalletCommand::ClaimableOutputs => claimable_outputs_command(wallet).await, + WalletCommand::Congestion { account_id } => congestion_command(wallet, account_id).await, WalletCommand::Consolidate => consolidate_command(wallet).await, WalletCommand::CreateAccountOutput => create_account_output_command(wallet).await, WalletCommand::CreateNativeToken { diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index 83f6391c68..804f5f7ca7 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -325,7 +325,7 @@ pub struct CongestionResponse { pub reference_mana_cost: u64, /// The Block Issuance Credits of the requested account. #[serde(with = "string")] - pub block_issuance_credits: u64, + pub block_issuance_credits: i128, } /// Response of POST /api/core/v3/blocks. From 8647df14b197a396a73682c0d811901e3123c14b Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:24:24 +0100 Subject: [PATCH 03/14] Remove try_from_dtos (#1817) * Remove try_from_dtos() * Update output builder in bindings * newlines --------- Co-authored-by: Thibault Martinez --- bindings/core/src/method_handler/client.rs | 114 +++++++++++---------- sdk/src/types/block/address/multi.rs | 2 +- sdk/src/types/block/output/account.rs | 83 --------------- sdk/src/types/block/output/anchor.rs | 83 --------------- sdk/src/types/block/output/basic.rs | 62 ----------- sdk/src/types/block/output/delegation.rs | 43 +------- sdk/src/types/block/output/foundry.rs | 83 +-------------- sdk/src/types/block/output/nft.rs | 79 +------------- 8 files changed, 66 insertions(+), 483 deletions(-) diff --git a/bindings/core/src/method_handler/client.rs b/bindings/core/src/method_handler/client.rs index b5f2ae738f..ca809129f5 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -9,7 +9,7 @@ use iota_sdk::{ api::core::OutputWithMetadataResponse, block::{ output::{ - AccountOutput, BasicOutput, FoundryOutput, MinimumOutputAmount, NftOutput, Output, OutputBuilderAmount, + AccountOutputBuilder, BasicOutputBuilder, FoundryOutputBuilder, MinimumOutputAmount, NftOutputBuilder, }, payload::Payload, Block, BlockDto, UnsignedBlockDto, @@ -64,21 +64,23 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM features, immutable_features, } => { - let output = Output::from(AccountOutput::try_from_dtos( - if let Some(amount) = amount { - OutputBuilderAmount::Amount(amount) - } else { - OutputBuilderAmount::MinimumAmount(client.get_storage_score_parameters().await?) - }, - mana, - &account_id, - foundry_counter, - unlock_conditions, - features, - immutable_features, - )?); + let mut output_builder = if let Some(amount) = amount { + AccountOutputBuilder::new_with_amount(amount, account_id) + } else { + AccountOutputBuilder::new_with_minimum_amount(client.get_storage_score_parameters().await?, account_id) + } + .with_mana(mana) + .with_foundry_counter(foundry_counter) + .with_unlock_conditions(unlock_conditions); + + if let Some(features) = features { + output_builder = output_builder.with_features(features); + } + if let Some(immutable_features) = immutable_features { + output_builder = output_builder.with_immutable_features(immutable_features) + } - Response::Output(output) + Response::Output(output_builder.finish_output()?) } ClientMethod::BuildBasicOutput { amount, @@ -86,18 +88,19 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM unlock_conditions, features, } => { - let output = Output::from(BasicOutput::try_from_dtos( - if let Some(amount) = amount { - OutputBuilderAmount::Amount(amount) - } else { - OutputBuilderAmount::MinimumAmount(client.get_storage_score_parameters().await?) - }, - mana, - unlock_conditions, - features, - )?); + let mut output_builder = if let Some(amount) = amount { + BasicOutputBuilder::new_with_amount(amount) + } else { + BasicOutputBuilder::new_with_minimum_amount(client.get_storage_score_parameters().await?) + } + .with_mana(mana) + .with_unlock_conditions(unlock_conditions); - Response::Output(output) + if let Some(features) = features { + output_builder = output_builder.with_features(features); + } + + Response::Output(output_builder.finish_output()?) } ClientMethod::BuildFoundryOutput { amount, @@ -107,20 +110,25 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM features, immutable_features, } => { - let output = Output::from(FoundryOutput::try_from_dtos( - if let Some(amount) = amount { - OutputBuilderAmount::Amount(amount) - } else { - OutputBuilderAmount::MinimumAmount(client.get_storage_score_parameters().await?) - }, - serial_number, - token_scheme, - unlock_conditions, - features, - immutable_features, - )?); + let mut output_builder = if let Some(amount) = amount { + FoundryOutputBuilder::new_with_amount(amount, serial_number, token_scheme) + } else { + FoundryOutputBuilder::new_with_minimum_amount( + client.get_storage_score_parameters().await?, + serial_number, + token_scheme, + ) + } + .with_unlock_conditions(unlock_conditions); - Response::Output(output) + if let Some(features) = features { + output_builder = output_builder.with_features(features); + } + if let Some(immutable_features) = immutable_features { + output_builder = output_builder.with_immutable_features(immutable_features) + } + + Response::Output(output_builder.finish_output()?) } ClientMethod::BuildNftOutput { amount, @@ -130,20 +138,22 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM features, immutable_features, } => { - let output = Output::from(NftOutput::try_from_dtos( - if let Some(amount) = amount { - OutputBuilderAmount::Amount(amount) - } else { - OutputBuilderAmount::MinimumAmount(client.get_storage_score_parameters().await?) - }, - mana, - &nft_id, - unlock_conditions, - features, - immutable_features, - )?); + let mut output_builder = if let Some(amount) = amount { + NftOutputBuilder::new_with_amount(amount, nft_id) + } else { + NftOutputBuilder::new_with_minimum_amount(client.get_storage_score_parameters().await?, nft_id) + } + .with_mana(mana) + .with_unlock_conditions(unlock_conditions); + + if let Some(features) = features { + output_builder = output_builder.with_features(features); + } + if let Some(immutable_features) = immutable_features { + output_builder = output_builder.with_immutable_features(immutable_features) + } - Response::Output(output) + Response::Output(output_builder.finish_output()?) } ClientMethod::BuildBasicBlock { issuer_id, payload } => { let payload = if let Some(payload) = payload { diff --git a/sdk/src/types/block/address/multi.rs b/sdk/src/types/block/address/multi.rs index 377a11cce2..593f300598 100644 --- a/sdk/src/types/block/address/multi.rs +++ b/sdk/src/types/block/address/multi.rs @@ -128,7 +128,7 @@ impl MultiAddress { pub fn hash(&self) -> [u8; 32] { let mut digest = Blake2b256::new(); - digest.update([MultiAddress::KIND]); + digest.update([Self::KIND]); digest.update(self.pack_to_vec()); digest.finalize().into() diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 942693651c..04f601d233 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -602,47 +602,6 @@ mod dto { } } - impl AccountOutput { - #[allow(clippy::too_many_arguments)] - pub fn try_from_dtos( - amount: OutputBuilderAmount, - mana: u64, - account_id: &AccountId, - foundry_counter: Option, - unlock_conditions: Vec, - features: Option>, - immutable_features: Option>, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => AccountOutputBuilder::new_with_amount(amount, *account_id), - OutputBuilderAmount::MinimumAmount(params) => { - AccountOutputBuilder::new_with_minimum_amount(params, *account_id) - } - } - .with_mana(mana); - - if let Some(foundry_counter) = foundry_counter { - builder = builder.with_foundry_counter(foundry_counter); - } - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - if let Some(features) = features { - builder = builder.with_features(features); - } - - if let Some(immutable_features) = immutable_features { - builder = builder.with_immutable_features(immutable_features); - } - - builder.finish() - } - } - crate::impl_serde_typed_dto!(AccountOutput, AccountOutputDto, "account output"); } @@ -667,47 +626,5 @@ mod tests { let dto = AccountOutputDto::from(&account_output); let output = Output::Account(AccountOutput::try_from(dto).unwrap()); assert_eq!(&account_output, output.as_account()); - - let output_split = AccountOutput::try_from_dtos( - OutputBuilderAmount::Amount(account_output.amount()), - account_output.mana(), - account_output.account_id(), - account_output.foundry_counter().into(), - account_output.unlock_conditions().to_vec(), - Some(account_output.features().to_vec()), - Some(account_output.immutable_features().to_vec()), - ) - .unwrap(); - assert_eq!(account_output, output_split); - - let account_id = rand_account_id(); - let address = rand_address_unlock_condition_different_from_account_id(&account_id); - - let test_split_dto = |builder: AccountOutputBuilder| { - let output_split = AccountOutput::try_from_dtos( - builder.amount, - builder.mana, - &builder.account_id, - builder.foundry_counter, - builder.unlock_conditions.iter().cloned().collect(), - Some(builder.features.iter().cloned().collect()), - Some(builder.immutable_features.iter().cloned().collect()), - ) - .unwrap(); - assert_eq!(builder.finish().unwrap(), output_split); - }; - - let builder = AccountOutput::build_with_amount(100, account_id) - .add_unlock_condition(address.clone()) - .with_features(rand_allowed_features(AccountOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(AccountOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); - - let builder = - AccountOutput::build_with_minimum_amount(protocol_parameters.storage_score_parameters(), account_id) - .add_unlock_condition(address) - .with_features(rand_allowed_features(AccountOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(AccountOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); } } diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index a2d2bcef20..6f5c926934 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -668,44 +668,6 @@ mod dto { } } - impl AnchorOutput { - #[allow(clippy::too_many_arguments)] - pub fn try_from_dtos( - amount: OutputBuilderAmount, - mana: u64, - anchor_id: &AnchorId, - state_index: u32, - unlock_conditions: Vec, - features: Option>, - immutable_features: Option>, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => AnchorOutputBuilder::new_with_amount(amount, *anchor_id), - OutputBuilderAmount::MinimumAmount(params) => { - AnchorOutputBuilder::new_with_minimum_amount(params, *anchor_id) - } - } - .with_mana(mana) - .with_state_index(state_index); - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - if let Some(features) = features { - builder = builder.with_features(features); - } - - if let Some(immutable_features) = immutable_features { - builder = builder.with_immutable_features(immutable_features); - } - - builder.finish() - } - } - crate::impl_serde_typed_dto!(AnchorOutput, AnchorOutputDto, "anchor output"); } @@ -732,50 +694,5 @@ mod tests { let dto = AnchorOutputDto::from(&anchor_output); let output = Output::Anchor(AnchorOutput::try_from(dto).unwrap()); assert_eq!(&anchor_output, output.as_anchor()); - - let output_split = AnchorOutput::try_from_dtos( - OutputBuilderAmount::Amount(output.amount()), - anchor_output.mana(), - anchor_output.anchor_id(), - anchor_output.state_index(), - anchor_output.unlock_conditions().to_vec(), - Some(anchor_output.features().to_vec()), - Some(anchor_output.immutable_features().to_vec()), - ) - .unwrap(); - assert_eq!(anchor_output, output_split); - - let anchor_id = rand_anchor_id(); - let gov_address = rand_governor_address_unlock_condition_different_from(&anchor_id); - let state_address = rand_state_controller_address_unlock_condition_different_from(&anchor_id); - - let test_split_dto = |builder: AnchorOutputBuilder| { - let output_split = AnchorOutput::try_from_dtos( - builder.amount, - builder.mana, - &builder.anchor_id, - builder.state_index, - builder.unlock_conditions.iter().cloned().collect(), - Some(builder.features.iter().cloned().collect()), - Some(builder.immutable_features.iter().cloned().collect()), - ) - .unwrap(); - assert_eq!(builder.finish().unwrap(), output_split); - }; - - let builder = AnchorOutput::build_with_amount(100, anchor_id) - .add_unlock_condition(gov_address.clone()) - .add_unlock_condition(state_address.clone()) - .with_features(rand_allowed_features(AnchorOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(AnchorOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); - - let builder = - AnchorOutput::build_with_minimum_amount(protocol_parameters.storage_score_parameters(), anchor_id) - .add_unlock_condition(gov_address) - .add_unlock_condition(state_address) - .with_features(rand_allowed_features(AnchorOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(AnchorOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); } } diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 6b07bd0f21..9b31c18782 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -446,33 +446,6 @@ mod dto { } } - impl BasicOutput { - pub fn try_from_dtos( - amount: OutputBuilderAmount, - mana: u64, - unlock_conditions: Vec, - features: Option>, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => BasicOutputBuilder::new_with_amount(amount), - OutputBuilderAmount::MinimumAmount(params) => BasicOutputBuilder::new_with_minimum_amount(params), - } - .with_mana(mana); - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - if let Some(features) = features { - builder = builder.with_features(features); - } - - builder.finish() - } - } - crate::impl_serde_typed_dto!(BasicOutput, BasicOutputDto, "basic output"); } @@ -499,41 +472,6 @@ mod tests { let dto = BasicOutputDto::from(&basic_output); let output = Output::Basic(BasicOutput::try_from(dto).unwrap()); assert_eq!(&basic_output, output.as_basic()); - - let output_split = BasicOutput::try_from_dtos( - OutputBuilderAmount::Amount(basic_output.amount()), - basic_output.mana(), - basic_output.unlock_conditions().to_vec(), - Some(basic_output.features().to_vec()), - ) - .unwrap(); - assert_eq!(basic_output, output_split); - - let foundry_id = FoundryId::build(&rand_account_address(), 0, SimpleTokenScheme::KIND); - let address = rand_address_unlock_condition(); - - let test_split_dto = |builder: BasicOutputBuilder| { - let output_split = BasicOutput::try_from_dtos( - builder.amount, - builder.mana, - builder.unlock_conditions.iter().cloned().collect(), - Some(builder.features.iter().cloned().collect()), - ) - .unwrap(); - assert_eq!(builder.finish().unwrap(), output_split); - }; - - let builder = BasicOutput::build_with_amount(100) - .with_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) - .add_unlock_condition(address.clone()) - .with_features(rand_allowed_features(BasicOutput::ALLOWED_FEATURES)); - test_split_dto(builder); - - let builder = BasicOutput::build_with_minimum_amount(protocol_parameters.storage_score_parameters()) - .with_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) - .add_unlock_condition(address) - .with_features(rand_allowed_features(BasicOutput::ALLOWED_FEATURES)); - test_split_dto(builder); } // TODO: re-enable when rent is figured out diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index 00c909c919..e3d8b9ffd9 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -400,10 +400,7 @@ mod dto { use super::*; use crate::{ - types::block::{ - output::{unlock_condition::UnlockCondition, OutputBuilderAmount}, - Error, - }, + types::block::{output::unlock_condition::UnlockCondition, Error}, utils::serde::string, }; @@ -459,43 +456,5 @@ mod dto { } } - impl DelegationOutput { - #[allow(clippy::too_many_arguments)] - pub fn try_from_dtos( - amount: OutputBuilderAmount, - delegated_amount: u64, - delegation_id: &DelegationId, - validator_address: &AccountAddress, - start_epoch: impl Into, - end_epoch: impl Into, - unlock_conditions: Vec, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => DelegationOutputBuilder::new_with_amount( - amount, - delegated_amount, - *delegation_id, - *validator_address, - ), - OutputBuilderAmount::MinimumAmount(params) => DelegationOutputBuilder::new_with_minimum_amount( - params, - delegated_amount, - *delegation_id, - *validator_address, - ), - } - .with_start_epoch(start_epoch) - .with_end_epoch(end_epoch); - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - builder.finish() - } - } - crate::impl_serde_typed_dto!(DelegationOutput, DelegationOutputDto, "delegation output"); } diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index ccc61cdbcc..66fa6789fd 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -638,43 +638,6 @@ mod dto { } } - impl FoundryOutput { - #[allow(clippy::too_many_arguments)] - pub fn try_from_dtos( - amount: OutputBuilderAmount, - serial_number: u32, - token_scheme: TokenScheme, - unlock_conditions: Vec, - features: Option>, - immutable_features: Option>, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => { - FoundryOutputBuilder::new_with_amount(amount, serial_number, token_scheme) - } - OutputBuilderAmount::MinimumAmount(params) => { - FoundryOutputBuilder::new_with_minimum_amount(params, serial_number, token_scheme) - } - }; - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - if let Some(features) = features { - builder = builder.with_features(features); - } - - if let Some(immutable_features) = immutable_features { - builder = builder.with_immutable_features(immutable_features); - } - - builder.finish() - } - } - crate::impl_serde_typed_dto!(FoundryOutput, FoundryOutputDto, "foundry output"); } @@ -684,18 +647,7 @@ mod tests { use super::*; use crate::types::block::{ - output::{ - foundry::dto::FoundryOutputDto, unlock_condition::ImmutableAccountAddressUnlockCondition, FoundryId, - SimpleTokenScheme, TokenId, - }, - protocol::protocol_parameters, - rand::{ - address::rand_account_address, - output::{ - feature::{rand_allowed_features, rand_metadata_feature}, - rand_foundry_output, rand_token_scheme, - }, - }, + output::foundry::dto::FoundryOutputDto, protocol::protocol_parameters, rand::output::rand_foundry_output, }; #[test] @@ -705,38 +657,5 @@ mod tests { let dto = FoundryOutputDto::from(&foundry_output); let output = Output::Foundry(FoundryOutput::try_from(dto).unwrap()); assert_eq!(&foundry_output, output.as_foundry()); - - let foundry_id = FoundryId::build(&rand_account_address(), 0, SimpleTokenScheme::KIND); - - let test_split_dto = |builder: FoundryOutputBuilder| { - let output_split = FoundryOutput::try_from_dtos( - builder.amount, - builder.serial_number, - builder.token_scheme.clone(), - builder.unlock_conditions.iter().cloned().collect(), - Some(builder.features.iter().cloned().collect()), - Some(builder.immutable_features.iter().cloned().collect()), - ) - .unwrap(); - assert_eq!(builder.finish().unwrap(), output_split); - }; - - let builder = FoundryOutput::build_with_amount(100, 123, rand_token_scheme()) - .with_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) - .add_unlock_condition(ImmutableAccountAddressUnlockCondition::new(rand_account_address())) - .add_immutable_feature(rand_metadata_feature()) - .with_features(rand_allowed_features(FoundryOutput::ALLOWED_FEATURES)); - test_split_dto(builder); - - let builder = FoundryOutput::build_with_minimum_amount( - protocol_parameters.storage_score_parameters(), - 123, - rand_token_scheme(), - ) - .with_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) - .add_unlock_condition(ImmutableAccountAddressUnlockCondition::new(rand_account_address())) - .add_immutable_feature(rand_metadata_feature()) - .with_features(rand_allowed_features(FoundryOutput::ALLOWED_FEATURES)); - test_split_dto(builder); } } diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 0e5930b880..9636399d5c 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -564,42 +564,6 @@ mod dto { } } - impl NftOutput { - #[allow(clippy::too_many_arguments)] - pub fn try_from_dtos( - amount: OutputBuilderAmount, - mana: u64, - nft_id: &NftId, - unlock_conditions: Vec, - features: Option>, - immutable_features: Option>, - ) -> Result { - let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => NftOutputBuilder::new_with_amount(amount, *nft_id), - OutputBuilderAmount::MinimumAmount(params) => { - NftOutputBuilder::new_with_minimum_amount(params, *nft_id) - } - } - .with_mana(mana); - - let unlock_conditions = unlock_conditions - .into_iter() - .map(UnlockCondition::from) - .collect::>(); - builder = builder.with_unlock_conditions(unlock_conditions); - - if let Some(features) = features { - builder = builder.with_features(features); - } - - if let Some(immutable_features) = immutable_features { - builder = builder.with_immutable_features(immutable_features); - } - - builder.finish() - } - } - crate::impl_serde_typed_dto!(NftOutput, NftOutputDto, "nft output"); } @@ -609,11 +573,7 @@ mod tests { use super::*; use crate::types::block::{ - output::nft::dto::NftOutputDto, - protocol::protocol_parameters, - rand::output::{ - feature::rand_allowed_features, rand_nft_output, unlock_condition::rand_address_unlock_condition, - }, + output::nft::dto::NftOutputDto, protocol::protocol_parameters, rand::output::rand_nft_output, }; #[test] @@ -623,42 +583,5 @@ mod tests { let dto = NftOutputDto::from(&nft_output); let output = Output::Nft(NftOutput::try_from(dto).unwrap()); assert_eq!(&nft_output, output.as_nft()); - - let output_split = NftOutput::try_from_dtos( - OutputBuilderAmount::Amount(nft_output.amount()), - nft_output.mana(), - nft_output.nft_id(), - nft_output.unlock_conditions().to_vec(), - Some(nft_output.features().to_vec()), - Some(nft_output.immutable_features().to_vec()), - ) - .unwrap(); - assert_eq!(nft_output, output_split); - - let test_split_dto = |builder: NftOutputBuilder| { - let output_split = NftOutput::try_from_dtos( - builder.amount, - builder.mana, - &builder.nft_id, - builder.unlock_conditions.iter().cloned().collect(), - Some(builder.features.iter().cloned().collect()), - Some(builder.immutable_features.iter().cloned().collect()), - ) - .unwrap(); - assert_eq!(builder.finish().unwrap(), output_split); - }; - - let builder = NftOutput::build_with_amount(100, NftId::null()) - .add_unlock_condition(rand_address_unlock_condition()) - .with_features(rand_allowed_features(NftOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(NftOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); - - let builder = - NftOutput::build_with_minimum_amount(protocol_parameters.storage_score_parameters(), NftId::null()) - .add_unlock_condition(rand_address_unlock_condition()) - .with_features(rand_allowed_features(NftOutput::ALLOWED_FEATURES)) - .with_immutable_features(rand_allowed_features(NftOutput::ALLOWED_IMMUTABLE_FEATURES)); - test_split_dto(builder); } } From a70e5081becbf0e13e2322b64c6e5781af3975ff Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 9 Jan 2024 16:32:41 +0100 Subject: [PATCH 04/14] Some TIP cleanup (#1791) * Some TIP cleanup * TaggedDataPayload nits * MultiAddress ordered_by_packed_bytes * nit * Nits * Nit * Add TIP link * Update sdk/src/types/block/output/storage_score.rs Co-authored-by: DaughterOfMars * format * Sort with a BTreeMap * format * Clippy --------- Co-authored-by: DaughterOfMars --- sdk/src/types/block/address/multi.rs | 35 ++++++++++++--------- sdk/src/types/block/address/restricted.rs | 8 ++++- sdk/src/types/block/output/mod.rs | 17 +++++----- sdk/src/types/block/output/storage_score.rs | 25 ++++++++------- sdk/src/types/block/payload/tagged_data.rs | 9 +++--- sdk/tests/types/address/multi.rs | 24 +++++++++++++- 6 files changed, 79 insertions(+), 39 deletions(-) diff --git a/sdk/src/types/block/address/multi.rs b/sdk/src/types/block/address/multi.rs index 593f300598..3db33a7d79 100644 --- a/sdk/src/types/block/address/multi.rs +++ b/sdk/src/types/block/address/multi.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use alloc::{boxed::Box, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, vec::Vec}; use core::{fmt, ops::RangeInclusive}; use crypto::hashes::{blake2b::Blake2b256, Digest}; @@ -11,10 +11,7 @@ use packable::{bounded::BoundedU8, prefix::BoxedSlicePrefix, Packable, PackableE use crate::types::block::{address::Address, output::StorageScore, Error}; -pub(crate) type WeightedAddressCount = - BoundedU8<{ *MultiAddress::ADDRESSES_COUNT.start() }, { *MultiAddress::ADDRESSES_COUNT.end() }>; - -/// An address with an assigned weight. +/// An [`Address`] with an assigned weight. #[derive(Clone, Debug, Display, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Packable)] #[display(fmt = "{address}")] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -30,7 +27,9 @@ pub struct WeightedAddress { impl WeightedAddress { /// Creates a new [`WeightedAddress`]. - pub fn new(address: Address, weight: u8) -> Result { + pub fn new(address: impl Into
, weight: u8) -> Result { + let address = address.into(); + verify_address::(&address)?; verify_weight::(&weight)?; @@ -69,9 +68,11 @@ fn verify_weight(weight: &u8) -> Result<(), Error> { } } -/// An address that consists of addresses with weights and a threshold value. -/// The Multi Address can be unlocked if the cumulative weight of all unlocked addresses is equal to or exceeds the -/// threshold. +pub(crate) type WeightedAddressCount = + BoundedU8<{ *MultiAddress::ADDRESSES_COUNT.start() }, { *MultiAddress::ADDRESSES_COUNT.end() }>; + +/// An [`Address`] that consists of addresses with weights and a threshold value. +/// It can be unlocked if the cumulative weight of all unlocked addresses is equal to or exceeds the threshold. #[derive(Clone, Debug, Deref, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)] #[packable(unpack_error = Error)] #[packable(verify_with = verify_multi_address)] @@ -95,10 +96,15 @@ impl MultiAddress { /// Creates a new [`MultiAddress`]. #[inline(always)] pub fn new(addresses: impl IntoIterator, threshold: u16) -> Result { - let mut addresses = addresses.into_iter().collect::>(); - - addresses.sort_by(|a, b| a.address().cmp(b.address())); - + // Using an intermediate BTreeMap to sort the addresses without having to repeatedly packing them. + let addresses = addresses + .into_iter() + .map(|address| (address.address().pack_to_vec(), address)) + .collect::>() + .into_values() + .collect::>(); + + verify_addresses::(&addresses)?; verify_threshold::(&threshold)?; let addresses = BoxedSlicePrefix::::try_from(addresses) @@ -136,7 +142,7 @@ impl MultiAddress { } fn verify_addresses(addresses: &[WeightedAddress]) -> Result<(), Error> { - if VERIFY && !is_unique_sorted(addresses.iter().map(WeightedAddress::address)) { + if VERIFY && !is_unique_sorted(addresses.iter().map(|a| a.address.pack_to_vec())) { Err(Error::WeightedAddressesNotUniqueSorted) } else { Ok(()) @@ -162,6 +168,7 @@ fn verify_multi_address(address: &MultiAddress) -> Result<() }); } } + Ok(()) } diff --git a/sdk/src/types/block/address/restricted.rs b/sdk/src/types/block/address/restricted.rs index 9ac601da5c..d52c316371 100644 --- a/sdk/src/types/block/address/restricted.rs +++ b/sdk/src/types/block/address/restricted.rs @@ -1,16 +1,22 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! An extension to the address format to make them configurable. +//! This enables an address to opt-in or -out of certain functionality, like disabling the receipt of Native Tokens, NFT +//! Outputs or Timelock Unlock Conditions. +//! [TIP-50: Configurable Addresses](https://github.com/iotaledger/tips/blob/tip50/tips/TIP-0050/tip-0050.md). + use getset::Getters; use packable::{Packable, PackableExt}; -use super::Address; use crate::types::block::{ + address::Address, capabilities::{Capabilities, CapabilityFlag}, output::{StorageScore, StorageScoreParameters}, Error, }; +/// An [`Address`] that contains another address and allows for configuring its capabilities. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Getters, Packable)] #[getset(get = "pub")] pub struct RestrictedAddress { diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index e2ec4241a5..196986c4bc 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -310,17 +310,18 @@ impl Output { }) } - /// Verifies if a valid storage deposit was made. Each [`Output`] has to have an amount that covers its associated - /// byte cost, given by [`StorageScoreParameters`]. + /// Verifies if a valid storage deposit was made. + /// Each [`Output`] has to have an amount that covers its associated byte cost, given by [`StorageScoreParameters`]. /// If there is a [`StorageDepositReturnUnlockCondition`](unlock_condition::StorageDepositReturnUnlockCondition), /// its amount is also checked. pub fn verify_storage_deposit(&self, params: StorageScoreParameters) -> Result<(), Error> { - let required_output_amount = self.minimum_amount(params); + let minimum_storage_deposit = self.minimum_amount(params); - if self.amount() < required_output_amount { + // For any created `Output` in a transaction, it must hold that `Output::Amount >= Minimum Storage Deposit`. + if self.amount() < minimum_storage_deposit { return Err(Error::InsufficientStorageDepositAmount { amount: self.amount(), - required: required_output_amount, + required: minimum_storage_deposit, }); } @@ -337,13 +338,13 @@ impl Output { }); } - let minimum_deposit = BasicOutput::minimum_amount(return_condition.return_address(), params); + let minimum_storage_deposit = BasicOutput::minimum_amount(return_condition.return_address(), params); // `Minimum Storage Deposit` ≤ `Return Amount` - if return_condition.amount() < minimum_deposit { + if return_condition.amount() < minimum_storage_deposit { return Err(Error::InsufficientStorageDepositReturnAmount { deposit: return_condition.amount(), - required: minimum_deposit, + required: minimum_storage_deposit, }); } } diff --git a/sdk/src/types/block/output/storage_score.rs b/sdk/src/types/block/output/storage_score.rs index 02d6b09be4..ad4bf77f36 100644 --- a/sdk/src/types/block/output/storage_score.rs +++ b/sdk/src/types/block/output/storage_score.rs @@ -1,6 +1,11 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Storage deposit is a concept that creates a monetary incentive to keep the ledger state small. +//! This is achieved by enforcing a minimum IOTA coin deposit in every output based on the disk space that will actually +//! be used to store it. +//! [TIP-47: Storage Deposit Dust Protection](https://github.com/iotaledger/tips/blob/tip47/tips/TIP-0047/tip-0047.md). + use packable::Packable; use crate::types::block::{ @@ -13,16 +18,14 @@ use crate::types::block::{ BlockId, }; -const DEFAULT_STORAGE_COST: u64 = 500; +const DEFAULT_STORAGE_COST: u64 = 100; const DEFAULT_FACTOR_DATA: u8 = 1; const DEFAULT_OFFSET_OUTPUT_OVERHEAD: u64 = 10; -const DEFAULT_OFFSET_ED25519_BLOCK_ISSUER_KEY: u64 = 50; +const DEFAULT_OFFSET_ED25519_BLOCK_ISSUER_KEY: u64 = 100; const DEFAULT_OFFSET_STAKING_FEATURE: u64 = 100; const DEFAULT_OFFSET_DELEGATION: u64 = 100; -// Defines the parameters of storage score calculations on objects which take node resources. -// This structure defines the minimum base token deposit required on an object. This deposit does not -// generate Mana, which serves as a payment in Mana for storing the object. +// Parameters of storage score calculations on objects which take node resources. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Packable)] #[cfg_attr( feature = "serde", @@ -30,21 +33,21 @@ const DEFAULT_OFFSET_DELEGATION: u64 = 100; serde(rename_all = "camelCase") )] pub struct StorageScoreParameters { - /// Defines the number of IOTA tokens required per unit of storage score. + /// Number of IOTA tokens required per unit of storage score. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] storage_cost: u64, - /// Defines the factor to be used for data only fields. + /// Factor to be used for data only fields. factor_data: u8, - /// Defines the offset to be applied to all outputs for the overhead of handling them in storage. + /// Offset to be applied to all outputs for the overhead of handling them in storage. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] offset_output_overhead: u64, - /// Defines the offset to be used for block issuer feature public keys. + /// Offset to be used for Ed25519-based block issuer keys. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] offset_ed25519_block_issuer_key: u64, - /// Defines the offset to be used for staking feature. + /// Offset to be used for staking feature. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] offset_staking_feature: u64, - /// Defines the offset to be used for delegation output. + /// Offset to be used for delegation. #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] offset_delegation: u64, } diff --git a/sdk/src/types/block/payload/tagged_data.rs b/sdk/src/types/block/payload/tagged_data.rs index f99a2e280e..0bdec46c8d 100644 --- a/sdk/src/types/block/payload/tagged_data.rs +++ b/sdk/src/types/block/payload/tagged_data.rs @@ -1,7 +1,8 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Module describing the tagged data payload. +//! A basic payload type that allows the addition of arbitrary data. +//! [TIP-53: Tagged Data](https://github.com/iotaledger/tips/blob/tip53/tips/TIP-0053/tip-0053.md). use alloc::boxed::Box; use core::ops::RangeInclusive; @@ -22,7 +23,7 @@ pub(crate) type TagLength = pub(crate) type TaggedDataLength = BoundedU32<{ *TaggedDataPayload::DATA_LENGTH_RANGE.start() }, { *TaggedDataPayload::DATA_LENGTH_RANGE.end() }>; -/// A payload which holds a tag and associated data. +/// A payload which holds optional data with an optional tag. #[derive(Clone, Eq, PartialEq, Packable)] #[packable(unpack_error = Error)] pub struct TaggedDataPayload { @@ -35,9 +36,9 @@ pub struct TaggedDataPayload { impl TaggedDataPayload { /// The [`Payload`](crate::types::block::payload::Payload) kind of a [`TaggedDataPayload`]. pub const KIND: u8 = 0; - /// Valid length range for the tag. + /// Valid tag length range. pub const TAG_LENGTH_RANGE: RangeInclusive = 0..=64; - /// Valid length range for the data. + /// Valid data length range. pub const DATA_LENGTH_RANGE: RangeInclusive = 0..=8192; /// Creates a new [`TaggedDataPayload`]. diff --git a/sdk/tests/types/address/multi.rs b/sdk/tests/types/address/multi.rs index 8705b358ec..a0ba5e6fb6 100644 --- a/sdk/tests/types/address/multi.rs +++ b/sdk/tests/types/address/multi.rs @@ -1,10 +1,32 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::types::block::address::{Address, ToBech32Ext}; +use iota_sdk::types::block::{ + address::{AccountAddress, Address, Ed25519Address, MultiAddress, ToBech32Ext, WeightedAddress}, + output::AccountId, + rand::bytes::rand_bytes_array, +}; use packable::PackableExt; use pretty_assertions::assert_eq; +#[test] +fn ordered_by_packed_bytes() { + let mut bytes_1 = rand_bytes_array::<32>(); + bytes_1[0] = 0; + let mut bytes_2 = bytes_1.clone(); + bytes_2[0] = 1; + + let weighted_1 = WeightedAddress::new(AccountAddress::from(AccountId::from(bytes_1)), 1).unwrap(); + let weighted_2 = WeightedAddress::new(Ed25519Address::from(bytes_2), 1).unwrap(); + + let multi_1 = MultiAddress::new([weighted_1, weighted_2], 2).unwrap(); + let bytes = multi_1.pack_to_vec(); + let multi_2 = MultiAddress::unpack_verified(bytes, &()).unwrap(); + + assert!(multi_2.addresses()[0].address().is_ed25519()); + assert!(multi_2.addresses()[1].address().is_account()); +} + #[test] fn json_packable_bech32() { // Test from https://github.com/iotaledger/tips/blob/tip52/tips/TIP-0052/tip-0052.md#bech32 From 25761748ed02f852e360e3acffb6af37ea46eff6 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:16:09 +0100 Subject: [PATCH 05/14] Nodejs: update core API routes (#1783) * Updated core API routes * Update to latest TIP changes * Remove test example * Add GetUtxoChangesFull routes * Order and add issue to comment * Update bindings/nodejs/lib/types/models/block-metadata.ts Co-authored-by: /alex/ * Apply suggestions from code review Co-authored-by: Thibault Martinez * 2024 * Apply suggestions from code review Co-authored-by: Thibault Martinez * Update comment * Remove returns from comments --------- Co-authored-by: Thibault Martinez Co-authored-by: /alex/ Co-authored-by: Thibault Martinez --- bindings/core/src/method/client.rs | 17 +- bindings/core/src/method_handler/client.rs | 12 +- bindings/nodejs/lib/client/client.ts | 221 ++++++++++++++++-- .../nodejs/lib/types/client/bridge/client.ts | 84 ++++++- .../nodejs/lib/types/client/bridge/index.ts | 24 +- .../types/models/api/congestion-response.ts | 27 +++ bindings/nodejs/lib/types/models/api/index.ts | 4 + .../types/models/api/mana-rewards-response.ts | 31 +++ .../types/models/api/utxo-changes-response.ts | 60 +++++ .../types/models/api/validators-response.ts | 64 +++++ .../nodejs/lib/types/models/block-metadata.ts | 29 ++- .../lib/types/models/gossip-heartbeat.ts | 28 --- .../nodejs/lib/types/models/gossip-metrics.ts | 52 ----- bindings/nodejs/lib/types/models/index.ts | 3 - .../types/models/info/node-info-protocol.ts | 5 + bindings/nodejs/lib/types/models/peer.ts | 43 ---- bindings/nodejs/lib/types/models/state.ts | 6 + .../nodejs/tests/client/infoMethods.spec.ts | 7 - .../python/iota_sdk/types/block/metadata.py | 16 +- sdk/src/types/api/core.rs | 30 +-- .../wallet/operations/syncing/transactions.rs | 6 +- 21 files changed, 571 insertions(+), 198 deletions(-) create mode 100644 bindings/nodejs/lib/types/models/api/congestion-response.ts create mode 100644 bindings/nodejs/lib/types/models/api/mana-rewards-response.ts create mode 100644 bindings/nodejs/lib/types/models/api/utxo-changes-response.ts create mode 100644 bindings/nodejs/lib/types/models/api/validators-response.ts delete mode 100644 bindings/nodejs/lib/types/models/gossip-heartbeat.ts delete mode 100644 bindings/nodejs/lib/types/models/gossip-metrics.ts delete mode 100644 bindings/nodejs/lib/types/models/peer.ts diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index 9fd4892db9..7ce48daf54 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -156,15 +156,18 @@ pub enum ClientMethod { /// The Account ID of the account. account_id: AccountId, }, - /// Returns the totally available Mana rewards of an account or delegation output decayed up to endEpoch index - /// provided in the response. + /// Returns all the available Mana rewards of an account or delegation output in the returned range of epochs. #[serde(rename_all = "camelCase")] GetRewards { /// Output ID of an account or delegation output. output_id: OutputId, /// A client can specify a slot index explicitly, which should be equal to the slot it uses as the commitment - /// input for the claiming transaction. This parameter is only recommended to be provided when requesting - /// rewards for a Delegation Output in delegating state (i.e. when Delegation ID is zeroed). + /// input for the claiming transaction to ensure the node calculates the rewards identically as during + /// transaction execution. Rewards are decayed up to the epoch corresponding to the given slotIndex + + /// MinCommittableAge. For a Delegation Output in delegating state (i.e. when Delegation ID is zeroed), that + /// epoch - 1 is also used as the last epoch for which rewards are fetched. Callers that do not build + /// transactions with the returned values may omit this value in which case it defaults to the latest committed + /// slot, which is good enough to, e.g. display estimated rewards to users. slot_index: Option, }, /// Returns information of all registered validators and if they are active, ordered by their holding stake. @@ -282,17 +285,17 @@ pub enum ClientMethod { /// Look up a commitment by a given commitment index. GetCommitmentByIndex { /// Index of the commitment to look up. - index: SlotIndex, + slot: SlotIndex, }, /// Get all UTXO changes of a given slot by commitment index. GetUtxoChangesByIndex { /// Index of the commitment to look up. - index: SlotIndex, + slot: SlotIndex, }, /// Get all full UTXO changes of a given slot by commitment index. GetUtxoChangesFullByIndex { /// Index of the commitment to look up. - index: SlotIndex, + slot: SlotIndex, }, ////////////////////////////////////////////////////////////////////// diff --git a/bindings/core/src/method_handler/client.rs b/bindings/core/src/method_handler/client.rs index ca809129f5..629ec5251b 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -251,14 +251,14 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM .get_utxo_changes_full_by_slot_commitment_id(&commitment_id) .await?, ), - ClientMethod::GetCommitmentByIndex { index } => { - Response::SlotCommitment(client.get_slot_commitment_by_slot(index).await?) + ClientMethod::GetCommitmentByIndex { slot } => { + Response::SlotCommitment(client.get_slot_commitment_by_slot(slot).await?) } - ClientMethod::GetUtxoChangesByIndex { index } => { - Response::UtxoChanges(client.get_utxo_changes_by_slot(index).await?) + ClientMethod::GetUtxoChangesByIndex { slot } => { + Response::UtxoChanges(client.get_utxo_changes_by_slot(slot).await?) } - ClientMethod::GetUtxoChangesFullByIndex { index } => { - Response::UtxoChangesFull(client.get_utxo_changes_full_by_slot(index).await?) + ClientMethod::GetUtxoChangesFullByIndex { slot } => { + Response::UtxoChangesFull(client.get_utxo_changes_full_by_slot(slot).await?) } ClientMethod::OutputIds { query_parameters } => { Response::OutputIdsResponse(client.output_ids(query_parameters).await?) diff --git a/bindings/nodejs/lib/client/client.ts b/bindings/nodejs/lib/client/client.ts index e67935cf7d..082c52bbfa 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -44,12 +44,14 @@ import { DelegationId, UnsignedBlock, parseUnsignedBlock, + SlotIndex, + SlotCommitmentId, + SlotCommitment, } from '../types/block'; import { HexEncodedString } from '../utils'; import { IBlockMetadata, INodeInfo, - IPeer, UTXOInput, Response, OutputId, @@ -58,10 +60,19 @@ import { TransactionId, Bech32Address, IBlockWithMetadata, + TransactionMetadata, } from '../types'; -import { OutputResponse, IOutputsResponse } from '../types/models/api'; +import { + OutputResponse, + IOutputsResponse, + CongestionResponse, + UtxoChangesResponse, + UtxoChangesFullResponse, +} from '../types/models/api'; import { plainToInstance } from 'class-transformer'; +import { ManaRewardsResponse } from '../types/models/api/mana-rewards-response'; +import { ValidatorsResponse } from '../types/models/api/validators-response'; /** The Client to interact with nodes. */ export class Client { @@ -106,6 +117,74 @@ export class Client { return JSON.parse(response).payload; } + /** + * Check the readiness of the node to issue a new block, the reference mana cost based on the rate setter and + * current network congestion, and the block issuance credits of the requested account. + */ + async getAccountCongestion( + accountId: AccountId, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getAccountCongestion', + data: { + accountId, + }, + }); + + return JSON.parse(response).payload; + } + + /** + * Returns the totally available Mana rewards of an account or delegation output decayed up to endEpoch index + * provided in the response. + */ + async getRewards( + outputId: OutputId, + slotIndex?: SlotIndex, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getRewards', + data: { + outputId, + slotIndex, + }, + }); + + return JSON.parse(response).payload; + } + + /** + * Returns information of all registered validators and if they are active, ordered by their holding stake. + */ + async getValidators( + pageSize?: number, + cursor?: string, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getValidators', + data: { + pageSize, + cursor, + }, + }); + + return JSON.parse(response).payload; + } + + /** + * Return information about a validator. + */ + async getValidator(accountId: AccountId): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getValidator', + data: { + accountId, + }, + }); + + return JSON.parse(response).payload; + } + /** * Get output from a given output ID. */ @@ -393,17 +472,6 @@ export class Client { return JSON.parse(response).payload; } - /** - * Get the peers of the node. - */ - async getPeers(): Promise { - const response = await this.methodHandler.callMethod({ - name: 'getPeers', - }); - - return JSON.parse(response).payload; - } - /** * Post block as raw bytes, returns the block ID. * @@ -463,15 +531,136 @@ export class Client { */ async getIncludedBlockMetadata( transactionId: TransactionId, - ): Promise { + ): Promise { const response = await this.methodHandler.callMethod({ name: 'getIncludedBlockMetadata', data: { transactionId, }, }); - const parsed = JSON.parse(response) as Response; - return parseBlock(parsed.payload); + return JSON.parse(response).payload; + } + + /** + * Find the metadata of a transaction. + * + * @param transactionId The ID of the transaction. + * @returns The transaction metadata. + */ + async getTransactionMetadata( + transactionId: TransactionId, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getTransactionMetadata', + data: { + transactionId, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Look up a commitment by a given commitment ID. + * + * @param commitmentId Commitment ID of the commitment to look up. + * @returns The commitment. + */ + async getCommitment( + commitmentId: SlotCommitmentId, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getCommitment', + data: { + commitmentId, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Get all UTXO changes of a given slot by Commitment ID. + * + * @param commitmentId Commitment ID of the commitment to look up. + * @returns The UTXO changes. + */ + async getUtxoChanges( + commitmentId: SlotCommitmentId, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getUtxoChanges', + data: { + commitmentId, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Get all full UTXO changes of a given slot by Commitment ID. + * + * @param commitmentId Commitment ID of the commitment to look up. + * @returns The UTXO changes. + */ + async getUtxoChangesFull( + commitmentId: SlotCommitmentId, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getUtxoChangesFull', + data: { + commitmentId, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Look up a commitment by a given commitment index. + * + * @param slot Index of the commitment to look up. + * @returns The commitment. + */ + async getCommitmentByIndex(slot: SlotIndex): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getCommitmentByIndex', + data: { + slot, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Get all UTXO changes of a given slot by commitment index. + * + * @param slot Index of the commitment to look up. + * @returns The UTXO changes. + */ + async getUtxoChangesByIndex(slot: SlotIndex): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getUtxoChangesByIndex', + data: { + slot, + }, + }); + return JSON.parse(response).payload; + } + + /** + * Get all full UTXO changes of a given slot by commitment index. + * + * @param slot Index of the commitment to look up. + * @returns The UTXO changes. + */ + async getUtxoChangesFullByIndex( + slot: SlotIndex, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'getUtxoChangesFullByIndex', + data: { + slot, + }, + }); + return JSON.parse(response).payload; } /** diff --git a/bindings/nodejs/lib/types/client/bridge/client.ts b/bindings/nodejs/lib/types/client/bridge/client.ts index 93bc8f9aec..2919d75f52 100644 --- a/bindings/nodejs/lib/types/client/bridge/client.ts +++ b/bindings/nodejs/lib/types/client/bridge/client.ts @@ -16,6 +16,7 @@ import type { Output, OutputId, Payload, + SlotIndex, } from '../../block'; import type { PreparedTransactionData } from '../prepared-transaction-data'; import type { @@ -68,6 +69,36 @@ export interface __GetNetworkInfoMethod__ { name: 'getNetworkInfo'; } +export interface __GetAccountCongestionMethod__ { + name: 'getAccountCongestion'; + data: { + accountId: AccountId; + }; +} + +export interface __GetRewardsMethod__ { + name: 'getRewards'; + data: { + outputId: OutputId; + slotIndex?: SlotIndex; + }; +} + +export interface __GetValidatorsMethod__ { + name: 'getValidators'; + data: { + pageSize?: number; + cursor?: string; + }; +} + +export interface __GetValidatorMethod__ { + name: 'getValidator'; + data: { + accountId: AccountId; + }; +} + export interface __GetBlockMethod__ { name: 'getBlock'; data: { @@ -153,10 +184,6 @@ export interface __GetNodeInfoMethod__ { }; } -export interface __GetPeersMethod__ { - name: 'getPeers'; -} - export interface __PostBlockRawMethod__ { name: 'postBlockRaw'; data: { @@ -185,6 +212,55 @@ export interface __GetIncludedBlockMetadataMethod__ { }; } +export interface __GetTransactionMetadataMethod__ { + name: 'getTransactionMetadata'; + data: { + transactionId: TransactionId; + }; +} + +export interface __GetCommitmentMethod__ { + name: 'getCommitment'; + data: { + commitmentId: HexEncodedString; + }; +} + +export interface __GetUtxoChangesMethod__ { + name: 'getUtxoChanges'; + data: { + commitmentId: HexEncodedString; + }; +} + +export interface __GetUtxoChangesFullMethod__ { + name: 'getUtxoChangesFull'; + data: { + commitmentId: HexEncodedString; + }; +} + +export interface __GetCommitmentByIndexMethod__ { + name: 'getCommitmentByIndex'; + data: { + slot: SlotIndex; + }; +} + +export interface __GetUtxoChangesByIndexMethod__ { + name: 'getUtxoChangesByIndex'; + data: { + slot: SlotIndex; + }; +} + +export interface __GetUtxoChangesFullByIndexMethod__ { + name: 'getUtxoChangesFullByIndex'; + data: { + slot: SlotIndex; + }; +} + export interface __HexToBech32Method__ { name: 'hexToBech32'; data: { diff --git a/bindings/nodejs/lib/types/client/bridge/index.ts b/bindings/nodejs/lib/types/client/bridge/index.ts index 20de332b70..743c428eda 100644 --- a/bindings/nodejs/lib/types/client/bridge/index.ts +++ b/bindings/nodejs/lib/types/client/bridge/index.ts @@ -10,6 +10,10 @@ import type { __PostBlockMethod__, __GetTipsMethod__, __GetNetworkInfoMethod__, + __GetAccountCongestionMethod__, + __GetRewardsMethod__, + __GetValidatorsMethod__, + __GetValidatorMethod__, __GetBlockMethod__, __GetBlockMetadataMethod__, __GetBlockWithMetadataMethod__, @@ -22,11 +26,17 @@ import type { __GetProtocolParametersMethod__, __GetHealthMethod__, __GetNodeInfoMethod__, - __GetPeersMethod__, __PostBlockRawMethod__, __GetBlockRawMethod__, __GetIncludedBlockMethod__, __GetIncludedBlockMetadataMethod__, + __GetTransactionMetadataMethod__, + __GetCommitmentMethod__, + __GetUtxoChangesMethod__, + __GetUtxoChangesFullMethod__, + __GetCommitmentByIndexMethod__, + __GetUtxoChangesByIndexMethod__, + __GetUtxoChangesFullByIndexMethod__, __HexToBech32Method__, __AccountIdToBech32Method__, __NftIdToBech32Method__, @@ -65,6 +75,10 @@ export type __ClientMethods__ = | __PostBlockMethod__ | __GetTipsMethod__ | __GetNetworkInfoMethod__ + | __GetAccountCongestionMethod__ + | __GetRewardsMethod__ + | __GetValidatorsMethod__ + | __GetValidatorMethod__ | __GetBlockMethod__ | __GetBlockMetadataMethod__ | __GetBlockWithMetadataMethod__ @@ -78,11 +92,17 @@ export type __ClientMethods__ = | __GetProtocolParametersMethod__ | __GetHealthMethod__ | __GetNodeInfoMethod__ - | __GetPeersMethod__ | __PostBlockRawMethod__ | __GetBlockRawMethod__ | __GetIncludedBlockMethod__ | __GetIncludedBlockMetadataMethod__ + | __GetTransactionMetadataMethod__ + | __GetCommitmentMethod__ + | __GetUtxoChangesMethod__ + | __GetUtxoChangesFullMethod__ + | __GetCommitmentByIndexMethod__ + | __GetUtxoChangesByIndexMethod__ + | __GetUtxoChangesFullByIndexMethod__ | __HexToBech32Method__ | __AccountIdToBech32Method__ | __NftIdToBech32Method__ diff --git a/bindings/nodejs/lib/types/models/api/congestion-response.ts b/bindings/nodejs/lib/types/models/api/congestion-response.ts new file mode 100644 index 0000000000..cf40d8effd --- /dev/null +++ b/bindings/nodejs/lib/types/models/api/congestion-response.ts @@ -0,0 +1,27 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SlotIndex } from '../../block/slot'; +import { u64 } from '../../utils'; + +/** + * Provides the cost and readiness to issue estimates. + */ +export class CongestionResponse { + /** + * The slot index for which the congestion estimate is provided. + */ + slot!: SlotIndex; + /** + * Indicates if a node is ready to issue a block in a current congestion or should wait. + */ + ready!: boolean; + /** + * The cost in mana for issuing a block in a current congestion estimated based on RMC and slot index. + */ + referenceManaCost!: u64; + /** + * The Block Issuance Credits of the requested account. + */ + blockIssuanceCredits!: u64; +} diff --git a/bindings/nodejs/lib/types/models/api/index.ts b/bindings/nodejs/lib/types/models/api/index.ts index bf835a1377..703fe9a5b0 100644 --- a/bindings/nodejs/lib/types/models/api/index.ts +++ b/bindings/nodejs/lib/types/models/api/index.ts @@ -4,7 +4,11 @@ export * from './plugins'; export * from './block-id-response'; +export * from './congestion-response'; +export * from './mana-rewards-response'; export * from './output-metadata-response'; export * from './output-response'; export * from './response'; export * from './tips-response'; +export * from './validators-response'; +export * from './utxo-changes-response'; diff --git a/bindings/nodejs/lib/types/models/api/mana-rewards-response.ts b/bindings/nodejs/lib/types/models/api/mana-rewards-response.ts new file mode 100644 index 0000000000..0a5f93d196 --- /dev/null +++ b/bindings/nodejs/lib/types/models/api/mana-rewards-response.ts @@ -0,0 +1,31 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { EpochIndex } from '../../block/slot'; +import { u64 } from '../../utils'; + +/** + * The mana rewards of an account or delegation output. + */ +export class ManaRewardsResponse { + /** + * The starting epoch index for which the mana rewards are returned. + */ + startEpoch!: EpochIndex; + /** + * The ending epoch index for which the mana rewards are returned, the decay is applied up to this point + * included. + */ + endEpoch!: EpochIndex; + /** + * The amount of totally available rewards the requested output may claim. + */ + rewards!: u64; + /** + * The rewards of the latest committed epoch of the staking pool to which this validator or delegator belongs. + * The ratio of this value and the maximally possible rewards for the latest committed epoch can be used to + * determine how well the validator of this staking pool performed in that epoch. Note that if the pool was not + * part of the committee in the latest committed epoch, this value is 0. + */ + latestCommittedEpochPoolRewards!: u64; +} diff --git a/bindings/nodejs/lib/types/models/api/utxo-changes-response.ts b/bindings/nodejs/lib/types/models/api/utxo-changes-response.ts new file mode 100644 index 0000000000..6db602b7d8 --- /dev/null +++ b/bindings/nodejs/lib/types/models/api/utxo-changes-response.ts @@ -0,0 +1,60 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { OutputId } from '../../block/output'; +import { SlotCommitmentId } from '../../block'; +import { Output, OutputDiscriminator } from '../../block/output'; +import { Type } from 'class-transformer'; + +/** + * All UTXO changes that happened at a specific slot. + */ +export class UtxoChangesResponse { + /** + * The commitment ID of the requested slot that contains the changes. + */ + commitmentId!: SlotCommitmentId; + /** + * The created outputs of the given slot. + */ + createdOutputs!: OutputId[]; + /** + * The consumed outputs of the given slot. + */ + consumedOutputs!: OutputId[]; +} + +/** + * An output with its id. + */ +export class OutputWithId { + /** + * The output id. + */ + outputId!: OutputId; + /** + * The output. + */ + @Type(() => Output, { + discriminator: OutputDiscriminator, + }) + output!: Output; +} + +/** + * All full UTXO changes that happened at a specific slot. + */ +export class UtxoChangesFullResponse { + /** + * The commitment ID of the requested slot that contains the changes. + */ + commitmentId!: SlotCommitmentId; + /** + * The created outputs of the given slot. + */ + createdOutputs!: OutputWithId[]; + /** + * The consumed outputs of the given slot. + */ + consumedOutputs!: OutputWithId[]; +} diff --git a/bindings/nodejs/lib/types/models/api/validators-response.ts b/bindings/nodejs/lib/types/models/api/validators-response.ts new file mode 100644 index 0000000000..ccb518fc9d --- /dev/null +++ b/bindings/nodejs/lib/types/models/api/validators-response.ts @@ -0,0 +1,64 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Bech32Address, EpochIndex } from '../../block'; +import { u64 } from '../../utils'; +import type { HexEncodedString } from '../../utils/hex-encoding'; + +/** + * Information of a validator. + */ +export interface ValidatorResponse { + /** + * Account address of the validator. + */ + address: Bech32Address; + /** + * The epoch index until which the validator registered to stake. + */ + stakingEndEpoch: EpochIndex; + /** + * The total stake of the pool, including delegators. + */ + poolStake: u64; + /** + * The stake of a validator. + */ + validatorStake: u64; + /** + * The fixed cost of the validator, which it receives as part of its Mana rewards. + */ + fixedCost: u64; + /** + * Shows whether the validator was active recently. + */ + active: boolean; + /** + * The latest protocol version the validator supported. + */ + latestSupportedProtocolVersion: number; + /** + * The latest protocol version the validator supported. + */ + latestSupportedProtocolHash: HexEncodedString; +} + +/** + * A paginated list of all registered validators ready for the next epoch and indicates if they were active recently + * (are eligible for committee selection). + */ +export interface ValidatorsResponse { + /** + * List of registered validators ready for the next epoch. + */ + validators: ValidatorResponse[]; + /** + * The number of validators returned per one API request with pagination. + */ + pageSize: number; + /** + * The cursor that needs to be provided as cursor query parameter to request the next page. If empty, this was the + * last page. + */ + cursor?: string; +} diff --git a/bindings/nodejs/lib/types/models/block-metadata.ts b/bindings/nodejs/lib/types/models/block-metadata.ts index 70043f2a60..ba7cee6f57 100644 --- a/bindings/nodejs/lib/types/models/block-metadata.ts +++ b/bindings/nodejs/lib/types/models/block-metadata.ts @@ -1,11 +1,12 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import type { TransactionFailureReason } from './transaction-failure-reason'; import type { HexEncodedString } from '../utils/hex-encoding'; import { BlockState, TransactionState } from './state'; import { BlockFailureReason } from './block-failure-reason'; import { Block } from '../block'; +import { TransactionId } from '../wallet'; +import { TransactionFailureReason } from './transaction-failure-reason'; /** * Response from the metadata endpoint. @@ -19,18 +20,14 @@ export interface IBlockMetadata { * The block state. */ blockState: BlockState; - /** - * The transaction state. - */ - transactionState?: TransactionState; /** * The block failure reason. */ blockFailureReason?: BlockFailureReason; /** - * The transaction failure reason. + * The metadata of the transaction in the block. */ - transactionFailureReason?: TransactionFailureReason; + transactionMetadata?: TransactionMetadata; } /** @@ -46,3 +43,21 @@ export interface IBlockWithMetadata { */ metadata: IBlockMetadata; } + +/** + * Metadata of a transaction. + */ +export interface TransactionMetadata { + /** + * The transaction id. + */ + transactionId: TransactionId; + /** + * The transaction state. + */ + transactionState: TransactionState; + /** + * The transaction failure reason. + */ + transactionFailureReason?: TransactionFailureReason; +} diff --git a/bindings/nodejs/lib/types/models/gossip-heartbeat.ts b/bindings/nodejs/lib/types/models/gossip-heartbeat.ts deleted file mode 100644 index 8ed9204451..0000000000 --- a/bindings/nodejs/lib/types/models/gossip-heartbeat.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/** - * Gossip heartbeat. - */ -export interface IGossipHeartbeat { - /** - * Solid milestone index. - */ - solidMilestoneIndex: number; - /** - * Pruned milestone index. - */ - prunedMilestoneIndex: number; - /** - * Latest milestone index. - */ - latestMilestoneIndex: number; - /** - * Connected peers. - */ - connectedPeers: number; - /** - * Synced peers. - */ - syncedPeers: number; -} diff --git a/bindings/nodejs/lib/types/models/gossip-metrics.ts b/bindings/nodejs/lib/types/models/gossip-metrics.ts deleted file mode 100644 index 9ce4f6119b..0000000000 --- a/bindings/nodejs/lib/types/models/gossip-metrics.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/** - * Gossip metrics. - */ -export interface IGossipMetrics { - /** - * The number of new blocks. - */ - newBlocks: number; - /** - * The number of known blocks. - */ - knownBlocks: number; - /** - * The number of received blocks. - */ - receivedBlocks: number; - /** - * The number of received block requests. - */ - receivedBlockRequests: number; - /** - * The number of received milestone requests. - */ - receivedMilestoneRequests: number; - /** - * The number of received heartbeats. - */ - receivedHeartbeats: number; - /** - * The number of sent blocks. - */ - sentBlocks: number; - /** - * The number of sent block requests. - */ - sentBlockRequests: number; - /** - * The number of sent miletsone requests. - */ - sentMilestoneRequests: number; - /** - * The number of sent heartbeats. - */ - sentHeartbeats: number; - /** - * The number of dropped sent packets. - */ - droppedPackets: number; -} diff --git a/bindings/nodejs/lib/types/models/index.ts b/bindings/nodejs/lib/types/models/index.ts index 8789189173..a4290a39a3 100644 --- a/bindings/nodejs/lib/types/models/index.ts +++ b/bindings/nodejs/lib/types/models/index.ts @@ -6,9 +6,6 @@ export * from './info'; export * from './transaction-failure-reason'; export * from './block-metadata'; -export * from './gossip-metrics'; -export * from './gossip-heartbeat'; export * from './native-token'; -export * from './peer'; export * from './storage-score'; export * from './state'; diff --git a/bindings/nodejs/lib/types/models/info/node-info-protocol.ts b/bindings/nodejs/lib/types/models/info/node-info-protocol.ts index 84f70cd5e4..e7a31d3251 100644 --- a/bindings/nodejs/lib/types/models/info/node-info-protocol.ts +++ b/bindings/nodejs/lib/types/models/info/node-info-protocol.ts @@ -119,6 +119,11 @@ export interface ProtocolParameters { * Target Committee Size defines the target size of the committee. If there's fewer candidates the actual committee size could be smaller in a given epoch. */ targetCommitteeSize: number; + /** + * Defines the number of heavier slots that a chain needs to be ahead of the current chain to be considered for + * switching. + */ + chainSwitchingThreshold: number; } /** diff --git a/bindings/nodejs/lib/types/models/peer.ts b/bindings/nodejs/lib/types/models/peer.ts deleted file mode 100644 index aa7094bce1..0000000000 --- a/bindings/nodejs/lib/types/models/peer.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import type { IGossipHeartbeat } from './gossip-heartbeat'; -import type { IGossipMetrics } from './gossip-metrics'; -/** - * Peer details. - */ -export interface IPeer { - /** - * The id of the peer. - */ - id: string; - /** - * The addresses of the peer. - */ - multiAddresses: string[]; - /** - * The alias of the peer. - */ - alias?: string; - /** - * The relation of the peer. - */ - relation: string; - /** - * Is it connected. - */ - connected: boolean; - /** - * Gossip metrics for the peer. - */ - gossip?: { - /** - * The peer heartbeat. - */ - heartbeat: IGossipHeartbeat; - /** - * The peer metrics. - */ - metrics: IGossipMetrics; - }; -} diff --git a/bindings/nodejs/lib/types/models/state.ts b/bindings/nodejs/lib/types/models/state.ts index 37b6d8b70b..a996330949 100644 --- a/bindings/nodejs/lib/types/models/state.ts +++ b/bindings/nodejs/lib/types/models/state.ts @@ -13,9 +13,15 @@ export declare type BlockState = /** * The different states of a transaction. + * If 'pending', the transaction is not included yet. + * If 'accepted', the transaction is included. + * If 'confirmed', the transaction is included and its included block is confirmed. + * If 'finalized', the transaction is included, its included block is finalized and cannot be reverted anymore. + * If 'failed', the transaction is not successfully issued due to failure reason. */ export declare type TransactionState = | 'pending' + | 'accepted' | 'confirmed' | 'finalized' | 'failed'; diff --git a/bindings/nodejs/tests/client/infoMethods.spec.ts b/bindings/nodejs/tests/client/infoMethods.spec.ts index afeeb165de..5ec63bba74 100644 --- a/bindings/nodejs/tests/client/infoMethods.spec.ts +++ b/bindings/nodejs/tests/client/infoMethods.spec.ts @@ -59,13 +59,6 @@ describe.skip('Client info methods', () => { expect(tips.length).toBeGreaterThan(0); }); - it('gets peers', async () => { - const client = await makeClient(); - await expect(client.getPeers()).rejects.toMatch( - 'missing or malformed jwt', - ); - }); - it('gets networkInfo', async () => { const client = await makeClient(); const networkInfo = await client.getNetworkInfo(); diff --git a/bindings/python/iota_sdk/types/block/metadata.py b/bindings/python/iota_sdk/types/block/metadata.py index 19a45386e2..06d12ebb6f 100644 --- a/bindings/python/iota_sdk/types/block/metadata.py +++ b/bindings/python/iota_sdk/types/block/metadata.py @@ -49,15 +49,17 @@ class TransactionState(Enum): """Describes the state of a transaction. Attributes: - Pending: Stored but not confirmed. - Confirmed: Confirmed with the first level of knowledge. - Finalized: Included and can no longer be reverted. - Failed: The block is not successfully issued due to failure reason. + Pending: Not included yet. + Accepted: Included. + Confirmed: Included and its included block is confirmed. + Finalized: Included, its included block is finalized and cannot be reverted anymore. + Failed: Not successfully issued due to failure reason. """ Pending = 0 - Confirmed = 1 - Finalized = 2 - Failed = 3 + Accepted = 1 + Confirmed = 2 + Finalized = 3 + Failed = 4 class BlockFailureReason(IntEnum): diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index 804f5f7ca7..bfe36ef4f1 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -25,7 +25,7 @@ use crate::{ }; /// Response of GET /api/core/v3/info. -/// Returns general information about the node. +/// General information about the node. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InfoResponse { @@ -206,8 +206,8 @@ pub struct ValidatorResponse { #[serde(rename_all = "camelCase")] pub struct ValidatorsResponse { /// List of registered validators ready for the next epoch. - validators: Vec, - /// The number of validators returned per one API request with pagination. + stakers: Vec, + /// The number of validators returned per one API request with pagination. page_size: u32, /// The cursor that needs to be provided as cursor query parameter to request the next page. If empty, this was the /// last page. @@ -216,7 +216,7 @@ pub struct ValidatorsResponse { } /// Response of GET /api/core/v3/rewards/{outputId}. -/// Returns the mana rewards of an account or delegation output. +/// The mana rewards of an account or delegation output. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManaRewardsResponse { @@ -254,7 +254,7 @@ pub struct CommitteeResponse { pub epoch: EpochIndex, } -/// Returns information of a committee member. +/// Information of a committee member. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CommitteeMember { @@ -356,11 +356,13 @@ pub enum BlockState { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TransactionState { - // Stored but not confirmed. + // Not included yet. Pending, - // Confirmed with the first level of knowledge. + // Included. + Accepted, + // Included and its included block is confirmed. Confirmed, - // Included and can no longer be reverted. + // Included, its included block is finalized and cannot be reverted anymore. Finalized, // The block is not successfully issued due to failure reason. Failed, @@ -411,7 +413,7 @@ pub struct TransactionMetadataResponse { } /// Response of GET /api/core/v3/blocks/{blockId}/metadata. -/// Returns the metadata of a block. +/// The metadata of a block. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BlockMetadataResponse { @@ -424,7 +426,7 @@ pub struct BlockMetadataResponse { } /// Response of GET /api/core/v3/blocks/{blockId}/full. -/// Returns a block and its metadata. +/// A block and its metadata. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct BlockWithMetadataResponse { pub block: BlockDto, @@ -432,7 +434,7 @@ pub struct BlockWithMetadataResponse { } /// Response of GET /api/core/v3/outputs/{output_id}. -/// Returns an output and its metadata. +/// An output and its metadata. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OutputWithMetadataResponse { @@ -456,7 +458,7 @@ impl From for OutputWithMetadataResponse { } /// Response of GET /api/routes. -/// Returns the available API route groups of the node. +/// The available API route groups of the node. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RoutesResponse { @@ -466,7 +468,7 @@ pub struct RoutesResponse { /// Response of /// - GET /api/core/v3/commitments/{commitmentId}/utxo-changes /// - GET /api/core/v3/commitments/by-slot/{slot}/utxo-changes -/// Returns all UTXO changes that happened at a specific slot. +/// All UTXO changes that happened at a specific slot. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UtxoChangesResponse { @@ -478,7 +480,7 @@ pub struct UtxoChangesResponse { /// Response of /// - GET /api/core/v3/commitments/{commitmentId}/utxo-changes/full /// - GET /api/core/v3/commitments/by-slot/{slot}/utxo-changes/full -/// Returns all full UTXO changes that happened at a specific slot. +/// All full UTXO changes that happened at a specific slot. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UtxoChangesFullResponse { diff --git a/sdk/src/wallet/operations/syncing/transactions.rs b/sdk/src/wallet/operations/syncing/transactions.rs index 609dff8c62..57e4662179 100644 --- a/sdk/src/wallet/operations/syncing/transactions.rs +++ b/sdk/src/wallet/operations/syncing/transactions.rs @@ -103,8 +103,10 @@ where Ok(metadata) => { if let Some(tx_state) = metadata.transaction_metadata.map(|m| m.transaction_state) { match tx_state { - // TODO: Separate TransactionState::Finalized? - TransactionState::Finalized | TransactionState::Confirmed => { + // TODO: Separate TransactionState::Finalized, TransactionState::Accepted? https://github.com/iotaledger/iota-sdk/issues/1814 + TransactionState::Accepted + | TransactionState::Confirmed + | TransactionState::Finalized => { log::debug!( "[SYNC] confirmed transaction {transaction_id} in block {}", metadata.block_id From 7dfc8e3bc3cf254e74d8d53eac8ad9215a5fc044 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Wed, 10 Jan 2024 11:05:05 +0100 Subject: [PATCH 06/14] feat(2.0, nodejs): Silently request to stop background syncing when `WalletMethodHandler` is dropped (#1793) * feat(nodejs): Silently stop background syncing when Wallet is dropped * fmt * chore: Also call request_stop_background_syncing when destroyed manually --- bindings/nodejs/src/wallet.rs | 51 ++++++++++++++++--- .../core/operations/background_syncing.rs | 8 ++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/bindings/nodejs/src/wallet.rs b/bindings/nodejs/src/wallet.rs index 6bc039b6fc..ae90b2131d 100644 --- a/bindings/nodejs/src/wallet.rs +++ b/bindings/nodejs/src/wallet.rs @@ -1,7 +1,10 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; use iota_sdk_bindings_core::{ call_wallet_method as rust_call_wallet_method, @@ -16,26 +19,58 @@ use crate::{ build_js_error, client::ClientMethodHandler, destroyed_err, secret_manager::SecretManagerMethodHandler, NodejsError, }; -pub type WalletMethodHandler = Arc>>; +pub struct WalletMethodHandlerInner(Option); + +impl Drop for WalletMethodHandlerInner { + fn drop(&mut self) { + log::debug!("drop WalletMethodHandlerInner"); + // Request to stop the background syncing silently if this wallet hasn't been destroyed yet + if let Some(wallet) = self.0.take() { + wallet.request_stop_background_syncing(); + } + } +} + +impl Deref for WalletMethodHandlerInner { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WalletMethodHandlerInner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub type WalletMethodHandler = Arc>; #[napi(js_name = "createWallet")] pub async fn create_wallet(options: String) -> Result> { let wallet_options = serde_json::from_str::(&options).map_err(NodejsError::new)?; let wallet = wallet_options.build().await.map_err(NodejsError::new)?; - Ok(External::new(Arc::new(RwLock::new(Some(wallet))))) + Ok(External::new(Arc::new(RwLock::new(WalletMethodHandlerInner(Some( + wallet, + )))))) } #[napi(js_name = "destroyWallet")] pub async fn destroy_wallet(wallet: External) { - *wallet.as_ref().write().await = None; + let mut wallet = wallet.as_ref().write().await; + if let Some(wallet) = &**wallet { + wallet.request_stop_background_syncing(); + } + **wallet = None; } #[napi(js_name = "callWalletMethod")] pub async fn call_wallet_method(wallet: External, method: String) -> Result { let method = serde_json::from_str::(&method).map_err(NodejsError::new)?; - match &*wallet.as_ref().read().await { + match &**wallet.as_ref().read().await { Some(wallet) => { let response = rust_call_wallet_method(wallet, method).await; match response { @@ -58,7 +93,7 @@ pub async fn listen_wallet( validated_event_types.push(WalletEventType::try_from(event_type).map_err(NodejsError::new)?); } - match &*wallet.as_ref().read().await { + match &**wallet.as_ref().read().await { Some(wallet) => { wallet .listen(validated_event_types, move |event_data| { @@ -78,7 +113,7 @@ pub async fn listen_wallet( #[napi(js_name = "getClient")] pub async fn get_client(wallet: External) -> Result> { - if let Some(wallet) = &*wallet.as_ref().read().await { + if let Some(wallet) = &**wallet.as_ref().read().await { Ok(External::new(Arc::new(RwLock::new(Some(wallet.client().clone()))))) } else { Err(destroyed_err("Wallet")) @@ -87,7 +122,7 @@ pub async fn get_client(wallet: External) -> Result) -> Result> { - if let Some(wallet) = &*wallet.as_ref().read().await { + if let Some(wallet) = &**wallet.as_ref().read().await { Ok(External::new(wallet.get_secret_manager().clone())) } else { Err(destroyed_err("Wallet")) diff --git a/sdk/src/wallet/core/operations/background_syncing.rs b/sdk/src/wallet/core/operations/background_syncing.rs index 9840b967b6..9bea901acc 100644 --- a/sdk/src/wallet/core/operations/background_syncing.rs +++ b/sdk/src/wallet/core/operations/background_syncing.rs @@ -72,6 +72,12 @@ where Ok(()) } + /// Request to stop the background syncing of the wallet + pub fn request_stop_background_syncing(&self) { + log::debug!("[request_stop_background_syncing]"); + self.background_syncing_status.store(2, Ordering::Relaxed); + } + /// Stop the background syncing of the wallet pub async fn stop_background_syncing(&self) -> crate::wallet::Result<()> { log::debug!("[stop_background_syncing]"); @@ -80,7 +86,7 @@ where return Ok(()); } // send stop request - self.background_syncing_status.store(2, Ordering::Relaxed); + self.request_stop_background_syncing(); // wait until it stopped while self.background_syncing_status.load(Ordering::Relaxed) != 0 { #[cfg(target_family = "wasm")] From 5620e3d2c74742777110199f8afff612c12956b8 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 10 Jan 2024 11:09:55 +0100 Subject: [PATCH 07/14] Move address caps verification to syntactic (#1804) * Move address caps verification to syntactic * verify_restricted_addresses * Use verify_restricted_addresses in builders * Use it with Packable * Fix match * Update sdk/src/types/block/output/account.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Self:: * Better error * Update sdk/src/types/block/output/unlock_condition/mod.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- sdk/src/types/block/error.rs | 4 +- sdk/src/types/block/output/account.rs | 20 +++-- sdk/src/types/block/output/anchor.rs | 24 ++--- sdk/src/types/block/output/basic.rs | 20 ++++- sdk/src/types/block/output/delegation.rs | 14 ++- sdk/src/types/block/output/nft.rs | 7 +- .../block/output/unlock_condition/mod.rs | 90 ++++++++++++++++++- sdk/src/types/block/semantic/mod.rs | 63 +------------ 8 files changed, 155 insertions(+), 87 deletions(-) diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index ee8d5b44c8..6e293465ad 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -11,7 +11,7 @@ use primitive_types::U256; use super::slot::EpochIndex; use crate::types::block::{ - address::WeightedAddressCount, + address::{AddressCapabilityFlag, WeightedAddressCount}, context_input::RewardContextInputIndex, input::UtxoInput, mana::ManaAllotmentCount, @@ -201,6 +201,7 @@ pub enum Error { target: EpochIndex, }, TrailingCapabilityBytes, + RestrictedAddressCapability(AddressCapabilityFlag), } #[cfg(feature = "std")] @@ -432,6 +433,7 @@ impl fmt::Display for Error { write!(f, "invalid epoch delta: created {created}, target {target}") } Self::TrailingCapabilityBytes => write!(f, "capability bytes have trailing zeroes"), + Self::RestrictedAddressCapability(cap) => write!(f, "restricted address capability: {cap:?}"), } } } diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 04f601d233..e2792c4362 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -15,7 +15,10 @@ use crate::types::block::{ address::{AccountAddress, Address}, output::{ feature::{verify_allowed_features, Feature, FeatureFlags, Features}, - unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions}, + unlock_condition::{ + verify_allowed_unlock_conditions, verify_restricted_addresses, UnlockCondition, UnlockConditionFlags, + UnlockConditions, + }, ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, @@ -217,6 +220,12 @@ impl AccountOutputBuilder { let features = Features::from_set(self.features)?; + verify_restricted_addresses( + &unlock_conditions, + AccountOutput::KIND, + features.native_token(), + self.mana, + )?; verify_allowed_features(&features, AccountOutput::ALLOWED_FEATURES)?; let immutable_features = Features::from_set(self.immutable_features)?; @@ -493,6 +502,8 @@ impl Packable for AccountOutput { let features = Features::unpack::<_, VERIFY>(unpacker, &())?; if VERIFY { + verify_restricted_addresses(&unlock_conditions, Self::KIND, features.native_token(), mana) + .map_err(UnpackError::Packable)?; verify_allowed_features(&features, Self::ALLOWED_FEATURES).map_err(UnpackError::Packable)?; } @@ -611,12 +622,7 @@ mod tests { use super::*; use crate::types::block::{ - output::account::dto::AccountOutputDto, - protocol::protocol_parameters, - rand::output::{ - feature::rand_allowed_features, rand_account_id, rand_account_output, - unlock_condition::rand_address_unlock_condition_different_from_account_id, - }, + output::account::dto::AccountOutputDto, protocol::protocol_parameters, rand::output::rand_account_output, }; #[test] diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 6f5c926934..958b8eecda 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -15,7 +15,10 @@ use crate::types::block::{ address::{Address, AnchorAddress}, output::{ feature::{verify_allowed_features, Feature, FeatureFlags, Features}, - unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions}, + unlock_condition::{ + verify_allowed_unlock_conditions, verify_restricted_addresses, UnlockCondition, UnlockConditionFlags, + UnlockConditions, + }, ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, @@ -248,6 +251,12 @@ impl AnchorOutputBuilder { let features = Features::from_set(self.features)?; + verify_restricted_addresses( + &unlock_conditions, + AnchorOutput::KIND, + features.native_token(), + self.mana, + )?; verify_allowed_features(&features, AnchorOutput::ALLOWED_FEATURES)?; let immutable_features = Features::from_set(self.immutable_features)?; @@ -549,6 +558,8 @@ impl Packable for AnchorOutput { let features = Features::unpack::<_, VERIFY>(unpacker, &())?; if VERIFY { + verify_restricted_addresses(&unlock_conditions, Self::KIND, features.native_token(), mana) + .map_err(UnpackError::Packable)?; verify_allowed_features(&features, Self::ALLOWED_FEATURES).map_err(UnpackError::Packable)?; } @@ -675,16 +686,7 @@ mod dto { mod tests { use super::*; use crate::types::block::{ - output::anchor::dto::AnchorOutputDto, - protocol::protocol_parameters, - rand::output::{ - feature::rand_allowed_features, - rand_anchor_id, rand_anchor_output, - unlock_condition::{ - rand_governor_address_unlock_condition_different_from, - rand_state_controller_address_unlock_condition_different_from, - }, - }, + output::anchor::dto::AnchorOutputDto, protocol::protocol_parameters, rand::output::rand_anchor_output, }; #[test] diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 9b31c18782..41f81ac911 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -10,8 +10,8 @@ use crate::types::block::{ output::{ feature::{verify_allowed_features, Feature, FeatureFlags, Features, NativeTokenFeature}, unlock_condition::{ - verify_allowed_unlock_conditions, AddressUnlockCondition, StorageDepositReturnUnlockCondition, - UnlockCondition, UnlockConditionFlags, UnlockConditions, + verify_allowed_unlock_conditions, verify_restricted_addresses, AddressUnlockCondition, + StorageDepositReturnUnlockCondition, UnlockCondition, UnlockConditionFlags, UnlockConditions, }, MinimumOutputAmount, NativeToken, Output, OutputBuilderAmount, StorageScore, StorageScoreParameters, }, @@ -192,6 +192,12 @@ impl BasicOutputBuilder { let features = Features::from_set(self.features)?; + verify_restricted_addresses( + &unlock_conditions, + BasicOutput::KIND, + features.native_token(), + self.mana, + )?; verify_features::(&features)?; let mut output = BasicOutput { @@ -230,6 +236,7 @@ impl From<&BasicOutput> for BasicOutputBuilder { #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)] #[packable(unpack_error = Error)] #[packable(unpack_visitor = ProtocolParameters)] +#[packable(verify_with = verify_basic_output)] pub struct BasicOutput { /// Amount of IOTA coins held by the output. amount: u64, @@ -392,6 +399,15 @@ fn verify_features_packable(features: &Features, _: &Protoco verify_features::(features) } +fn verify_basic_output(output: &BasicOutput, _: &ProtocolParameters) -> Result<(), Error> { + verify_restricted_addresses( + output.unlock_conditions(), + BasicOutput::KIND, + output.features.native_token(), + output.mana, + ) +} + #[cfg(feature = "serde")] mod dto { use alloc::vec::Vec; diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index e3d8b9ffd9..d9e880019c 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -9,7 +9,10 @@ use crate::types::block::{ address::{AccountAddress, Address}, output::{ chain_id::ChainId, - unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions}, + unlock_condition::{ + verify_allowed_unlock_conditions, verify_restricted_addresses, UnlockCondition, UnlockConditionFlags, + UnlockConditions, + }, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, @@ -172,6 +175,7 @@ impl DelegationOutputBuilder { let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?; verify_unlock_conditions::(&unlock_conditions)?; + verify_restricted_addresses(&unlock_conditions, DelegationOutput::KIND, None, 0)?; let mut output = DelegationOutput { amount: 0, @@ -215,6 +219,7 @@ impl From<&DelegationOutput> for DelegationOutputBuilder { #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)] #[packable(unpack_error = Error)] #[packable(unpack_visitor = ProtocolParameters)] +#[packable(verify_with = verify_delegation_output)] pub struct DelegationOutput { /// Amount of IOTA coins held by the output. amount: u64, @@ -392,6 +397,13 @@ fn verify_unlock_conditions_packable( verify_unlock_conditions::(unlock_conditions) } +fn verify_delegation_output( + output: &DelegationOutput, + _: &ProtocolParameters, +) -> Result<(), Error> { + verify_restricted_addresses(output.unlock_conditions(), DelegationOutput::KIND, None, 0) +} + #[cfg(feature = "serde")] mod dto { use alloc::vec::Vec; diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 9636399d5c..d641ee1f46 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -15,8 +15,8 @@ use crate::types::block::{ output::{ feature::{verify_allowed_features, Feature, FeatureFlags, Features}, unlock_condition::{ - verify_allowed_unlock_conditions, AddressUnlockCondition, StorageDepositReturnUnlockCondition, - UnlockCondition, UnlockConditionFlags, UnlockConditions, + verify_allowed_unlock_conditions, verify_restricted_addresses, AddressUnlockCondition, + StorageDepositReturnUnlockCondition, UnlockCondition, UnlockConditionFlags, UnlockConditions, }, BasicOutputBuilder, ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, @@ -256,6 +256,7 @@ impl NftOutputBuilder { let features = Features::from_set(self.features)?; + verify_restricted_addresses(&unlock_conditions, NftOutput::KIND, features.native_token(), self.mana)?; verify_allowed_features(&features, NftOutput::ALLOWED_FEATURES)?; let immutable_features = Features::from_set(self.immutable_features)?; @@ -469,6 +470,8 @@ impl Packable for NftOutput { let features = Features::unpack::<_, VERIFY>(unpacker, &())?; if VERIFY { + verify_restricted_addresses(&unlock_conditions, Self::KIND, features.native_token(), mana) + .map_err(UnpackError::Packable)?; verify_allowed_features(&features, Self::ALLOWED_FEATURES).map_err(UnpackError::Packable)?; } diff --git a/sdk/src/types/block/output/unlock_condition/mod.rs b/sdk/src/types/block/output/unlock_condition/mod.rs index 6b2bc79ed5..b3506172c1 100644 --- a/sdk/src/types/block/output/unlock_condition/mod.rs +++ b/sdk/src/types/block/output/unlock_condition/mod.rs @@ -24,8 +24,11 @@ pub use self::{ storage_deposit_return::StorageDepositReturnUnlockCondition, timelock::TimelockUnlockCondition, }; use crate::types::block::{ - address::Address, - output::{StorageScore, StorageScoreParameters}, + address::{Address, AddressCapabilityFlag, RestrictedAddress}, + output::{ + feature::NativeTokenFeature, AccountOutput, AnchorOutput, DelegationOutput, NftOutput, StorageScore, + StorageScoreParameters, + }, protocol::{CommittableAgeRange, ProtocolParameters, WorkScore}, slot::SlotIndex, Error, @@ -314,6 +317,22 @@ impl UnlockConditions { Ok(address) } + + /// Returns an iterator over all addresses except StorageDepositReturn address. + pub fn addresses(&self) -> impl Iterator { + self.iter().filter_map(|uc| match uc { + UnlockCondition::Address(uc) => Some(uc.address()), + UnlockCondition::Expiration(uc) => Some(uc.return_address()), + UnlockCondition::StateControllerAddress(uc) => Some(uc.address()), + UnlockCondition::GovernorAddress(uc) => Some(uc.address()), + _ => None, + }) + } + + /// Returns an iterator over all restricted addresses. + pub fn restricted_addresses(&self) -> impl Iterator { + self.addresses().filter_map(Address::as_restricted_opt) + } } impl StorageScore for UnlockConditions { @@ -355,6 +374,73 @@ pub(crate) fn verify_allowed_unlock_conditions( Ok(()) } +pub(crate) fn verify_restricted_addresses( + unlock_conditions: &UnlockConditions, + output_kind: u8, + native_token: Option<&NativeTokenFeature>, + mana: u64, +) -> Result<(), Error> { + let addresses = unlock_conditions.restricted_addresses(); + + for address in addresses { + if native_token.is_some() && !address.has_capability(AddressCapabilityFlag::OutputsWithNativeTokens) { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::OutputsWithNativeTokens, + )); + } + + if mana > 0 && !address.has_capability(AddressCapabilityFlag::OutputsWithMana) { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::OutputsWithMana, + )); + } + + if unlock_conditions.timelock().is_some() && !address.has_capability(AddressCapabilityFlag::OutputsWithTimelock) + { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::OutputsWithTimelock, + )); + } + + if unlock_conditions.expiration().is_some() + && !address.has_capability(AddressCapabilityFlag::OutputsWithExpiration) + { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::OutputsWithExpiration, + )); + } + + if unlock_conditions.storage_deposit_return().is_some() + && !address.has_capability(AddressCapabilityFlag::OutputsWithStorageDepositReturn) + { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::OutputsWithStorageDepositReturn, + )); + } + + match output_kind { + AccountOutput::KIND if !address.has_capability(AddressCapabilityFlag::AccountOutputs) => { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::AccountOutputs, + )); + } + AnchorOutput::KIND if !address.has_capability(AddressCapabilityFlag::AnchorOutputs) => { + return Err(Error::RestrictedAddressCapability(AddressCapabilityFlag::AnchorOutputs)); + } + NftOutput::KIND if !address.has_capability(AddressCapabilityFlag::NftOutputs) => { + return Err(Error::RestrictedAddressCapability(AddressCapabilityFlag::NftOutputs)); + } + DelegationOutput::KIND if !address.has_capability(AddressCapabilityFlag::DelegationOutputs) => { + return Err(Error::RestrictedAddressCapability( + AddressCapabilityFlag::DelegationOutputs, + )); + } + _ => {} + } + } + Ok(()) +} + #[cfg(test)] mod test { use pretty_assertions::assert_eq; diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index b7573384e2..f92e5bab2e 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -15,8 +15,8 @@ pub use self::{ state_transition::{StateTransitionError, StateTransitionVerifier}, }; use crate::types::block::{ - address::{Address, AddressCapabilityFlag}, - output::{AccountId, AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, TokenId, UnlockCondition}, + address::Address, + output::{AccountId, AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, TokenId}, payload::signed_transaction::{Transaction, TransactionCapabilityFlag, TransactionSigningHash}, protocol::ProtocolParameters, unlock::Unlock, @@ -210,65 +210,6 @@ impl<'a> SemanticValidationContext<'a> { Output::Delegation(output) => (output.amount(), 0, None, None), }; - if let Some(unlock_conditions) = created_output.unlock_conditions() { - // Check the possibly restricted address-containing conditions - let addresses = unlock_conditions - .iter() - .filter_map(|uc| match uc { - UnlockCondition::Address(uc) => Some(uc.address()), - UnlockCondition::Expiration(uc) => Some(uc.return_address()), - UnlockCondition::StateControllerAddress(uc) => Some(uc.address()), - UnlockCondition::GovernorAddress(uc) => Some(uc.address()), - _ => None, - }) - .filter_map(Address::as_restricted_opt); - for address in addresses { - if created_native_token.is_some() - && !address.has_capability(AddressCapabilityFlag::OutputsWithNativeTokens) - { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - - if mana > 0 && !address.has_capability(AddressCapabilityFlag::OutputsWithMana) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - - if unlock_conditions.timelock().is_some() - && !address.has_capability(AddressCapabilityFlag::OutputsWithTimelock) - { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - - if unlock_conditions.expiration().is_some() - && !address.has_capability(AddressCapabilityFlag::OutputsWithExpiration) - { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - - if unlock_conditions.storage_deposit_return().is_some() - && !address.has_capability(AddressCapabilityFlag::OutputsWithStorageDepositReturn) - { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - - if match &created_output { - Output::Account(_) => !address.has_capability(AddressCapabilityFlag::AccountOutputs), - Output::Anchor(_) => !address.has_capability(AddressCapabilityFlag::AnchorOutputs), - Output::Nft(_) => !address.has_capability(AddressCapabilityFlag::NftOutputs), - Output::Delegation(_) => !address.has_capability(AddressCapabilityFlag::DelegationOutputs), - _ => false, - } { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } - } - } - if let Some(sender) = features.and_then(|f| f.sender()) { if !self.unlocked_addresses.contains(sender.address()) { return Ok(Some(TransactionFailureReason::SenderNotUnlocked)); From a54b4f8338af11abe3293d8384bceae2092223f2 Mon Sep 17 00:00:00 2001 From: Brord van Wierst Date: Wed, 10 Jan 2024 13:14:28 +0100 Subject: [PATCH 08/14] Update wallet events (#1789) * initial wallet event pass * env fix * removed consolidationrequired * fix event example --------- Co-authored-by: /alex/ Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- .../examples/exchange/4-listen-events.ts | 11 ++-- bindings/nodejs/examples/wallet/events.ts | 6 +- bindings/nodejs/lib/types/wallet/event.ts | 46 ++-------------- .../lib/wallet/wallet-method-handler.ts | 8 +-- bindings/nodejs/lib/wallet/wallet.ts | 3 +- .../examples/exchange/4_listen_events.py | 5 +- bindings/python/iota_sdk/types/event.py | 22 ++++---- sdk/src/client/error.rs | 3 - sdk/src/wallet/core/builder.rs | 5 +- .../core/operations/address_generation.rs | 55 ++++++++----------- sdk/src/wallet/error.rs | 3 - sdk/src/wallet/events/mod.rs | 31 ++++++----- sdk/src/wallet/events/types.rs | 49 +++++++---------- .../operations/transaction/input_selection.rs | 17 +----- sdk/tests/wallet/events.rs | 2 - 15 files changed, 91 insertions(+), 175 deletions(-) diff --git a/bindings/nodejs/examples/exchange/4-listen-events.ts b/bindings/nodejs/examples/exchange/4-listen-events.ts index 09e93b7f45..d80cfd2046 100644 --- a/bindings/nodejs/examples/exchange/4-listen-events.ts +++ b/bindings/nodejs/examples/exchange/4-listen-events.ts @@ -5,14 +5,14 @@ // Run with command: // yarn run-example ./exchange/4-listen-events.ts -import { Wallet, Event, WalletEventType } from '@iota/sdk'; +import { Wallet, WalletEvent, WalletEventType } from '@iota/sdk'; // This example uses secrets in environment variables for simplicity which should not be done in production. require('dotenv').config({ path: '.env' }); async function run() { try { - for (const envVar of ['WALLET_DB_PATH']) { + for (const envVar of ['WALLET_DB_PATH', 'FAUCET_URL']) { if (!(envVar in process.env)) { throw new Error( `.env ${envVar} is undefined, see .env.example`, @@ -24,9 +24,8 @@ async function run() { storagePath: process.env.WALLET_DB_PATH, }); - const callback = function (err: any, event: Event) { - console.log('AccountIndex:', event.accountIndex); - console.log('Event:', event.event); + const callback = function (err: any, event: WalletEvent) { + console.log('Event:', event); // Exit after receiving an event. process.exit(0); @@ -37,7 +36,7 @@ async function run() { // Use the faucet to send testnet tokens to your address. console.log( - 'Fill your address with the faucet: https://faucet.testnet.shimmer.network/', + `Fill your address with the faucet: ${process.env.FAUCET_URL}`, ); const address = await wallet.address(); diff --git a/bindings/nodejs/examples/wallet/events.ts b/bindings/nodejs/examples/wallet/events.ts index f11178fc82..d0f8f4f5fd 100644 --- a/bindings/nodejs/examples/wallet/events.ts +++ b/bindings/nodejs/examples/wallet/events.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { - Event, - ConsolidationRequiredWalletEvent, + WalletEvent, TransactionProgressWalletEvent, SelectingInputsProgress, } from '@iota/sdk'; @@ -24,13 +23,12 @@ async function run() { // Create the wallet const wallet = await getUnlockedWallet(); - const callback = function (err: any, event: Event) { + const callback = function (err: any, event: WalletEvent) { console.log('Event:', event); }; await wallet.listen([], callback); - await wallet.emitTestEvent(new ConsolidationRequiredWalletEvent()); await wallet.emitTestEvent( new TransactionProgressWalletEvent(new SelectingInputsProgress()), ); diff --git a/bindings/nodejs/lib/types/wallet/event.ts b/bindings/nodejs/lib/types/wallet/event.ts index 2ade953208..fbe8d2b7fa 100644 --- a/bindings/nodejs/lib/types/wallet/event.ts +++ b/bindings/nodejs/lib/types/wallet/event.ts @@ -13,45 +13,20 @@ import { HexEncodedString } from '../utils'; */ export type TransactionId = string; -/** - * An wallet account event. - */ -class Event { - /** - * The account index for which the event was emitted. - */ - accountIndex: number; - /** - * The wallet event. - */ - event: WalletEvent; - - /** - * @param accountIndex The account index. - * @param event The wallet event. - */ - constructor(accountIndex: number, event: WalletEvent) { - this.accountIndex = accountIndex; - this.event = event; - } -} - /** * All of the wallet event types. */ enum WalletEventType { - /** Consolidation is required. */ - ConsolidationRequired = 0, /** Nano Ledger has generated an address. */ - LedgerAddressGeneration = 1, + LedgerAddressGeneration = 0, /** A new output was created. */ - NewOutput = 2, + NewOutput = 1, /** An output was spent. */ - SpentOutput = 3, + SpentOutput = 2, /** A transaction was included into the ledger. */ - TransactionInclusion = 4, + TransactionInclusion = 3, /** A progress update while submitting a transaction. */ - TransactionProgress = 5, + TransactionProgress = 4, } /** @@ -68,15 +43,6 @@ abstract class WalletEvent { } } -/** - * A 'consolidation required' wallet event. - */ -class ConsolidationRequiredWalletEvent extends WalletEvent { - constructor() { - super(WalletEventType.ConsolidationRequired); - } -} - /** * A 'ledger address generation' wallet event. */ @@ -280,10 +246,8 @@ class BroadcastingProgress extends TransactionProgress { } export { - Event, WalletEventType, WalletEvent, - ConsolidationRequiredWalletEvent, LedgerAddressGenerationWalletEvent, NewOutputWalletEvent, SpentOutputWalletEvent, diff --git a/bindings/nodejs/lib/wallet/wallet-method-handler.ts b/bindings/nodejs/lib/wallet/wallet-method-handler.ts index b91eda5834..7bb9783da5 100644 --- a/bindings/nodejs/lib/wallet/wallet-method-handler.ts +++ b/bindings/nodejs/lib/wallet/wallet-method-handler.ts @@ -11,9 +11,9 @@ import { } from '../bindings'; import { WalletEventType, + WalletEvent, WalletOptions, __WalletMethod__, - Event, } from '../types/wallet'; import { Client, ClientMethodHandler } from '../client'; import { SecretManager, SecretManagerMethodHandler } from '../secret_manager'; @@ -74,17 +74,17 @@ export class WalletMethodHandler { */ async listen( eventTypes: WalletEventType[], - callback: (error: Error, event: Event) => void, + callback: (error: Error, event: WalletEvent) => void, ): Promise { return listenWallet( this.methodHandler, eventTypes, function (err: any, data: string) { - const parsed = JSON.parse(data); + const parsed: WalletEvent = JSON.parse(data); callback( // Send back raw error instead of parsing err, - new Event(parsed.accountIndex, parsed.event), + parsed, ); }, ).catch((error: any) => { diff --git a/bindings/nodejs/lib/wallet/wallet.ts b/bindings/nodejs/lib/wallet/wallet.ts index a8b1a53a2a..380b55a7b9 100644 --- a/bindings/nodejs/lib/wallet/wallet.ts +++ b/bindings/nodejs/lib/wallet/wallet.ts @@ -52,7 +52,6 @@ import type { WalletOptions, WalletEventType, WalletEvent, - Event, } from '../types/wallet'; import { IAuth, IClientOptions, LedgerNanoStatus } from '../types/client'; import { SecretManager } from '../secret_manager'; @@ -169,7 +168,7 @@ export class Wallet { */ async listen( eventTypes: WalletEventType[], - callback: (error: Error, event: Event) => void, + callback: (error: Error, event: WalletEvent) => void, ): Promise { return this.methodHandler.listen(eventTypes, callback); } diff --git a/bindings/python/examples/exchange/4_listen_events.py b/bindings/python/examples/exchange/4_listen_events.py index ca89373e93..7de0246462 100644 --- a/bindings/python/examples/exchange/4_listen_events.py +++ b/bindings/python/examples/exchange/4_listen_events.py @@ -27,9 +27,8 @@ def callback(event): """Callback function for the event listener""" - event_dict = json.loads(event) - print('AccountIndex:', event_dict["accountIndex"]) - print('Event:', event_dict["event"]) + event = json.loads(event) + print('Event:', event) # Exit after receiving an event. # pylint: disable=global-statement diff --git a/bindings/python/iota_sdk/types/event.py b/bindings/python/iota_sdk/types/event.py index 9e378dea09..75ed34886a 100644 --- a/bindings/python/iota_sdk/types/event.py +++ b/bindings/python/iota_sdk/types/event.py @@ -8,16 +8,14 @@ class WalletEventType(IntEnum): """Types of wallet events. Attributes: - ConsolidationRequired (0): Consolidation is required. - LedgerAddressGeneration (1): Nano Ledger has generated an address. - NewOutput (2): A new output was created. - SpentOutput (3): An output was spent. - TransactionInclusion (4): A transaction was included into the ledger. - TransactionProgress (5): A progress update while submitting a transaction. + LedgerAddressGeneration (0): Nano Ledger has generated an address. + NewOutput (1): A new output was created. + SpentOutput (2): An output was spent. + TransactionInclusion (3): A transaction was included into the ledger. + TransactionProgress (4): A progress update while submitting a transaction. """ - ConsolidationRequired = 0 - LedgerAddressGeneration = 1 - NewOutput = 2 - SpentOutput = 3 - TransactionInclusion = 4 - TransactionProgress = 5 + LedgerAddressGeneration = 0 + NewOutput = 1 + SpentOutput = 2 + TransactionInclusion = 3 + TransactionProgress = 4 diff --git a/sdk/src/client/error.rs b/sdk/src/client/error.rs index 5c9bdcbdff..8094d81ea2 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -36,9 +36,6 @@ pub enum Error { /// Block types error #[error("{0}")] Block(#[from] crate::types::block::Error), - /// The wallet has enough funds, but split on too many outputs - #[error("the wallet has enough funds, but split on too many outputs: {0}, max. is 128, consolidate them")] - ConsolidationRequired(usize), /// Crypto.rs error #[error("{0}")] Crypto(#[from] crypto::Error), diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index 63e87715ee..c9aa3e7e23 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -227,9 +227,6 @@ where #[cfg(feature = "storage")] self.save(&storage_manager).await?; - #[cfg(feature = "events")] - let event_emitter = tokio::sync::RwLock::new(EventEmitter::new()); - // It happened that inputs got locked, the transaction failed, but they weren't unlocked again, so we do this // here #[cfg(feature = "storage")] @@ -253,7 +250,7 @@ where client, secret_manager: self.secret_manager.expect("make WalletInner::secret_manager optional?"), #[cfg(feature = "events")] - event_emitter, + event_emitter: tokio::sync::RwLock::new(EventEmitter::new()), #[cfg(feature = "storage")] storage_options, #[cfg(feature = "storage")] diff --git a/sdk/src/wallet/core/operations/address_generation.rs b/sdk/src/wallet/core/operations/address_generation.rs index fe902ee8ec..50427dd324 100644 --- a/sdk/src/wallet/core/operations/address_generation.rs +++ b/sdk/src/wallet/core/operations/address_generation.rs @@ -36,41 +36,34 @@ impl Wallet { // needs to have it visible on the computer first, so we need to generate it without the // prompt first let options = options.into(); + #[cfg(feature = "events")] if options.as_ref().map_or(false, |o| o.ledger_nano_prompt) { - #[cfg(feature = "events")] - { - let changed_options = options.map(|mut options| { - // Change options so ledger will not show the prompt the first time - options.ledger_nano_prompt = false; - options - }); - // Generate without prompt to be able to display it - let address = ledger_nano - .generate_ed25519_addresses( - coin_type, - account_index, - address_index..address_index + 1, - changed_options, - ) - .await?; + let changed_options = options.map(|mut options| { + // Change options so ledger will not show the prompt the first time + options.ledger_nano_prompt = false; + options + }); + // Generate without prompt to be able to display it + let address = ledger_nano + .generate_ed25519_addresses( + coin_type, + account_index, + address_index..address_index + 1, + changed_options, + ) + .await?; - let bech32_hrp = self.bech32_hrp().await; + let bech32_hrp = self.bech32_hrp().await; - self.emit(WalletEvent::LedgerAddressGeneration(AddressData { - address: address[0].to_bech32(bech32_hrp), - })) - .await; - } - - // Generate with prompt so the user can verify - ledger_nano - .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) - .await? - } else { - ledger_nano - .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) - .await? + self.emit(WalletEvent::LedgerAddressGeneration(AddressData { + address: address[0].to_bech32(bech32_hrp), + })) + .await; } + // Generate with prompt so the user can verify + ledger_nano + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) + .await? } #[cfg(feature = "stronghold")] SecretManager::Stronghold(stronghold) => { diff --git a/sdk/src/wallet/error.rs b/sdk/src/wallet/error.rs index ad26c39a91..e6b222517f 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -33,9 +33,6 @@ pub enum Error { new_bip_path: Option, old_bip_path: Option, }, - /// Funds are spread over too many outputs - #[error("funds are spread over too many outputs {output_count}/{output_count_max}, consolidation required")] - ConsolidationRequired { output_count: usize, output_count_max: u16 }, /// Crypto.rs error #[error("{0}")] Crypto(#[from] crypto::Error), diff --git a/sdk/src/wallet/events/mod.rs b/sdk/src/wallet/events/mod.rs index e908a0d1f9..c75cc39acc 100644 --- a/sdk/src/wallet/events/mod.rs +++ b/sdk/src/wallet/events/mod.rs @@ -41,7 +41,6 @@ impl EventEmitter { WalletEventType::SpentOutput, WalletEventType::TransactionInclusion, WalletEventType::TransactionProgress, - WalletEventType::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEventType::LedgerAddressGeneration, ] { @@ -74,7 +73,6 @@ impl EventEmitter { WalletEvent::SpentOutput(_) => WalletEventType::SpentOutput, WalletEvent::TransactionInclusion(_) => WalletEventType::TransactionInclusion, WalletEvent::TransactionProgress(_) => WalletEventType::TransactionProgress, - WalletEvent::ConsolidationRequired => WalletEventType::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEvent::LedgerAddressGeneration(_) => WalletEventType::LedgerAddressGeneration, }; @@ -126,18 +124,18 @@ mod tests { let event_counter = Arc::new(AtomicUsize::new(0)); // single event - emitter.on([WalletEventType::ConsolidationRequired], |_name| { - // println!("ConsolidationRequired: {:?}", name); + emitter.on([WalletEventType::TransactionInclusion], |_name| { + // println!("TransactionInclusion: {:?}", name); }); // listen to two events emitter.on( [ WalletEventType::TransactionProgress, - WalletEventType::ConsolidationRequired, + WalletEventType::TransactionInclusion, ], move |_name| { - // println!("TransactionProgress or ConsolidationRequired: {:?}", name); + // println!("TransactionProgress or TransactionInclusion: {:?}", name); }, ); @@ -149,7 +147,6 @@ mod tests { }); // emit events - emitter.emit(WalletEvent::ConsolidationRequired); emitter.emit(WalletEvent::TransactionProgress( TransactionProgressEvent::SelectingInputs, )); @@ -161,14 +158,16 @@ mod tests { inclusion_state: InclusionState::Confirmed, })); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // remove handlers of single event - emitter.clear([WalletEventType::ConsolidationRequired]); + emitter.clear([WalletEventType::TransactionProgress]); // emit event of removed type - emitter.emit(WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // remove handlers of all events emitter.clear([]); @@ -183,18 +182,20 @@ mod tests { .expect("invalid tx id"), inclusion_state: InclusionState::Confirmed, })); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // listen to a single event let event_counter_clone = Arc::clone(&event_counter); - emitter.on([WalletEventType::ConsolidationRequired], move |_name| { + emitter.on([WalletEventType::TransactionProgress], move |_name| { // println!("Any event: {:?}", name); event_counter_clone.fetch_add(1, Ordering::SeqCst); }); for _ in 0..1_000_000 { - emitter.emit(WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); } - assert_eq!(1_000_003, event_counter.load(Ordering::SeqCst)); + assert_eq!(1_000_002, event_counter.load(Ordering::SeqCst)); } } diff --git a/sdk/src/wallet/events/types.rs b/sdk/src/wallet/events/types.rs index 71a4e7b927..475fd5ab2e 100644 --- a/sdk/src/wallet/events/types.rs +++ b/sdk/src/wallet/events/types.rs @@ -22,7 +22,6 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum WalletEvent { - ConsolidationRequired, #[cfg(feature = "ledger_nano")] #[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))] LedgerAddressGeneration(AddressData), @@ -45,13 +44,12 @@ impl Serialize for WalletEvent { #[derive(Serialize)] #[serde(untagged)] enum WalletEvent_<'a> { - T0, #[cfg(feature = "ledger_nano")] - T1(&'a AddressData), - T2(&'a NewOutputEvent), - T3(&'a SpentOutputEvent), - T4(&'a TransactionInclusionEvent), - T5(TransactionProgressEvent_<'a>), + T0(&'a AddressData), + T1(&'a NewOutputEvent), + T2(&'a SpentOutputEvent), + T3(&'a TransactionInclusionEvent), + T4(TransactionProgressEvent_<'a>), } #[derive(Serialize)] struct TypedWalletEvent_<'a> { @@ -61,30 +59,26 @@ impl Serialize for WalletEvent { event: WalletEvent_<'a>, } let event = match self { - Self::ConsolidationRequired => TypedWalletEvent_ { - kind: WalletEventType::ConsolidationRequired as u8, - event: WalletEvent_::T0, - }, #[cfg(feature = "ledger_nano")] Self::LedgerAddressGeneration(e) => TypedWalletEvent_ { kind: WalletEventType::LedgerAddressGeneration as u8, - event: WalletEvent_::T1(e), + event: WalletEvent_::T0(e), }, Self::NewOutput(e) => TypedWalletEvent_ { kind: WalletEventType::NewOutput as u8, - event: WalletEvent_::T2(e), + event: WalletEvent_::T1(e), }, Self::SpentOutput(e) => TypedWalletEvent_ { kind: WalletEventType::SpentOutput as u8, - event: WalletEvent_::T3(e), + event: WalletEvent_::T2(e), }, Self::TransactionInclusion(e) => TypedWalletEvent_ { kind: WalletEventType::TransactionInclusion as u8, - event: WalletEvent_::T4(e), + event: WalletEvent_::T3(e), }, Self::TransactionProgress(e) => TypedWalletEvent_ { kind: WalletEventType::TransactionProgress as u8, - event: WalletEvent_::T5(TransactionProgressEvent_ { progress: e }), + event: WalletEvent_::T4(TransactionProgressEvent_ { progress: e }), }, }; event.serialize(serializer) @@ -108,7 +102,6 @@ impl<'de> Deserialize<'de> for WalletEvent { ) .map_err(serde::de::Error::custom)? { - WalletEventType::ConsolidationRequired => Self::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEventType::LedgerAddressGeneration => { Self::LedgerAddressGeneration(AddressData::deserialize(value).map_err(|e| { @@ -146,14 +139,13 @@ impl<'de> Deserialize<'de> for WalletEvent { #[repr(u8)] #[non_exhaustive] pub enum WalletEventType { - ConsolidationRequired = 0, #[cfg(feature = "ledger_nano")] #[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))] - LedgerAddressGeneration = 1, - NewOutput = 2, - SpentOutput = 3, - TransactionInclusion = 4, - TransactionProgress = 5, + LedgerAddressGeneration = 0, + NewOutput = 1, + SpentOutput = 2, + TransactionInclusion = 3, + TransactionProgress = 4, } impl TryFrom for WalletEventType { @@ -161,13 +153,12 @@ impl TryFrom for WalletEventType { fn try_from(value: u8) -> Result { let event_type = match value { - 0 => Self::ConsolidationRequired, #[cfg(feature = "ledger_nano")] - 1 => Self::LedgerAddressGeneration, - 2 => Self::NewOutput, - 3 => Self::SpentOutput, - 4 => Self::TransactionInclusion, - 5 => Self::TransactionProgress, + 0 => Self::LedgerAddressGeneration, + 1 => Self::NewOutput, + 2 => Self::SpentOutput, + 3 => Self::TransactionInclusion, + 4 => Self::TransactionProgress, _ => return Err(Error::InvalidEventType(value)), }; Ok(event_type) diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index 5422f2a922..9a5b8c8bb2 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -171,22 +171,7 @@ where input_selection = input_selection.with_burn(burn.clone()); } - let selected_transaction_data = match input_selection.select() { - Ok(r) => r, - // TODO this error doesn't exist with the new ISA - // Err(crate::client::Error::ConsolidationRequired(output_count)) => { - // #[cfg(feature = "events")] - // self.event_emitter - // .lock() - // .await - // .emit(account.index, WalletEvent::ConsolidationRequired); - // return Err(crate::wallet::Error::ConsolidationRequired { - // output_count, - // output_count_max: INPUT_COUNT_MAX, - // }); - // } - Err(e) => return Err(e.into()), - }; + let selected_transaction_data = input_selection.select()?; // lock outputs so they don't get used by another transaction for output in &selected_transaction_data.inputs { diff --git a/sdk/tests/wallet/events.rs b/sdk/tests/wallet/events.rs index 62892ea721..9dac2168ea 100644 --- a/sdk/tests/wallet/events.rs +++ b/sdk/tests/wallet/events.rs @@ -40,8 +40,6 @@ fn assert_serde_eq(event_0: WalletEvent) { #[test] fn wallet_events_serde() { - assert_serde_eq(WalletEvent::ConsolidationRequired); - #[cfg(feature = "ledger_nano")] assert_serde_eq(WalletEvent::LedgerAddressGeneration(AddressData { address: Bech32Address::try_from_str("rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy") From 91e0a80fd11dc65687dc3347b3515dbf486130cb Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:27:59 +0100 Subject: [PATCH 09/14] Consider backed ed25519 addresses in more places in the ISA (#1820) * Consider backed ed25519 addresses in more places in the ISA * Update sdk/src/client/api/block_builder/input_selection/remainder.rs Co-authored-by: DaughterOfMars * Suggestions --------- Co-authored-by: DaughterOfMars Co-authored-by: Thibault Martinez --- sdk/Cargo.toml | 1 + .../api/block_builder/input_selection/mod.rs | 11 +++- .../input_selection/remainder.rs | 2 +- .../input_selection/requirement/ed25519.rs | 4 +- .../client/input_selection/basic_outputs.rs | 66 ++++++++++++++++++- 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index d36f4d4ccc..d946ff4c62 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -134,6 +134,7 @@ dotenvy = { version = "0.15.7", default-features = false } fern-logger = { version = "0.5.0", default-features = false } num_cpus = { version = "1.16.0", default-features = false } once_cell = { version = "1.19.0", default-features = false } +regex = { version = "1.10.2", default-features = false } tokio = { version = "1.35.1", default-features = false, features = [ "macros", "rt", diff --git a/sdk/src/client/api/block_builder/input_selection/mod.rs b/sdk/src/client/api/block_builder/input_selection/mod.rs index f38bcd6e2d..e7dca476b7 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -154,7 +154,16 @@ impl InputSelection { protocol_parameters: ProtocolParameters, ) -> Self { let available_inputs = available_inputs.into(); - let mut addresses = HashSet::from_iter(addresses); + + let mut addresses = HashSet::from_iter(addresses.into_iter().map(|a| { + // Get a potential Ed25519 address directly since we're only interested in that + #[allow(clippy::option_if_let_else)] // clippy's suggestion requires a clone + if let Some(address) = a.backing_ed25519() { + Address::Ed25519(*address) + } else { + a + } + })); addresses.extend(available_inputs.iter().filter_map(|input| match &input.output { Output::Account(output) => Some(Address::Account(AccountAddress::from( diff --git a/sdk/src/client/api/block_builder/input_selection/remainder.rs b/sdk/src/client/api/block_builder/input_selection/remainder.rs index 89127e8a18..3f3c06adfb 100644 --- a/sdk/src/client/api/block_builder/input_selection/remainder.rs +++ b/sdk/src/client/api/block_builder/input_selection/remainder.rs @@ -42,7 +42,7 @@ impl InputSelection { .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? .expect("expiration unlockable outputs already filtered out"); - if required_address.is_ed25519() { + if required_address.is_ed25519_backed() { return Ok(Some((required_address, input.chain))); } } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs b/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs index af0619e949..cf466f84ff 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs @@ -27,7 +27,9 @@ impl InputSelection { .unwrap() .expect("expiration unlockable outputs already filtered out"); - &required_address == address + required_address + .backing_ed25519() + .map_or(false, |a| a == address.as_ed25519()) } /// Fulfills an ed25519 sender requirement by selecting an available input that unlocks its address. diff --git a/sdk/tests/client/input_selection/basic_outputs.rs b/sdk/tests/client/input_selection/basic_outputs.rs index fd1a6440e9..7b190eeb3c 100644 --- a/sdk/tests/client/input_selection/basic_outputs.rs +++ b/sdk/tests/client/input_selection/basic_outputs.rs @@ -6,7 +6,10 @@ use std::str::FromStr; use iota_sdk::{ client::api::input_selection::{Error, InputSelection, Requirement}, types::block::{ - address::{Address, MultiAddress, RestrictedAddress, WeightedAddress}, + address::{ + Address, AddressCapabilities, ImplicitAccountCreationAddress, MultiAddress, RestrictedAddress, + WeightedAddress, + }, output::{AccountId, NftId}, protocol::protocol_parameters, }, @@ -2060,3 +2063,64 @@ fn multi_address_sender_already_fulfilled() { assert!(unsorted_eq(&selected.inputs, &inputs)); assert!(unsorted_eq(&selected.outputs, &outputs)); } + +#[test] +fn ed25519_backed_available_address() { + let protocol_parameters = protocol_parameters(); + let ed25519 = Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(); + let restricted_address = Address::from( + RestrictedAddress::new(ed25519.clone()) + .unwrap() + .with_allowed_capabilities(AddressCapabilities::all()), + ); + + let inputs = build_inputs([ + Basic( + 1_000_000, + restricted_address.clone(), + None, + None, + None, + None, + None, + None, + ), + Basic(1_000_000, ed25519.clone(), None, None, None, None, None, None), + ]); + let outputs = build_outputs([ + Basic( + 1_000_000, + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + None, + None, + None, + None, + None, + None, + ), + Basic( + 1_000_000, + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + None, + Some(restricted_address.clone()), + None, + None, + None, + None, + ), + ]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + // Restricted address is provided, but it can also unlock the ed25519 one + [restricted_address], + protocol_parameters, + ) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs, &inputs)); + // Provided outputs + assert_eq!(selected.outputs, outputs); +} From a2660e60b5af227a62417af97913287c57246cbd Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:36:44 +0100 Subject: [PATCH 10/14] Add issuing_time check (#1821) --- sdk/src/client/api/block_builder/mod.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/sdk/src/client/api/block_builder/mod.rs b/sdk/src/client/api/block_builder/mod.rs index 45bb293a2d..1977c2bde5 100644 --- a/sdk/src/client/api/block_builder/mod.rs +++ b/sdk/src/client/api/block_builder/mod.rs @@ -6,7 +6,7 @@ pub mod transaction; pub use self::transaction::verify_semantic; use crate::{ - client::{ClientInner, Result}, + client::{constants::FIVE_MINUTES_IN_NANOSECONDS, ClientInner, Error, Result}, types::block::{ core::{BlockHeader, UnsignedBlock}, output::AccountId, @@ -19,7 +19,6 @@ impl ClientInner { pub async fn build_basic_block(&self, issuer_id: AccountId, payload: Option) -> Result { let issuance = self.get_issuance().await?; - // TODO https://github.com/iotaledger/iota-sdk/issues/1753 let issuing_time = { #[cfg(feature = "std")] let issuing_time = std::time::SystemTime::now() @@ -30,7 +29,20 @@ impl ClientInner { // https://github.com/iotaledger/iota-sdk/issues/647 #[cfg(not(feature = "std"))] let issuing_time = 0; - issuing_time + + // Check that the issuing_time is in the range of +-5 minutes of the node to prevent potential issues + if !(issuance.latest_parent_block_issuing_time - FIVE_MINUTES_IN_NANOSECONDS + ..issuance.latest_parent_block_issuing_time + FIVE_MINUTES_IN_NANOSECONDS) + .contains(&issuing_time) + { + return Err(Error::TimeNotSynced { + current_time: issuing_time, + tangle_time: issuance.latest_parent_block_issuing_time, + }); + } + // If timestamp is below latest_parent_block_issuing_time, just increase it to that +1 so the block doesn't + // get rejected + issuing_time.max(issuance.latest_parent_block_issuing_time + 1) }; let protocol_params = self.get_protocol_parameters().await?; From 656f20f43c29fc372e026b10f46d39c48ada2c2d Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 10 Jan 2024 15:40:42 +0100 Subject: [PATCH 11/14] Add RewardsParameters::retention_period (#1813) * Add RewardsParameters::retention_period * u16 * Fix test * Format --- .../types/models/info/node-info-protocol.ts | 4 ++ bindings/python/iota_sdk/types/node_info.py | 2 + sdk/src/types/block/mana/rewards.rs | 3 ++ .../types/fixtures/protocol_parameters.json | 41 ++++++++++--------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/bindings/nodejs/lib/types/models/info/node-info-protocol.ts b/bindings/nodejs/lib/types/models/info/node-info-protocol.ts index e7a31d3251..02a85a991d 100644 --- a/bindings/nodejs/lib/types/models/info/node-info-protocol.ts +++ b/bindings/nodejs/lib/types/models/info/node-info-protocol.ts @@ -155,6 +155,10 @@ export interface RewardsParameters { * in the pool rewards calculations. */ poolCoefficientExponent: number; + /** + * The number of epochs for which rewards are retained. + */ + retentionPeriod: number; } /** diff --git a/bindings/python/iota_sdk/types/node_info.py b/bindings/python/iota_sdk/types/node_info.py index 8395afc8fe..64dc0648bc 100644 --- a/bindings/python/iota_sdk/types/node_info.py +++ b/bindings/python/iota_sdk/types/node_info.py @@ -209,6 +209,7 @@ class RewardsParameters: decay_balancing_constant_exponent: The exponent used for calculation of the initial reward. decay_balancing_constant: An integer approximation which is calculated using the `decay_balancing_constant_exponent`. pool_coefficient_exponent: The exponent used for shifting operation during the pool rewards calculations. + retention_period: The number of epochs for which rewards are retained. """ profit_margin_exponent: int bootstrapping_duration: int @@ -220,6 +221,7 @@ class RewardsParameters: encoder=str )) pool_coefficient_exponent: int + retention_period: int @json diff --git a/sdk/src/types/block/mana/rewards.rs b/sdk/src/types/block/mana/rewards.rs index 9a73b391e8..bfe0692417 100644 --- a/sdk/src/types/block/mana/rewards.rs +++ b/sdk/src/types/block/mana/rewards.rs @@ -29,6 +29,8 @@ pub struct RewardsParameters { decay_balancing_constant: u64, /// The exponent used for shifting operation during the pool rewards calculations. pool_coefficient_exponent: u8, + // The number of epochs for which rewards are retained. + retention_period: u16, } impl Default for RewardsParameters { @@ -41,6 +43,7 @@ impl Default for RewardsParameters { decay_balancing_constant_exponent: Default::default(), decay_balancing_constant: Default::default(), pool_coefficient_exponent: Default::default(), + retention_period: Default::default(), } } } diff --git a/sdk/tests/types/fixtures/protocol_parameters.json b/sdk/tests/types/fixtures/protocol_parameters.json index 6e9c4d18d5..df58b43736 100644 --- a/sdk/tests/types/fixtures/protocol_parameters.json +++ b/sdk/tests/types/fixtures/protocol_parameters.json @@ -5,24 +5,24 @@ "networkName":"testnet", "bech32Hrp":"rms", "storageScoreParameters":{ - "storageCost":"0", - "factorData":0, - "offsetOutputOverhead":"0", - "offsetEd25519BlockIssuerKey":"0", - "offsetStakingFeature":"0", - "offsetDelegation":"0" + "storageCost":"100", + "factorData":1, + "offsetOutputOverhead":"10", + "offsetEd25519BlockIssuerKey":"100", + "offsetStakingFeature":"100", + "offsetDelegation":"100" }, "workScoreParameters":{ - "dataByte":0, - "block":1, - "input":0, - "contextInput":0, - "output":0, - "nativeToken":0, - "staking":0, - "blockIssuer":0, - "allotment":0, - "signatureEd25519":0 + "dataByte":1, + "block":2, + "input":3, + "contextInput":4, + "output":5, + "nativeToken":6, + "staking":7, + "blockIssuer":8, + "allotment":9, + "signatureEd25519":10 }, "manaParameters":{ "bitsCount":63, @@ -421,7 +421,7 @@ }, "tokenSupply":"1813620509061365", "genesisSlot":0, - "genesisUnixTimestamp":"1702037100", + "genesisUnixTimestamp":"1695275822", "slotDurationInSeconds":10, "slotsPerEpochExponent":13, "stakingUnbondingPeriod":10, @@ -453,11 +453,12 @@ "manaShareCoefficient":"2", "decayBalancingConstantExponent":8, "decayBalancingConstant":"1", - "poolCoefficientExponent":31 + "poolCoefficientExponent":11, + "retentionPeriod":384 }, "targetCommitteeSize":32, "chainSwitchingThreshold":3 }, - "bytes":"0x000307746573746e657403726d730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000003f01118001bb4ec3ffdaab86ff59174aff35910dff6a19d1fef5af94fed35458fe00081cfe78c9dffd3999a3fd3f7767fd87632bfd0d5eeffccd66b3fcc67d77fcf2a23bfc4fd6fffbd917c4fb8e6788fb69c54cfb673111fb85abd5fac0339afa14ca5efa7e6e23fafb20e8f986e1acf91eb071f9be8c36f96477fbf80b70c0f8b17685f8528b4af8ecad0ff87aded4f7f91c9af766695ff7bec324f7fe2beaf621a2aff6262675f608b83af6c45700f65705c6f5bec08bf5f68951f5fa6017f5c845ddf45d38a3f4b63869f4ce462ff4a462f5f3328cbbf378c381f3700848f3185b0ef36cbbd4f26a299bf20ea561f2552e28f23bc5eef1be69b5f1da1b7cf18cdb42f1d1a809f1a583d0f0056c97f0ef615ef05e6525f05076ecefc294b3efb0c07aef16fa41eff34009ef4295d0ee01f797ee2c665feec0e226eeba6ceeed1704b6edd4a87deded5a45ed5f1a0ded27e7d4ec42c19cecada864ec659d2cec669ff4ebadaebceb37cb84eb02f54ceb092c15eb4a70ddeac2c1a5ea6d206eea488c36ea5105ffe9848bc7e9dd1e90e95bbf58e9f96c21e9b527eae88befb2e879c47be87ba644e88e950de8b091d6e7dc9a9fe710b168e749d431e78404fbe6be41c4e6f38b8de620e356e6434720e658b8e9e55d36b3e54ec17ce5285946e5e8fd0fe58bafd9e40e6ea3e46e396de4a81137e4b8f600e49de8cae352e794e3d4f25ee3220b29e33730f3e21162bde2aca087e206ec51e21b441ce2e9a8e6e16d1ab1e1a2987be1882346e11abb10e1555fdbe03710a6e0bccd70e0e1973be0a46e06e00252d1dff7419cdf813e67df9c4732df455dfdde7b7fc8de38ae93de7ce95ede42312ade8885f5dd4ae6c0dd86538cdd39cd57dd605323ddf8e5eedcfe84badc6e3086dc47e851dc85ac1ddc257de9db245ab5db804381db35394ddb403b19db9f49e5da4f64b1da4c8b7dda94be49da24fe15daf849e2d90fa2aed965067bd9f77647d9c3f313d9c57ce0d8fa11add860b379d8f46046d8b31a13d899e0dfd7a5b2acd7d39079d7207b46d78a7113d70d74e0d6a782add6559d7ad614c447d6e1f614d6b935e2d59a80afd580d77cd5693a4ad552a917d53824e5d418abb2d4ef3d80d4bbdc4dd478871bd4253ee9d3bd00b7d33fcf84d3a7a952d3f28f20d31e82eed22880bcd20d8a8ad2cb9f58d25ec126d2c3eef4d1f927c3d1fb6c91d1c8bd5fd15c1a2ed1b582fcd0d0f6cad0aa7699d0400268d08f9936d0963c05d050ebd3cfbca5a2cfd66b71cf9c3d40cf0a1b0fcf1f04deced7f8acce30f97bce26054bceb81c1acee23fe9cda16eb8cdf4a887cdd6ee56cd464026cd419df5ccc305c5cccb7994cc55f963cc5e8433cce51a03cce6bcd2cb5e6aa2cb4b2372cbabe741cb79b711cbb592e1ca5a79b1ca666b81cad76851caaa7121cadc85f1c96ba5c1c953d091c9930662c9264832c90c9502c940edd2c8c150a3c88bbf73c89c3944c8f2be14c8894fe5c760ebb5c7729286c7be4457c7410228c7f9caf8c6e29ec9c6fa7d9ac63e686bc6ad5d3cc6425e0dc6fb69dec5d780afc5d1a280c5e8cf51c5190823c5614bf4c4be99c5c42cf396c4aa5768c435c739c4ca410bc466c7dcc30758aec3aaf37fc34d9a51c3ed4b23c38808f5c21ad0c6c2a2a298c21c806ac286683cc2de5b0ec2205ae0c14b63b2c15c7784c14f9656c123c028c1d5f4fac06334cdc0c97e9fc005d471c0153444c0f69e16c0a614e9bf2295bbbf66208ebf72b660bf425733bfd40206bf25b9d8be337aabbefa457ebe7a1c51beaefd23be95e9f6bd2be0c9bd6fe19cbd5eed6fbdf50343bd322516bd1351e9bc9487bcbcb3c88fbc6e1463bcc26a36bcadcb09bc2c37ddbb3dadb0bbdd2d84bb09b957bbc04e2bbbffeefebac299d2ba084fa6bacf0e7aba13d94dbad2ad21ba0b8df5b9b976c9b9db6a9db96e6971b9707245b9df8519b9b7a3edb8f7cbc1b89cfe95b8a33b6ab80a833eb8cfd412b8ee30e7b76797bbb7350890b7588364b7cb0839b78e980db79c32e2b6f5d6b6b695858bb67b3e60b6a30135b60bcf09b6b1a6deb59288b3b5ac7488b5fd6a5db5816b32b5387607b51d8bdcb430aab1b46dd386b4d2065cb45c4431b40a8c06b4d8dddbb3c539b1b3ce9f86b3f00f5cb32099c0d9861546f530336e7a710600000000006c067365000000000a0d0a0000000a0a0000000f001e000a000000140000003c00000001000000000000000000000000000000000000000000000000350c0020a10700a0860100e803000064000000070507083704000002000000000000000801000000000000001f2003", - "hash":"0x92c887fb3dd070200c83a8ad9f20824eae3b40f641b23896391679abec60de14" + "bytes":"0x000307746573746e657403726d736400000000000000010a000000000000006400000000000000640000000000000064000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000003f01118001bb4ec3ffdaab86ff59174aff35910dff6a19d1fef5af94fed35458fe00081cfe78c9dffd3999a3fd3f7767fd87632bfd0d5eeffccd66b3fcc67d77fcf2a23bfc4fd6fffbd917c4fb8e6788fb69c54cfb673111fb85abd5fac0339afa14ca5efa7e6e23fafb20e8f986e1acf91eb071f9be8c36f96477fbf80b70c0f8b17685f8528b4af8ecad0ff87aded4f7f91c9af766695ff7bec324f7fe2beaf621a2aff6262675f608b83af6c45700f65705c6f5bec08bf5f68951f5fa6017f5c845ddf45d38a3f4b63869f4ce462ff4a462f5f3328cbbf378c381f3700848f3185b0ef36cbbd4f26a299bf20ea561f2552e28f23bc5eef1be69b5f1da1b7cf18cdb42f1d1a809f1a583d0f0056c97f0ef615ef05e6525f05076ecefc294b3efb0c07aef16fa41eff34009ef4295d0ee01f797ee2c665feec0e226eeba6ceeed1704b6edd4a87deded5a45ed5f1a0ded27e7d4ec42c19cecada864ec659d2cec669ff4ebadaebceb37cb84eb02f54ceb092c15eb4a70ddeac2c1a5ea6d206eea488c36ea5105ffe9848bc7e9dd1e90e95bbf58e9f96c21e9b527eae88befb2e879c47be87ba644e88e950de8b091d6e7dc9a9fe710b168e749d431e78404fbe6be41c4e6f38b8de620e356e6434720e658b8e9e55d36b3e54ec17ce5285946e5e8fd0fe58bafd9e40e6ea3e46e396de4a81137e4b8f600e49de8cae352e794e3d4f25ee3220b29e33730f3e21162bde2aca087e206ec51e21b441ce2e9a8e6e16d1ab1e1a2987be1882346e11abb10e1555fdbe03710a6e0bccd70e0e1973be0a46e06e00252d1dff7419cdf813e67df9c4732df455dfdde7b7fc8de38ae93de7ce95ede42312ade8885f5dd4ae6c0dd86538cdd39cd57dd605323ddf8e5eedcfe84badc6e3086dc47e851dc85ac1ddc257de9db245ab5db804381db35394ddb403b19db9f49e5da4f64b1da4c8b7dda94be49da24fe15daf849e2d90fa2aed965067bd9f77647d9c3f313d9c57ce0d8fa11add860b379d8f46046d8b31a13d899e0dfd7a5b2acd7d39079d7207b46d78a7113d70d74e0d6a782add6559d7ad614c447d6e1f614d6b935e2d59a80afd580d77cd5693a4ad552a917d53824e5d418abb2d4ef3d80d4bbdc4dd478871bd4253ee9d3bd00b7d33fcf84d3a7a952d3f28f20d31e82eed22880bcd20d8a8ad2cb9f58d25ec126d2c3eef4d1f927c3d1fb6c91d1c8bd5fd15c1a2ed1b582fcd0d0f6cad0aa7699d0400268d08f9936d0963c05d050ebd3cfbca5a2cfd66b71cf9c3d40cf0a1b0fcf1f04deced7f8acce30f97bce26054bceb81c1acee23fe9cda16eb8cdf4a887cdd6ee56cd464026cd419df5ccc305c5cccb7994cc55f963cc5e8433cce51a03cce6bcd2cb5e6aa2cb4b2372cbabe741cb79b711cbb592e1ca5a79b1ca666b81cad76851caaa7121cadc85f1c96ba5c1c953d091c9930662c9264832c90c9502c940edd2c8c150a3c88bbf73c89c3944c8f2be14c8894fe5c760ebb5c7729286c7be4457c7410228c7f9caf8c6e29ec9c6fa7d9ac63e686bc6ad5d3cc6425e0dc6fb69dec5d780afc5d1a280c5e8cf51c5190823c5614bf4c4be99c5c42cf396c4aa5768c435c739c4ca410bc466c7dcc30758aec3aaf37fc34d9a51c3ed4b23c38808f5c21ad0c6c2a2a298c21c806ac286683cc2de5b0ec2205ae0c14b63b2c15c7784c14f9656c123c028c1d5f4fac06334cdc0c97e9fc005d471c0153444c0f69e16c0a614e9bf2295bbbf66208ebf72b660bf425733bfd40206bf25b9d8be337aabbefa457ebe7a1c51beaefd23be95e9f6bd2be0c9bd6fe19cbd5eed6fbdf50343bd322516bd1351e9bc9487bcbcb3c88fbc6e1463bcc26a36bcadcb09bc2c37ddbb3dadb0bbdd2d84bb09b957bbc04e2bbbffeefebac299d2ba084fa6bacf0e7aba13d94dbad2ad21ba0b8df5b9b976c9b9db6a9db96e6971b9707245b9df8519b9b7a3edb8f7cbc1b89cfe95b8a33b6ab80a833eb8cfd412b8ee30e7b76797bbb7350890b7588364b7cb0839b78e980db79c32e2b6f5d6b6b695858bb67b3e60b6a30135b60bcf09b6b1a6deb59288b3b5ac7488b5fd6a5db5816b32b5387607b51d8bdcb430aab1b46dd386b4d2065cb45c4431b40a8c06b4d8dddbb3c539b1b3ce9f86b3f00f5cb32099c0d9861546f530336e7a710600000000002edb0b65000000000a0d0a0000000a0a0000000f001e000a000000140000003c00000001000000000000000000000000000000000000000000000000350c0020a10700a0860100e803000064000000070507083704000002000000000000000801000000000000000b80012003", + "hash":"0x28ccbc633e0d22e19752f5e65c0d22055a7d59756bfa754b8839088e18a6a5a6" } \ No newline at end of file From 679b8fa9fde015444b24265fe9a0ea5624cd2b18 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 10 Jan 2024 15:44:46 +0100 Subject: [PATCH 12/14] ISA: add mana requirement (#1762) * Add ManaAllotments to InputSelection * Add fulfill_mana_requirement * Add allotments to requirement * Already fulfilled check * Select inputs for mana * Cleanup allotments * Remove ManaAllotment * Pass mana allotments to ISA * Build tw with mana allotments * Add allot_mana and prepare_allot_mana * Add allot_mana_command to CLI * Add CLI log * Fix deadlock * Temporarily disable mana semantic check * fallback to implicit accounts * ManaAllotment camelCase * Tmp mana burn * Move mana burn cap to a single place * Some clippy * Nits * Copyright date * Add TODO * Cleanup TODO * Add mana_allotments to ISA output count check * Nit * Remove check * Fix no_std * move dto.outputs * Nit --------- Co-authored-by: /alex/ --- cli/src/wallet_cli/completer.rs | 1 + cli/src/wallet_cli/mod.rs | 34 +++- .../api/block_builder/input_selection/mod.rs | 15 +- .../input_selection/requirement/mana.rs | 31 ++++ .../input_selection/requirement/mod.rs | 4 + sdk/src/types/block/mana/allotment.rs | 166 ++++++++++++++---- sdk/src/types/block/mana/mod.rs | 138 +-------------- sdk/src/types/block/output/delegation.rs | 2 +- .../payload/signed_transaction/transaction.rs | 16 +- sdk/src/types/block/rand/mana.rs | 1 - sdk/src/types/block/semantic/mod.rs | 9 +- sdk/src/wallet/error.rs | 6 +- sdk/src/wallet/operations/block.rs | 18 +- .../wallet/operations/transaction/account.rs | 6 - .../transaction/build_transaction.rs | 20 ++- .../transaction/high_level/allot_mana.rs | 58 ++++++ .../operations/transaction/high_level/mod.rs | 1 + .../operations/transaction/input_selection.rs | 14 ++ .../wallet/operations/transaction/options.rs | 3 + .../transaction/prepare_transaction.rs | 16 +- 20 files changed, 337 insertions(+), 222 deletions(-) create mode 100644 sdk/src/client/api/block_builder/input_selection/requirement/mana.rs create mode 100644 sdk/src/wallet/operations/transaction/high_level/allot_mana.rs diff --git a/cli/src/wallet_cli/completer.rs b/cli/src/wallet_cli/completer.rs index b3f316db4f..9b9a55af48 100644 --- a/cli/src/wallet_cli/completer.rs +++ b/cli/src/wallet_cli/completer.rs @@ -11,6 +11,7 @@ use rustyline::{ const WALLET_COMMANDS: &[&str] = &[ "accounts", "address", + "allot-mana", "balance", "burn-native-token", "burn-nft", diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 96d6386e7c..0de3d7b01b 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -14,6 +14,7 @@ use iota_sdk::{ api::plugins::participation::types::ParticipationEventId, block::{ address::{Bech32Address, ToBech32Ext}, + mana::ManaAllotment, output::{ unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder, FoundryId, NativeToken, NativeTokensBuilder, NftId, Output, OutputId, TokenId, @@ -60,6 +61,8 @@ pub enum WalletCommand { Accounts, /// Print the wallet address. Address, + /// Allots mana to an account. + AllotMana { mana: u64, account_id: Option }, /// Print the wallet balance. Balance, /// Burn an amount of native token. @@ -344,6 +347,32 @@ pub async fn address_command(wallet: &Wallet) -> Result<(), Error> { Ok(()) } +// `allot-mana` command +pub async fn allot_mana_command(wallet: &Wallet, mana: u64, account_id: Option) -> Result<(), Error> { + let wallet_data = wallet.data().await; + let account_id = account_id + .or(wallet_data + .accounts() + .next() + .map(|o| o.output.as_account().account_id_non_null(&o.output_id))) + .or(wallet_data + .implicit_accounts() + .next() + .map(|o| AccountId::from(&o.output_id))) + .ok_or(WalletError::AccountNotFound)?; + drop(wallet_data); + + let transaction = wallet.allot_mana([ManaAllotment::new(account_id, mana)?], None).await?; + + println_log_info!( + "Mana allotment transaction sent:\n{:?}\n{:?}", + transaction.transaction_id, + transaction.block_id + ); + + Ok(()) +} + // `balance` command pub async fn balance_command(wallet: &Wallet) -> Result<(), Error> { let balance = wallet.balance().await?; @@ -497,7 +526,7 @@ pub async fn congestion_command(wallet: &Wallet, account_id: Option) .next() .map(|o| AccountId::from(&o.output_id)) }) - .ok_or(WalletError::NoAccountToIssueBlock)? + .ok_or(WalletError::AccountNotFound)? }; let congestion = wallet.client().get_account_congestion(&account_id).await?; @@ -1173,6 +1202,9 @@ pub async fn prompt_internal( match protocol_cli.command { WalletCommand::Accounts => accounts_command(wallet).await, WalletCommand::Address => address_command(wallet).await, + WalletCommand::AllotMana { mana, account_id } => { + allot_mana_command(wallet, mana, account_id).await + } WalletCommand::Balance => balance_command(wallet).await, WalletCommand::BurnNativeToken { token_id, amount } => { burn_native_token_command(wallet, token_id, amount).await diff --git a/sdk/src/client/api/block_builder/input_selection/mod.rs b/sdk/src/client/api/block_builder/input_selection/mod.rs index e7dca476b7..ef13ae2fa1 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -21,6 +21,7 @@ use crate::{ types::block::{ address::{AccountAddress, Address, NftAddress}, input::INPUT_COUNT_RANGE, + mana::ManaAllotment, output::{ AccountOutput, ChainId, FoundryOutput, NativeTokensBuilder, NftOutput, Output, OutputId, OUTPUT_COUNT_RANGE, }, @@ -45,6 +46,7 @@ pub struct InputSelection { slot_index: SlotIndex, requirements: Vec, automatically_transitioned: HashSet, + mana_allotments: u64, } /// Result of the input selection algorithm. @@ -101,6 +103,8 @@ impl InputSelection { } fn init(&mut self) -> Result<(), Error> { + // Adds an initial mana requirement. + self.requirements.push(Requirement::Mana(self.mana_allotments)); // Adds an initial amount requirement. self.requirements.push(Requirement::Amount); // Adds an initial native tokens requirement. @@ -190,6 +194,7 @@ impl InputSelection { slot_index: SlotIndex::from(0), requirements: Vec::new(), automatically_transitioned: HashSet::new(), + mana_allotments: 0, } } @@ -223,6 +228,12 @@ impl InputSelection { self } + /// Sets the mana allotments sum of an [`InputSelection`]. + pub fn with_mana_allotments<'a>(mut self, mana_allotments: impl Iterator) -> Self { + self.mana_allotments = mana_allotments.map(ManaAllotment::mana).sum(); + self + } + fn filter_inputs(&mut self) { self.available_inputs.retain(|input| { // TODO what about other kinds? @@ -373,8 +384,8 @@ impl InputSelection { /// transaction. Also creates a remainder output and chain transition outputs if required. pub fn select(mut self) -> Result { if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) { - // If burn is provided, outputs will be added later - if !(self.outputs.is_empty() && self.burn.is_some()) { + // If burn or mana allotments are provided, outputs will be added later. + if !(self.outputs.is_empty() && (self.burn.is_some() || self.mana_allotments != 0)) { return Err(Error::InvalidOutputCount(self.outputs.len())); } } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs new file mode 100644 index 0000000000..281302d7b1 --- /dev/null +++ b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs @@ -0,0 +1,31 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::{Error, InputSelection}; +use crate::client::secret::types::InputSigningData; + +impl InputSelection { + pub(crate) fn fulfill_mana_requirement(&mut self, allotments: u64) -> Result, Error> { + let required_mana = self.outputs.iter().map(|o| o.mana()).sum::() + allotments; + let mut selected_mana = self.selected_inputs.iter().map(|o| o.output.mana()).sum::(); + + if selected_mana >= required_mana { + log::debug!("Mana requirement already fulfilled"); + Ok(Vec::new()) + } else { + let mut inputs = Vec::new(); + + // TODO we should do as for the amount and have preferences on which inputs to pick. + while let Some(input) = self.available_inputs.pop() { + selected_mana += input.output.mana(); + inputs.push(input); + + if selected_mana >= required_mana { + break; + } + } + + Ok(inputs) + } + } +} diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs b/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs index 9da2517062..9fa273d02b 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod amount; pub(crate) mod ed25519; pub(crate) mod foundry; pub(crate) mod issuer; +pub(crate) mod mana; pub(crate) mod native_tokens; pub(crate) mod nft; pub(crate) mod sender; @@ -39,6 +40,8 @@ pub enum Requirement { NativeTokens, /// Amount requirement. Amount, + /// Mana requirement. + Mana(u64), } impl InputSelection { @@ -56,6 +59,7 @@ impl InputSelection { Requirement::Nft(nft_id) => self.fulfill_nft_requirement(nft_id), Requirement::NativeTokens => self.fulfill_native_tokens_requirement(), Requirement::Amount => self.fulfill_amount_requirement(), + Requirement::Mana(allotments) => self.fulfill_mana_requirement(allotments), } } diff --git a/sdk/src/types/block/mana/allotment.rs b/sdk/src/types/block/mana/allotment.rs index dc9295c11d..d191fdbd87 100644 --- a/sdk/src/types/block/mana/allotment.rs +++ b/sdk/src/types/block/mana/allotment.rs @@ -1,7 +1,12 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use packable::Packable; +use alloc::{boxed::Box, collections::BTreeSet, vec::Vec}; +use core::ops::RangeInclusive; + +use derive_more::Deref; +use iterator_sorted::is_unique_sorted; +use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable}; use crate::types::block::{ output::AccountId, @@ -13,16 +18,21 @@ use crate::types::block::{ /// in the form of Block Issuance Credits to the account. #[derive(Copy, Clone, Debug, Eq, PartialEq, Packable)] #[packable(unpack_error = Error)] -#[packable(unpack_visitor = ProtocolParameters)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] pub struct ManaAllotment { pub(crate) account_id: AccountId, #[packable(verify_with = verify_mana)] + #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::string"))] pub(crate) mana: u64, } impl ManaAllotment { - pub fn new(account_id: AccountId, mana: u64, protocol_params: &ProtocolParameters) -> Result { - verify_mana::(&mana, protocol_params)?; + pub fn new(account_id: AccountId, mana: u64) -> Result { + verify_mana::(&mana)?; Ok(Self { account_id, mana }) } @@ -54,53 +64,131 @@ impl WorkScore for ManaAllotment { } } -fn verify_mana(mana: &u64, params: &ProtocolParameters) -> Result<(), Error> { - if VERIFY && *mana > params.mana_parameters().max_mana() { +fn verify_mana(mana: &u64) -> Result<(), Error> { + if VERIFY && *mana == 0 { return Err(Error::InvalidManaValue(*mana)); } Ok(()) } -#[cfg(feature = "serde")] -pub(super) mod dto { - use serde::{Deserialize, Serialize}; +pub(crate) type ManaAllotmentCount = + BoundedU16<{ *ManaAllotments::COUNT_RANGE.start() }, { *ManaAllotments::COUNT_RANGE.end() }>; + +/// A list of [`ManaAllotment`]s with unique [`AccountId`]s. +#[derive(Clone, Debug, Eq, PartialEq, Deref, Packable)] +#[packable(unpack_visitor = ProtocolParameters)] +#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidManaAllotmentCount(p.into())))] +pub struct ManaAllotments( + #[packable(verify_with = verify_mana_allotments)] BoxedSlicePrefix, +); + +impl ManaAllotments { + /// The minimum number of mana allotments of a transaction. + pub const COUNT_MIN: u16 = 0; + /// The maximum number of mana allotments of a transaction. + pub const COUNT_MAX: u16 = 128; + /// The range of valid numbers of mana allotments of a transaction. + pub const COUNT_RANGE: RangeInclusive = Self::COUNT_MIN..=Self::COUNT_MAX; // [0..128] + + /// Creates a new [`ManaAllotments`] from a vec. + pub fn from_vec(allotments: Vec) -> Result { + verify_mana_allotments_unique_sorted(&allotments)?; + + Ok(Self( + allotments + .into_boxed_slice() + .try_into() + .map_err(Error::InvalidManaAllotmentCount)?, + )) + } - use super::*; - use crate::{types::TryFromDto, utils::serde::string}; + /// Creates a new [`ManaAllotments`] from an ordered set. + pub fn from_set(allotments: BTreeSet) -> Result { + Ok(Self( + allotments + .into_iter() + .collect::>() + .try_into() + .map_err(Error::InvalidManaAllotmentCount)?, + )) + } - #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ManaAllotmentDto { - pub account_id: AccountId, - #[serde(with = "string")] - pub mana: u64, + /// Gets a reference to an [`ManaAllotment`], if one exists, using an [`AccountId`]. + #[inline(always)] + pub fn get(&self, account_id: &AccountId) -> Option<&ManaAllotment> { + self.0.iter().find(|a| a.account_id() == account_id) } +} - impl From<&ManaAllotment> for ManaAllotmentDto { - fn from(value: &ManaAllotment) -> Self { - Self { - account_id: value.account_id, - mana: value.mana, - } - } +fn verify_mana_allotments( + allotments: &[ManaAllotment], + protocol_params: &ProtocolParameters, +) -> Result<(), Error> { + if VERIFY { + verify_mana_allotments_unique_sorted(allotments)?; + verify_mana_allotments_sum(allotments, protocol_params)?; } - impl TryFromDto for ManaAllotment { - type Error = Error; - - fn try_from_dto_with_params_inner( - dto: ManaAllotmentDto, - params: Option<&ProtocolParameters>, - ) -> Result { - Ok(if let Some(params) = params { - Self::new(dto.account_id, dto.mana, params)? - } else { - Self { - account_id: dto.account_id, - mana: dto.mana, - } - }) + Ok(()) +} + +fn verify_mana_allotments_unique_sorted<'a>( + allotments: impl IntoIterator, +) -> Result<(), Error> { + if !is_unique_sorted(allotments.into_iter()) { + return Err(Error::ManaAllotmentsNotUniqueSorted); + } + Ok(()) +} + +pub(crate) fn verify_mana_allotments_sum<'a>( + allotments: impl IntoIterator, + protocol_params: &ProtocolParameters, +) -> Result<(), Error> { + let mut mana_sum: u64 = 0; + let max_mana = protocol_params.mana_parameters().max_mana(); + + for ManaAllotment { mana, .. } in allotments { + mana_sum = mana_sum.checked_add(*mana).ok_or(Error::InvalidManaAllotmentSum { + sum: mana_sum as u128 + *mana as u128, + max: max_mana, + })?; + + if mana_sum > max_mana { + return Err(Error::InvalidManaAllotmentSum { + sum: mana_sum as u128, + max: max_mana, + }); } } + + Ok(()) +} + +impl TryFrom> for ManaAllotments { + type Error = Error; + + #[inline(always)] + fn try_from(allotments: Vec) -> Result { + Self::from_vec(allotments) + } +} + +impl TryFrom> for ManaAllotments { + type Error = Error; + + #[inline(always)] + fn try_from(allotments: BTreeSet) -> Result { + Self::from_set(allotments) + } +} + +impl IntoIterator for ManaAllotments { + type Item = ManaAllotment; + type IntoIter = alloc::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + Vec::from(Into::>::into(self.0)).into_iter() + } } diff --git a/sdk/src/types/block/mana/mod.rs b/sdk/src/types/block/mana/mod.rs index 72328c74db..a4f22ba61a 100644 --- a/sdk/src/types/block/mana/mod.rs +++ b/sdk/src/types/block/mana/mod.rs @@ -5,135 +5,9 @@ mod allotment; mod parameters; mod rewards; -use alloc::{boxed::Box, collections::BTreeSet, vec::Vec}; -use core::ops::RangeInclusive; - -use derive_more::Deref; -use iterator_sorted::is_unique_sorted; -use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable}; - -#[cfg(feature = "serde")] -pub use self::allotment::dto::ManaAllotmentDto; -pub use self::{allotment::ManaAllotment, parameters::ManaParameters, rewards::RewardsParameters}; -use super::{output::AccountId, protocol::ProtocolParameters, Error}; - -pub(crate) type ManaAllotmentCount = - BoundedU16<{ *ManaAllotments::COUNT_RANGE.start() }, { *ManaAllotments::COUNT_RANGE.end() }>; - -/// A list of [`ManaAllotment`]s with unique [`AccountId`]s. -#[derive(Clone, Debug, Eq, PartialEq, Deref, Packable)] -#[packable(unpack_visitor = ProtocolParameters)] -#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidManaAllotmentCount(p.into())))] -pub struct ManaAllotments( - #[packable(verify_with = verify_mana_allotments)] BoxedSlicePrefix, -); - -impl ManaAllotments { - /// The minimum number of mana allotments of a transaction. - pub const COUNT_MIN: u16 = 0; - /// The maximum number of mana allotments of a transaction. - pub const COUNT_MAX: u16 = 128; - /// The range of valid numbers of mana allotments of a transaction. - pub const COUNT_RANGE: RangeInclusive = Self::COUNT_MIN..=Self::COUNT_MAX; // [1..128] - - /// Creates a new [`ManaAllotments`] from a vec. - pub fn from_vec(allotments: Vec) -> Result { - verify_mana_allotments_unique_sorted(&allotments)?; - - Ok(Self( - allotments - .into_boxed_slice() - .try_into() - .map_err(Error::InvalidManaAllotmentCount)?, - )) - } - - /// Creates a new [`ManaAllotments`] from an ordered set. - pub fn from_set(allotments: BTreeSet) -> Result { - Ok(Self( - allotments - .into_iter() - .collect::>() - .try_into() - .map_err(Error::InvalidManaAllotmentCount)?, - )) - } - - /// Gets a reference to an [`ManaAllotment`], if one exists, using an [`AccountId`]. - #[inline(always)] - pub fn get(&self, account_id: &AccountId) -> Option<&ManaAllotment> { - self.0.iter().find(|a| a.account_id() == account_id) - } -} - -fn verify_mana_allotments( - allotments: &[ManaAllotment], - protocol_params: &ProtocolParameters, -) -> Result<(), Error> { - if VERIFY { - verify_mana_allotments_unique_sorted(allotments)?; - verify_mana_allotments_sum(allotments, protocol_params)?; - } - - Ok(()) -} - -fn verify_mana_allotments_unique_sorted<'a>( - allotments: impl IntoIterator, -) -> Result<(), Error> { - if !is_unique_sorted(allotments.into_iter()) { - return Err(Error::ManaAllotmentsNotUniqueSorted); - } - Ok(()) -} - -pub(crate) fn verify_mana_allotments_sum<'a>( - allotments: impl IntoIterator, - protocol_params: &ProtocolParameters, -) -> Result<(), Error> { - let mut mana_sum: u64 = 0; - let max_mana = protocol_params.mana_parameters().max_mana(); - - for ManaAllotment { mana, .. } in allotments { - mana_sum = mana_sum.checked_add(*mana).ok_or(Error::InvalidManaAllotmentSum { - sum: mana_sum as u128 + *mana as u128, - max: max_mana, - })?; - - if mana_sum > max_mana { - return Err(Error::InvalidManaAllotmentSum { - sum: mana_sum as u128, - max: max_mana, - }); - } - } - - Ok(()) -} - -impl TryFrom> for ManaAllotments { - type Error = Error; - - #[inline(always)] - fn try_from(allotments: Vec) -> Result { - Self::from_vec(allotments) - } -} - -impl TryFrom> for ManaAllotments { - type Error = Error; - - #[inline(always)] - fn try_from(allotments: BTreeSet) -> Result { - Self::from_set(allotments) - } -} - -impl IntoIterator for ManaAllotments { - type Item = ManaAllotment; - type IntoIter = alloc::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - Vec::from(Into::>::into(self.0)).into_iter() - } -} +pub(crate) use self::allotment::{verify_mana_allotments_sum, ManaAllotmentCount}; +pub use self::{ + allotment::{ManaAllotment, ManaAllotments}, + parameters::ManaParameters, + rewards::RewardsParameters, +}; diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index d9e880019c..ea16d4f4b8 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -288,7 +288,7 @@ impl DelegationOutput { /// Returns the validator address of the [`DelegationOutput`]. pub fn validator_address(&self) -> &AccountAddress { - &self.validator_address.as_account() + self.validator_address.as_account() } /// Returns the start epoch of the [`DelegationOutput`]. diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index a6116b09bc..b97e42c954 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -542,7 +542,7 @@ pub(crate) mod dto { use super::*; use crate::types::{ - block::{mana::ManaAllotmentDto, payload::dto::PayloadDto, Error}, + block::{payload::dto::PayloadDto, Error}, TryFromDto, }; @@ -555,7 +555,7 @@ pub(crate) mod dto { pub context_inputs: Vec, pub inputs: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub allotments: Vec, + pub allotments: Vec, #[serde(default, skip_serializing_if = "TransactionCapabilities::is_none")] pub capabilities: TransactionCapabilities, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -570,7 +570,7 @@ pub(crate) mod dto { creation_slot: value.creation_slot(), context_inputs: value.context_inputs().to_vec(), inputs: value.inputs().to_vec(), - allotments: value.allotments().iter().map(Into::into).collect(), + allotments: value.allotments().to_vec(), capabilities: value.capabilities().clone(), payload: match value.payload() { Some(p @ Payload::TaggedData(_)) => Some(p.into()), @@ -593,20 +593,14 @@ pub(crate) mod dto { .network_id .parse::() .map_err(|_| Error::InvalidField("network_id"))?; - let mana_allotments = dto - .allotments - .into_iter() - .map(|o| ManaAllotment::try_from_dto_with_params_inner(o, params)) - .collect::, Error>>()?; - let outputs = dto.outputs; let mut builder = Self::builder(network_id) .with_creation_slot(dto.creation_slot) .with_context_inputs(dto.context_inputs) .with_inputs(dto.inputs) - .with_mana_allotments(mana_allotments) + .with_mana_allotments(dto.allotments) .with_capabilities(dto.capabilities) - .with_outputs(outputs); + .with_outputs(dto.outputs); builder = if let Some(p) = dto.payload { if let PayloadDto::TaggedData(i) = p { diff --git a/sdk/src/types/block/rand/mana.rs b/sdk/src/types/block/rand/mana.rs index c8c32df3a5..2476a75cdf 100644 --- a/sdk/src/types/block/rand/mana.rs +++ b/sdk/src/types/block/rand/mana.rs @@ -12,7 +12,6 @@ pub fn rand_mana_allotment(params: &ProtocolParameters) -> ManaAllotment { ManaAllotment::new( rand_account_id(), rand_number_range(0..params.mana_parameters().max_mana()), - params, ) .unwrap() } diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index f92e5bab2e..109915b869 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -251,10 +251,11 @@ impl<'a> SemanticValidationContext<'a> { return Ok(Some(TransactionFailureReason::SumInputsOutputsAmountMismatch)); } - if self.input_mana > self.output_mana && !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); - } + // TODO re-enable with https://github.com/iotaledger/iota-sdk/issues/1692 + // if self.input_mana > self.output_mana && + // !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) { // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 + // return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); + // } // Validation of input native tokens. let mut native_token_ids = self.input_native_tokens.keys().collect::>(); diff --git a/sdk/src/wallet/error.rs b/sdk/src/wallet/error.rs index e6b222517f..60b849ef0d 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -124,9 +124,9 @@ pub enum Error { /// Implicit account not found. #[error("implicit account not found")] ImplicitAccountNotFound, - /// No account was provided or found to issue the block. - #[error("no account was provided or found to issue the block")] - NoAccountToIssueBlock, + /// Account not found. + #[error("account not found")] + AccountNotFound, } impl Error { diff --git a/sdk/src/wallet/operations/block.rs b/sdk/src/wallet/operations/block.rs index 0026ede84c..4c3d269202 100644 --- a/sdk/src/wallet/operations/block.rs +++ b/sdk/src/wallet/operations/block.rs @@ -19,16 +19,22 @@ where ) -> Result { log::debug!("submit_basic_block"); - let issuer_id = match issuer_id.into() { - Some(issuer_id) => Some(issuer_id), - None => self + // If an issuer ID is provided, use it; otherwise, use the first available account or implicit account. + let issuer_id = issuer_id + .into() + .or(self .data() .await .accounts() .next() - .map(|o| o.output.as_account().account_id_non_null(&o.output_id)), - } - .ok_or(Error::NoAccountToIssueBlock)?; + .map(|o| o.output.as_account().account_id_non_null(&o.output_id))) + .or(self + .data() + .await + .implicit_accounts() + .next() + .map(|o| AccountId::from(&o.output_id))) + .ok_or(Error::AccountNotFound)?; let block = self .client() diff --git a/sdk/src/wallet/operations/transaction/account.rs b/sdk/src/wallet/operations/transaction/account.rs index 04a2964ec3..81d64777a3 100644 --- a/sdk/src/wallet/operations/transaction/account.rs +++ b/sdk/src/wallet/operations/transaction/account.rs @@ -14,7 +14,6 @@ use crate::{ unlock_condition::AddressUnlockCondition, AccountId, AccountOutput, OutputId, }, - payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, }, wallet::{ operations::transaction::{TransactionOptions, TransactionWithMetadata}, @@ -103,17 +102,12 @@ where // TODO https://github.com/iotaledger/iota-sdk/issues/1740 let issuance = self.client().get_issuance().await?; - // TODO remove when https://github.com/iotaledger/iota-sdk/issues/1744 is done - let mut capabilities = TransactionCapabilities::default(); - capabilities.add_capability(TransactionCapabilityFlag::BurnMana); - let transaction_options = TransactionOptions { context_inputs: Some(vec![ CommitmentContextInput::new(issuance.latest_commitment.id()).into(), BlockIssuanceCreditContextInput::new(account_id).into(), ]), custom_inputs: Some(vec![*output_id]), - capabilities: Some(capabilities), ..Default::default() }; diff --git a/sdk/src/wallet/operations/transaction/build_transaction.rs b/sdk/src/wallet/operations/transaction/build_transaction.rs index c922c68b93..079ceb3f35 100644 --- a/sdk/src/wallet/operations/transaction/build_transaction.rs +++ b/sdk/src/wallet/operations/transaction/build_transaction.rs @@ -10,7 +10,7 @@ use crate::{ }, types::block::{ input::{Input, UtxoInput}, - payload::signed_transaction::Transaction, + payload::signed_transaction::{Transaction, TransactionCapabilities, TransactionCapabilityFlag}, }, wallet::{operations::transaction::TransactionOptions, Wallet}, }; @@ -46,7 +46,7 @@ where .with_inputs(inputs) .with_outputs(selected_transaction_data.outputs); - if let Some(options) = options.into() { + if let Some(mut options) = options.into() { // Optional add a tagged payload builder = builder.with_payload(options.tagged_data_payload); @@ -54,9 +54,25 @@ where builder = builder.with_context_inputs(context_inputs); } + // TODO remove when https://github.com/iotaledger/iota-sdk/issues/1744 is done + match options.capabilities.as_mut() { + Some(capabilities) => { + capabilities.add_capability(TransactionCapabilityFlag::BurnMana); + } + None => { + let mut capabilities = TransactionCapabilities::default(); + capabilities.add_capability(TransactionCapabilityFlag::BurnMana); + options.capabilities = Some(capabilities); + } + } + if let Some(capabilities) = options.capabilities { builder = builder.add_capabilities(capabilities.capabilities_iter()); } + + if let Some(mana_allotments) = options.mana_allotments { + builder = builder.with_mana_allotments(mana_allotments); + } } let transaction = builder.finish_with_params(&protocol_parameters)?; diff --git a/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs b/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs new file mode 100644 index 0000000000..f1ac22f624 --- /dev/null +++ b/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs @@ -0,0 +1,58 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + client::{api::PreparedTransactionData, secret::SecretManage}, + types::block::mana::ManaAllotment, + wallet::{ + operations::transaction::{TransactionOptions, TransactionWithMetadata}, + Wallet, + }, +}; + +impl Wallet +where + crate::wallet::Error: From, + crate::client::Error: From, +{ + pub async fn allot_mana( + &self, + allotments: impl IntoIterator> + Send, + options: impl Into> + Send, + ) -> crate::wallet::Result { + let options = options.into(); + let prepared_transaction = self.prepare_allot_mana(allotments, options.clone()).await?; + + self.sign_and_submit_transaction(prepared_transaction, None, options) + .await + } + + pub async fn prepare_allot_mana( + &self, + allotments: impl IntoIterator> + Send, + options: impl Into> + Send, + ) -> crate::wallet::Result { + log::debug!("[TRANSACTION] prepare_allot_mana"); + + let mut options = options.into().unwrap_or_default(); + + for allotment in allotments { + let allotment = allotment.into(); + + match options.mana_allotments.as_mut() { + Some(mana_allotments) => { + match mana_allotments + .iter_mut() + .find(|a| a.account_id == allotment.account_id) + { + Some(mana_allotment) => mana_allotment.mana += allotment.mana, + None => mana_allotments.push(allotment), + } + } + None => options.mana_allotments = Some(vec![allotment]), + } + } + + self.prepare_transaction([], options).await + } +} diff --git a/sdk/src/wallet/operations/transaction/high_level/mod.rs b/sdk/src/wallet/operations/transaction/high_level/mod.rs index 9f5093c997..cca34dd311 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 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod allot_mana; pub(crate) mod burning_melting; pub(crate) mod create_account; pub(crate) mod minting; diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index 9a5b8c8bb2..0460ad4f4f 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -12,6 +12,7 @@ use crate::{ }, types::block::{ address::Address, + mana::ManaAllotment, output::{Output, OutputId}, protocol::CommittableAgeRange, slot::SlotIndex, @@ -35,6 +36,7 @@ where mandatory_inputs: Option>, remainder_address: Option
, burn: Option<&Burn>, + mana_allotments: Option>, ) -> crate::wallet::Result { log::debug!("[TRANSACTION] select_inputs"); // Voting output needs to be requested before to prevent a deadlock @@ -105,6 +107,10 @@ where input_selection = input_selection.with_burn(burn.clone()); } + if let Some(mana_allotments) = mana_allotments { + input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); + } + let selected_transaction_data = input_selection.select()?; // lock outputs so they don't get used by another transaction @@ -140,6 +146,10 @@ where input_selection = input_selection.with_burn(burn.clone()); } + if let Some(mana_allotments) = mana_allotments { + input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); + } + let selected_transaction_data = input_selection.select()?; // lock outputs so they don't get used by another transaction @@ -171,6 +181,10 @@ where input_selection = input_selection.with_burn(burn.clone()); } + if let Some(mana_allotments) = mana_allotments { + input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); + } + let selected_transaction_data = input_selection.select()?; // lock outputs so they don't get used by another transaction diff --git a/sdk/src/wallet/operations/transaction/options.rs b/sdk/src/wallet/operations/transaction/options.rs index c19d15739f..3fe8655d8d 100644 --- a/sdk/src/wallet/operations/transaction/options.rs +++ b/sdk/src/wallet/operations/transaction/options.rs @@ -8,6 +8,7 @@ use crate::{ types::block::{ address::Address, context_input::ContextInput, + mana::ManaAllotment, output::OutputId, payload::{signed_transaction::TransactionCapabilities, tagged_data::TaggedDataPayload}, }, @@ -35,6 +36,8 @@ pub struct TransactionOptions { pub allow_micro_amount: bool, #[serde(default)] pub capabilities: Option, + #[serde(default)] + pub mana_allotments: Option>, } #[allow(clippy::enum_variant_names)] diff --git a/sdk/src/wallet/operations/transaction/prepare_transaction.rs b/sdk/src/wallet/operations/transaction/prepare_transaction.rs index 9b7e9ac490..04cc7d1371 100644 --- a/sdk/src/wallet/operations/transaction/prepare_transaction.rs +++ b/sdk/src/wallet/operations/transaction/prepare_transaction.rs @@ -8,10 +8,7 @@ use packable::bounded::TryIntoBoundedU16Error; use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, - types::block::{ - input::INPUT_COUNT_RANGE, - output::{Output, OUTPUT_COUNT_RANGE}, - }, + types::block::{input::INPUT_COUNT_RANGE, output::Output}, wallet::{ operations::transaction::{RemainderValueStrategy, TransactionOptions}, Wallet, @@ -40,16 +37,6 @@ where output.verify_storage_deposit(storage_score_params)?; } - let is_burn_present = options.as_ref().map(|options| options.burn.is_some()).unwrap_or(false); - - // Validate the number of outputs. The validation shouldn't be performed if [`Burn`] is present. - // The outputs will be generated by the input selection algorithm (ISA). - if !OUTPUT_COUNT_RANGE.contains(&(outputs.len() as u16)) && !is_burn_present { - return Err(crate::types::block::Error::InvalidOutputCount( - TryIntoBoundedU16Error::Truncated(outputs.len()), - ))?; - } - if let Some(custom_inputs) = options.as_ref().and_then(|options| options.custom_inputs.as_ref()) { // validate inputs amount if !INPUT_COUNT_RANGE.contains(&(custom_inputs.len() as u16)) { @@ -88,6 +75,7 @@ where .map(|inputs| HashSet::from_iter(inputs.clone())), remainder_address, options.as_ref().and_then(|options| options.burn.as_ref()), + options.as_ref().and_then(|options| options.mana_allotments.clone()), ) .await?; From a2cfbe3bb7582a4a1cf6bcb6ab14d3a8db8b73a4 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 10 Jan 2024 15:48:49 +0100 Subject: [PATCH 13/14] CLI: add BIC to accounts lists (#1826) --- cli/src/wallet_cli/mod.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 0de3d7b01b..88616d9bb4 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -328,12 +328,18 @@ pub async fn accounts_command(wallet: &Wallet) -> Result<(), Error> { let output_id = account.output_id; let account_id = account.output.as_account().account_id_non_null(&output_id); let account_address = account_id.to_bech32(wallet.client().get_bech32_hrp().await?); + let bic = wallet + .client() + .get_account_congestion(&account_id) + .await? + .block_issuance_credits; println_log_info!( - "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n", + "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n{:<16} {bic}\n", "Output ID:", "Account ID:", - "Account Address:" + "Account Address:", + "BIC:" ); } @@ -697,12 +703,18 @@ pub async fn implicit_accounts_command(wallet: &Wallet) -> Result<(), Error> { let output_id = implicit_account.output_id; let account_id = AccountId::from(&output_id); let account_address = account_id.to_bech32(wallet.client().get_bech32_hrp().await?); + let bic = wallet + .client() + .get_account_congestion(&account_id) + .await? + .block_issuance_credits; println_log_info!( - "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n", + "{:<16} {output_id}\n{:<16} {account_id}\n{:<16} {account_address}\n{:<16} {bic}\n", "Output ID:", "Account ID:", - "Account Address:" + "Account Address:", + "BIC:" ); } From 4374ef026a7f0091fe9c8cadfd7eb6faf3d8e626 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Wed, 10 Jan 2024 16:50:57 +0100 Subject: [PATCH 14/14] feat(2.0): Optimized background sync (#1797) * feat(nodejs): Silently stop background syncing when Wallet is dropped * feat(2.0): Optimized background sync * fix * switch to tokio notify * chore: Remove comment * final touches * remove unnecessary named loop * fmt * enhancements * ooops * ooops again * fmt --- sdk/src/wallet/core/builder.rs | 9 +- sdk/src/wallet/core/mod.rs | 9 +- .../core/operations/background_syncing.rs | 116 ++++++++++-------- 3 files changed, 77 insertions(+), 57 deletions(-) diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index c9aa3e7e23..23eca5fe22 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -3,7 +3,7 @@ #[cfg(feature = "storage")] use std::collections::HashSet; -use std::sync::{atomic::AtomicUsize, Arc}; +use std::sync::Arc; use serde::Serialize; use tokio::sync::{Mutex, RwLock}; @@ -19,7 +19,7 @@ use crate::{ client::secret::{GenerateAddressOptions, SecretManage, SecretManager}, types::block::address::{Address, Bech32Address}, wallet::{ - core::{Bip44, WalletData, WalletInner}, + core::{operations::background_syncing::BackgroundSyncStatus, Bip44, WalletData, WalletInner}, operations::syncing::SyncOptions, ClientOptions, Wallet, }, @@ -242,11 +242,14 @@ where .finish() .await?; + let background_syncing_status = tokio::sync::watch::channel(BackgroundSyncStatus::Stopped); + let background_syncing_status = (Arc::new(background_syncing_status.0), background_syncing_status.1); + // Build the wallet. let wallet_inner = WalletInner { default_sync_options: Mutex::new(SyncOptions::default()), last_synced: Mutex::new(0), - background_syncing_status: AtomicUsize::new(0), + background_syncing_status, client, secret_manager: self.secret_manager.expect("make WalletInner::secret_manager optional?"), #[cfg(feature = "events")] diff --git a/sdk/src/wallet/core/mod.rs b/sdk/src/wallet/core/mod.rs index 88f0e3bc2a..16db6cdf27 100644 --- a/sdk/src/wallet/core/mod.rs +++ b/sdk/src/wallet/core/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod operations; use std::{ collections::{HashMap, HashSet}, - sync::{atomic::AtomicUsize, Arc}, + sync::Arc, }; use crypto::keys::{ @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; pub use self::builder::WalletBuilder; +use self::operations::background_syncing::BackgroundSyncStatus; use super::types::{TransactionWithMetadata, TransactionWithMetadataDto}; #[cfg(feature = "events")] use crate::wallet::events::{ @@ -84,8 +85,10 @@ pub struct WalletInner { // again, because sending transactions can change that pub(crate) last_synced: Mutex, pub(crate) default_sync_options: Mutex, - // 0 = not running, 1 = running, 2 = stopping - pub(crate) background_syncing_status: AtomicUsize, + pub(crate) background_syncing_status: ( + Arc>, + tokio::sync::watch::Receiver, + ), pub(crate) client: Client, // TODO: make this optional? pub(crate) secret_manager: Arc>, diff --git a/sdk/src/wallet/core/operations/background_syncing.rs b/sdk/src/wallet/core/operations/background_syncing.rs index 9bea901acc..7328441fef 100644 --- a/sdk/src/wallet/core/operations/background_syncing.rs +++ b/sdk/src/wallet/core/operations/background_syncing.rs @@ -1,18 +1,25 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::{sync::atomic::Ordering, time::Duration}; +use std::time::Duration; -use tokio::time::sleep; +use tokio::time::timeout; use crate::{ client::secret::SecretManage, - wallet::{operations::syncing::SyncOptions, Wallet}, + wallet::{operations::syncing::SyncOptions, task, Wallet}, }; /// The default interval for background syncing pub(crate) const DEFAULT_BACKGROUNDSYNCING_INTERVAL: Duration = Duration::from_secs(7); +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum BackgroundSyncStatus { + Stopped, + Running, + Stopping, +} + impl Wallet where crate::wallet::Error: From, @@ -25,49 +32,49 @@ where interval: Option, ) -> crate::wallet::Result<()> { log::debug!("[start_background_syncing]"); + + let (tx_background_sync, mut rx_background_sync) = self.background_syncing_status.clone(); + // stop existing process if running - if self.background_syncing_status.load(Ordering::Relaxed) == 1 { - self.background_syncing_status.store(2, Ordering::Relaxed); - }; - while self.background_syncing_status.load(Ordering::Relaxed) == 2 { - log::debug!("[background_syncing]: waiting for the old process to stop"); - sleep(Duration::from_secs(1)).await; + if *rx_background_sync.borrow() == BackgroundSyncStatus::Running { + tx_background_sync.send(BackgroundSyncStatus::Stopping).ok(); } - self.background_syncing_status.store(1, Ordering::Relaxed); + log::debug!("[background_syncing]: waiting for the old process to stop"); + rx_background_sync + .wait_for(|status| *status != BackgroundSyncStatus::Stopping) + .await + .ok(); + + tx_background_sync.send(BackgroundSyncStatus::Running).ok(); + let wallet = self.clone(); - let _background_syncing = std::thread::spawn(move || { - #[cfg(not(target_family = "wasm"))] - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - #[cfg(target_family = "wasm")] - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - 'outer: loop { - log::debug!("[background_syncing]: syncing wallet"); - - if let Err(err) = wallet.sync(options.clone()).await { - log::debug!("[background_syncing] error: {}", err) - } - - // split interval syncing to seconds so stopping the process doesn't have to wait long - let seconds = interval.unwrap_or(DEFAULT_BACKGROUNDSYNCING_INTERVAL).as_secs(); - for _ in 0..seconds { - if wallet.background_syncing_status.load(Ordering::Relaxed) == 2 { - log::debug!("[background_syncing]: stopping"); - break 'outer; - } - sleep(Duration::from_secs(1)).await; - } + let interval_seconds = interval.unwrap_or(DEFAULT_BACKGROUNDSYNCING_INTERVAL); + + task::spawn(async move { + loop { + log::debug!("[background_syncing]: syncing wallet"); + + if let Err(err) = wallet.sync(options.clone()).await { + log::debug!("[background_syncing] error: {}", err) } - wallet.background_syncing_status.store(0, Ordering::Relaxed); - log::debug!("[background_syncing]: stopped"); - }); + + let res = timeout(interval_seconds, async { + rx_background_sync + .wait_for(|status| *status == BackgroundSyncStatus::Stopping) + .await + .is_ok() + }) + .await; + + // If true it means rx_background_sync changed to BackgroundSyncStatus::Stopping + if Ok(true) == res { + log::debug!("[background_syncing]: stopping"); + break; + } + } + tx_background_sync.send(BackgroundSyncStatus::Stopped).ok(); + log::debug!("[background_syncing]: stopped"); }); Ok(()) } @@ -75,25 +82,32 @@ where /// Request to stop the background syncing of the wallet pub fn request_stop_background_syncing(&self) { log::debug!("[request_stop_background_syncing]"); - self.background_syncing_status.store(2, Ordering::Relaxed); + self.background_syncing_status + .0 + .send(BackgroundSyncStatus::Stopping) + .ok(); } /// Stop the background syncing of the wallet pub async fn stop_background_syncing(&self) -> crate::wallet::Result<()> { log::debug!("[stop_background_syncing]"); - // immediately return if not running - if self.background_syncing_status.load(Ordering::Relaxed) == 0 { + + let mut rx_background_sync = self.background_syncing_status.1.clone(); + + // immediately return if is stopped + if *rx_background_sync.borrow() == BackgroundSyncStatus::Stopped { return Ok(()); } + // send stop request self.request_stop_background_syncing(); - // wait until it stopped - while self.background_syncing_status.load(Ordering::Relaxed) != 0 { - #[cfg(target_family = "wasm")] - gloo_timers::future::TimeoutFuture::new(10).await; - #[cfg(not(target_family = "wasm"))] - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } + + // wait until it has stopped + rx_background_sync + .wait_for(|status| *status == BackgroundSyncStatus::Stopped) + .await + .ok(); + Ok(()) } }