Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BlockMetadataResponse 2.0 update #700

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 77 additions & 83 deletions sdk/src/client/api/high_level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@
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,
},
},
Expand Down Expand Up @@ -63,23 +63,39 @@
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 {

Check failure on line 86 in sdk/src/client/api/high_level.rs

View workflow job for this annotation

GitHub Actions / crate (ubuntu-latest)

no method named `get_local_pow` found for reference `&client::core::Client` in the current scope

Check failure on line 86 in sdk/src/client/api/high_level.rs

View workflow job for this annotation

GitHub Actions / crate (windows-latest)

no method named `get_local_pow` found for reference `&client::core::Client` in the current scope

Check failure on line 86 in sdk/src/client/api/high_level.rs

View workflow job for this annotation

GitHub Actions / Check Unused Dependencies

no method named `get_local_pow` found for reference `&client::core::Client` in the current scope
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,
Expand All @@ -91,6 +107,7 @@
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(
Expand All @@ -109,41 +126,38 @@
// 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 {
Expand All @@ -156,6 +170,7 @@
}
}
}

Err(Error::TangleInclusion(block_id.to_string()))
}

Expand Down Expand Up @@ -220,53 +235,32 @@
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<Vec<OutputWithMetadata>> {
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
Expand Down
6 changes: 3 additions & 3 deletions sdk/src/client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 4 additions & 4 deletions sdk/src/client/node_api/core/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Block> {
let path = &format!("api/core/v3/blocks/{block_id}");

Expand All @@ -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<Vec<u8>> {
let path = &format!("api/core/v3/blocks/{block_id}");

Expand Down Expand Up @@ -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<Block> {
let path = &format!("api/core/v3/transactions/{transaction_id}/included-block");
Expand All @@ -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<Vec<u8>> {
let path = &format!("api/core/v3/transactions/{transaction_id}/included-block");
Expand Down
46 changes: 32 additions & 14 deletions sdk/src/types/api/core/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Included and cannot be reverted anymore.
// Included and can no longer be reverted.

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.
Expand All @@ -141,22 +160,21 @@ pub enum LedgerInclusionState {
)]
pub struct BlockMetadataResponse {
pub block_id: BlockId,
pub parents: Vec<BlockId>,
pub is_solid: bool,
pub strong_parents: Vec<BlockId>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add default to all of these?

pub referenced_by_milestone_index: Option<u32>,
pub weak_parents: Option<Vec<BlockId>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub milestone_index: Option<u32>,
pub shallow_like_parents: Option<Vec<BlockId>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub ledger_inclusion_state: Option<LedgerInclusionState>,
pub block_state: Option<BlockTransactionState>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub conflict_reason: Option<u8>,
pub tx_state: Option<BlockTransactionState>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub white_flag_index: Option<u32>,
pub block_state_reason: Option<BlockStateReason>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub should_promote: Option<bool>,
pub tx_state_reason: Option<ConflictReason>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub should_reattach: Option<bool>,
pub reissue_payload: Option<bool>,
}

/// Response of GET /api/core/v3/outputs/{output_id}.
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/types/block/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very weird!

/// * `2` - denotes that the transaction is conflicting with another transaction.
InputUtxoAlreadySpentInThisMilestone = 2,
/// The referenced Utxo cannot be found.
InputUtxoNotFound = 3,
Expand Down
Loading