diff --git a/bindings/core/src/method/secret_manager.rs b/bindings/core/src/method/secret_manager.rs index d8e9150b31..3df9f6f0a1 100644 --- a/bindings/core/src/method/secret_manager.rs +++ b/bindings/core/src/method/secret_manager.rs @@ -79,6 +79,14 @@ pub enum SecretManagerMethod { #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] mnemonic: String, }, + /// Set the stronghold password. + /// Expected response: [`Ok`](crate::Response::Ok) + #[cfg(feature = "stronghold")] + #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] + SetStrongholdPassword { + #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] + password: String, + }, } #[cfg(test)] diff --git a/bindings/core/src/method_handler/secret_manager.rs b/bindings/core/src/method_handler/secret_manager.rs index d766194ee1..09d1f690fb 100644 --- a/bindings/core/src/method_handler/secret_manager.rs +++ b/bindings/core/src/method_handler/secret_manager.rs @@ -135,6 +135,18 @@ where return Err(iota_sdk::client::Error::SecretManagerMismatch.into()); } } + #[cfg(feature = "stronghold")] + SecretManagerMethod::SetStrongholdPassword { password } => { + let stronghold = if let Some(secret_manager) = secret_manager.downcast::() { + secret_manager + } else if let Some(SecretManager::Stronghold(secret_manager)) = secret_manager.downcast::() { + secret_manager + } else { + return Err(iota_sdk::client::Error::SecretManagerMismatch.into()); + }; + stronghold.set_password(password).await?; + Response::Ok + } }; Ok(response) } diff --git a/bindings/nodejs/lib/secret_manager/secret-manager.ts b/bindings/nodejs/lib/secret_manager/secret-manager.ts index c1f4366b7c..ca6bcc3258 100644 --- a/bindings/nodejs/lib/secret_manager/secret-manager.ts +++ b/bindings/nodejs/lib/secret_manager/secret-manager.ts @@ -216,4 +216,14 @@ export class SecretManager { return JSON.parse(response).payload; } + + /** + * Set the Stronghold password. + */ + async setStrongholdPassword(password: string): Promise { + await this.methodHandler.callMethod({ + name: 'setStrongholdPassword', + data: { password }, + }); + } } 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/nodejs/lib/types/secret_manager/bridge/index.ts b/bindings/nodejs/lib/types/secret_manager/bridge/index.ts index c9901c3440..0584199625 100644 --- a/bindings/nodejs/lib/types/secret_manager/bridge/index.ts +++ b/bindings/nodejs/lib/types/secret_manager/bridge/index.ts @@ -8,6 +8,7 @@ import type { __SignatureUnlockMethod__, __SignEd25519Method__, __SignSecp256k1EcdsaMethod__, + __SetStrongholdPasswordMethod__, } from './secret-manager'; export type __SecretManagerMethods__ = @@ -19,4 +20,5 @@ export type __SecretManagerMethods__ = | __SignatureUnlockMethod__ | __StoreMnemonicMethod__ | __SignEd25519Method__ - | __SignSecp256k1EcdsaMethod__; + | __SignSecp256k1EcdsaMethod__ + | __SetStrongholdPasswordMethod__; diff --git a/bindings/nodejs/lib/types/secret_manager/bridge/secret-manager.ts b/bindings/nodejs/lib/types/secret_manager/bridge/secret-manager.ts index 31bca06493..6daf86dffb 100644 --- a/bindings/nodejs/lib/types/secret_manager/bridge/secret-manager.ts +++ b/bindings/nodejs/lib/types/secret_manager/bridge/secret-manager.ts @@ -70,3 +70,8 @@ export interface __SignSecp256k1EcdsaMethod__ { export interface __GetLedgerNanoStatusMethod__ { name: 'getLedgerNanoStatus'; } + +export interface __SetStrongholdPasswordMethod__ { + name: 'setStrongholdPassword'; + data: { password: string }; +} 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/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..88616d9bb4 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. @@ -325,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:" ); } @@ -344,6 +353,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 +532,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?; @@ -668,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:" ); } @@ -1173,6 +1214,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/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?; 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/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/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/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(()) } } 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?; 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