From 6d3e4c9d55c3a8c14a0dd7d311f0c5d8fa0a852f Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 5 Dec 2023 18:31:54 +0100 Subject: [PATCH 01/10] Add capabilities to TransactionOptions (#1742) * Add capabilities to TransactionOptions * TODO --- .../types/block/payload/signed_transaction/transaction.rs | 5 +++++ sdk/src/wallet/operations/transaction/account.rs | 6 ++++++ sdk/src/wallet/operations/transaction/build_transaction.rs | 4 ++++ sdk/src/wallet/operations/transaction/options.rs | 7 ++++++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index 50260c10d9..8f8669419d 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -101,6 +101,11 @@ impl TransactionBuilder { self } + pub fn add_capabilities(mut self, capabilities: impl IntoIterator) -> Self { + self.capabilities.add_capabilities(capabilities); + self + } + /// Sets the payload of a [`TransactionBuilder`]. pub fn with_payload(mut self, payload: impl Into) -> Self { self.payload = payload.into(); diff --git a/sdk/src/wallet/operations/transaction/account.rs b/sdk/src/wallet/operations/transaction/account.rs index 81d64777a3..04a2964ec3 100644 --- a/sdk/src/wallet/operations/transaction/account.rs +++ b/sdk/src/wallet/operations/transaction/account.rs @@ -14,6 +14,7 @@ use crate::{ unlock_condition::AddressUnlockCondition, AccountId, AccountOutput, OutputId, }, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, }, wallet::{ operations::transaction::{TransactionOptions, TransactionWithMetadata}, @@ -102,12 +103,17 @@ 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 2f126b6fff..c922c68b93 100644 --- a/sdk/src/wallet/operations/transaction/build_transaction.rs +++ b/sdk/src/wallet/operations/transaction/build_transaction.rs @@ -53,6 +53,10 @@ where if let Some(context_inputs) = options.context_inputs { builder = builder.with_context_inputs(context_inputs); } + + if let Some(capabilities) = options.capabilities { + builder = builder.add_capabilities(capabilities.capabilities_iter()); + } } let transaction = builder.finish_with_params(&protocol_parameters)?; diff --git a/sdk/src/wallet/operations/transaction/options.rs b/sdk/src/wallet/operations/transaction/options.rs index 2e98c2e0d2..c19d15739f 100644 --- a/sdk/src/wallet/operations/transaction/options.rs +++ b/sdk/src/wallet/operations/transaction/options.rs @@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize}; use crate::{ client::api::input_selection::Burn, types::block::{ - address::Address, context_input::ContextInput, output::OutputId, payload::tagged_data::TaggedDataPayload, + address::Address, + context_input::ContextInput, + output::OutputId, + payload::{signed_transaction::TransactionCapabilities, tagged_data::TaggedDataPayload}, }, }; @@ -30,6 +33,8 @@ pub struct TransactionOptions { pub note: Option, #[serde(default)] pub allow_micro_amount: bool, + #[serde(default)] + pub capabilities: Option, } #[allow(clippy::enum_variant_names)] From dc7dfc26840b7071509321e3aeedb95d2c1138bd Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 09:58:22 +0100 Subject: [PATCH 02/10] ISA: disallow unrequired implicit accounts from being selected (#1745) --- .../client/api/block_builder/input_selection/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 dd4c60fd6f..437cb962d6 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -31,6 +31,7 @@ use crate::{ }; /// Working state for the input selection algorithm. +#[derive(Debug)] pub struct InputSelection { available_inputs: Vec, required_inputs: HashSet, @@ -245,9 +246,12 @@ impl InputSelection { match required_address { Address::Anchor(_) => false, - Address::ImplicitAccountCreation(implicit_account_creation) => self - .addresses - .contains(&Address::from(*implicit_account_creation.ed25519_address())), + Address::ImplicitAccountCreation(implicit_account_creation) => { + self.required_inputs.contains(input.output_id()) + && self + .addresses + .contains(&Address::from(*implicit_account_creation.ed25519_address())) + } _ => self.addresses.contains(&required_address), } }) From 092eb61dc6f2ff4402f70e4a8e33da1ecf007bbc Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 09:58:46 +0100 Subject: [PATCH 03/10] Fix UtxoChangesResponse (#1749) --- sdk/src/types/api/core.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index 48e89e3451..c4f1dce37f 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -521,7 +521,7 @@ pub struct RoutesResponse { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UtxoChangesResponse { - pub index: u32, + pub commitment_id: SlotCommitmentId, pub created_outputs: Vec, pub consumed_outputs: Vec, } From 747a59f5114da65eb6abd5f6b585450d6c4f6ac3 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 11:39:56 +0100 Subject: [PATCH 04/10] Fix audit and python fmt CIs (#1751) --- Cargo.lock | 192 ++++-------------- .../python/tests/test_protocol_parameters.py | 1 + 2 files changed, 40 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1f2a11221..0e1bd2ff89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,30 +101,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -235,12 +235,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bech32" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" - [[package]] name = "bech32" version = "0.10.0-beta" @@ -462,9 +456,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.10" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -472,9 +466,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -613,9 +607,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -623,9 +617,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -787,9 +781,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", "serde", @@ -909,12 +903,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dotenvy" version = "0.15.7" @@ -1004,26 +992,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "enum-iterator" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7" -dependencies = [ - "enum-iterator-derive", -] - -[[package]] -name = "enum-iterator-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1403,9 +1371,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hidapi" -version = "1.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +checksum = "723777263b0dcc5730aec947496bd8c3940ba63c15f5633b288cc615f4f6af79" dependencies = [ "cc", "libc", @@ -1625,23 +1593,17 @@ dependencies = [ [[package]] name = "iota-ledger-nano" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82990c4050a18fcd5d78af7069ab3cd33e7a44c961526910171529bd5a4f6bb1" +checksum = "9fef85d9f5e9cf947d8a4f6aabb74f38ef970aade7d60112c8b8637596f07f93" dependencies = [ - "bech32 0.7.3", - "enum-iterator", - "futures", + "arrayref", + "byteorder", + "hex", "hidapi", "lazy_static", - "ledger-apdu", - "ledger-transport", - "ledger-transport-hid", "log", - "once_cell", - "serde", "thiserror", - "trait-async", ] [[package]] @@ -1650,7 +1612,7 @@ version = "1.1.2" dependencies = [ "anymap", "async-trait", - "bech32 0.10.0-beta", + "bech32", "bitflags 2.4.1", "bs58", "derive_more", @@ -1841,43 +1803,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "ledger-apdu" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" -dependencies = [ - "arrayref", - "no-std-compat", - "snafu", -] - -[[package]] -name = "ledger-transport" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" -dependencies = [ - "async-trait", - "ledger-apdu", -] - -[[package]] -name = "ledger-transport-hid" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" -dependencies = [ - "byteorder", - "cfg-if", - "hex", - "hidapi", - "ledger-transport", - "libc", - "log", - "thiserror", -] - [[package]] name = "libc" version = "0.2.150" @@ -1955,9 +1880,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -2131,12 +2056,6 @@ dependencies = [ "libc", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -2626,9 +2545,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", @@ -2710,15 +2629,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3026,28 +2945,6 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" -[[package]] -name = "snafu" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "socket2" version = "0.4.10" @@ -3419,17 +3316,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "trait-async" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe8c654712ee594c93b7222d98b4e61c7e003aec49e73877edac607a213699d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -3971,9 +3857,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "b7e87b8dfbe3baffbe687eef2e164e32286eff31a5ee16463ce03d991643ec94" dependencies = [ "memchr", ] @@ -4036,18 +3922,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.27" +version = "0.7.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43de342578a3a14a9314a2dab1942cbfcbe5686e1f91acdc513058063eafe18" +checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.27" +version = "0.7.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1012d89e3acb79fad7a799ce96866cfb8098b74638465ea1b1533d35900ca90" +checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/python/tests/test_protocol_parameters.py b/bindings/python/tests/test_protocol_parameters.py index 3aecfa5056..2450b86cf5 100644 --- a/bindings/python/tests/test_protocol_parameters.py +++ b/bindings/python/tests/test_protocol_parameters.py @@ -9,6 +9,7 @@ with open('../../sdk/tests/types/fixtures/protocol_parameters.json', "r", encoding="utf-8") as json_file: protocol_params_json = json.load(json_file) + def test_protocol_parameters(): protocol_params_dict = protocol_params_json['params'] protocol_params = ProtocolParameters.from_dict(protocol_params_dict) From fa0bcc987a3257dd19af2748796987566a335e18 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 12:01:26 +0100 Subject: [PATCH 05/10] Fallback issuer_id if None is provided (#1748) * ISA: disallow unrequired implicit accounts from being selected * Default block issuer * Don't use map * Add NoAccountToIssueBlock error * account_id_non_null * Update sdk/src/wallet/error.rs Co-authored-by: /alex/ * Update sdk/src/wallet/error.rs Co-authored-by: /alex/ --------- Co-authored-by: /alex/ --- sdk/src/wallet/error.rs | 3 +++ sdk/src/wallet/operations/block.rs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/sdk/src/wallet/error.rs b/sdk/src/wallet/error.rs index a490e5a232..ad26c39a91 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -127,6 +127,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, } impl Error { diff --git a/sdk/src/wallet/operations/block.rs b/sdk/src/wallet/operations/block.rs index d0e988ab60..0026ede84c 100644 --- a/sdk/src/wallet/operations/block.rs +++ b/sdk/src/wallet/operations/block.rs @@ -19,8 +19,16 @@ where ) -> Result { log::debug!("submit_basic_block"); - // TODO https://github.com/iotaledger/iota-sdk/issues/1741 - let issuer_id = issuer_id.into().unwrap_or(AccountId::null()); + let issuer_id = match issuer_id.into() { + Some(issuer_id) => Some(issuer_id), + None => self + .data() + .await + .accounts() + .next() + .map(|o| o.output.as_account().account_id_non_null(&o.output_id)), + } + .ok_or(Error::NoAccountToIssueBlock)?; let block = self .client() From 0738cfc59395419930a5c03a35d8b1e2b7b31b90 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 14:51:18 +0100 Subject: [PATCH 06/10] Add IssuanceBlockHeaderResponse::latest_parent_block_issuing_time (#1752) * Add IssuanceBlockHeaderResponse::latest_parent_block_issuing_time * Add TODO --- sdk/src/client/api/block_builder/mod.rs | 1 + sdk/src/types/api/core.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/sdk/src/client/api/block_builder/mod.rs b/sdk/src/client/api/block_builder/mod.rs index 9222463327..45bb293a2d 100644 --- a/sdk/src/client/api/block_builder/mod.rs +++ b/sdk/src/client/api/block_builder/mod.rs @@ -19,6 +19,7 @@ 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() diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index c4f1dce37f..34b6472ad8 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -277,6 +277,9 @@ pub struct IssuanceBlockHeaderResponse { /// Blocks that are directly referenced to adjust opinion. #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] pub shallow_like_parents: BTreeSet, + // Latest issuing time of the returned parents. + #[serde(with = "string")] + pub latest_parent_block_issuing_time: u64, /// The slot index of the latest finalized slot. pub latest_finalized_slot: SlotIndex, /// The latest slot commitment. From 2b192618878ea9ac8e04bf89313f75036b540398 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 15:19:34 +0100 Subject: [PATCH 07/10] Add ProtocolParameters::chain_switching_threshold (#1754) * Add ProtocolParameters::chain_switching_threshold * Update sdk/src/types/block/protocol/mod.rs Co-authored-by: /alex/ --------- Co-authored-by: /alex/ --- sdk/src/types/block/protocol/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index e4e2adaeb0..2c200bc596 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -91,6 +91,8 @@ pub struct ProtocolParameters { /// Defines the target size of the committee. If there's fewer candidates the actual committee size could be /// smaller in a given epoch. pub(crate) target_committee_size: u8, + /// Defines the number of heavier slots that a chain needs to be ahead of the current chain to be considered for switching. + pub(crate) chain_switching_threshold: u8, } // This implementation is required to make [`ProtocolParameters`] a [`Packable`] visitor. @@ -128,6 +130,7 @@ impl Default for ProtocolParameters { version_signaling_parameters: Default::default(), rewards_parameters: Default::default(), target_committee_size: 32, + chain_switching_threshold: 3, } } } From 9cd91215033b8a2dd64d21e374ff108bc838b3ca Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Dec 2023 16:20:32 +0100 Subject: [PATCH 08/10] Add ManaParameters::annual_decay_factor_percentage (#1755) * Add ManaParameters::annual_decay_factor_percentage * Out of date docs * fmt --- sdk/src/types/block/mana/parameters.rs | 11 ++++++++--- sdk/src/types/block/protocol/mod.rs | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sdk/src/types/block/mana/parameters.rs b/sdk/src/types/block/mana/parameters.rs index fd5ffd44c8..65e8ca8d04 100644 --- a/sdk/src/types/block/mana/parameters.rs +++ b/sdk/src/types/block/mana/parameters.rs @@ -21,11 +21,13 @@ use crate::types::block::{ pub struct ManaParameters { /// The number of bits used to represent Mana. pub(crate) bits_count: u8, - /// The amount of potential Mana generated by 1 IOTA in 1 slot. + /// The amount of potential Mana generated by 1 microIOTA in 1 slot multiplied by 2^generation_rate_exponent. pub(crate) generation_rate: u8, - /// The scaling of `mana_generation_rate` expressed as an exponent of 2. + /// The scaling of `generation_rate` expressed as an exponent of 2. + /// The actual generation rate of Mana is given by generation_rate * 2^(-generation_rate_exponent). pub(crate) generation_rate_exponent: u8, /// A lookup table of epoch index diff to mana decay factor. + /// The actual decay factor is given by decay_factors[epoch_diff] * 2^(-decay_factors_exponent). #[packable(unpack_error_with = |_| Error::InvalidManaDecayFactors)] #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::boxed_slice_prefix"))] #[getset(skip)] @@ -34,8 +36,10 @@ pub struct ManaParameters { pub(crate) decay_factors_exponent: u8, /// An integer approximation of the sum of decay over epochs. pub(crate) decay_factor_epochs_sum: u32, - /// The scaling of `mana_decay_factor_epochs_sum` expressed as an exponent of 2. + /// The scaling of `decay_factor_epochs_sum` expressed as an exponent of 2. pub(crate) decay_factor_epochs_sum_exponent: u8, + // Decay factor for 1 year. + pub(crate) annual_decay_factor_percentage: u8, } impl ManaParameters { @@ -99,6 +103,7 @@ impl Default for ManaParameters { decay_factors_exponent: Default::default(), decay_factor_epochs_sum: Default::default(), decay_factor_epochs_sum_exponent: Default::default(), + annual_decay_factor_percentage: Default::default(), } } } diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index 2c200bc596..43a6ebd316 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -91,7 +91,8 @@ pub struct ProtocolParameters { /// Defines the target size of the committee. If there's fewer candidates the actual committee size could be /// smaller in a given epoch. pub(crate) target_committee_size: u8, - /// Defines the number of heavier slots that a chain needs to be ahead of the current chain to be considered for switching. + /// Defines the number of heavier slots that a chain needs to be ahead of the current chain to be considered for + /// switching. pub(crate) chain_switching_threshold: u8, } From d691ceaebc11d3ae5b9af9a3c9da9189a0fefd2e Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:54:39 +0100 Subject: [PATCH 09/10] Timelock Expiration SlotIndex (#1661) * Timelock/Expiration validation with a CommitmentInput * Oh no * Correct is_expired * TODO * meh * TODO * Nits * Fix is_timelocked * Fix is_expired * First batch of changes * Second batch of changes * Fixes * Simplified required_address * Infallible required_address * Some fixes * Remove commented code * Fix conflicts * Fix rebase * Address some todos * Simplify delegation unlock * Address more todos * Fix some checks and cleanup * Fix test * Address review comments * Store protocol parameters in file * Fix params * Order * Use wrong tx failure reason * Rename error * Update sdk/src/client/error.rs Co-authored-by: Thibault Martinez * Update merged part * Update sdk/src/types/block/output/anchor.rs Co-authored-by: Thibault Martinez * Address reviews * Use flat_map() * Review suggestion * Remove superfluous Copy bounds * Update sdk/src/client/secret/ledger_nano.rs Co-authored-by: DaughterOfMars * Cleanup find_map * Update sdk/src/wallet/operations/helpers/time.rs Co-authored-by: /alex/ * Address review comments * Rename to CommittableAgeRange * Use is_some_and() * Fix match * Update sdk/src/types/block/protocol/mod.rs Co-authored-by: /alex/ * Rename params * Review suggestions * Return Result from locked_address() * Review suggestions * Remove into --------- Co-authored-by: Thibault Martinez Co-authored-by: DaughterOfMars Co-authored-by: /alex/ --- bindings/core/src/method/secret_manager.rs | 3 +- bindings/core/src/method/utils.rs | 1 + bindings/core/src/method/wallet.rs | 2 +- .../core/src/method_handler/secret_manager.rs | 6 +- bindings/core/src/method_handler/utils.rs | 4 +- cli/src/wallet_cli/mod.rs | 10 +- .../offline_signing/1_prepare_transaction.rs | 27 +++-- .../offline_signing/2_sign_transaction.rs | 21 ++-- .../api/block_builder/input_selection/mod.rs | 100 +++++++++++------- .../input_selection/remainder.rs | 9 +- .../input_selection/requirement/amount.rs | 16 ++- .../input_selection/requirement/ed25519.rs | 14 +-- .../input_selection/requirement/sender.rs | 4 +- .../client/api/block_builder/transaction.rs | 3 + sdk/src/client/error.rs | 3 + sdk/src/client/secret/ledger_nano.rs | 37 +++++-- sdk/src/client/secret/mnemonic.rs | 9 +- sdk/src/client/secret/mod.rs | 74 +++++++++---- sdk/src/client/secret/private_key.rs | 9 +- sdk/src/client/stronghold/secret.rs | 9 +- .../types/block/context_input/commitment.rs | 13 ++- sdk/src/types/block/context_input/mod.rs | 2 +- sdk/src/types/block/error.rs | 2 + sdk/src/types/block/output/account.rs | 9 +- sdk/src/types/block/output/anchor.rs | 3 +- sdk/src/types/block/output/basic.rs | 15 ++- sdk/src/types/block/output/delegation.rs | 9 +- sdk/src/types/block/output/mod.rs | 60 ++++------- sdk/src/types/block/output/nft.rs | 15 ++- .../output/unlock_condition/expiration.rs | 33 +++++- .../block/output/unlock_condition/mod.rs | 59 +++++++---- .../block/output/unlock_condition/timelock.rs | 5 + .../payload/signed_transaction/transaction.rs | 1 + sdk/src/types/block/protocol/mod.rs | 18 ++++ sdk/src/types/block/semantic.rs | 50 ++++++--- sdk/src/wallet/operations/balance.rs | 13 +-- sdk/src/wallet/operations/helpers/time.rs | 34 +++--- sdk/src/wallet/operations/output_claiming.rs | 27 +++-- .../wallet/operations/output_consolidation.rs | 25 +++-- .../operations/transaction/input_selection.rs | 25 ++--- sdk/src/wallet/operations/transaction/mod.rs | 6 +- .../transaction/sign_transaction.rs | 3 +- sdk/src/wallet/types/mod.rs | 17 +-- sdk/tests/client/signing/account.rs | 12 ++- sdk/tests/client/signing/basic.rs | 18 ++-- sdk/tests/client/signing/mod.rs | 16 ++- sdk/tests/client/signing/nft.rs | 6 +- sdk/tests/types/slot.rs | 9 +- sdk/tests/wallet/output_preparation.rs | 8 +- 49 files changed, 580 insertions(+), 294 deletions(-) diff --git a/bindings/core/src/method/secret_manager.rs b/bindings/core/src/method/secret_manager.rs index b70c10db53..d8e9150b31 100644 --- a/bindings/core/src/method/secret_manager.rs +++ b/bindings/core/src/method/secret_manager.rs @@ -5,7 +5,7 @@ use crypto::keys::bip44::Bip44; use derivative::Derivative; use iota_sdk::{ client::api::{GetAddressesOptions, PreparedTransactionDataDto}, - types::block::UnsignedBlockDto, + types::block::{protocol::ProtocolParameters, UnsignedBlockDto}, utils::serde::bip44::Bip44Def, }; use serde::{Deserialize, Serialize}; @@ -61,6 +61,7 @@ pub enum SecretManagerMethod { SignTransaction { /// Prepared transaction data prepared_transaction_data: PreparedTransactionDataDto, + protocol_parameters: Box, }, // Sign a block. #[serde(rename_all = "camelCase")] diff --git a/bindings/core/src/method/utils.rs b/bindings/core/src/method/utils.rs index be706f9d46..c45583acb4 100644 --- a/bindings/core/src/method/utils.rs +++ b/bindings/core/src/method/utils.rs @@ -126,5 +126,6 @@ pub enum UtilsMethod { transaction: TransactionDto, inputs: Vec, unlocks: Option>, + protocol_parameters: ProtocolParameters, }, } diff --git a/bindings/core/src/method/wallet.rs b/bindings/core/src/method/wallet.rs index 42fc4e4ace..0ed337c3f4 100644 --- a/bindings/core/src/method/wallet.rs +++ b/bindings/core/src/method/wallet.rs @@ -4,7 +4,7 @@ #[cfg(feature = "stronghold")] use std::path::PathBuf; -use crypto::{keys::bip44::Bip44, signatures::ed25519::PublicKey}; +use crypto::keys::bip44::Bip44; use derivative::Derivative; #[cfg(feature = "events")] use iota_sdk::wallet::events::types::{WalletEvent, WalletEventType}; diff --git a/bindings/core/src/method_handler/secret_manager.rs b/bindings/core/src/method_handler/secret_manager.rs index 3e3ac2ec34..d766194ee1 100644 --- a/bindings/core/src/method_handler/secret_manager.rs +++ b/bindings/core/src/method_handler/secret_manager.rs @@ -75,9 +75,13 @@ where } SecretManagerMethod::SignTransaction { prepared_transaction_data, + protocol_parameters, } => { let transaction = &secret_manager - .sign_transaction(PreparedTransactionData::try_from_dto(prepared_transaction_data)?) + .sign_transaction( + PreparedTransactionData::try_from_dto(prepared_transaction_data)?, + &protocol_parameters, + ) .await .map_err(iota_sdk::client::Error::from)?; Response::SignedTransaction(transaction.into()) diff --git a/bindings/core/src/method_handler/utils.rs b/bindings/core/src/method_handler/utils.rs index 55510b8453..2777b7a993 100644 --- a/bindings/core/src/method_handler/utils.rs +++ b/bindings/core/src/method_handler/utils.rs @@ -110,6 +110,7 @@ pub(crate) fn call_utils_method_internal(method: UtilsMethod) -> Result { let transaction = Transaction::try_from_dto(transaction)?; let inputs = inputs @@ -117,7 +118,8 @@ pub(crate) fn call_utils_method_internal(method: UtilsMethod) -> Result>(); - let context = SemanticValidationContext::new(&transaction, &inputs, unlocks.as_deref()); + let context = + SemanticValidationContext::new(&transaction, &inputs, unlocks.as_deref(), protocol_parameters); Response::TransactionFailureReason(context.validate()?) } diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 175f1b5c74..ad8d0be003 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -971,6 +971,7 @@ async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> { ); let slot_index = wallet.client().get_slot_index().await?; + let protocol_parameters = wallet.client().get_protocol_parameters().await?; let mut output_ids = Vec::new(); let mut amount = 0; @@ -986,11 +987,14 @@ async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> { output_ids.push(output_id); // Output might be associated with the address, but can't be unlocked by it, so we check that here. - let (required_address, _) = &output_data + let required_address = &output_data .output - .required_and_unlocked_address(slot_index, &output_id)?; + .required_address(slot_index, protocol_parameters.committable_age_range())?; - if address.inner() == required_address { + if required_address + .as_ref() + .is_some_and(|required_address| required_address == address.inner()) + { if let Some(nt) = output_data.output.native_token() { native_tokens.add_native_token(*nt)?; } diff --git a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs index d5cfbfca99..4b57d9aadb 100644 --- a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs +++ b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs @@ -9,11 +9,7 @@ //! ``` use iota_sdk::{ - client::{ - api::{PreparedTransactionData, PreparedTransactionDataDto}, - constants::SHIMMER_COIN_TYPE, - secret::SecretManager, - }, + client::{api::PreparedTransactionDataDto, constants::SHIMMER_COIN_TYPE, secret::SecretManager}, crypto::keys::bip44::Bip44, wallet::{types::Bip44Address, ClientOptions, Result, SendParams, Wallet}, }; @@ -21,6 +17,7 @@ use iota_sdk::{ const ONLINE_WALLET_DB_PATH: &str = "./examples/wallet/offline_signing/example-online-walletdb"; const ADDRESS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.address.json"; const PREPARED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.prepared_transaction.json"; +const PROTOCOL_PARAMETERS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.protocol_parameters.json"; // Address to which we want to send the amount. const RECV_ADDRESS: &str = "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu"; // The amount to send. @@ -60,7 +57,17 @@ async fn main() -> Result<()> { println!("Prepared transaction sending {params:?}"); - write_transaction_to_file(prepared_transaction).await?; + write_data_to_file( + PreparedTransactionDataDto::from(&prepared_transaction), + PREPARED_TRANSACTION_FILE_PATH, + ) + .await?; + + write_data_to_file( + wallet.client().get_protocol_parameters().await?, + PROTOCOL_PARAMETERS_FILE_PATH, + ) + .await?; Ok(()) } @@ -75,12 +82,12 @@ async fn read_address_from_file() -> Result { Ok(serde_json::from_str(&json)?) } -async fn write_transaction_to_file(prepared_transaction: PreparedTransactionData) -> Result<()> { +async fn write_data_to_file(data: impl serde::Serialize, path: &str) -> Result<()> { use tokio::io::AsyncWriteExt; - let json = serde_json::to_string_pretty(&PreparedTransactionDataDto::from(&prepared_transaction))?; - let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(PREPARED_TRANSACTION_FILE_PATH).await?); - println!("example.prepared_transaction.json:\n{json}"); + let json = serde_json::to_string_pretty(&data)?; + let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(path).await?); + println!("{path}:\n{json}"); file.write_all(json.as_bytes()).await?; file.flush().await?; Ok(()) diff --git a/sdk/examples/wallet/offline_signing/2_sign_transaction.rs b/sdk/examples/wallet/offline_signing/2_sign_transaction.rs index 14b9e0f4b3..7dea48651f 100644 --- a/sdk/examples/wallet/offline_signing/2_sign_transaction.rs +++ b/sdk/examples/wallet/offline_signing/2_sign_transaction.rs @@ -11,8 +11,8 @@ use iota_sdk::{ client::{ api::{ - transaction::validate_signed_transaction_payload_length, PreparedTransactionData, - PreparedTransactionDataDto, SignedTransactionData, SignedTransactionDataDto, + transaction::validate_signed_transaction_payload_length, PreparedTransactionData, SignedTransactionData, + SignedTransactionDataDto, }, secret::{stronghold::StrongholdSecretManager, SecretManage, SecretManager}, }, @@ -22,6 +22,7 @@ use iota_sdk::{ const STRONGHOLD_SNAPSHOT_PATH: &str = "./examples/wallet/offline_signing/example.stronghold"; const PREPARED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.prepared_transaction.json"; +const PROTOCOL_PARAMETERS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.protocol_parameters.json"; const SIGNED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.signed_transaction.json"; #[tokio::main] @@ -39,11 +40,15 @@ async fn main() -> Result<()> { .password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .build(STRONGHOLD_SNAPSHOT_PATH)?; - let prepared_transaction_data = read_prepared_transaction_from_file().await?; + let prepared_transaction_data = PreparedTransactionData::try_from_dto(serde_json::from_str( + &read_data_from_file(PREPARED_TRANSACTION_FILE_PATH).await?, + )?)?; + + let protocol_parameters = serde_json::from_str(&read_data_from_file(PROTOCOL_PARAMETERS_FILE_PATH).await?)?; // Signs prepared transaction offline. let unlocks = SecretManager::Stronghold(secret_manager) - .transaction_unlocks(&prepared_transaction_data) + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) .await?; let signed_transaction = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; @@ -62,16 +67,14 @@ async fn main() -> Result<()> { Ok(()) } -async fn read_prepared_transaction_from_file() -> Result { +async fn read_data_from_file(path: &str) -> Result { use tokio::io::AsyncReadExt; - let mut file = tokio::io::BufReader::new(tokio::fs::File::open(PREPARED_TRANSACTION_FILE_PATH).await?); + let mut file = tokio::io::BufReader::new(tokio::fs::File::open(path).await?); let mut json = String::new(); file.read_to_string(&mut json).await?; - Ok(PreparedTransactionData::try_from_dto(serde_json::from_str::< - PreparedTransactionDataDto, - >(&json)?)?) + Ok(json) } async fn write_signed_transaction_to_file(signed_transaction_data: &SignedTransactionData) -> Result<()> { 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 437cb962d6..f38bcd6e2d 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -25,7 +25,7 @@ use crate::{ AccountOutput, ChainId, FoundryOutput, NativeTokensBuilder, NftOutput, Output, OutputId, OUTPUT_COUNT_RANGE, }, payload::signed_transaction::TransactionCapabilities, - protocol::ProtocolParameters, + protocol::{CommittableAgeRange, ProtocolParameters}, slot::SlotIndex, }, }; @@ -62,8 +62,9 @@ impl InputSelection { fn required_account_nft_addresses(&self, input: &InputSigningData) -> Result, Error> { let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id())? - .0; + .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .expect("expiration unlockable outputs already filtered out"); + let required_address = if let Address::Restricted(restricted) = &required_address { restricted.address() } else { @@ -176,6 +177,7 @@ impl InputSelection { remainder_address: None, protocol_parameters, // TODO may want to make this mandatory at some point + // Should be set from a commitment context input slot_index: SlotIndex::from(0), requirements: Vec::new(), automatically_transitioned: HashSet::new(), @@ -227,21 +229,27 @@ impl InputSelection { // PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out. let unlock_conditions = input.output.unlock_conditions().unwrap(); - if unlock_conditions.is_time_locked(self.slot_index) { + if unlock_conditions.is_timelocked(self.slot_index, self.protocol_parameters.min_committable_age()) { return false; } let required_address = input .output // Account transition is irrelevant here as we keep accounts anyway. - .required_and_unlocked_address(self.slot_index, input.output_id()) + .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) // PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out. - .unwrap() - .0; - let required_address = if let Address::Restricted(restricted) = &required_address { - restricted.address() - } else { - &required_address + .unwrap(); + + let required_address = match &required_address { + Some(address) => { + if let Address::Restricted(restricted) = address { + restricted.address() + } else { + address + } + } + // Time in which no address can unlock the output because of an expiration unlock condition + None => return false, }; match required_address { @@ -252,7 +260,7 @@ impl InputSelection { .addresses .contains(&Address::from(*implicit_account_creation.ed25519_address())) } - _ => self.addresses.contains(&required_address), + _ => self.addresses.contains(required_address), } }) } @@ -261,6 +269,7 @@ impl InputSelection { pub(crate) fn sort_input_signing_data( mut inputs: Vec, slot_index: SlotIndex, + committable_age_range: CommittableAgeRange, ) -> Result, Error> { // initially sort by output to make it deterministic // TODO: rethink this, we only need it deterministic for tests, for the protocol it doesn't matter, also there @@ -269,38 +278,42 @@ impl InputSelection { // filter for ed25519 address first let (mut sorted_inputs, account_nft_address_inputs): (Vec, Vec) = inputs.into_iter().partition(|input_signing_data| { - let (input_address, _) = input_signing_data + let required_address = input_signing_data .output - .required_and_unlocked_address(slot_index, input_signing_data.output_id()) - // PANIC: safe to unwrap, because we filtered irrelevant outputs out before - .unwrap(); + .required_address(slot_index, committable_age_range) + // PANIC: safe to unwrap as non basic/alias/foundry/nft outputs are already filtered out. + .unwrap() + .expect("expiration unlockable outputs already filtered out"); - input_address.is_ed25519() + required_address.is_ed25519() }); for input in account_nft_address_inputs { - let (input_address, _) = input + let required_address = input .output - .required_and_unlocked_address(slot_index, input.output_id())?; + .required_address(slot_index, committable_age_range)? + .expect("expiration unlockable outputs already filtered out"); - match sorted_inputs.iter().position(|input_signing_data| match input_address { - Address::Account(unlock_address) => { - if let Output::Account(account_output) = &input_signing_data.output { - *unlock_address.account_id() - == account_output.account_id_non_null(input_signing_data.output_id()) - } else { - false + match sorted_inputs + .iter() + .position(|input_signing_data| match required_address { + Address::Account(unlock_address) => { + if let Output::Account(account_output) = &input_signing_data.output { + *unlock_address.account_id() + == account_output.account_id_non_null(input_signing_data.output_id()) + } else { + false + } } - } - Address::Nft(unlock_address) => { - if let Output::Nft(nft_output) = &input_signing_data.output { - *unlock_address.nft_id() == nft_output.nft_id_non_null(input_signing_data.output_id()) - } else { - false + Address::Nft(unlock_address) => { + if let Output::Nft(nft_output) = &input_signing_data.output { + *unlock_address.nft_id() == nft_output.nft_id_non_null(input_signing_data.output_id()) + } else { + false + } } - } - _ => false, - }) { + _ => false, + }) { Some(position) => { // Insert after the output we need sorted_inputs.insert(position + 1, input); @@ -320,13 +333,14 @@ impl InputSelection { if let Some(account_or_nft_address) = account_or_nft_address { // Check for existing outputs for this address, and insert before match sorted_inputs.iter().position(|input_signing_data| { - let (input_address, _) = input_signing_data + let required_address = input_signing_data .output - .required_and_unlocked_address(slot_index, input.output_id()) - // PANIC: safe to unwrap, because we filtered irrelevant outputs out before - .unwrap(); + .required_address(slot_index, committable_age_range) + // PANIC: safe to unwrap as non basic/alias/foundry/nft outputs are already filtered + .unwrap() + .expect("expiration unlockable outputs already filtered out"); - input_address == account_or_nft_address + required_address == account_or_nft_address }) { Some(position) => { // Insert before the output with this address required for unlocking @@ -396,7 +410,11 @@ impl InputSelection { self.validate_transitions()?; Ok(Selected { - inputs: Self::sort_input_signing_data(self.selected_inputs, self.slot_index)?, + inputs: Self::sort_input_signing_data( + self.selected_inputs, + self.slot_index, + self.protocol_parameters.committable_age_range(), + )?, outputs: self.outputs, remainder, }) 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 e0a37a56fa..89127e8a18 100644 --- a/sdk/src/client/api/block_builder/input_selection/remainder.rs +++ b/sdk/src/client/api/block_builder/input_selection/remainder.rs @@ -24,9 +24,10 @@ impl InputSelection { if let Some(remainder_address) = &self.remainder_address { // Search in inputs for the Bip44 chain for the remainder address, so the ledger can regenerate it for input in self.available_inputs.iter().chain(self.selected_inputs.iter()) { - let (required_address, _) = input + let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id())?; + .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .expect("expiration unlockable outputs already filtered out"); if &required_address == remainder_address { return Ok(Some((remainder_address.clone(), input.chain))); @@ -38,8 +39,8 @@ impl InputSelection { for input in &self.selected_inputs { let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id())? - .0; + .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .expect("expiration unlockable outputs already filtered out"); if required_address.is_ed25519() { return Ok(Some((required_address, input.chain))); diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs b/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs index e0822901dc..57598f4e58 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs @@ -347,7 +347,13 @@ impl InputSelection { if let Output::Basic(output) = &input.output { output .unlock_conditions() - .locked_address(output.address(), self.slot_index) + .locked_address( + output.address(), + self.slot_index, + self.protocol_parameters.committable_age_range(), + ) + .expect("slot index was provided") + .expect("expiration unlockable outputs already filtered out") .is_ed25519() } else { false @@ -362,7 +368,13 @@ impl InputSelection { if let Output::Basic(output) = &input.output { !output .unlock_conditions() - .locked_address(output.address(), self.slot_index) + .locked_address( + output.address(), + self.slot_index, + self.protocol_parameters.committable_age_range(), + ) + .expect("slot index was provided") + .expect("expiration unlockable outputs already filtered out") .is_ed25519() } else { false 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 ce6fb8ee6e..af0619e949 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 @@ -7,12 +7,12 @@ use crate::{client::secret::types::InputSigningData, types::block::address::Addr impl InputSelection { // Checks if a selected input unlocks a given ED25519 address. fn selected_unlocks_ed25519_address(&self, input: &InputSigningData, address: &Address) -> bool { - // PANIC: safe to unwrap as outputs with no address have been filtered out already. let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id()) + .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) + // PANIC: safe to unwrap as outputs with no address have been filtered out already. .unwrap() - .0; + .expect("expiration unlockable outputs already filtered out"); &required_address == address } @@ -20,10 +20,12 @@ impl InputSelection { // Checks if an available input can unlock a given ED25519 address. // In case an account input is selected, also tells if it needs to be state or governance transitioned. fn available_has_ed25519_address(&self, input: &InputSigningData, address: &Address) -> bool { - let (required_address, _) = input + let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id()) - .unwrap(); + .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) + // PANIC: safe to unwrap as outputs with no address have been filtered out already. + .unwrap() + .expect("expiration unlockable outputs already filtered out"); &required_address == address } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs index e79dfc82cf..74561ed554 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs @@ -50,8 +50,8 @@ impl InputSelection { for input in self.selected_inputs.iter() { let required_address = input .output - .required_and_unlocked_address(self.slot_index, input.output_id())? - .0; + .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .expect("expiration unlockable outputs already filtered out"); if &required_address == weight_address.address() { cumulative_weight += weight_address.weight() as u16; diff --git a/sdk/src/client/api/block_builder/transaction.rs b/sdk/src/client/api/block_builder/transaction.rs index 76e01d4991..54ef0e2d53 100644 --- a/sdk/src/client/api/block_builder/transaction.rs +++ b/sdk/src/client/api/block_builder/transaction.rs @@ -10,6 +10,7 @@ use crate::{ types::block::{ output::{Output, OutputId}, payload::signed_transaction::{SignedTransactionPayload, Transaction}, + protocol::ProtocolParameters, semantic::{SemanticValidationContext, TransactionFailureReason}, signature::Ed25519Signature, Block, BlockId, @@ -28,6 +29,7 @@ const REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH: usize = 1 + 2; pub fn verify_semantic( input_signing_data: &[InputSigningData], transaction_payload: &SignedTransactionPayload, + protocol_parameters: ProtocolParameters, ) -> crate::client::Result> { let inputs = input_signing_data .iter() @@ -38,6 +40,7 @@ pub fn verify_semantic( transaction_payload.transaction(), &inputs, Some(transaction_payload.unlocks()), + protocol_parameters, ); Ok(context.validate()?) diff --git a/sdk/src/client/error.rs b/sdk/src/client/error.rs index 3333218288..5c9bdcbdff 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -160,6 +160,9 @@ pub enum Error { /// Missing transaction payload. #[error("missing transaction payload")] MissingTransactionPayload, + /// Output not unlockable due to deadzone in expiration unlock condition. + #[error("output not unlockable due to deadzone in expiration unlock condition")] + ExpirationDeadzone, /// Participation error #[cfg(feature = "participation")] diff --git a/sdk/src/client/secret/ledger_nano.rs b/sdk/src/client/secret/ledger_nano.rs index 0519b5447f..4de26ece4d 100644 --- a/sdk/src/client/secret/ledger_nano.rs +++ b/sdk/src/client/secret/ledger_nano.rs @@ -32,6 +32,7 @@ use crate::{ address::{AccountAddress, Address, AnchorAddress, NftAddress}, output::Output, payload::signed_transaction::SignedTransactionPayload, + protocol::ProtocolParameters, signature::{Ed25519Signature, Signature}, unlock::{AccountUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, Unlocks}, Error as BlockError, @@ -78,6 +79,9 @@ pub enum Error { /// No available inputs provided #[error("No available inputs provided")] NoAvailableInputsProvided, + /// Output not unlockable due to deadzone in expiration unlock condition. + #[error("output not unlockable due to deadzone in expiration unlock condition")] + ExpirationDeadzone, } impl From for Error { @@ -246,6 +250,7 @@ impl SecretManage for LedgerSecretManager { async fn transaction_unlocks( &self, prepared_transaction: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result::Error> { let mut input_bip32_indices = Vec::new(); let mut coin_type = None; @@ -400,7 +405,7 @@ impl SecretManage for LedgerSecretManager { // With blind signing the ledger only returns SignatureUnlocks, so we might have to merge them with // Account/Nft/Reference unlocks if blind_signing { - unlocks = merge_unlocks(prepared_transaction, unlocks.into_iter())?; + unlocks = merge_unlocks(prepared_transaction, unlocks.into_iter(), protocol_parameters)?; } Ok(Unlocks::new(unlocks)?) @@ -409,8 +414,9 @@ impl SecretManage for LedgerSecretManager { async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - super::default_sign_transaction(self, prepared_transaction_data).await + super::default_sign_transaction(self, prepared_transaction_data, protocol_parameters).await } } @@ -513,8 +519,13 @@ impl LedgerSecretManager { fn merge_unlocks( prepared_transaction_data: &PreparedTransactionData, mut unlocks: impl Iterator, + protocol_parameters: &ProtocolParameters, ) -> Result, Error> { - let slot_index = prepared_transaction_data.transaction.creation_slot(); + let slot_index = prepared_transaction_data + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); let transaction_signing_hash = prepared_transaction_data.transaction.signing_hash(); let mut merged_unlocks = Vec::new(); @@ -523,14 +534,16 @@ fn merge_unlocks( // Assuming inputs_data is ordered by address type for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() { // Get the address that is required to unlock the input - let (input_address, _) = input + let required_address = input .output - .required_and_unlocked_address(slot_index, input.output_metadata.output_id())?; + .required_address(slot_index, protocol_parameters.committable_age_range())? + // Time in which no address can unlock the output because of an expiration unlock condition + .ok_or(Error::ExpirationDeadzone)?; // Check if we already added an [Unlock] for this address - match block_indexes.get(&input_address) { + match block_indexes.get(&required_address) { // If we already have an [Unlock] for this address, add a [Unlock] based on the address type - Some(block_index) => match input_address { + Some(block_index) => match required_address { Address::Ed25519(_ed25519) => { merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?)); } @@ -545,15 +558,17 @@ fn merge_unlocks( // We can only sign ed25519 addresses and block_indexes needs to contain the account or nft // address already at this point, because the reference index needs to be lower // than the current block index - if !input_address.is_ed25519() { - return Err(Error::MissingInputWithEd25519Address); + match &required_address { + Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => {} + Address::Restricted(restricted) if restricted.address().is_ed25519() => {} + _ => Err(Error::MissingInputWithEd25519Address)?, } let unlock = unlocks.next().ok_or(Error::MissingInputWithEd25519Address)?; if let Unlock::Signature(signature_unlock) = &unlock { let Signature::Ed25519(ed25519_signature) = signature_unlock.signature(); - let ed25519_address = match input_address { + let ed25519_address = match required_address { Address::Ed25519(ed25519_address) => ed25519_address, _ => return Err(Error::MissingInputWithEd25519Address), }; @@ -564,7 +579,7 @@ fn merge_unlocks( // Add the ed25519 address to the block_indexes, so it gets referenced if further inputs have // the same address in their unlock condition - block_indexes.insert(input_address, current_block_index); + block_indexes.insert(required_address, current_block_index); } } diff --git a/sdk/src/client/secret/mnemonic.rs b/sdk/src/client/secret/mnemonic.rs index ee705d0c8e..0c460ec640 100644 --- a/sdk/src/client/secret/mnemonic.rs +++ b/sdk/src/client/secret/mnemonic.rs @@ -19,7 +19,8 @@ use super::{GenerateAddressOptions, SecretManage}; use crate::{ client::{api::PreparedTransactionData, Client, Error}, types::block::{ - payload::signed_transaction::SignedTransactionPayload, signature::Ed25519Signature, unlock::Unlocks, + payload::signed_transaction::SignedTransactionPayload, protocol::ProtocolParameters, + signature::Ed25519Signature, unlock::Unlocks, }, }; @@ -117,15 +118,17 @@ impl SecretManage for MnemonicSecretManager { async fn transaction_unlocks( &self, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - super::default_transaction_unlocks(self, prepared_transaction_data).await + super::default_transaction_unlocks(self, prepared_transaction_data, protocol_parameters).await } async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - super::default_sign_transaction(self, prepared_transaction_data).await + super::default_sign_transaction(self, prepared_transaction_data, protocol_parameters).await } } diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index 91a7867860..366e12a3c8 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -59,6 +59,7 @@ use crate::{ core::UnsignedBlock, output::Output, payload::SignedTransactionPayload, + protocol::ProtocolParameters, signature::{Ed25519Signature, Signature}, unlock::{AccountUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, Unlocks}, Block, Error as BlockError, @@ -127,11 +128,13 @@ pub trait SecretManage: Send + Sync { async fn transaction_unlocks( &self, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result; async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result; } @@ -424,19 +427,28 @@ impl SecretManage for SecretManager { async fn transaction_unlocks( &self, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { match self { #[cfg(feature = "stronghold")] - Self::Stronghold(secret_manager) => { - Ok(secret_manager.transaction_unlocks(prepared_transaction_data).await?) - } + Self::Stronghold(secret_manager) => Ok(secret_manager + .transaction_unlocks(prepared_transaction_data, protocol_parameters) + .await?), #[cfg(feature = "ledger_nano")] - Self::LedgerNano(secret_manager) => { - Ok(secret_manager.transaction_unlocks(prepared_transaction_data).await?) + Self::LedgerNano(secret_manager) => Ok(secret_manager + .transaction_unlocks(prepared_transaction_data, protocol_parameters) + .await?), + Self::Mnemonic(secret_manager) => { + secret_manager + .transaction_unlocks(prepared_transaction_data, protocol_parameters) + .await } - Self::Mnemonic(secret_manager) => secret_manager.transaction_unlocks(prepared_transaction_data).await, #[cfg(feature = "private_key_secret_manager")] - Self::PrivateKey(secret_manager) => secret_manager.transaction_unlocks(prepared_transaction_data).await, + Self::PrivateKey(secret_manager) => { + secret_manager + .transaction_unlocks(prepared_transaction_data, protocol_parameters) + .await + } Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -444,15 +456,28 @@ impl SecretManage for SecretManager { async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { match self { #[cfg(feature = "stronghold")] - Self::Stronghold(secret_manager) => Ok(secret_manager.sign_transaction(prepared_transaction_data).await?), + Self::Stronghold(secret_manager) => Ok(secret_manager + .sign_transaction(prepared_transaction_data, protocol_parameters) + .await?), #[cfg(feature = "ledger_nano")] - Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_transaction(prepared_transaction_data).await?), - Self::Mnemonic(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await, + Self::LedgerNano(secret_manager) => Ok(secret_manager + .sign_transaction(prepared_transaction_data, protocol_parameters) + .await?), + Self::Mnemonic(secret_manager) => { + secret_manager + .sign_transaction(prepared_transaction_data, protocol_parameters) + .await + } #[cfg(feature = "private_key_secret_manager")] - Self::PrivateKey(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await, + Self::PrivateKey(secret_manager) => { + secret_manager + .sign_transaction(prepared_transaction_data, protocol_parameters) + .await + } Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -520,6 +545,7 @@ impl SecretManager { pub(crate) async fn default_transaction_unlocks( secret_manager: &M, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> crate::client::Result where crate::client::Error: From, @@ -527,19 +553,24 @@ where let transaction_signing_hash = prepared_transaction_data.transaction.signing_hash(); let mut blocks = Vec::new(); let mut block_indexes = HashMap::::new(); - let slot_index = prepared_transaction_data.transaction.creation_slot(); + let slot_index = prepared_transaction_data + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); // Assuming inputs_data is ordered by address type for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() { // Get the address that is required to unlock the input - let (input_address, _) = input + let required_address = input .output - .required_and_unlocked_address(slot_index, input.output_metadata.output_id())?; + .required_address(slot_index, protocol_parameters.committable_age_range())? + .ok_or(crate::client::Error::ExpirationDeadzone)?; // Check if we already added an [Unlock] for this address - match block_indexes.get(&input_address) { + match block_indexes.get(&required_address) { // If we already have an [Unlock] for this address, add a [Unlock] based on the address type - Some(block_index) => match input_address { + Some(block_index) => match required_address { Address::Ed25519(_ed25519) => { blocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?)); } @@ -552,7 +583,7 @@ where // We can only sign ed25519 addresses and block_indexes needs to contain the account or nft // address already at this point, because the reference index needs to be lower // than the current block index - match &input_address { + match &required_address { Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => {} Address::Restricted(restricted) if restricted.address().is_ed25519() => {} _ => Err(InputSelectionError::MissingInputWithEd25519Address)?, @@ -567,7 +598,7 @@ where // Add the ed25519 address to the block_indexes, so it gets referenced if further inputs have // the same address in their unlock condition - block_indexes.insert(input_address, current_block_index); + block_indexes.insert(required_address, current_block_index); } } @@ -593,13 +624,16 @@ where pub(crate) async fn default_sign_transaction( secret_manager: &M, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> crate::client::Result where crate::client::Error: From, { log::debug!("[sign_transaction] {:?}", prepared_transaction_data); - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, protocol_parameters) + .await?; let PreparedTransactionData { transaction, @@ -610,7 +644,7 @@ where validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&inputs_data, &tx_payload)?; + let conflict = verify_semantic(&inputs_data, &tx_payload, protocol_parameters.clone())?; if let Some(conflict) = conflict { log::debug!("[sign_transaction] conflict: {conflict:?} for {:#?}", tx_payload); diff --git a/sdk/src/client/secret/private_key.rs b/sdk/src/client/secret/private_key.rs index 0be00cd7a3..4a07392e14 100644 --- a/sdk/src/client/secret/private_key.rs +++ b/sdk/src/client/secret/private_key.rs @@ -19,7 +19,8 @@ use super::{GenerateAddressOptions, SecretManage}; use crate::{ client::{api::PreparedTransactionData, Error}, types::block::{ - payload::signed_transaction::SignedTransactionPayload, signature::Ed25519Signature, unlock::Unlocks, + payload::signed_transaction::SignedTransactionPayload, protocol::ProtocolParameters, + signature::Ed25519Signature, unlock::Unlocks, }, }; @@ -76,15 +77,17 @@ impl SecretManage for PrivateKeySecretManager { async fn transaction_unlocks( &self, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - super::default_transaction_unlocks(self, prepared_transaction_data).await + super::default_transaction_unlocks(self, prepared_transaction_data, protocol_parameters).await } async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - super::default_sign_transaction(self, prepared_transaction_data).await + super::default_sign_transaction(self, prepared_transaction_data, protocol_parameters).await } } diff --git a/sdk/src/client/stronghold/secret.rs b/sdk/src/client/stronghold/secret.rs index 773068d8d8..8878069aa9 100644 --- a/sdk/src/client/stronghold/secret.rs +++ b/sdk/src/client/stronghold/secret.rs @@ -35,7 +35,8 @@ use crate::{ stronghold::Error, }, types::block::{ - payload::signed_transaction::SignedTransactionPayload, signature::Ed25519Signature, unlock::Unlocks, + payload::signed_transaction::SignedTransactionPayload, protocol::ProtocolParameters, + signature::Ed25519Signature, unlock::Unlocks, }, }; @@ -276,15 +277,17 @@ impl SecretManage for StrongholdAdapter { async fn transaction_unlocks( &self, prepared_transaction_data: &PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - crate::client::secret::default_transaction_unlocks(self, prepared_transaction_data).await + crate::client::secret::default_transaction_unlocks(self, prepared_transaction_data, protocol_parameters).await } async fn sign_transaction( &self, prepared_transaction_data: PreparedTransactionData, + protocol_parameters: &ProtocolParameters, ) -> Result { - crate::client::secret::default_sign_transaction(self, prepared_transaction_data).await + crate::client::secret::default_sign_transaction(self, prepared_transaction_data, protocol_parameters).await } } diff --git a/sdk/src/types/block/context_input/commitment.rs b/sdk/src/types/block/context_input/commitment.rs index 30aa10699f..cbe5d5e33c 100644 --- a/sdk/src/types/block/context_input/commitment.rs +++ b/sdk/src/types/block/context_input/commitment.rs @@ -5,7 +5,7 @@ use derive_more::{Display, From}; use crate::types::block::{ protocol::{WorkScore, WorkScoreParameters}, - slot::SlotCommitmentId, + slot::{SlotCommitmentId, SlotIndex}, }; /// A Commitment Context Input references a commitment to a certain slot. @@ -21,10 +21,15 @@ impl CommitmentContextInput { Self(commitment_id) } - /// Returns the commitment id of the [`CommitmentContextInput`]. - pub fn commitment_id(&self) -> SlotCommitmentId { + /// Returns the slot commitment id of the [`CommitmentContextInput`]. + pub fn slot_commitment_id(&self) -> SlotCommitmentId { self.0 } + + /// Returns the slot index of the [`CommitmentContextInput`]. + pub fn slot_index(&self) -> SlotIndex { + self.0.slot_index() + } } impl WorkScore for CommitmentContextInput { @@ -52,7 +57,7 @@ mod dto { fn from(value: &CommitmentContextInput) -> Self { Self { kind: CommitmentContextInput::KIND, - commitment_id: value.commitment_id(), + commitment_id: value.slot_commitment_id(), } } } diff --git a/sdk/src/types/block/context_input/mod.rs b/sdk/src/types/block/context_input/mod.rs index 4d8ee73af6..adabad9aee 100644 --- a/sdk/src/types/block/context_input/mod.rs +++ b/sdk/src/types/block/context_input/mod.rs @@ -92,7 +92,7 @@ mod tests { .unwrap(); assert!(commitment.is_commitment()); assert_eq!( - commitment.as_commitment().commitment_id().to_string(), + commitment.as_commitment().slot_commitment_id().to_string(), "0xedf5f572c58ddf4b4f9567d82bf96689cc68b730df796d822b4b9fb643f5efda4f9567d8" ); diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index 76698b2943..ee8d5b44c8 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -154,6 +154,7 @@ pub enum Error { MissingAddressUnlockCondition, MissingGovernorUnlockCondition, MissingStateControllerUnlockCondition, + MissingSlotIndex, NativeTokensNotUniqueSorted, NativeTokensNullAmount, NativeTokensOverflow, @@ -367,6 +368,7 @@ impl fmt::Display for Error { Self::MissingAddressUnlockCondition => write!(f, "missing address unlock condition"), Self::MissingGovernorUnlockCondition => write!(f, "missing governor unlock condition"), Self::MissingStateControllerUnlockCondition => write!(f, "missing state controller unlock condition"), + Self::MissingSlotIndex => write!(f, "missing slot index"), Self::NativeTokensNotUniqueSorted => write!(f, "native tokens are not unique and/or sorted"), Self::NativeTokensNullAmount => write!(f, "native tokens null amount"), Self::NativeTokensOverflow => write!(f, "native tokens overflow"), diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 63ef94ea6e..0cb82fbbde 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -383,7 +383,14 @@ impl AccountOutput { context: &mut SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { self.unlock_conditions() - .locked_address(self.address(), context.transaction.creation_slot()) + .locked_address( + self.address(), + None, + context.protocol_parameters.committable_age_range(), + ) + // Safe to unwrap, AccountOutput can't have an expiration unlock condition. + .unwrap() + .unwrap() .unlock(unlock, context)?; let account_id = if self.account_id().is_null() { diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 115ab33f72..4390b562ad 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use alloc::{collections::BTreeSet, vec::Vec}; +use alloc::collections::BTreeSet; use hashbrown::HashMap; use packable::{ @@ -653,6 +653,7 @@ fn verify_unlock_conditions(unlock_conditions: &UnlockConditions, anchor_id: &An #[cfg(feature = "serde")] mod dto { + use alloc::vec::Vec; use serde::{Deserialize, Serialize}; diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 79230880d5..9444a6d979 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -319,8 +319,21 @@ impl BasicOutput { unlock: &Unlock, context: &mut SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { + let slot_index = context + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + self.unlock_conditions() - .locked_address(self.address(), context.transaction.creation_slot()) + .locked_address( + self.address(), + slot_index, + context.protocol_parameters.committable_age_range(), + ) + .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? + // because of expiration the input can't be unlocked at this time + .ok_or(TransactionFailureReason::SemanticValidationFailed)? .unlock(unlock, context) } diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index 313393a005..af6d927762 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -324,7 +324,14 @@ impl DelegationOutput { context: &mut SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { self.unlock_conditions() - .locked_address(self.address(), context.transaction.creation_slot()) + .locked_address( + self.address(), + None, + context.protocol_parameters.committable_age_range(), + ) + // Safe to unwrap, DelegationOutput can't have an expiration unlock condition. + .unwrap() + .unwrap() .unlock(unlock, context) } diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index cb44a711ca..d4f41e337a 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -54,7 +54,7 @@ pub(crate) use self::{ }; use crate::types::block::{ address::Address, - protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, + protocol::{CommittableAgeRange, ProtocolParameters, WorkScore, WorkScoreParameters}, semantic::SemanticValidationContext, slot::SlotIndex, Error, @@ -269,46 +269,26 @@ impl Output { crate::def_is_as_opt!(Output: Basic, Account, Foundry, Nft, Delegation, Anchor); - /// Returns the address that is required to unlock this [`Output`] and the account or nft address that gets - /// unlocked by it, if it's an account or nft. - /// If no `account_transition` has been provided, assumes a state transition. - pub fn required_and_unlocked_address( + /// Returns the address that is required to unlock this [`Output`]. + pub fn required_address( &self, - slot_index: SlotIndex, - output_id: &OutputId, - ) -> Result<(Address, Option
), Error> { - match self { - Self::Basic(output) => Ok(( - output - .unlock_conditions() - .locked_address(output.address(), slot_index) - .clone(), - None, - )), - Self::Account(output) => Ok(( - output - .unlock_conditions() - .locked_address(output.address(), slot_index) - .clone(), - Some(Address::Account(output.account_address(output_id))), - )), - Self::Anchor(_) => Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), - Self::Foundry(output) => Ok((Address::Account(*output.account_address()), None)), - Self::Nft(output) => Ok(( - output - .unlock_conditions() - .locked_address(output.address(), slot_index) - .clone(), - Some(Address::Nft(output.nft_address(output_id))), - )), - Self::Delegation(output) => Ok(( - output - .unlock_conditions() - .locked_address(output.address(), slot_index) - .clone(), - None, - )), - } + slot_index: impl Into>, + committable_age_range: CommittableAgeRange, + ) -> Result, Error> { + Ok(match self { + Self::Basic(output) => output + .unlock_conditions() + .locked_address(output.address(), slot_index, committable_age_range)? + .cloned(), + Self::Account(output) => Some(output.address().clone()), + Self::Anchor(_) => return Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), + Self::Foundry(output) => Some(Address::Account(*output.account_address())), + Self::Nft(output) => output + .unlock_conditions() + .locked_address(output.address(), slot_index, committable_age_range)? + .cloned(), + Self::Delegation(output) => Some(output.address().clone()), + }) } /// diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index ad041567d5..5a51ca072f 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -414,8 +414,21 @@ impl NftOutput { unlock: &Unlock, context: &mut SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { + let slot_index = context + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + self.unlock_conditions() - .locked_address(self.address(), context.transaction.creation_slot()) + .locked_address( + self.address(), + slot_index, + context.protocol_parameters.committable_age_range(), + ) + .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? + // because of expiration the input can't be unlocked at this time + .ok_or(TransactionFailureReason::SemanticValidationFailed)? .unlock(unlock, context)?; let nft_id = if self.nft_id().is_null() { diff --git a/sdk/src/types/block/output/unlock_condition/expiration.rs b/sdk/src/types/block/output/unlock_condition/expiration.rs index 5cb4b1a539..5a81a2bf24 100644 --- a/sdk/src/types/block/output/unlock_condition/expiration.rs +++ b/sdk/src/types/block/output/unlock_condition/expiration.rs @@ -6,6 +6,7 @@ use derive_more::From; use crate::types::block::{ address::Address, output::{StorageScore, StorageScoreParameters}, + protocol::CommittableAgeRange, slot::SlotIndex, Error, }; @@ -53,14 +54,38 @@ impl ExpirationUnlockCondition { self.slot_index } - /// Returns the return address if the condition has expired. - pub fn return_address_expired(&self, slot_index: SlotIndex) -> Option<&Address> { - if slot_index >= self.slot_index() { - Some(&self.return_address) + /// Checks whether the expiration is expired. If None is returned, then expiration is in the deadzone where it can't + /// be unlocked. + pub fn is_expired( + &self, + slot_index: impl Into, + committable_age_range: CommittableAgeRange, + ) -> Option { + let slot_index = slot_index.into(); + + if self.slot_index() > (slot_index + committable_age_range.max) { + Some(false) + } else if self.slot_index() <= (slot_index + committable_age_range.min) { + Some(true) } else { None } } + + /// Returns the address that can unlock the output. If there is an expiration unlock condition, then there is a + /// small interval around the expiration slot index in which no address can unlock the output. + pub fn return_address_expired<'a>( + &'a self, + address: &'a Address, + slot_index: impl Into, + committable_age_range: CommittableAgeRange, + ) -> Option<&'a Address> { + self.is_expired(slot_index, committable_age_range).map( + |expired| { + if expired { &self.return_address } else { address } + }, + ) + } } impl StorageScore for ExpirationUnlockCondition { diff --git a/sdk/src/types/block/output/unlock_condition/mod.rs b/sdk/src/types/block/output/unlock_condition/mod.rs index 6407184f94..6b2bc79ed5 100644 --- a/sdk/src/types/block/output/unlock_condition/mod.rs +++ b/sdk/src/types/block/output/unlock_condition/mod.rs @@ -26,7 +26,7 @@ pub use self::{ use crate::types::block::{ address::Address, output::{StorageScore, StorageScoreParameters}, - protocol::{ProtocolParameters, WorkScore}, + protocol::{CommittableAgeRange, ProtocolParameters, WorkScore}, slot::SlotIndex, Error, }; @@ -248,6 +248,14 @@ impl UnlockConditions { .map(UnlockCondition::as_timelock) } + /// Checks whether a timelock exists and is still relevant. + #[inline(always)] + pub fn is_timelocked(&self, slot_index: impl Into, min_committable_age: impl Into) -> bool { + self.timelock().map_or(false, |timelock| { + timelock.is_timelocked(slot_index, min_committable_age) + }) + } + /// Gets a reference to an [`ExpirationUnlockCondition`], if any. #[inline(always)] pub fn expiration(&self) -> Option<&ExpirationUnlockCondition> { @@ -255,6 +263,19 @@ impl UnlockConditions { .map(UnlockCondition::as_expiration) } + /// Checks whether an expiration exists and is expired. If None is returned, then expiration is in the deadzone + /// where it can't be unlocked. + #[inline(always)] + pub fn is_expired( + &self, + slot_index: impl Into, + committable_age_range: CommittableAgeRange, + ) -> Option { + self.expiration().map_or(Some(false), |expiration| { + expiration.is_expired(slot_index, committable_age_range) + }) + } + /// Gets a reference to a [`StateControllerAddressUnlockCondition`], if any. #[inline(always)] pub fn state_controller_address(&self) -> Option<&StateControllerAddressUnlockCondition> { @@ -278,28 +299,20 @@ impl UnlockConditions { /// Returns the address to be unlocked. #[inline(always)] - pub fn locked_address<'a>(&'a self, address: &'a Address, slot_index: SlotIndex) -> &'a Address { - self.expiration() - .and_then(|e| e.return_address_expired(slot_index)) - .unwrap_or(address) - } - - /// Returns whether a time lock exists and is still relevant. - #[inline(always)] - pub fn is_time_locked(&self, slot_index: impl Into) -> bool { - let slot_index = slot_index.into(); - - self.timelock() - .map_or(false, |timelock| slot_index < timelock.slot_index()) - } - - /// Returns whether an expiration exists and is expired. - #[inline(always)] - pub fn is_expired(&self, slot_index: impl Into) -> bool { - let slot_index = slot_index.into(); - - self.expiration() - .map_or(false, |expiration| slot_index >= expiration.slot_index()) + pub fn locked_address<'a>( + &'a self, + address: &'a Address, + slot_index: impl Into>, + committable_age_range: CommittableAgeRange, + ) -> Result, Error> { + let address = if let Some(expiration) = self.expiration() { + let slot_index = slot_index.into().ok_or(Error::MissingSlotIndex)?; + expiration.return_address_expired(address, slot_index, committable_age_range) + } else { + Some(address) + }; + + Ok(address) } } diff --git a/sdk/src/types/block/output/unlock_condition/timelock.rs b/sdk/src/types/block/output/unlock_condition/timelock.rs index 0395668986..2b3f50ccd9 100644 --- a/sdk/src/types/block/output/unlock_condition/timelock.rs +++ b/sdk/src/types/block/output/unlock_condition/timelock.rs @@ -29,6 +29,11 @@ impl TimelockUnlockCondition { pub fn slot_index(&self) -> SlotIndex { self.0 } + + /// Checks whether the timelock is still relevant. + pub fn is_timelocked(&self, slot_index: impl Into, min_committable_age: impl Into) -> bool { + (slot_index.into() + min_committable_age.into()) < self.0 + } } impl StorageScore for TimelockUnlockCondition {} diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index 8f8669419d..a6116b09bc 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -376,6 +376,7 @@ fn verify_context_inputs(context_inputs: &[ContextInput]) -> Result<(), Error> { let mut reward_index_set = HashSet::new(); let mut bic_account_id_set = HashSet::new(); + for input in context_inputs.iter() { match input { ContextInput::BlockIssuanceCredit(bic) => { diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index 43a6ebd316..fc3cd29ee7 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -210,6 +210,24 @@ impl ProtocolParameters { pub fn hash(&self) -> ProtocolParametersHash { ProtocolParametersHash::new(Blake2b256::digest(self.pack_to_vec()).into()) } + + /// Returns the [`CommittableAgeRange`]. + pub fn committable_age_range(&self) -> CommittableAgeRange { + CommittableAgeRange { + min: self.min_committable_age(), + max: self.max_committable_age(), + } + } +} + +/// Defines the age in which a block can be issued. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Packable, CopyGetters)] +pub struct CommittableAgeRange { + /// Minimum age relative to the accepted tangle time slot index that a slot can be committed. + pub min: u32, + /// Maximum age for a slot commitment to be included in a block relative to the slot index of the block issuing + /// time. + pub max: u32, } /// Defines the parameters used to calculate the Reference Mana Cost (RMC). diff --git a/sdk/src/types/block/semantic.rs b/sdk/src/types/block/semantic.rs index d233c4ec02..bdc896f8cc 100644 --- a/sdk/src/types/block/semantic.rs +++ b/sdk/src/types/block/semantic.rs @@ -14,6 +14,7 @@ use crate::types::block::{ UnlockCondition, }, payload::signed_transaction::{Transaction, TransactionCapabilityFlag, TransactionSigningHash}, + protocol::ProtocolParameters, unlock::Unlock, Error, }; @@ -206,6 +207,7 @@ pub struct SemanticValidationContext<'a> { pub(crate) unlocked_addresses: HashSet
, pub(crate) storage_deposit_returns: HashMap, pub(crate) simple_deposits: HashMap, + pub(crate) protocol_parameters: ProtocolParameters, } impl<'a> SemanticValidationContext<'a> { @@ -214,6 +216,7 @@ impl<'a> SemanticValidationContext<'a> { transaction: &'a Transaction, inputs: &'a [(&'a OutputId, &'a Output)], unlocks: Option<&'a [Unlock]>, + protocol_parameters: ProtocolParameters, ) -> Self { let transaction_id = transaction.id(); let input_chains = inputs @@ -258,6 +261,7 @@ impl<'a> SemanticValidationContext<'a> { unlocked_addresses: HashSet::new(), storage_deposit_returns: HashMap::new(), simple_deposits: HashMap::new(), + protocol_parameters, } } @@ -279,20 +283,42 @@ impl<'a> SemanticValidationContext<'a> { Output::Delegation(output) => (output.amount(), 0, None, output.unlock_conditions()), }; - if unlock_conditions.is_time_locked(self.transaction.creation_slot()) { - return Ok(Some(TransactionFailureReason::TimelockNotExpired)); - } + let commitment_slot_index = self + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); - if !unlock_conditions.is_expired(self.transaction.creation_slot()) { - if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() { - let amount = self - .storage_deposit_returns - .entry(storage_deposit_return.return_address().clone()) - .or_default(); + if let Some(timelock) = unlock_conditions.timelock() { + if let Some(commitment_slot_index) = commitment_slot_index { + if timelock.is_timelocked(commitment_slot_index, self.protocol_parameters.min_committable_age()) { + return Ok(Some(TransactionFailureReason::TimelockNotExpired)); + } + } else { + // Missing CommitmentContextInput + return Ok(Some(TransactionFailureReason::InvalidCommitmentContextInput)); + } + } - *amount = amount - .checked_add(storage_deposit_return.amount()) - .ok_or(Error::StorageDepositReturnOverflow)?; + if let Some(expiration) = unlock_conditions.expiration() { + if let Some(commitment_slot_index) = commitment_slot_index { + if expiration.is_expired(commitment_slot_index, self.protocol_parameters.committable_age_range()) + == Some(false) + { + if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() { + let amount = self + .storage_deposit_returns + .entry(storage_deposit_return.return_address().clone()) + .or_default(); + + *amount = amount + .checked_add(storage_deposit_return.amount()) + .ok_or(Error::StorageDepositReturnOverflow)?; + } + } + } else { + // Missing CommitmentContextInput + return Ok(Some(TransactionFailureReason::InvalidCommitmentContextInput)); } } diff --git a/sdk/src/wallet/operations/balance.rs b/sdk/src/wallet/operations/balance.rs index d3bcbdc9de..8723878780 100644 --- a/sdk/src/wallet/operations/balance.rs +++ b/sdk/src/wallet/operations/balance.rs @@ -35,9 +35,11 @@ where // addresses_with_unspent_outputs: impl Iterator + Send, wallet_data: &WalletData, ) -> Result { - let network_id = self.client().get_network_id().await?; - let storage_score_params = self.client().get_storage_score_parameters().await?; + let protocol_parameters = self.client().get_protocol_parameters().await?; let slot_index = self.client().get_slot_index().await?; + let network_id = protocol_parameters.network_id(); + let storage_score_params = protocol_parameters.storage_score_parameters(); + let mut balance = Balance::default(); let mut total_storage_cost = 0; let mut total_native_tokens = NativeTokensBuilder::default(); @@ -45,9 +47,7 @@ where #[cfg(feature = "participation")] let voting_output = wallet_data.get_voting_output()?; - let wallet_address = self.address().await.into_inner(); - - let claimable_outputs = wallet_data.claimable_outputs(OutputsToClaim::All, slot_index)?; + let claimable_outputs = wallet_data.claimable_outputs(OutputsToClaim::All, slot_index, &protocol_parameters)?; #[cfg(feature = "participation")] { @@ -164,6 +164,7 @@ where wallet_data.address.inner(), output, slot_index, + protocol_parameters.committable_age_range(), ); if output_can_be_unlocked_now_and_in_future { @@ -176,7 +177,7 @@ where .map_or_else( || output.amount(), |sdr| { - if &wallet_address == sdr.return_address() { + if wallet_data.address.inner() == sdr.return_address() { // sending to ourself, we get the full amount output.amount() } else { diff --git a/sdk/src/wallet/operations/helpers/time.rs b/sdk/src/wallet/operations/helpers/time.rs index ebd3675688..ac85ce9a8e 100644 --- a/sdk/src/wallet/operations/helpers/time.rs +++ b/sdk/src/wallet/operations/helpers/time.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - types::block::{address::Address, output::Output, slot::SlotIndex}, + types::block::{address::Address, output::Output, protocol::CommittableAgeRange, slot::SlotIndex}, wallet::types::OutputData, }; @@ -10,40 +10,48 @@ use crate::{ pub(crate) fn can_output_be_unlocked_now( wallet_address: &Address, output_data: &OutputData, - slot_index: SlotIndex, + slot_index: impl Into + Copy, + committable_age_range: CommittableAgeRange, ) -> crate::wallet::Result { if let Some(unlock_conditions) = output_data.output.unlock_conditions() { - if unlock_conditions.is_time_locked(slot_index) { + if unlock_conditions.is_timelocked(slot_index, committable_age_range.min) { return Ok(false); } } - let (required_unlock_address, _unlocked_account_or_nft_address) = output_data + let required_address = output_data .output - .required_and_unlocked_address(slot_index, &output_data.output_id)?; + .required_address(slot_index.into(), committable_age_range)?; - Ok(wallet_address == &required_unlock_address) + // In case of `None` the output can currently not be unlocked because of expiration unlock condition + Ok(required_address.map_or_else(|| false, |required_address| wallet_address == &required_address)) } -// Check if an output can be unlocked by one of the account addresses at the current time and at any +// Check if an output can be unlocked by the wallet address at the current time and at any // point in the future pub(crate) fn can_output_be_unlocked_forever_from_now_on( wallet_address: &Address, output: &Output, - slot_index: SlotIndex, + slot_index: impl Into + Copy, + committable_age_range: CommittableAgeRange, ) -> bool { if let Some(unlock_conditions) = output.unlock_conditions() { - if unlock_conditions.is_time_locked(slot_index) { + if unlock_conditions.is_timelocked(slot_index, committable_age_range.min) { return false; } // If there is an expiration unlock condition, we can only unlock it forever from now on, if it's expired and - // the return address belongs to the account + // the return address belongs to the wallet if let Some(expiration) = unlock_conditions.expiration() { - if let Some(return_address) = expiration.return_address_expired(slot_index) { - if wallet_address != return_address { + if let Some(address) = expiration.return_address_expired( + // Safe to unwrap, if there is an expiration, then there also needs to be an address unlock condition + unlock_conditions.address().unwrap().address(), + slot_index, + committable_age_range, + ) { + if address != expiration.return_address() || address != wallet_address { return false; - }; + } } else { return false; } diff --git a/sdk/src/wallet/operations/output_claiming.rs b/sdk/src/wallet/operations/output_claiming.rs index fa64145091..9d74e32abf 100644 --- a/sdk/src/wallet/operations/output_claiming.rs +++ b/sdk/src/wallet/operations/output_claiming.rs @@ -13,6 +13,7 @@ use crate::{ unlock_condition::{AddressUnlockCondition, StorageDepositReturnUnlockCondition}, BasicOutput, BasicOutputBuilder, NativeTokensBuilder, NftOutputBuilder, Output, OutputId, }, + protocol::ProtocolParameters, slot::SlotIndex, }, wallet::{ @@ -45,6 +46,7 @@ impl WalletData { &self, outputs_to_claim: OutputsToClaim, slot_index: SlotIndex, + protocol_parameters: &ProtocolParameters, ) -> crate::wallet::Result> { log::debug!("[OUTPUT_CLAIMING] claimable_outputs"); @@ -68,18 +70,24 @@ impl WalletData { self.address.inner(), output_data, slot_index, + protocol_parameters.committable_age_range(), )? { match outputs_to_claim { OutputsToClaim::MicroTransactions => { if let Some(sdr) = unlock_conditions.storage_deposit_return() { // If expired, it's not a micro transaction anymore - if unlock_conditions.is_expired(slot_index) { - continue; - } - // Only micro transaction if not the same - if sdr.amount() != output_data.output.amount() { - output_ids_to_claim.insert(output_data.output_id); + match unlock_conditions + .is_expired(slot_index, protocol_parameters.committable_age_range()) + { + Some(false) => { + // Only micro transaction if not the same amount needs to be returned + // (resulting in 0 amount to claim) + if sdr.amount() != output_data.output.amount() { + output_ids_to_claim.insert(output_data.output_id); + } + } + _ => continue, } } } @@ -96,7 +104,9 @@ impl WalletData { } OutputsToClaim::Amount => { let mut claimable_amount = output_data.output.amount(); - if !unlock_conditions.is_expired(slot_index) { + if unlock_conditions.is_expired(slot_index, protocol_parameters.committable_age_range()) + == Some(false) + { claimable_amount -= unlock_conditions .storage_deposit_return() .map(|s| s.amount()) @@ -137,8 +147,9 @@ where let wallet_data = self.data().await; let slot_index = self.client().get_slot_index().await?; + let protocol_parameters = self.client().get_protocol_parameters().await?; - wallet_data.claimable_outputs(outputs_to_claim, slot_index) + wallet_data.claimable_outputs(outputs_to_claim, slot_index, &protocol_parameters) } /// Get basic outputs that have only one unlock condition which is [AddressUnlockCondition], so they can be used as diff --git a/sdk/src/wallet/operations/output_consolidation.rs b/sdk/src/wallet/operations/output_consolidation.rs index fdb29b815b..55ebe9370b 100644 --- a/sdk/src/wallet/operations/output_consolidation.rs +++ b/sdk/src/wallet/operations/output_consolidation.rs @@ -70,30 +70,39 @@ where crate::wallet::Error: From, crate::client::Error: From, { - fn should_consolidate_output( + async fn should_consolidate_output( &self, output_data: &OutputData, slot_index: SlotIndex, wallet_address: &Address, ) -> Result { Ok(if let Output::Basic(basic_output) = &output_data.output { + let protocol_parameters = self.client().get_protocol_parameters().await?; let unlock_conditions = basic_output.unlock_conditions(); - let is_time_locked = unlock_conditions.is_time_locked(slot_index); + let is_time_locked = unlock_conditions.is_timelocked(slot_index, protocol_parameters.min_committable_age()); if is_time_locked { // If the output is timelocked, then it cannot be consolidated. return Ok(false); } let has_storage_deposit_return = unlock_conditions.storage_deposit_return().is_some(); - let has_expiration = unlock_conditions.expiration().is_some(); - let is_expired = unlock_conditions.is_expired(slot_index); - if has_storage_deposit_return && (!has_expiration || !is_expired) { + let is_expired = unlock_conditions.is_expired(slot_index, protocol_parameters.committable_age_range()); + if is_expired.is_none() { + // If the output is in a deadzone because of expiration, then it cannot be consolidated. + return Ok(false); + } + if has_storage_deposit_return && is_expired == Some(false) { // If the output has not expired and must return a storage deposit, then it cannot be consolidated. return Ok(false); } - can_output_be_unlocked_now(wallet_address, output_data, slot_index)? + can_output_be_unlocked_now( + wallet_address, + output_data, + slot_index, + protocol_parameters.committable_age_range(), + )? } else { false }) @@ -139,7 +148,9 @@ where } let is_locked_output = wallet_data.locked_outputs.contains(output_id); - let should_consolidate_output = self.should_consolidate_output(output_data, slot_index, wallet_address)?; + let should_consolidate_output = self + .should_consolidate_output(output_data, slot_index, wallet_address) + .await?; if !is_locked_output && should_consolidate_output { outputs_to_consolidate.push(output_data.clone()); } diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index 71e437b38c..5422f2a922 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -13,6 +13,7 @@ use crate::{ types::block::{ address::Address, output::{Output, OutputId}, + protocol::CommittableAgeRange, slot::SlotIndex, }, wallet::{ @@ -70,6 +71,7 @@ where &wallet_data, wallet_data.unspent_outputs.values(), slot_index, + protocol_parameters.committable_age_range(), custom_inputs.as_ref(), mandatory_inputs.as_ref(), )?; @@ -196,29 +198,15 @@ where } } -/// Filter available outputs to only include outputs that don't have unlock conditions, that could create -/// conflicting transactions or need a new output for the storage deposit return -/// Also only include Account, Nft and Foundry outputs, if a corresponding output with the same id exists in the output, -/// so they don't get burned -/// +/// Filter available outputs to only include outputs that can be unlocked forever from this moment. /// Note: this is only for the default input selection, it's still possible to send these outputs by using /// `claim_outputs` or providing their OutputId's in the custom_inputs -/// -/// Some examples for which outputs should be included in the inputs to select from: -/// | Unlock conditions | Include in inputs | -/// | --------------------------------------------------- | ----------------- | -/// | [Address] | yes | -/// | [Address, expired Timelock] | yes | -/// | [Address, not expired Timelock, ...] | no | -/// | [Address, expired Expiration, ...] | yes | -/// | [Address, not expired Expiration, ...] | no | -/// | [Address, StorageDepositReturn, ...] | no | -/// | [Address, StorageDepositReturn, expired Expiration] | yes | #[allow(clippy::too_many_arguments)] fn filter_inputs( wallet_data: &WalletData, available_outputs: Values<'_, OutputId, OutputData>, - slot_index: SlotIndex, + slot_index: impl Into + Copy, + committable_age_range: CommittableAgeRange, custom_inputs: Option<&HashSet>, mandatory_inputs: Option<&HashSet>, ) -> crate::wallet::Result> { @@ -238,6 +226,7 @@ fn filter_inputs( &wallet_data.address.inner, &output_data.output, slot_index, + committable_age_range, ); // Outputs that could get unlocked in the future will not be included @@ -246,7 +235,7 @@ fn filter_inputs( } } - if let Some(available_input) = output_data.input_signing_data(wallet_data, slot_index)? { + if let Some(available_input) = output_data.input_signing_data(wallet_data, slot_index, committable_age_range)? { available_outputs_signing_data.push(available_input); } } diff --git a/sdk/src/wallet/operations/transaction/mod.rs b/sdk/src/wallet/operations/transaction/mod.rs index 84fb9a78b2..fcf6a967e3 100644 --- a/sdk/src/wallet/operations/transaction/mod.rs +++ b/sdk/src/wallet/operations/transaction/mod.rs @@ -132,7 +132,11 @@ where let options = options.into(); // Validate transaction before sending and storing it - let conflict = verify_semantic(&signed_transaction_data.inputs_data, &signed_transaction_data.payload)?; + let conflict = verify_semantic( + &signed_transaction_data.inputs_data, + &signed_transaction_data.payload, + self.client().get_protocol_parameters().await?, + )?; if let Some(conflict) = conflict { log::debug!( diff --git a/sdk/src/wallet/operations/transaction/sign_transaction.rs b/sdk/src/wallet/operations/transaction/sign_transaction.rs index 2421bda089..8b27321923 100644 --- a/sdk/src/wallet/operations/transaction/sign_transaction.rs +++ b/sdk/src/wallet/operations/transaction/sign_transaction.rs @@ -74,11 +74,12 @@ where } } + let protocol_parameters = self.client().get_protocol_parameters().await?; let unlocks = match self .secret_manager .read() .await - .transaction_unlocks(prepared_transaction_data) + .transaction_unlocks(prepared_transaction_data, &protocol_parameters) .await { Ok(res) => res, diff --git a/sdk/src/wallet/types/mod.rs b/sdk/src/wallet/types/mod.rs index 2fdc2dd5be..39c2c5721d 100644 --- a/sdk/src/wallet/types/mod.rs +++ b/sdk/src/wallet/types/mod.rs @@ -24,7 +24,7 @@ use crate::{ address::Address, output::{Output, OutputId, OutputMetadata}, payload::signed_transaction::{dto::SignedTransactionPayloadDto, SignedTransactionPayload, TransactionId}, - protocol::ProtocolParameters, + protocol::{CommittableAgeRange, ProtocolParameters}, slot::SlotIndex, BlockId, Error as BlockError, }, @@ -59,15 +59,18 @@ impl OutputData { pub fn input_signing_data( &self, wallet_data: &WalletData, - slot_index: SlotIndex, + slot_index: impl Into, + committable_age_range: CommittableAgeRange, ) -> crate::wallet::Result> { - let (unlock_address, _unlocked_account_or_nft_address) = - self.output.required_and_unlocked_address(slot_index, &self.output_id)?; + let required_address = self + .output + .required_address(slot_index.into(), committable_age_range)? + .ok_or(crate::client::Error::ExpirationDeadzone)?; - let chain = if unlock_address == self.address { + let chain = if required_address == self.address { self.chain - } else if let Address::Ed25519(_) = unlock_address { - if wallet_data.address.inner() == &unlock_address { + } else if required_address.is_ed25519() { + if wallet_data.address.inner() == &required_address { // TODO #1279: do we need a check to make sure that `wallet_data.address` and `wallet_data.bip_path` are // never conflicting? wallet_data.bip_path diff --git a/sdk/tests/client/signing/account.rs b/sdk/tests/client/signing/account.rs index e88faab90d..30eafbb97c 100644 --- a/sdk/tests/client/signing/account.rs +++ b/sdk/tests/client/signing/account.rs @@ -77,7 +77,9 @@ async fn sign_account_state_transition() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -86,7 +88,7 @@ async fn sign_account_state_transition() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); @@ -148,7 +150,9 @@ async fn account_reference_unlocks() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 3); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -169,7 +173,7 @@ async fn account_reference_unlocks() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); diff --git a/sdk/tests/client/signing/basic.rs b/sdk/tests/client/signing/basic.rs index 31a60ba833..ba9a2eb5b0 100644 --- a/sdk/tests/client/signing/basic.rs +++ b/sdk/tests/client/signing/basic.rs @@ -79,7 +79,9 @@ async fn single_ed25519_unlock() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -88,7 +90,7 @@ async fn single_ed25519_unlock() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); @@ -174,7 +176,9 @@ async fn ed25519_reference_unlocks() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 3); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -195,7 +199,7 @@ async fn ed25519_reference_unlocks() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); @@ -280,7 +284,9 @@ async fn two_signature_unlocks() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 2); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -290,7 +296,7 @@ async fn two_signature_unlocks() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); diff --git a/sdk/tests/client/signing/mod.rs b/sdk/tests/client/signing/mod.rs index c5efd789c9..a5149ec73a 100644 --- a/sdk/tests/client/signing/mod.rs +++ b/sdk/tests/client/signing/mod.rs @@ -19,13 +19,14 @@ use iota_sdk::{ Result, }, types::block::{ - address::{AccountAddress, Address, NftAddress}, + address::{AccountAddress, Address, NftAddress, ToBech32Ext}, + context_input::{CommitmentContextInput, ContextInput}, input::{Input, UtxoInput}, output::{AccountId, NftId}, payload::{signed_transaction::Transaction, SignedTransactionPayload}, protocol::protocol_parameters, rand::mana::rand_mana_allotment, - slot::SlotIndex, + slot::{SlotCommitmentId, SlotIndex}, unlock::{SignatureUnlock, Unlock}, }, }; @@ -201,7 +202,7 @@ async fn all_combined() -> Result<()> { Nft(1_000_000, nft_id_4, ed25519_0.clone(), None, None, None, None, None), ]); - let slot_index = SlotIndex::from(100); + let slot_index = SlotIndex::from(90); let selected = InputSelection::new( inputs.clone(), @@ -214,6 +215,9 @@ async fn all_combined() -> Result<()> { .unwrap(); let transaction = Transaction::builder(protocol_parameters.network_id()) + .with_context_inputs(vec![ContextInput::Commitment(CommitmentContextInput::new( + SlotCommitmentId::from_str("0x000000000000000000000000000000000000000000000000000000000000000064000000")?, + ))]) .with_inputs( selected .inputs @@ -232,7 +236,9 @@ async fn all_combined() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 15); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -316,7 +322,7 @@ async fn all_combined() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); diff --git a/sdk/tests/client/signing/nft.rs b/sdk/tests/client/signing/nft.rs index e36ea3c16d..65e0470d18 100644 --- a/sdk/tests/client/signing/nft.rs +++ b/sdk/tests/client/signing/nft.rs @@ -87,7 +87,9 @@ async fn nft_reference_unlocks() -> Result<()> { remainder: None, }; - let unlocks = secret_manager.transaction_unlocks(&prepared_transaction_data).await?; + let unlocks = secret_manager + .transaction_unlocks(&prepared_transaction_data, &protocol_parameters) + .await?; assert_eq!(unlocks.len(), 3); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); @@ -108,7 +110,7 @@ async fn nft_reference_unlocks() -> Result<()> { validate_signed_transaction_payload_length(&tx_payload)?; - let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload)?; + let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, protocol_parameters)?; if let Some(conflict) = conflict { panic!("{conflict:?}, with {tx_payload:#?}"); diff --git a/sdk/tests/types/slot.rs b/sdk/tests/types/slot.rs index 8b2795816a..3fd5dbda57 100644 --- a/sdk/tests/types/slot.rs +++ b/sdk/tests/types/slot.rs @@ -5,8 +5,8 @@ use iota_sdk::types::block::slot::SlotCommitment; use packable::PackableExt; // #[test] -// fn slot_commitment_id() { -// // Test from https://github.com/iotaledger/tips-draft/blob/tip46/tips/TIP-0046/tip-0046.md#slot-commitment-id-1 +// fn slot_commitment_id_index() { +// Test from https://github.com/iotaledger/tips-draft/blob/tip46/tips/TIP-0046/tip-0046.md#slot-commitment-id-1 // let slot_commitment_json = serde_json::json!({ // "protocolVersion": 3, @@ -30,10 +30,11 @@ use packable::PackableExt; // ] // ); -// let slot_commitment_id = slot_commitment.id().to_string(); +// let slot_commitment_id = slot_commitment.id(); // assert_eq!( -// slot_commitment_id, +// slot_commitment_id.to_string(), // "0x3a73079f3dbf8c1744ae0b020b9767546e32f5bbbf4c6f0233da7b64f16581f80a00000000000000" // ); +// assert_eq!(slot_commitment_id.index(), 10); // } diff --git a/sdk/tests/wallet/output_preparation.rs b/sdk/tests/wallet/output_preparation.rs index 9eccccaae6..f6bc818ffc 100644 --- a/sdk/tests/wallet/output_preparation.rs +++ b/sdk/tests/wallet/output_preparation.rs @@ -7,6 +7,7 @@ use iota_sdk::{ types::block::{ address::{Address, Bech32Address, ToBech32Ext}, output::{BasicOutput, MinimumOutputAmount, NativeToken, NftId, TokenId}, + protocol::CommittableAgeRange, slot::SlotIndex, }, wallet::{Assets, Features, MintNftParams, OutputParams, Result, ReturnStrategy, StorageDeposit, Unlocks}, @@ -352,8 +353,11 @@ async fn output_preparation() -> Result<()> { assert_eq!(sender_feature.address(), issuer_and_sender_address.inner()); // Unlocks let conditions = output.unlock_conditions().unwrap(); - assert!(conditions.is_time_locked(0)); - assert!(conditions.is_expired(2)); + assert!(conditions.is_timelocked(0, 0)); + assert_eq!( + conditions.is_expired(2, CommittableAgeRange { min: 0, max: 0 }), + Some(false) + ); // nft with expiration let output = wallet From 49c946834038486a1648dbb087e6e572a9fe6fa3 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 7 Dec 2023 10:18:44 +0100 Subject: [PATCH 10/10] TIP cleaning nits (#1683) * TIP cleaning * Address nits * Nits --- sdk/src/types/block/address/account.rs | 10 +++++++--- sdk/src/types/block/address/anchor.rs | 10 +++++++--- sdk/src/types/block/address/ed25519.rs | 9 ++++++--- .../types/block/address/implicit_account_creation.rs | 2 +- sdk/src/types/block/address/nft.rs | 11 +++++++---- sdk/src/types/block/mana/parameters.rs | 2 +- sdk/src/types/block/output/account.rs | 8 +++++--- sdk/src/types/block/output/anchor.rs | 1 + sdk/src/types/block/output/basic.rs | 7 +++---- sdk/src/types/block/output/delegation.rs | 2 +- sdk/src/types/block/output/foundry.rs | 2 +- sdk/src/types/block/output/nft.rs | 4 ++-- 12 files changed, 42 insertions(+), 26 deletions(-) diff --git a/sdk/src/types/block/address/account.rs b/sdk/src/types/block/address/account.rs index d144377789..0e075b7a82 100644 --- a/sdk/src/types/block/address/account.rs +++ b/sdk/src/types/block/address/account.rs @@ -10,13 +10,17 @@ use crate::types::block::{ Error, }; -/// An account address. +/// An [`Address`](super::Address) derived from an account ID which can be unlocked by unlocking the corresponding +/// account. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Display, packable::Packable)] #[as_ref(forward)] -pub struct AccountAddress(AccountId); +pub struct AccountAddress( + /// BLAKE2b-256 hash of the Output ID that created the account. + AccountId, +); impl AccountAddress { - /// The [`Address`](crate::types::block::address::Address) kind of an [`AccountAddress`]. + /// The [`Address`](super::Address) kind of an [`AccountAddress`]. pub const KIND: u8 = 8; /// The length of an [`AccountAddress`]. pub const LENGTH: usize = AccountId::LENGTH; diff --git a/sdk/src/types/block/address/anchor.rs b/sdk/src/types/block/address/anchor.rs index 613bd83248..4444602c98 100644 --- a/sdk/src/types/block/address/anchor.rs +++ b/sdk/src/types/block/address/anchor.rs @@ -10,13 +10,17 @@ use crate::types::block::{ Error, }; -/// An anchor address. +/// An [`Address`](super::Address) derived from an anchor ID which can be unlocked by unlocking the corresponding +/// anchor. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Display, packable::Packable)] #[as_ref(forward)] -pub struct AnchorAddress(AnchorId); +pub struct AnchorAddress( + /// BLAKE2b-256 hash of the Output ID that created the anchor. + AnchorId, +); impl AnchorAddress { - /// The [`Address`](crate::types::block::address::Address) kind of an [`AnchorAddress`]. + /// The [`Address`](super::Address) kind of an [`AnchorAddress`]. pub const KIND: u8 = 24; /// The length of an [`AnchorAddress`]. pub const LENGTH: usize = AnchorId::LENGTH; diff --git a/sdk/src/types/block/address/ed25519.rs b/sdk/src/types/block/address/ed25519.rs index 964ecac162..6cd9b572ed 100644 --- a/sdk/src/types/block/address/ed25519.rs +++ b/sdk/src/types/block/address/ed25519.rs @@ -12,13 +12,16 @@ use packable::Packable; use crate::types::block::{output::StorageScore, Error}; -/// An Ed25519 address. +/// An [`Address`](super::Address) derived from an Ed25519 public key. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Packable)] #[as_ref(forward)] -pub struct Ed25519Address([u8; Self::LENGTH]); +pub struct Ed25519Address( + /// BLAKE2b-256 hash of the Ed25519 public key. + [u8; Self::LENGTH], +); impl Ed25519Address { - /// The [`Address`](crate::types::block::address::Address) kind of an [`Ed25519Address`]. + /// The [`Address`](super::Address) kind of an [`Ed25519Address`]. pub const KIND: u8 = 0; /// The length of an [`Ed25519Address`]. pub const LENGTH: usize = PublicKey::LENGTH; diff --git a/sdk/src/types/block/address/implicit_account_creation.rs b/sdk/src/types/block/address/implicit_account_creation.rs index 21e3f3e877..62197989d9 100644 --- a/sdk/src/types/block/address/implicit_account_creation.rs +++ b/sdk/src/types/block/address/implicit_account_creation.rs @@ -17,7 +17,7 @@ use crate::types::block::{ pub struct ImplicitAccountCreationAddress(Ed25519Address); impl ImplicitAccountCreationAddress { - /// The [`Address`](crate::types::block::address::Address) kind of an [`ImplicitAccountCreationAddress`]. + /// The [`Address`](super::Address) kind of an [`ImplicitAccountCreationAddress`]. pub const KIND: u8 = 32; /// The length of an [`ImplicitAccountCreationAddress`]. pub const LENGTH: usize = Ed25519Address::LENGTH; diff --git a/sdk/src/types/block/address/nft.rs b/sdk/src/types/block/address/nft.rs index ff7e4f3201..d3e1b6d14b 100644 --- a/sdk/src/types/block/address/nft.rs +++ b/sdk/src/types/block/address/nft.rs @@ -10,15 +10,18 @@ use crate::types::block::{ Error, }; -/// An NFT address. +/// An [`Address`](super::Address) derived from an NFT ID which can be unlocked by unlocking the corresponding NFT. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Display, packable::Packable)] #[as_ref(forward)] -pub struct NftAddress(NftId); +pub struct NftAddress( + /// BLAKE2b-256 hash of the Output ID that created the NFT. + NftId, +); impl NftAddress { - /// The [`Address`](crate::types::block::address::Address) kind of an NFT address. + /// The [`Address`](super::Address) kind of an [`NftAddress`]. pub const KIND: u8 = 16; - /// The length of a [`NftAddress`]. + /// The length of an [`NftAddress`]. pub const LENGTH: usize = NftId::LENGTH; /// Creates a new [`NftAddress`]. diff --git a/sdk/src/types/block/mana/parameters.rs b/sdk/src/types/block/mana/parameters.rs index 65e8ca8d04..e6d68aa698 100644 --- a/sdk/src/types/block/mana/parameters.rs +++ b/sdk/src/types/block/mana/parameters.rs @@ -27,7 +27,7 @@ pub struct ManaParameters { /// The actual generation rate of Mana is given by generation_rate * 2^(-generation_rate_exponent). pub(crate) generation_rate_exponent: u8, /// A lookup table of epoch index diff to mana decay factor. - /// The actual decay factor is given by decay_factors[epoch_diff] * 2^(-decay_factors_exponent). + /// The actual decay factor is given by decay_factors\[epoch_diff\] * 2^(-decay_factors_exponent). #[packable(unpack_error_with = |_| Error::InvalidManaDecayFactors)] #[cfg_attr(feature = "serde", serde(with = "crate::utils::serde::boxed_slice_prefix"))] #[getset(skip)] diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 0cb82fbbde..dc6bcbe39d 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -267,17 +267,19 @@ impl From<&AccountOutput> for AccountOutputBuilder { /// Describes an account in the ledger that can be controlled by the state and governance controllers. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct AccountOutput { - // Amount of IOTA coins held by the output. + /// Amount of IOTA coins held by the output. amount: u64, + /// Amount of stored Mana held by the output. mana: u64, // Unique identifier of the account. account_id: AccountId, // A counter that denotes the number of foundries created by this account. foundry_counter: u32, + /// Define how the output can be unlocked in a transaction. unlock_conditions: UnlockConditions, - // + /// Features of the output. features: Features, - // + /// Immutable features of the output. immutable_features: Features, } diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 4390b562ad..de5db1b0ab 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -299,6 +299,7 @@ impl From<&AnchorOutput> for AnchorOutputBuilder { pub struct AnchorOutput { /// Amount of IOTA coins held by the output. amount: u64, + /// Amount of stored Mana held by the output. mana: u64, /// Unique identifier of the anchor. anchor_id: AnchorId, diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 9444a6d979..060f2ec022 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -233,9 +233,9 @@ impl From<&BasicOutput> for BasicOutputBuilder { #[packable(unpack_error = Error)] #[packable(unpack_visitor = ProtocolParameters)] pub struct BasicOutput { - /// Amount of IOTA coins to deposit with this output. + /// Amount of IOTA coins held by the output. amount: u64, - /// Amount of stored Mana held by this output. + /// Amount of stored Mana held by the output. mana: u64, /// Define how the output can be unlocked in a transaction. #[packable(verify_with = verify_unlock_conditions_packable)] @@ -248,9 +248,8 @@ pub struct BasicOutput { impl BasicOutput { /// The [`Output`] kind of an [`BasicOutput`]. pub const KIND: u8 = 0; - /// The set of allowed [`UnlockCondition`]s for an [`BasicOutput`]. - const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS + pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN) .union(UnlockConditionFlags::TIMELOCK) .union(UnlockConditionFlags::EXPIRATION); diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index af6d927762..885a5afba5 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -216,7 +216,7 @@ impl From<&DelegationOutput> for DelegationOutputBuilder { #[packable(unpack_error = Error)] #[packable(unpack_visitor = ProtocolParameters)] pub struct DelegationOutput { - /// Amount of IOTA coins to deposit with this output. + /// Amount of IOTA coins held by the output. amount: u64, /// Amount of delegated IOTA coins. delegated_amount: u64, diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index c098ab73bb..497d459969 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -293,7 +293,7 @@ impl From<&FoundryOutput> for FoundryOutputBuilder { /// Describes a foundry output that is controlled by an account. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct FoundryOutput { - /// Amount of IOTA coins to deposit with this output. + /// Amount of IOTA coins held by the output. amount: u64, /// The serial number of the foundry with respect to the controlling account. serial_number: u32, diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 5a51ca072f..b580db81fd 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -303,9 +303,9 @@ impl From<&NftOutput> for NftOutputBuilder { /// Describes an NFT output, a globally unique token with metadata attached. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct NftOutput { - /// Amount of IOTA coins to deposit with this output. + /// Amount of IOTA coins held by the output. amount: u64, - /// Amount of stored Mana held by this output. + /// Amount of stored Mana held by the output. mana: u64, /// Unique identifier of the NFT. nft_id: NftId,