From 999a68c6931c04572a73a943dbcee0bc5ff85938 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 29 Jun 2023 15:17:07 +0200 Subject: [PATCH 1/2] BlockMetadataResponse 2.0 update --- sdk/src/client/node_api/core/routes.rs | 8 ++--- sdk/src/types/api/core/response.rs | 46 ++++++++++++++++++-------- sdk/src/types/block/semantic.rs | 3 ++ 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index 8f1d38177b..3cf857767d 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -142,7 +142,7 @@ impl ClientInner { } /// Finds a block by its ID and returns it as object. - /// GET /api/core/v3/blocks/{BlockId} + /// GET /api/core/v3/blocks/{blockId} pub async fn get_block(&self, block_id: &BlockId) -> Result { let path = &format!("api/core/v3/blocks/{block_id}"); @@ -160,7 +160,7 @@ impl ClientInner { } /// Finds a block by its ID and returns it as raw bytes. - /// GET /api/core/v3/blocks/{BlockId} + /// GET /api/core/v3/blocks/{blockId} pub async fn get_block_raw(&self, block_id: &BlockId) -> Result> { let path = &format!("api/core/v3/blocks/{block_id}"); @@ -225,7 +225,7 @@ impl ClientInner { .await } - /// Returns the block that was included in the ledger for a given transaction ID, as object. + /// Returns the earliest block containing the transaction that get confirmed, as object. /// GET /api/core/v3/transactions/{transactionId}/included-block pub async fn get_included_block(&self, transaction_id: &TransactionId) -> Result { let path = &format!("api/core/v3/transactions/{transaction_id}/included-block"); @@ -243,7 +243,7 @@ impl ClientInner { )?) } - /// Returns the block that was included in the ledger for a given transaction ID, as object, as raw bytes. + /// Returns the earliest block containing the transaction that get confirmed, as raw bytes. /// GET /api/core/v3/transactions/{transactionId}/included-block pub async fn get_included_block_raw(&self, transaction_id: &TransactionId) -> Result> { let path = &format!("api/core/v3/transactions/{transaction_id}/included-block"); diff --git a/sdk/src/types/api/core/response.rs b/sdk/src/types/api/core/response.rs index 56d7afafea..7660d7c995 100644 --- a/sdk/src/types/api/core/response.rs +++ b/sdk/src/types/api/core/response.rs @@ -6,6 +6,7 @@ use alloc::{string::String, vec::Vec}; use crate::types::block::{ output::{dto::OutputDto, OutputId, OutputMetadata, OutputWithMetadata}, protocol::ProtocolParameters, + semantic::ConflictReason, slot::SlotIndex, BlockId, IssuerId, }; @@ -118,17 +119,35 @@ pub struct SubmitBlockResponse { pub block_id: BlockId, } -/// Describes the ledger inclusion state of a transaction. +/// Describes the state of a block and/or transaction. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] -pub enum LedgerInclusionState { - Conflicting, - Included, - NoTransaction, +pub enum BlockTransactionState { + // Stored but not confirmed or contains not yet included transaction. + Pending, + // Confirmed with the first level of knowledge. + Confirmed, + // Included and cannot be reverted anymore. + Finalized, +} + +/// Describes the reason of a block state. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +#[non_exhaustive] +#[repr(u8)] +pub enum BlockStateReason { + Invalid = 1, + OrphanedCongestionControl = 2, + OrphanedNegativeManaBalance = 3, } /// Response of GET /api/core/v3/blocks/{block_id}/metadata. @@ -141,22 +160,21 @@ pub enum LedgerInclusionState { )] pub struct BlockMetadataResponse { pub block_id: BlockId, - pub parents: Vec, - pub is_solid: bool, + pub strong_parents: Vec, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub referenced_by_milestone_index: Option, + pub weak_parents: Option>, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub milestone_index: Option, + pub shallow_like_parents: Option>, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub ledger_inclusion_state: Option, + pub block_state: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub conflict_reason: Option, + pub tx_state: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub white_flag_index: Option, + pub block_state_reason: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub should_promote: Option, + pub tx_state_reason: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub should_reattach: Option, + pub reissue_payload: Option, } /// Response of GET /api/core/v3/outputs/{output_id}. diff --git a/sdk/src/types/block/semantic.rs b/sdk/src/types/block/semantic.rs index f60cda7d9c..9878ca6926 100644 --- a/sdk/src/types/block/semantic.rs +++ b/sdk/src/types/block/semantic.rs @@ -45,12 +45,15 @@ impl std::error::Error for ConflictError {} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[packable(unpack_error = ConflictError)] #[packable(tag_type = u8, with_error = ConflictError::InvalidConflict)] +// TODO may want to rename this to TransactionStateReason pub enum ConflictReason { /// The block has no conflict. None = 0, /// The referenced Utxo was already spent. InputUtxoAlreadySpent = 1, /// The referenced Utxo was already spent while confirming this milestone. + /// TODO weird + /// * `2` - denotes that the transaction is conflicting with another transaction. InputUtxoAlreadySpentInThisMilestone = 2, /// The referenced Utxo cannot be found. InputUtxoNotFound = 3, From e92ba4dad57b882ef212f741dbb7ee98d64a4e16 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 29 Jun 2023 16:42:43 +0200 Subject: [PATCH 2/2] Fix retry_until_included ? --- sdk/src/client/api/high_level.rs | 160 +++++++++++++++---------------- sdk/src/client/error.rs | 6 +- 2 files changed, 80 insertions(+), 86 deletions(-) diff --git a/sdk/src/client/api/high_level.rs b/sdk/src/client/api/high_level.rs index 328797157c..5beda3ec42 100644 --- a/sdk/src/client/api/high_level.rs +++ b/sdk/src/client/api/high_level.rs @@ -16,16 +16,16 @@ use crate::{ Client, }, types::{ - api::core::response::LedgerInclusionState, + api::core::response::BlockTransactionState, block::{ address::Bech32Address, input::{Input, UtxoInput, INPUT_COUNT_MAX}, - output::OutputWithMetadata, - parent::Parents, + output::{OutputId, OutputWithMetadata}, payload::{ transaction::{TransactionEssence, TransactionId}, Payload, }, + semantic::ConflictReason, Block, BlockId, }, }, @@ -63,23 +63,39 @@ impl Client { futures::future::try_join_all(block_ids.iter().map(|block_id| self.get_block(block_id))).await } - /// Retries (promotes or reattaches) a block for provided block id. Block should only be - /// retried only if they are valid and haven't been confirmed for a while. - pub async fn retry(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { - // Get the metadata to check if it needs to promote or reattach - let block_metadata = self.get_block_metadata(block_id).await?; - if block_metadata.should_promote.unwrap_or(false) { - self.promote_unchecked(block_id).await - } else if block_metadata.should_reattach.unwrap_or(false) { - self.reattach_unchecked(block_id).await + /// Tries to reissue a block's payload. + pub async fn reissue(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { + let metadata = self.get_block_metadata(block_id).await?; + + if metadata.reissue_payload.unwrap_or(false) { + self.reissue_unchecked(block_id).await } else { - Err(Error::NoNeedPromoteOrReattach(block_id.to_string())) + Err(Error::NoNeedToReissue(block_id.to_string())) } } + /// Tries to reissue a block's payload without checking if it should be reissued. + pub async fn reissue_unchecked(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { + // Get the block by the ID. + let block = self.get_block(block_id).await?; + let reissue_block = self.finish_block_builder(None, block.payload().cloned()).await?; + + // Post the modified + let block_id = self.post_block_raw(&reissue_block).await?; + // Get block if we use remote Pow, because the node will change parents and nonce + let block = if self.get_local_pow().await { + reissue_block + } else { + self.get_block(&block_id).await? + }; + + Ok((block_id, block)) + } + /// Retries (promotes or reattaches) a block for provided block id until it's included (referenced by a /// milestone). Default interval is 5 seconds and max attempts is 40. Returns the included block at first position /// and additional reattached blocks + /// TODO may want to rename this reissue_until_included pub async fn retry_until_included( &self, block_id: &BlockId, @@ -91,6 +107,7 @@ impl Client { let mut block_ids = vec![*block_id]; // Reattached Blocks that get returned let mut blocks_with_id = Vec::new(); + for _ in 0..max_attempts.unwrap_or(DEFAULT_RETRY_UNTIL_INCLUDED_MAX_AMOUNT) { #[cfg(target_family = "wasm")] gloo_timers::future::TimeoutFuture::new( @@ -109,41 +126,38 @@ impl Client { // Check inclusion state for each attachment let block_ids_len = block_ids.len(); let mut conflicting = false; + for (index, id) in block_ids.clone().iter().enumerate() { let block_metadata = self.get_block_metadata(id).await?; - if let Some(inclusion_state) = block_metadata.ledger_inclusion_state { - match inclusion_state { - LedgerInclusionState::Included | LedgerInclusionState::NoTransaction => { - // if original block, request it so we can return it on first position - if id == block_id { - let mut included_and_reattached_blocks = - vec![(*block_id, self.get_block(block_id).await?)]; - included_and_reattached_blocks.extend(blocks_with_id); - return Ok(included_and_reattached_blocks); - } else { - // Move included block to first position - blocks_with_id.rotate_left(index); - return Ok(blocks_with_id); - } + if let Some(block_state) = block_metadata.block_state { + if let BlockTransactionState::Finalized = block_state { + // if original block, request it so we can return it on first position + if id == block_id { + let mut included_and_reattached_blocks = vec![(*block_id, self.get_block(block_id).await?)]; + included_and_reattached_blocks.extend(blocks_with_id); + return Ok(included_and_reattached_blocks); + } else { + // Move included block to first position + blocks_with_id.rotate_left(index); + return Ok(blocks_with_id); } - // only set it as conflicting here and don't return, because another reattached block could - // have the included transaction - LedgerInclusionState::Conflicting => conflicting = true, }; } + // Only reattach or promote latest attachment of the block - if index == block_ids_len - 1 { - if block_metadata.should_promote.unwrap_or(false) { - // Safe to unwrap since we iterate over it - self.promote_unchecked(block_ids.last().unwrap()).await?; - } else if block_metadata.should_reattach.unwrap_or(false) { - // Safe to unwrap since we iterate over it - let reattached = self.reattach_unchecked(block_ids.last().unwrap()).await?; - block_ids.push(reattached.0); - blocks_with_id.push(reattached); - } + if index == block_ids_len - 1 && block_metadata.reissue_payload.unwrap_or(false) { + // Safe to unwrap since we iterate over it + let reissued = self.reissue_unchecked(block_ids.last().unwrap()).await?; + block_ids.push(reissued.0); + blocks_with_id.push(reissued); } + + conflicting = block_metadata + .tx_state_reason + .map(|reason| reason != ConflictReason::None) + .unwrap_or(false); } + // After we checked all our reattached blocks, check if the transaction got reattached in another block // and confirmed if conflicting { @@ -156,6 +170,7 @@ impl Client { } } } + Err(Error::TangleInclusion(block_id.to_string())) } @@ -220,53 +235,32 @@ impl Client { Ok(selected_inputs) } - /// Reattaches blocks for provided block id. Blocks can be reattached only if they are valid and haven't been - /// confirmed for a while. - pub async fn reattach(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { - let metadata = self.get_block_metadata(block_id).await?; - if metadata.should_reattach.unwrap_or(false) { - self.reattach_unchecked(block_id).await - } else { - Err(Error::NoNeedPromoteOrReattach(block_id.to_string())) - } - } - - /// Reattach a block without checking if it should be reattached - pub async fn reattach_unchecked(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { - // Get the Block object by the BlockID. - let block = self.get_block(block_id).await?; - let reattach_block = self.finish_block_builder(None, block.payload().cloned()).await?; - - // Post the modified - let block_id = self.post_block_raw(&reattach_block).await?; - - Ok((block_id, reattach_block)) - } + /// Find all outputs based on the requests criteria. This method will try to query multiple nodes if + /// the request amount exceeds individual node limit. + pub async fn find_outputs( + &self, + output_ids: &[OutputId], + addresses: &[Bech32Address], + ) -> Result> { + let mut output_responses = self.get_outputs_with_metadata(output_ids).await?; - /// Promotes a block. The method should validate if a promotion is necessary through get_block. If not, the - /// method should error out and should not allow unnecessary promotions. - pub async fn promote(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { - let metadata = self.get_block_metadata(block_id).await?; - if metadata.should_promote.unwrap_or(false) { - self.promote_unchecked(block_id).await - } else { - Err(Error::NoNeedPromoteOrReattach(block_id.to_string())) - } - } + // Use `get_address()` API to get the address outputs first, + // then collect the `UtxoInput` in the HashSet. + for address in addresses { + // Get output ids of outputs that can be controlled by this address without further unlock constraints + let output_ids_response = self + .basic_output_ids([ + QueryParameter::Address(*address), + QueryParameter::HasExpiration(false), + QueryParameter::HasTimelock(false), + QueryParameter::HasStorageDepositReturn(false), + ]) + .await?; - /// Promote a block without checking if it should be promoted - pub async fn promote_unchecked(&self, block_id: &BlockId) -> Result<(BlockId, Block)> { - // Create a new block (zero value block) for which one tip would be the actual block. - let mut tips = self.get_tips().await?; - if let Some(tip) = tips.first_mut() { - *tip = *block_id; + output_responses.extend(self.get_outputs_with_metadata(&output_ids_response.items).await?); } - let promote_block = self.finish_block_builder(Some(Parents::from_vec(tips)?), None).await?; - - let block_id = self.post_block_raw(&promote_block).await?; - - Ok((block_id, promote_block)) + Ok(output_responses.clone()) } /// Returns the local time checked with the timestamp of the latest milestone, if the difference is larger than 5 diff --git a/sdk/src/client/error.rs b/sdk/src/client/error.rs index b18b13e599..c78b071b8f 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -84,9 +84,9 @@ pub enum Error { /// Error on API request #[error("node error: {0}")] Node(#[from] crate::client::node_api::error::Error), - /// The block doesn't need to be promoted or reattached - #[error("block ID `{0}` doesn't need to be promoted or reattached")] - NoNeedPromoteOrReattach(String), + /// The block's payload doesn't need to be reissued + #[error("block ID `{0}`'s payload doesn't need to be reissued")] + NoNeedToReissue(String), /// Requested output id not found for this type #[error("No output found for {0}")] NoOutput(String),