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/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/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..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. @@ -154,7 +158,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( @@ -181,6 +194,7 @@ impl InputSelection { slot_index: SlotIndex::from(0), requirements: Vec::new(), automatically_transitioned: HashSet::new(), + mana_allotments: 0, } } @@ -214,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? @@ -364,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/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/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 20a771148b..d191fdbd87 100644 --- a/sdk/src/types/block/mana/allotment.rs +++ b/sdk/src/types/block/mana/allotment.rs @@ -18,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 }) } @@ -59,8 +64,8 @@ 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)); } @@ -84,7 +89,7 @@ impl ManaAllotments { /// 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] + 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 { @@ -187,46 +192,3 @@ impl IntoIterator for ManaAllotments { Vec::from(Into::>::into(self.0)).into_iter() } } - -#[cfg(feature = "serde")] -pub(super) mod dto { - use serde::{Deserialize, Serialize}; - - use super::*; - use crate::{types::TryFromDto, utils::serde::string}; - - #[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, - } - - impl From<&ManaAllotment> for ManaAllotmentDto { - fn from(value: &ManaAllotment) -> Self { - Self { - account_id: value.account_id, - mana: value.mana, - } - } - } - - 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, - } - }) - } - } -} diff --git a/sdk/src/types/block/mana/mod.rs b/sdk/src/types/block/mana/mod.rs index f9eb15e924..a4f22ba61a 100644 --- a/sdk/src/types/block/mana/mod.rs +++ b/sdk/src/types/block/mana/mod.rs @@ -5,8 +5,6 @@ mod allotment; mod parameters; mod rewards; -#[cfg(feature = "serde")] -pub use self::allotment::dto::ManaAllotmentDto; pub(crate) use self::allotment::{verify_mana_allotments_sum, ManaAllotmentCount}; pub use self::{ allotment::{ManaAllotment, ManaAllotments}, 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 5e6a7a429f..1b270eb125 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -292,6 +292,7 @@ impl<'a> SemanticValidationContext<'a> { return Ok(Some(TransactionFailureReason::SumInputsOutputsAmountMismatch)); } + // TODO re-enable with https://github.com/iotaledger/iota-sdk/issues/1692 if self.input_mana != self.output_mana { if self.input_mana > self.output_mana && !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) 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/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); +} 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