Skip to content

Commit

Permalink
Add /api/core/v3/outputs/{outputId}/full route (#1733)
Browse files Browse the repository at this point in the history
* Add /api/core/v3/outputs/{outputId}/full route

* Fix test

* Update sdk/src/wallet/types/mod.rs

Co-authored-by: Thibault Martinez <[email protected]>

* Comment OutputResponse

* Fix comment

* Import format

* Move OutputIdProof to block types

* Exports imports

---------

Co-authored-by: Thibault Martinez <[email protected]>
Co-authored-by: Thibault Martinez <[email protected]>
  • Loading branch information
3 people authored Dec 11, 2023
1 parent f6fe77f commit 82f6556
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 28 deletions.
5 changes: 5 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 44 additions & 0 deletions sdk/examples/client/node_api_core/12_get_output_full.rs
Original file line number Diff line number Diff line change
@@ -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 <OUTPUT ID> [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::<OutputId>()?;

// Get the output with its metadata.
let output_with_metadata = client.get_output_with_metadata(&output_id).await?;

println!("{output_with_metadata:?}");

Ok(())
}
15 changes: 0 additions & 15 deletions sdk/src/client/node_api/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutputWithMetadata> {
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<Vec<Output>> {
futures::future::try_join_all(output_ids.iter().map(|id| self.get_output(id))).await
Expand Down
10 changes: 9 additions & 1 deletion sdk/src/client/node_api/core/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<OutputWithMetadata> {
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<Block> {
Expand Down
9 changes: 9 additions & 0 deletions sdk/src/types/api/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,12 @@ pub struct UtxoChangesResponse {
pub created_outputs: Vec<OutputId>,
pub consumed_outputs: Vec<OutputId>,
}

// 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,
// }
32 changes: 27 additions & 5 deletions sdk/src/types/block/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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`].
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions sdk/src/types/block/output/output_id_proof.rs
Original file line number Diff line number Diff line change
@@ -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: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
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::<HashableNode>(value)
.map_err(|e| serde::de::Error::custom(format!("cannot deserialize hashable node: {e}")))?,
),
1 => Self::LeafHash(
serde_json::from_value::<LeafHash>(value)
.map_err(|e| serde::de::Error::custom(format!("cannot deserialize leaf hash: {e}")))?,
),
2 => Self::ValueHash(
serde_json::from_value::<ValueHash>(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<OutputCommitmentProof>,
pub r: Box<OutputCommitmentProof>,
}

/// 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],
}
5 changes: 1 addition & 4 deletions sdk/src/wallet/operations/syncing/addresses/outputs.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,7 +22,7 @@ where
pub(crate) async fn get_outputs_from_address_output_ids(
&self,
addresses_with_unspent_outputs: Vec<AddressWithUnspentOutputs>,
) -> crate::wallet::Result<(Vec<(AddressWithUnspentOutputs, Vec<OutputData>)>)> {
) -> crate::wallet::Result<Vec<(AddressWithUnspentOutputs, Vec<OutputData>)>> {
log::debug!("[SYNC] start get_outputs_from_address_output_ids");
let address_outputs_start_time = Instant::now();

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/wallet/operations/syncing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/wallet/operations/syncing/outputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
));
}
Expand Down
4 changes: 3 additions & 1 deletion sdk/src/wallet/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
15 changes: 14 additions & 1 deletion sdk/tests/wallet/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 82f6556

Please sign in to comment.