From 82f6556a2ce39d5eb1bdc8fa3cd016403e3e2c35 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:17:04 +0100 Subject: [PATCH] Add /api/core/v3/outputs/{outputId}/full route (#1733) * Add /api/core/v3/outputs/{outputId}/full route * Fix test * Update sdk/src/wallet/types/mod.rs Co-authored-by: Thibault Martinez * Comment OutputResponse * Fix comment * Import format * Move OutputIdProof to block types * Exports imports --------- Co-authored-by: Thibault Martinez Co-authored-by: Thibault Martinez --- sdk/Cargo.toml | 5 ++ .../node_api_core/12_get_output_full.rs | 44 +++++++++ sdk/src/client/node_api/core/mod.rs | 15 ---- sdk/src/client/node_api/core/routes.rs | 10 ++- sdk/src/types/api/core.rs | 9 ++ sdk/src/types/block/output/mod.rs | 32 +++++-- sdk/src/types/block/output/output_id_proof.rs | 90 +++++++++++++++++++ .../operations/syncing/addresses/outputs.rs | 5 +- sdk/src/wallet/operations/syncing/mod.rs | 2 +- sdk/src/wallet/operations/syncing/outputs.rs | 2 + sdk/src/wallet/types/mod.rs | 4 +- sdk/tests/wallet/events.rs | 15 +++- 12 files changed, 205 insertions(+), 28 deletions(-) create mode 100644 sdk/examples/client/node_api_core/12_get_output_full.rs create mode 100644 sdk/src/types/block/output/output_id_proof.rs diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e9bb13942f..250f10bb85 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -453,6 +453,11 @@ name = "node_api_core_get_output_metadata" path = "examples/client/node_api_core/11_get_output_metadata.rs" required-features = ["client"] +[[example]] +name = "node_api_core_get_output_full" +path = "examples/client/node_api_core/12_get_output_full.rs" +required-features = ["client"] + [[example]] name = "node_api_core_get_included_block" path = "examples/client/node_api_core/15_get_included_block.rs" diff --git a/sdk/examples/client/node_api_core/12_get_output_full.rs b/sdk/examples/client/node_api_core/12_get_output_full.rs new file mode 100644 index 0000000000..fe82758ea1 --- /dev/null +++ b/sdk/examples/client/node_api_core/12_get_output_full.rs @@ -0,0 +1,44 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Find an output with its metadata, by its identifier by querying the `/api/core/v3/outputs/{outputId}/full` node +//! endpoint. +//! +//! Make sure to provide a somewhat recent output id to make this example run successfully! +//! +//! Rename `.env.example` to `.env` first, then run the command: +//! ```sh +//! cargo run --release --example node_api_core_get_output_full [NODE URL] +//! ``` + +use iota_sdk::{ + client::{Client, Result}, + types::block::output::OutputId, +}; + +#[tokio::main] +async fn main() -> Result<()> { + // If not provided we use the default node from the `.env` file. + dotenvy::dotenv().ok(); + + // Take the node URL from command line argument or use one from env as default. + let node_url = std::env::args() + .nth(2) + .unwrap_or_else(|| std::env::var("NODE_URL").expect("NODE_URL not set")); + + // Create a node client. + let client = Client::builder().with_node(&node_url)?.finish().await?; + + // Take the output id from the command line, or panic. + let output_id = std::env::args() + .nth(1) + .expect("missing example argument: OUTPUT ID") + .parse::()?; + + // Get the output with its metadata. + let output_with_metadata = client.get_output_with_metadata(&output_id).await?; + + println!("{output_with_metadata:?}"); + + Ok(()) +} diff --git a/sdk/src/client/node_api/core/mod.rs b/sdk/src/client/node_api/core/mod.rs index 7f31339855..e243916590 100644 --- a/sdk/src/client/node_api/core/mod.rs +++ b/sdk/src/client/node_api/core/mod.rs @@ -5,27 +5,12 @@ pub mod routes; -use packable::PackableExt; - use crate::{ client::{node_api::error::Error as NodeApiError, Client, Error, Result}, types::block::output::{Output, OutputId, OutputMetadata, OutputWithMetadata}, }; impl Client { - // Finds output and its metadata by output ID. - /// GET /api/core/v3/outputs/{outputId} - /// + GET /api/core/v3/outputs/{outputId}/metadata - pub async fn get_output_with_metadata(&self, output_id: &OutputId) -> Result { - let output = Output::unpack_verified( - self.get_output_raw(output_id).await?, - &self.get_protocol_parameters().await?, - )?; - let metadata = self.get_output_metadata(output_id).await?; - - Ok(OutputWithMetadata::new(output, metadata)) - } - /// Requests outputs by their output ID in parallel. pub async fn get_outputs(&self, output_ids: &[OutputId]) -> Result> { futures::future::try_join_all(output_ids.iter().map(|id| self.get_output(id))).await diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index aace690693..80c09664dd 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -22,7 +22,7 @@ use crate::{ }, block::{ address::ToBech32Ext, - output::{AccountId, Output, OutputId, OutputMetadata}, + output::{AccountId, Output, OutputId, OutputMetadata, OutputWithMetadata}, payload::signed_transaction::TransactionId, slot::{EpochIndex, SlotCommitment, SlotCommitmentId, SlotIndex}, Block, BlockDto, BlockId, @@ -256,6 +256,14 @@ impl ClientInner { self.get_request(path, None, false, true).await } + /// Finds an output with its metadata by output ID. + /// GET /api/core/v3/outputs/{outputId}/full + pub async fn get_output_with_metadata(&self, output_id: &OutputId) -> Result { + let path = &format!("api/core/v3/outputs/{output_id}/full"); + + self.get_request(path, None, false, true).await + } + /// Returns the earliest confirmed block containing the transaction with the given ID. /// GET /api/core/v3/transactions/{transactionId}/included-block pub async fn get_included_block(&self, transaction_id: &TransactionId) -> Result { diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index 34b6472ad8..a6e093e80d 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -528,3 +528,12 @@ pub struct UtxoChangesResponse { pub created_outputs: Vec, pub consumed_outputs: Vec, } + +// TODO use for outputs route https://github.com/iotaledger/iota-sdk/issues/1686 +// /// Contains the generic [`Output`] with associated [`OutputIdProof`]. +// #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct OutputResponse { +// pub output: Output, +// pub output_id_proof: OutputIdProof, +// } diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index d4f41e337a..501a2b93d1 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -7,6 +7,7 @@ mod delegation; mod metadata; mod native_token; mod output_id; +mod output_id_proof; mod state_transition; mod storage_score; mod token_scheme; @@ -41,6 +42,7 @@ pub use self::{ native_token::{NativeToken, NativeTokens, NativeTokensBuilder, TokenId}, nft::{NftId, NftOutput, NftOutputBuilder}, output_id::OutputId, + output_id_proof::{HashableNode, LeafHash, OutputCommitmentProof, OutputIdProof, ValueHash}, state_transition::{StateTransitionError, StateTransitionVerifier}, storage_score::{StorageScore, StorageScoreParameters}, token_scheme::{SimpleTokenScheme, TokenScheme}, @@ -75,17 +77,27 @@ pub enum OutputBuilderAmount { MinimumAmount(StorageScoreParameters), } -/// Contains the generic [`Output`] with associated [`OutputMetadata`]. +/// Contains the generic [`Output`] with associated [`OutputIdProof`] and [`OutputMetadata`]. #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] pub struct OutputWithMetadata { - pub(crate) output: Output, - pub(crate) metadata: OutputMetadata, + pub output: Output, + pub output_id_proof: OutputIdProof, + pub metadata: OutputMetadata, } impl OutputWithMetadata { /// Creates a new [`OutputWithMetadata`]. - pub fn new(output: Output, metadata: OutputMetadata) -> Self { - Self { output, metadata } + pub fn new(output: Output, output_id_proof: OutputIdProof, metadata: OutputMetadata) -> Self { + Self { + output, + output_id_proof, + metadata, + } } /// Returns the [`Output`]. @@ -98,6 +110,16 @@ impl OutputWithMetadata { self.output } + /// Returns the [`OutputIdProof`]. + pub fn output_id_proof(&self) -> &OutputIdProof { + &self.output_id_proof + } + + /// Consumes self and returns the [`OutputIdProof`]. + pub fn into_output_id_proof(self) -> OutputIdProof { + self.output_id_proof + } + /// Returns the [`OutputMetadata`]. pub fn metadata(&self) -> &OutputMetadata { &self.metadata diff --git a/sdk/src/types/block/output/output_id_proof.rs b/sdk/src/types/block/output/output_id_proof.rs new file mode 100644 index 0000000000..7f7bab6933 --- /dev/null +++ b/sdk/src/types/block/output/output_id_proof.rs @@ -0,0 +1,90 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use alloc::{boxed::Box, string::String}; + +#[cfg(feature = "serde")] +use {crate::utils::serde::prefix_hex_bytes, alloc::format, serde::de::Deserialize, serde_json::Value}; + +use crate::types::block::slot::SlotIndex; + +/// The proof of the output identifier. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct OutputIdProof { + pub slot: SlotIndex, + pub output_index: u16, + pub transaction_commitment: String, + pub output_commitment_proof: OutputCommitmentProof, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))] +pub enum OutputCommitmentProof { + HashableNode(HashableNode), + LeafHash(LeafHash), + ValueHash(ValueHash), +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for OutputCommitmentProof { + fn deserialize>(d: D) -> Result { + let value = Value::deserialize(d)?; + Ok( + match value + .get("type") + .and_then(Value::as_u64) + .ok_or_else(|| serde::de::Error::custom("invalid output commitment proof type"))? + as u8 + { + 0 => Self::HashableNode( + serde_json::from_value::(value) + .map_err(|e| serde::de::Error::custom(format!("cannot deserialize hashable node: {e}")))?, + ), + 1 => Self::LeafHash( + serde_json::from_value::(value) + .map_err(|e| serde::de::Error::custom(format!("cannot deserialize leaf hash: {e}")))?, + ), + 2 => Self::ValueHash( + serde_json::from_value::(value) + .map_err(|e| serde::de::Error::custom(format!("cannot deserialize value hash: {e}")))?, + ), + _ => return Err(serde::de::Error::custom("invalid output commitment proof")), + }, + ) + } +} + +/// Node contains the hashes of the left and right children of a node in the tree. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HashableNode { + #[cfg_attr(feature = "serde", serde(rename = "type"))] + pub kind: u8, + pub l: Box, + pub r: Box, +} + +/// Leaf Hash contains the hash of a leaf in the tree. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct LeafHash { + #[cfg_attr(feature = "serde", serde(rename = "type"))] + pub kind: u8, + #[cfg_attr(feature = "serde", serde(with = "prefix_hex_bytes"))] + pub hash: [u8; 32], +} + +/// Value Hash contains the hash of the value for which the proof is being computed. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ValueHash { + #[cfg_attr(feature = "serde", serde(rename = "type"))] + pub kind: u8, + #[cfg_attr(feature = "serde", serde(with = "prefix_hex_bytes"))] + pub hash: [u8; 32], +} diff --git a/sdk/src/wallet/operations/syncing/addresses/outputs.rs b/sdk/src/wallet/operations/syncing/addresses/outputs.rs index ee4640127b..1a3424fbd1 100644 --- a/sdk/src/wallet/operations/syncing/addresses/outputs.rs +++ b/sdk/src/wallet/operations/syncing/addresses/outputs.rs @@ -1,13 +1,10 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashMap; - use instant::Instant; use crate::{ client::secret::SecretManage, - types::block::address::Address, wallet::{ constants::PARALLEL_REQUESTS_AMOUNT, task, @@ -25,7 +22,7 @@ where pub(crate) async fn get_outputs_from_address_output_ids( &self, addresses_with_unspent_outputs: Vec, - ) -> crate::wallet::Result<(Vec<(AddressWithUnspentOutputs, Vec)>)> { + ) -> crate::wallet::Result)>> { log::debug!("[SYNC] start get_outputs_from_address_output_ids"); let address_outputs_start_time = Instant::now(); diff --git a/sdk/src/wallet/operations/syncing/mod.rs b/sdk/src/wallet/operations/syncing/mod.rs index b4a0ca8379..b915ebf936 100644 --- a/sdk/src/wallet/operations/syncing/mod.rs +++ b/sdk/src/wallet/operations/syncing/mod.rs @@ -13,7 +13,7 @@ pub use self::options::SyncOptions; use crate::{ client::secret::SecretManage, types::block::{ - address::{AccountAddress, Address, Bech32Address, NftAddress, ToBech32Ext}, + address::{AccountAddress, Address, Bech32Address, NftAddress}, output::{FoundryId, Output, OutputId, OutputMetadata}, }, wallet::{ diff --git a/sdk/src/wallet/operations/syncing/outputs.rs b/sdk/src/wallet/operations/syncing/outputs.rs index 011e268891..9bf4004dec 100644 --- a/sdk/src/wallet/operations/syncing/outputs.rs +++ b/sdk/src/wallet/operations/syncing/outputs.rs @@ -60,6 +60,7 @@ where output_id: output_with_meta.metadata().output_id().to_owned(), metadata: *output_with_meta.metadata(), output: output_with_meta.output().clone(), + output_id_proof: output_with_meta.output_id_proof().clone(), is_spent: output_with_meta.metadata().is_spent(), network_id, remainder, @@ -90,6 +91,7 @@ where unspent_outputs.push((output_id, output_data.clone())); outputs.push(OutputWithMetadata::new( output_data.output.clone(), + output_data.output_id_proof.clone(), output_data.metadata, )); } diff --git a/sdk/src/wallet/types/mod.rs b/sdk/src/wallet/types/mod.rs index 8c0e0a41d2..9c25c02b50 100644 --- a/sdk/src/wallet/types/mod.rs +++ b/sdk/src/wallet/types/mod.rs @@ -21,7 +21,7 @@ use crate::{ types::{ api::core::OutputWithMetadataResponse, block::{ - output::{Output, OutputId, OutputMetadata}, + output::{Output, OutputId, OutputIdProof, OutputMetadata}, payload::signed_transaction::{dto::SignedTransactionPayloadDto, SignedTransactionPayload, TransactionId}, protocol::{CommittableAgeRange, ProtocolParameters}, slot::SlotIndex, @@ -42,6 +42,8 @@ pub struct OutputData { pub metadata: OutputMetadata, /// The actual Output pub output: Output, + /// The output ID proof + pub output_id_proof: OutputIdProof, /// If an output is spent pub is_spent: bool, /// Network ID diff --git a/sdk/tests/wallet/events.rs b/sdk/tests/wallet/events.rs index a7d7a52b9a..d30eedb89f 100644 --- a/sdk/tests/wallet/events.rs +++ b/sdk/tests/wallet/events.rs @@ -6,13 +6,17 @@ use iota_sdk::{ types::block::{ address::{Address, Bech32Address, Ed25519Address}, input::{Input, UtxoInput}, - output::{unlock_condition::AddressUnlockCondition, BasicOutput, Output}, + output::{ + unlock_condition::AddressUnlockCondition, BasicOutput, LeafHash, Output, OutputCommitmentProof, + OutputIdProof, + }, payload::signed_transaction::{Transaction, TransactionHash, TransactionId}, protocol::protocol_parameters, rand::{ mana::rand_mana_allotment, output::{rand_basic_output, rand_output_metadata}, }, + slot::SlotIndex, }, wallet::{ events::types::{ @@ -48,6 +52,15 @@ fn wallet_events_serde() { output_id: TransactionHash::null().into_transaction_id(0).into_output_id(0), metadata: rand_output_metadata(), output: Output::from(rand_basic_output(1_813_620_509_061_365)), + output_id_proof: OutputIdProof { + slot: SlotIndex(1), + output_index: 0, + transaction_commitment: "0x".to_string(), + output_commitment_proof: OutputCommitmentProof::LeafHash(LeafHash { + kind: 1, + hash: [0u8; 32], + }), + }, is_spent: false, network_id: 42, remainder: true,