diff --git a/bindings/core/src/method/secret_manager.rs b/bindings/core/src/method/secret_manager.rs index d8e9150b31..a4298f2b1e 100644 --- a/bindings/core/src/method/secret_manager.rs +++ b/bindings/core/src/method/secret_manager.rs @@ -79,6 +79,27 @@ 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, + }, + /// Change the stronghold password. + /// Expected response: [`Ok`](crate::Response::Ok) + #[cfg(feature = "stronghold")] + #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] + ChangeStrongholdPassword { + #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] + password: String, + }, + /// Clear the stronghold password. + /// Expected response: [`Ok`](crate::Response::Ok) + #[cfg(feature = "stronghold")] + #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] + ClearStrongholdPassword } #[cfg(test)] diff --git a/bindings/core/src/method_handler/secret_manager.rs b/bindings/core/src/method_handler/secret_manager.rs index d766194ee1..19f4806d31 100644 --- a/bindings/core/src/method_handler/secret_manager.rs +++ b/bindings/core/src/method_handler/secret_manager.rs @@ -135,6 +135,42 @@ 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 + } + #[cfg(feature = "stronghold")] + SecretManagerMethod::ChangeStrongholdPassword { 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.change_password(password).await?; + Response::Ok + } + #[cfg(feature = "stronghold")] + SecretManagerMethod::ClearStrongholdPassword => { + 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.clear_key().await; + Response::Ok + } }; Ok(response) } diff --git a/bindings/nodejs/examples/exchange/4-listen-events.ts b/bindings/nodejs/examples/exchange/4-listen-events.ts index 09e93b7f45..d80cfd2046 100644 --- a/bindings/nodejs/examples/exchange/4-listen-events.ts +++ b/bindings/nodejs/examples/exchange/4-listen-events.ts @@ -5,14 +5,14 @@ // Run with command: // yarn run-example ./exchange/4-listen-events.ts -import { Wallet, Event, WalletEventType } from '@iota/sdk'; +import { Wallet, WalletEvent, WalletEventType } from '@iota/sdk'; // This example uses secrets in environment variables for simplicity which should not be done in production. require('dotenv').config({ path: '.env' }); async function run() { try { - for (const envVar of ['WALLET_DB_PATH']) { + for (const envVar of ['WALLET_DB_PATH', 'FAUCET_URL']) { if (!(envVar in process.env)) { throw new Error( `.env ${envVar} is undefined, see .env.example`, @@ -24,9 +24,8 @@ async function run() { storagePath: process.env.WALLET_DB_PATH, }); - const callback = function (err: any, event: Event) { - console.log('AccountIndex:', event.accountIndex); - console.log('Event:', event.event); + const callback = function (err: any, event: WalletEvent) { + console.log('Event:', event); // Exit after receiving an event. process.exit(0); @@ -37,7 +36,7 @@ async function run() { // Use the faucet to send testnet tokens to your address. console.log( - 'Fill your address with the faucet: https://faucet.testnet.shimmer.network/', + `Fill your address with the faucet: ${process.env.FAUCET_URL}`, ); const address = await wallet.address(); diff --git a/bindings/nodejs/examples/wallet/events.ts b/bindings/nodejs/examples/wallet/events.ts index f11178fc82..d0f8f4f5fd 100644 --- a/bindings/nodejs/examples/wallet/events.ts +++ b/bindings/nodejs/examples/wallet/events.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { - Event, - ConsolidationRequiredWalletEvent, + WalletEvent, TransactionProgressWalletEvent, SelectingInputsProgress, } from '@iota/sdk'; @@ -24,13 +23,12 @@ async function run() { // Create the wallet const wallet = await getUnlockedWallet(); - const callback = function (err: any, event: Event) { + const callback = function (err: any, event: WalletEvent) { console.log('Event:', event); }; await wallet.listen([], callback); - await wallet.emitTestEvent(new ConsolidationRequiredWalletEvent()); await wallet.emitTestEvent( new TransactionProgressWalletEvent(new SelectingInputsProgress()), ); diff --git a/bindings/nodejs/lib/secret_manager/secret-manager.ts b/bindings/nodejs/lib/secret_manager/secret-manager.ts index c1f4366b7c..82196bd6c5 100644 --- a/bindings/nodejs/lib/secret_manager/secret-manager.ts +++ b/bindings/nodejs/lib/secret_manager/secret-manager.ts @@ -216,4 +216,33 @@ 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 }, + }); + } + + /** + * Change the Stronghold password. + */ + async changeStrongholdPassword(password: string): Promise { + await this.methodHandler.callMethod({ + name: 'changeStrongholdPassword', + data: { password }, + }); + } + + /** + * Clear the Stronghold password. + */ + async clearStrongholdPassword(): Promise { + await this.methodHandler.callMethod({ + name: 'clearStrongholdPassword', + }); + } } 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..c7027781ae 100644 --- a/bindings/nodejs/lib/types/secret_manager/bridge/index.ts +++ b/bindings/nodejs/lib/types/secret_manager/bridge/index.ts @@ -8,6 +8,9 @@ import type { __SignatureUnlockMethod__, __SignEd25519Method__, __SignSecp256k1EcdsaMethod__, + __SetStrongholdPasswordMethod__, + __ChangeStrongholdPasswordMethod__, + __ClearStrongholdPasswordMethod__, } from './secret-manager'; export type __SecretManagerMethods__ = @@ -19,4 +22,7 @@ export type __SecretManagerMethods__ = | __SignatureUnlockMethod__ | __StoreMnemonicMethod__ | __SignEd25519Method__ - | __SignSecp256k1EcdsaMethod__; + | __SignSecp256k1EcdsaMethod__ + | __SetStrongholdPasswordMethod__ + | __ChangeStrongholdPasswordMethod__ + | __ClearStrongholdPasswordMethod__; 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..2e1390d34b 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,17 @@ export interface __SignSecp256k1EcdsaMethod__ { export interface __GetLedgerNanoStatusMethod__ { name: 'getLedgerNanoStatus'; } + +export interface __SetStrongholdPasswordMethod__ { + name: 'setStrongholdPassword'; + data: { password: string }; +} + +export interface __ChangeStrongholdPasswordMethod__ { + name: 'changeStrongholdPassword'; + data: { password: string }; +} + +export interface __ClearStrongholdPasswordMethod__ { + name: 'clearStrongholdPassword'; +} diff --git a/bindings/nodejs/lib/types/wallet/event.ts b/bindings/nodejs/lib/types/wallet/event.ts index 2ade953208..fbe8d2b7fa 100644 --- a/bindings/nodejs/lib/types/wallet/event.ts +++ b/bindings/nodejs/lib/types/wallet/event.ts @@ -13,45 +13,20 @@ import { HexEncodedString } from '../utils'; */ export type TransactionId = string; -/** - * An wallet account event. - */ -class Event { - /** - * The account index for which the event was emitted. - */ - accountIndex: number; - /** - * The wallet event. - */ - event: WalletEvent; - - /** - * @param accountIndex The account index. - * @param event The wallet event. - */ - constructor(accountIndex: number, event: WalletEvent) { - this.accountIndex = accountIndex; - this.event = event; - } -} - /** * All of the wallet event types. */ enum WalletEventType { - /** Consolidation is required. */ - ConsolidationRequired = 0, /** Nano Ledger has generated an address. */ - LedgerAddressGeneration = 1, + LedgerAddressGeneration = 0, /** A new output was created. */ - NewOutput = 2, + NewOutput = 1, /** An output was spent. */ - SpentOutput = 3, + SpentOutput = 2, /** A transaction was included into the ledger. */ - TransactionInclusion = 4, + TransactionInclusion = 3, /** A progress update while submitting a transaction. */ - TransactionProgress = 5, + TransactionProgress = 4, } /** @@ -68,15 +43,6 @@ abstract class WalletEvent { } } -/** - * A 'consolidation required' wallet event. - */ -class ConsolidationRequiredWalletEvent extends WalletEvent { - constructor() { - super(WalletEventType.ConsolidationRequired); - } -} - /** * A 'ledger address generation' wallet event. */ @@ -280,10 +246,8 @@ class BroadcastingProgress extends TransactionProgress { } export { - Event, WalletEventType, WalletEvent, - ConsolidationRequiredWalletEvent, LedgerAddressGenerationWalletEvent, NewOutputWalletEvent, SpentOutputWalletEvent, diff --git a/bindings/nodejs/lib/wallet/wallet-method-handler.ts b/bindings/nodejs/lib/wallet/wallet-method-handler.ts index b91eda5834..7bb9783da5 100644 --- a/bindings/nodejs/lib/wallet/wallet-method-handler.ts +++ b/bindings/nodejs/lib/wallet/wallet-method-handler.ts @@ -11,9 +11,9 @@ import { } from '../bindings'; import { WalletEventType, + WalletEvent, WalletOptions, __WalletMethod__, - Event, } from '../types/wallet'; import { Client, ClientMethodHandler } from '../client'; import { SecretManager, SecretManagerMethodHandler } from '../secret_manager'; @@ -74,17 +74,17 @@ export class WalletMethodHandler { */ async listen( eventTypes: WalletEventType[], - callback: (error: Error, event: Event) => void, + callback: (error: Error, event: WalletEvent) => void, ): Promise { return listenWallet( this.methodHandler, eventTypes, function (err: any, data: string) { - const parsed = JSON.parse(data); + const parsed: WalletEvent = JSON.parse(data); callback( // Send back raw error instead of parsing err, - new Event(parsed.accountIndex, parsed.event), + parsed, ); }, ).catch((error: any) => { diff --git a/bindings/nodejs/lib/wallet/wallet.ts b/bindings/nodejs/lib/wallet/wallet.ts index a8b1a53a2a..380b55a7b9 100644 --- a/bindings/nodejs/lib/wallet/wallet.ts +++ b/bindings/nodejs/lib/wallet/wallet.ts @@ -52,7 +52,6 @@ import type { WalletOptions, WalletEventType, WalletEvent, - Event, } from '../types/wallet'; import { IAuth, IClientOptions, LedgerNanoStatus } from '../types/client'; import { SecretManager } from '../secret_manager'; @@ -169,7 +168,7 @@ export class Wallet { */ async listen( eventTypes: WalletEventType[], - callback: (error: Error, event: Event) => void, + callback: (error: Error, event: WalletEvent) => void, ): Promise { return this.methodHandler.listen(eventTypes, callback); } diff --git a/bindings/python/examples/exchange/4_listen_events.py b/bindings/python/examples/exchange/4_listen_events.py index ca89373e93..7de0246462 100644 --- a/bindings/python/examples/exchange/4_listen_events.py +++ b/bindings/python/examples/exchange/4_listen_events.py @@ -27,9 +27,8 @@ def callback(event): """Callback function for the event listener""" - event_dict = json.loads(event) - print('AccountIndex:', event_dict["accountIndex"]) - print('Event:', event_dict["event"]) + event = json.loads(event) + print('Event:', event) # Exit after receiving an event. # pylint: disable=global-statement diff --git a/bindings/python/iota_sdk/types/event.py b/bindings/python/iota_sdk/types/event.py index 9e378dea09..75ed34886a 100644 --- a/bindings/python/iota_sdk/types/event.py +++ b/bindings/python/iota_sdk/types/event.py @@ -8,16 +8,14 @@ class WalletEventType(IntEnum): """Types of wallet events. Attributes: - ConsolidationRequired (0): Consolidation is required. - LedgerAddressGeneration (1): Nano Ledger has generated an address. - NewOutput (2): A new output was created. - SpentOutput (3): An output was spent. - TransactionInclusion (4): A transaction was included into the ledger. - TransactionProgress (5): A progress update while submitting a transaction. + LedgerAddressGeneration (0): Nano Ledger has generated an address. + NewOutput (1): A new output was created. + SpentOutput (2): An output was spent. + TransactionInclusion (3): A transaction was included into the ledger. + TransactionProgress (4): A progress update while submitting a transaction. """ - ConsolidationRequired = 0 - LedgerAddressGeneration = 1 - NewOutput = 2 - SpentOutput = 3 - TransactionInclusion = 4 - TransactionProgress = 5 + LedgerAddressGeneration = 0 + NewOutput = 1 + SpentOutput = 2 + TransactionInclusion = 3 + TransactionProgress = 4 diff --git a/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/client/error.rs b/sdk/src/client/error.rs index 5c9bdcbdff..8094d81ea2 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -36,9 +36,6 @@ pub enum Error { /// Block types error #[error("{0}")] Block(#[from] crate::types::block::Error), - /// The wallet has enough funds, but split on too many outputs - #[error("the wallet has enough funds, but split on too many outputs: {0}, max. is 128, consolidate them")] - ConsolidationRequired(usize), /// Crypto.rs error #[error("{0}")] Crypto(#[from] crypto::Error), diff --git a/sdk/src/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 63e87715ee..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, }, @@ -227,9 +227,6 @@ where #[cfg(feature = "storage")] self.save(&storage_manager).await?; - #[cfg(feature = "events")] - let event_emitter = tokio::sync::RwLock::new(EventEmitter::new()); - // It happened that inputs got locked, the transaction failed, but they weren't unlocked again, so we do this // here #[cfg(feature = "storage")] @@ -245,15 +242,18 @@ 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")] - event_emitter, + event_emitter: tokio::sync::RwLock::new(EventEmitter::new()), #[cfg(feature = "storage")] storage_options, #[cfg(feature = "storage")] diff --git a/sdk/src/wallet/core/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/address_generation.rs b/sdk/src/wallet/core/operations/address_generation.rs index fe902ee8ec..50427dd324 100644 --- a/sdk/src/wallet/core/operations/address_generation.rs +++ b/sdk/src/wallet/core/operations/address_generation.rs @@ -36,41 +36,34 @@ impl Wallet { // needs to have it visible on the computer first, so we need to generate it without the // prompt first let options = options.into(); + #[cfg(feature = "events")] if options.as_ref().map_or(false, |o| o.ledger_nano_prompt) { - #[cfg(feature = "events")] - { - let changed_options = options.map(|mut options| { - // Change options so ledger will not show the prompt the first time - options.ledger_nano_prompt = false; - options - }); - // Generate without prompt to be able to display it - let address = ledger_nano - .generate_ed25519_addresses( - coin_type, - account_index, - address_index..address_index + 1, - changed_options, - ) - .await?; + let changed_options = options.map(|mut options| { + // Change options so ledger will not show the prompt the first time + options.ledger_nano_prompt = false; + options + }); + // Generate without prompt to be able to display it + let address = ledger_nano + .generate_ed25519_addresses( + coin_type, + account_index, + address_index..address_index + 1, + changed_options, + ) + .await?; - let bech32_hrp = self.bech32_hrp().await; + let bech32_hrp = self.bech32_hrp().await; - self.emit(WalletEvent::LedgerAddressGeneration(AddressData { - address: address[0].to_bech32(bech32_hrp), - })) - .await; - } - - // Generate with prompt so the user can verify - ledger_nano - .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) - .await? - } else { - ledger_nano - .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) - .await? + self.emit(WalletEvent::LedgerAddressGeneration(AddressData { + address: address[0].to_bech32(bech32_hrp), + })) + .await; } + // Generate with prompt so the user can verify + ledger_nano + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) + .await? } #[cfg(feature = "stronghold")] SecretManager::Stronghold(stronghold) => { diff --git a/sdk/src/wallet/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 ad26c39a91..60b849ef0d 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -33,9 +33,6 @@ pub enum Error { new_bip_path: Option, old_bip_path: Option, }, - /// Funds are spread over too many outputs - #[error("funds are spread over too many outputs {output_count}/{output_count_max}, consolidation required")] - ConsolidationRequired { output_count: usize, output_count_max: u16 }, /// Crypto.rs error #[error("{0}")] Crypto(#[from] crypto::Error), @@ -127,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/events/mod.rs b/sdk/src/wallet/events/mod.rs index e908a0d1f9..c75cc39acc 100644 --- a/sdk/src/wallet/events/mod.rs +++ b/sdk/src/wallet/events/mod.rs @@ -41,7 +41,6 @@ impl EventEmitter { WalletEventType::SpentOutput, WalletEventType::TransactionInclusion, WalletEventType::TransactionProgress, - WalletEventType::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEventType::LedgerAddressGeneration, ] { @@ -74,7 +73,6 @@ impl EventEmitter { WalletEvent::SpentOutput(_) => WalletEventType::SpentOutput, WalletEvent::TransactionInclusion(_) => WalletEventType::TransactionInclusion, WalletEvent::TransactionProgress(_) => WalletEventType::TransactionProgress, - WalletEvent::ConsolidationRequired => WalletEventType::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEvent::LedgerAddressGeneration(_) => WalletEventType::LedgerAddressGeneration, }; @@ -126,18 +124,18 @@ mod tests { let event_counter = Arc::new(AtomicUsize::new(0)); // single event - emitter.on([WalletEventType::ConsolidationRequired], |_name| { - // println!("ConsolidationRequired: {:?}", name); + emitter.on([WalletEventType::TransactionInclusion], |_name| { + // println!("TransactionInclusion: {:?}", name); }); // listen to two events emitter.on( [ WalletEventType::TransactionProgress, - WalletEventType::ConsolidationRequired, + WalletEventType::TransactionInclusion, ], move |_name| { - // println!("TransactionProgress or ConsolidationRequired: {:?}", name); + // println!("TransactionProgress or TransactionInclusion: {:?}", name); }, ); @@ -149,7 +147,6 @@ mod tests { }); // emit events - emitter.emit(WalletEvent::ConsolidationRequired); emitter.emit(WalletEvent::TransactionProgress( TransactionProgressEvent::SelectingInputs, )); @@ -161,14 +158,16 @@ mod tests { inclusion_state: InclusionState::Confirmed, })); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // remove handlers of single event - emitter.clear([WalletEventType::ConsolidationRequired]); + emitter.clear([WalletEventType::TransactionProgress]); // emit event of removed type - emitter.emit(WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // remove handlers of all events emitter.clear([]); @@ -183,18 +182,20 @@ mod tests { .expect("invalid tx id"), inclusion_state: InclusionState::Confirmed, })); - assert_eq!(3, event_counter.load(Ordering::SeqCst)); + assert_eq!(2, event_counter.load(Ordering::SeqCst)); // listen to a single event let event_counter_clone = Arc::clone(&event_counter); - emitter.on([WalletEventType::ConsolidationRequired], move |_name| { + emitter.on([WalletEventType::TransactionProgress], move |_name| { // println!("Any event: {:?}", name); event_counter_clone.fetch_add(1, Ordering::SeqCst); }); for _ in 0..1_000_000 { - emitter.emit(WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); } - assert_eq!(1_000_003, event_counter.load(Ordering::SeqCst)); + assert_eq!(1_000_002, event_counter.load(Ordering::SeqCst)); } } diff --git a/sdk/src/wallet/events/types.rs b/sdk/src/wallet/events/types.rs index 71a4e7b927..475fd5ab2e 100644 --- a/sdk/src/wallet/events/types.rs +++ b/sdk/src/wallet/events/types.rs @@ -22,7 +22,6 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum WalletEvent { - ConsolidationRequired, #[cfg(feature = "ledger_nano")] #[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))] LedgerAddressGeneration(AddressData), @@ -45,13 +44,12 @@ impl Serialize for WalletEvent { #[derive(Serialize)] #[serde(untagged)] enum WalletEvent_<'a> { - T0, #[cfg(feature = "ledger_nano")] - T1(&'a AddressData), - T2(&'a NewOutputEvent), - T3(&'a SpentOutputEvent), - T4(&'a TransactionInclusionEvent), - T5(TransactionProgressEvent_<'a>), + T0(&'a AddressData), + T1(&'a NewOutputEvent), + T2(&'a SpentOutputEvent), + T3(&'a TransactionInclusionEvent), + T4(TransactionProgressEvent_<'a>), } #[derive(Serialize)] struct TypedWalletEvent_<'a> { @@ -61,30 +59,26 @@ impl Serialize for WalletEvent { event: WalletEvent_<'a>, } let event = match self { - Self::ConsolidationRequired => TypedWalletEvent_ { - kind: WalletEventType::ConsolidationRequired as u8, - event: WalletEvent_::T0, - }, #[cfg(feature = "ledger_nano")] Self::LedgerAddressGeneration(e) => TypedWalletEvent_ { kind: WalletEventType::LedgerAddressGeneration as u8, - event: WalletEvent_::T1(e), + event: WalletEvent_::T0(e), }, Self::NewOutput(e) => TypedWalletEvent_ { kind: WalletEventType::NewOutput as u8, - event: WalletEvent_::T2(e), + event: WalletEvent_::T1(e), }, Self::SpentOutput(e) => TypedWalletEvent_ { kind: WalletEventType::SpentOutput as u8, - event: WalletEvent_::T3(e), + event: WalletEvent_::T2(e), }, Self::TransactionInclusion(e) => TypedWalletEvent_ { kind: WalletEventType::TransactionInclusion as u8, - event: WalletEvent_::T4(e), + event: WalletEvent_::T3(e), }, Self::TransactionProgress(e) => TypedWalletEvent_ { kind: WalletEventType::TransactionProgress as u8, - event: WalletEvent_::T5(TransactionProgressEvent_ { progress: e }), + event: WalletEvent_::T4(TransactionProgressEvent_ { progress: e }), }, }; event.serialize(serializer) @@ -108,7 +102,6 @@ impl<'de> Deserialize<'de> for WalletEvent { ) .map_err(serde::de::Error::custom)? { - WalletEventType::ConsolidationRequired => Self::ConsolidationRequired, #[cfg(feature = "ledger_nano")] WalletEventType::LedgerAddressGeneration => { Self::LedgerAddressGeneration(AddressData::deserialize(value).map_err(|e| { @@ -146,14 +139,13 @@ impl<'de> Deserialize<'de> for WalletEvent { #[repr(u8)] #[non_exhaustive] pub enum WalletEventType { - ConsolidationRequired = 0, #[cfg(feature = "ledger_nano")] #[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))] - LedgerAddressGeneration = 1, - NewOutput = 2, - SpentOutput = 3, - TransactionInclusion = 4, - TransactionProgress = 5, + LedgerAddressGeneration = 0, + NewOutput = 1, + SpentOutput = 2, + TransactionInclusion = 3, + TransactionProgress = 4, } impl TryFrom for WalletEventType { @@ -161,13 +153,12 @@ impl TryFrom for WalletEventType { fn try_from(value: u8) -> Result { let event_type = match value { - 0 => Self::ConsolidationRequired, #[cfg(feature = "ledger_nano")] - 1 => Self::LedgerAddressGeneration, - 2 => Self::NewOutput, - 3 => Self::SpentOutput, - 4 => Self::TransactionInclusion, - 5 => Self::TransactionProgress, + 0 => Self::LedgerAddressGeneration, + 1 => Self::NewOutput, + 2 => Self::SpentOutput, + 3 => Self::TransactionInclusion, + 4 => Self::TransactionProgress, _ => return Err(Error::InvalidEventType(value)), }; Ok(event_type) diff --git a/sdk/src/wallet/operations/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 5422f2a922..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,22 +181,11 @@ where input_selection = input_selection.with_burn(burn.clone()); } - let selected_transaction_data = match input_selection.select() { - Ok(r) => r, - // TODO this error doesn't exist with the new ISA - // Err(crate::client::Error::ConsolidationRequired(output_count)) => { - // #[cfg(feature = "events")] - // self.event_emitter - // .lock() - // .await - // .emit(account.index, WalletEvent::ConsolidationRequired); - // return Err(crate::wallet::Error::ConsolidationRequired { - // output_count, - // output_count_max: INPUT_COUNT_MAX, - // }); - // } - Err(e) => return Err(e.into()), - }; + 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 for output in &selected_transaction_data.inputs { 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 diff --git a/sdk/tests/wallet/events.rs b/sdk/tests/wallet/events.rs index 62892ea721..9dac2168ea 100644 --- a/sdk/tests/wallet/events.rs +++ b/sdk/tests/wallet/events.rs @@ -40,8 +40,6 @@ fn assert_serde_eq(event_0: WalletEvent) { #[test] fn wallet_events_serde() { - assert_serde_eq(WalletEvent::ConsolidationRequired); - #[cfg(feature = "ledger_nano")] assert_serde_eq(WalletEvent::LedgerAddressGeneration(AddressData { address: Bech32Address::try_from_str("rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy")