From efc036034c1d2b3029b877549c806c7cbd39c23e Mon Sep 17 00:00:00 2001 From: Lukasz Rubaszewski <117115317+lrubasze@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:14:30 +0100 Subject: [PATCH] Add fee support to MeshAPI block endpoints --- .../src/mesh_api/conversions/common.rs | 196 +++++++++++------- .../src/mesh_api/conversions/errors.rs | 5 + .../src/mesh_api/handlers/block.rs | 28 ++- .../mesh-api-server/src/mesh_api/types.rs | 16 +- 4 files changed, 165 insertions(+), 80 deletions(-) diff --git a/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs b/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs index 1f1b8647b6..d22e480573 100644 --- a/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs +++ b/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs @@ -4,11 +4,11 @@ pub fn from_hex>(v: T) -> Result, ExtractionError> { hex::decode(v).map_err(|_| ExtractionError::InvalidHex) } -pub fn to_mesh_api_operation( +pub fn to_mesh_api_operation_no_fee( mapping_context: &MappingContext, database: &StateManagerDatabase, index: i64, - status: MeshApiOperationStatus, + status: &MeshApiOperationStatus, account_address: &GlobalAddress, resource_address: &ResourceAddress, amount: Decimal, @@ -36,86 +36,140 @@ pub fn to_mesh_api_operation( }) } +pub fn to_mesh_api_operation_fee( + mapping_context: &MappingContext, + database: &StateManagerDatabase, + index: i64, + status: &MeshApiOperationStatus, + account_address: &GlobalAddress, + resource_address: &ResourceAddress, + amount: Decimal, + fee_payment_type: FeePaymentBalanceChangeType, +) -> Result { + // TODO:MESH what about fee locking, burning, minting? + let op_type = MeshApiOperationTypes::from(fee_payment_type); + let currency = + to_mesh_api_currency_from_resource_address(mapping_context, database, resource_address)?; + let account = to_mesh_api_account_from_address(mapping_context, account_address)?; + + // see https://docs.cdp.coinbase.com/mesh/docs/models#operation + Ok(models::Operation { + operation_identifier: Box::new(models::OperationIdentifier::new(index)), + related_operations: None, + _type: op_type.to_string(), + status: Some(status.to_string()), + account: Some(Box::new(account)), + amount: Some(Box::new(to_mesh_api_amount(amount, currency)?)), + coin_change: None, + metadata: None, + }) +} + pub fn to_mesh_api_transaction_identifier( + mapping_context: &MappingContext, + transaction_identifiers: &CommittedTransactionIdentifiers, + state_version: StateVersion, +) -> Result { + let transaction_identifier = match transaction_identifiers.transaction_hashes.as_user() { + // Unfortunately non-user transactions don't have txid, let's use state_version as + // transaction_identifier. + None => format!("state_version_{}", state_version), + Some(user_hashes) => { + to_api_transaction_hash_bech32m(mapping_context, &user_hashes.transaction_intent_hash)? + } + }; + + Ok(models::TransactionIdentifier::new(transaction_identifier)) +} + +pub fn to_mesh_api_operations( mapping_context: &MappingContext, database: &StateManagerDatabase, state_version: StateVersion, - transaction_identifiers: &CommittedTransactionIdentifiers, - requested_transaction_identifier: Option<&str>, -) -> Result<(Vec, models::TransactionIdentifier), MappingError> { - let (operations, transaction_identifier) = match transaction_identifiers - .transaction_hashes - .as_user() - { - // TODO:MESH Support non-user transactions. - // For now we take into account only user transactions. - // For non-user we return empty operations vector and artificial transaction identifier - // (unfortunately non-user transactions don't have txid, let's use state_version as - // transaction_identifier). - None => { - let transaction_identifier = format!("state_version_{}", state_version); - if requested_transaction_identifier.is_some_and(|tx_id| tx_id != transaction_identifier) - { - return Err(MappingError::InvalidTransactionIdentifier { - message: format!("transaction_identifier does not match with block_identifier"), - }); - } +) -> Result, MappingError> { + let local_execution = database + .get_committed_local_transaction_execution(state_version) + .ok_or_else(|| MappingError::InvalidTransactionIdentifier { + message: format!( + "No transaction found at state version {}", + state_version.number() + ), + })?; + let status = MeshApiOperationStatus::from(local_execution.outcome); + let fee_balance_changes = + resolve_global_fee_balance_changes(database, &local_execution.fee_source)?; - (vec![], transaction_identifier) - } - Some(user_hashes) => { - let transaction_identifier = to_api_transaction_hash_bech32m( - mapping_context, - &user_hashes.transaction_intent_hash, - )?; + let fee_payment_computation = FeePaymentComputer::compute(FeePaymentComputationInputs { + fee_balance_changes, + fee_summary: &local_execution.fee_summary, + fee_destination: &local_execution.fee_destination, + balance_changes: &local_execution + .global_balance_summary + .global_balance_changes, + }); - if requested_transaction_identifier.is_some_and(|tx_id| tx_id != transaction_identifier) + let mut output = Vec::with_capacity(fee_payment_computation.relevant_entities.len()); + for entity in &fee_payment_computation.relevant_entities { + if entity.is_account() { + if let Some(fee_balance_changes) = + fee_payment_computation.fee_balance_changes.get(&entity) { - return Err(MappingError::InvalidTransactionIdentifier { - message: format!("transaction_identifier does not match with block_identifier"), - }); - } - let local_transaction_execution = database - .get_committed_local_transaction_execution(state_version) - .ok_or_else(|| MappingError::InvalidTransactionIdentifier { - message: format!( - "No transaction found at state version {}", - state_version.number() - ), - })?; + for (fee_payment_type, amount) in fee_balance_changes { + let operation = to_mesh_api_operation_fee( + mapping_context, + database, + output.len() as i64, + &status, + entity, + &XRD, + *amount, + *fee_payment_type, + )?; - let status = MeshApiOperationStatus::from(local_transaction_execution.outcome); + output.push(operation) + } + } - let mut index = 0_i64; - let mut operations = vec![]; - for (address, balance_changes) in local_transaction_execution - .global_balance_summary - .global_balance_changes + if let Some(non_fee_balance_changes) = + fee_payment_computation.non_fee_balance_changes.get(&entity) { - // TODO:MESH support LockFee, Mint, Burn - // see https://github.com/radixdlt/babylon-node/pull/1018#discussion_r1834905560 - if address.is_account() { - for (resource_address, balance_change) in balance_changes { - if let BalanceChange::Fungible(amount) = balance_change { - operations.push(to_mesh_api_operation( - mapping_context, - database, - index, - status.clone(), - &address, - &resource_address, - amount, - )?); - index += 1; - } - } + for (resource_address, amount) in non_fee_balance_changes { + let operation = to_mesh_api_operation_no_fee( + mapping_context, + database, + output.len() as i64, + &status, + entity, + resource_address, + *amount, + )?; + output.push(operation) } } - (operations, transaction_identifier) } - }; - Ok(( - operations, - models::TransactionIdentifier::new(transaction_identifier), - )) + } + + Ok(output) +} + +/// Uses the [`SubstateNodeAncestryStore`] (from the given DB) to transform the input +/// `vault ID -> payment` map into a `global address -> balance change` map. +fn resolve_global_fee_balance_changes( + database: &StateManagerDatabase, + fee_source: &FeeSource, +) -> Result, MappingError> { + let paying_vaults = &fee_source.paying_vaults; + let ancestries = database.batch_get_ancestry(paying_vaults.keys()); + let mut fee_balance_changes = index_map_new(); + for ((vault_id, paid_fee_amount_xrd), ancestry) in paying_vaults.iter().zip(ancestries) { + let ancestry = ancestry.ok_or_else(|| MappingError::InternalIndexDataMismatch { + message: format!("no ancestry record for vault {}", vault_id.to_hex()), + })?; + let global_ancestor_address = GlobalAddress::new_or_panic(ancestry.root.0.into()); + let fee_balance_change = fee_balance_changes + .entry(global_ancestor_address) + .or_insert_with(Decimal::zero); + *fee_balance_change = fee_balance_change.sub_or_panic(*paid_fee_amount_xrd); + } + Ok(fee_balance_changes) } diff --git a/core-rust/mesh-api-server/src/mesh_api/conversions/errors.rs b/core-rust/mesh-api-server/src/mesh_api/conversions/errors.rs index 81293dce11..025e17fc62 100644 --- a/core-rust/mesh-api-server/src/mesh_api/conversions/errors.rs +++ b/core-rust/mesh-api-server/src/mesh_api/conversions/errors.rs @@ -26,6 +26,11 @@ pub enum MappingError { InvalidTransactionIdentifier { message: String, }, + /// An error occurring when the contents of some Node-maintained index table do not match the + /// Engine-owned data (most likely due to a bug on either side). + InternalIndexDataMismatch { + message: String, + }, } impl From for ResponseError { diff --git a/core-rust/mesh-api-server/src/mesh_api/handlers/block.rs b/core-rust/mesh-api-server/src/mesh_api/handlers/block.rs index 685fbd5e60..31a055be4e 100644 --- a/core-rust/mesh-api-server/src/mesh_api/handlers/block.rs +++ b/core-rust/mesh-api-server/src/mesh_api/handlers/block.rs @@ -1,3 +1,5 @@ +use models::transaction_identifier; + use crate::prelude::*; pub(crate) async fn handle_block( @@ -30,12 +32,12 @@ pub(crate) async fn handle_block( )) })?; - let (operations, transaction_identifier) = to_mesh_api_transaction_identifier( + let operations = to_mesh_api_operations(&mapping_context, database.deref(), state_version)?; + + let transaction_identifier = to_mesh_api_transaction_identifier( &mapping_context, - database.deref(), - state_version, &transaction_identifiers, - None, + state_version, )?; // see https://docs.cdp.coinbase.com/mesh/docs/models#transaction @@ -88,13 +90,23 @@ pub(crate) async fn handle_block_transaction( )) })?; - let (operations, transaction_identifier) = to_mesh_api_transaction_identifier( + let transaction_identifier = to_mesh_api_transaction_identifier( &mapping_context, - database.deref(), - state_version, &transaction_identifiers, - Some(&request.transaction_identifier.hash), + state_version, )?; + if !request + .transaction_identifier + .as_ref() + .eq(&transaction_identifier) + { + return Err(MappingError::InvalidTransactionIdentifier { + message: format!("transaction_identifier does not match with block_identifier"), + } + .into()); + } + + let operations = to_mesh_api_operations(&mapping_context, database.deref(), state_version)?; // see https://docs.cdp.coinbase.com/mesh/docs/models#transaction let transaction = models::Transaction { diff --git a/core-rust/mesh-api-server/src/mesh_api/types.rs b/core-rust/mesh-api-server/src/mesh_api/types.rs index 3530aaed39..f351ec61a2 100644 --- a/core-rust/mesh-api-server/src/mesh_api/types.rs +++ b/core-rust/mesh-api-server/src/mesh_api/types.rs @@ -6,7 +6,10 @@ use crate::prelude::*; pub(crate) enum MeshApiOperationTypes { Withdraw, Deposit, - // LockFee, + FeePayment, + FeeDistributed, + TipDistributed, + RoyaltyDistributed, // Mint, // Burn, } @@ -37,3 +40,14 @@ impl From for models::OperationStatus { Self::new(value.to_string(), successful) } } + +impl From for MeshApiOperationTypes { + fn from(value: FeePaymentBalanceChangeType) -> Self { + match value { + FeePaymentBalanceChangeType::FeePayment => Self::FeePayment, + FeePaymentBalanceChangeType::FeeDistributed => Self::FeeDistributed, + FeePaymentBalanceChangeType::TipDistributed => Self::TipDistributed, + FeePaymentBalanceChangeType::RoyaltyDistributed => Self::RoyaltyDistributed, + } + } +}