Skip to content

Commit

Permalink
Add fee support to MeshAPI block endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
lrubasze committed Nov 13, 2024
1 parent 18a5e2d commit efc0360
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 80 deletions.
196 changes: 125 additions & 71 deletions core-rust/mesh-api-server/src/mesh_api/conversions/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ pub fn from_hex<T: AsRef<[u8]>>(v: T) -> Result<Vec<u8>, 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<impl ReadableRocks>,
index: i64,
status: MeshApiOperationStatus,
status: &MeshApiOperationStatus,
account_address: &GlobalAddress,
resource_address: &ResourceAddress,
amount: Decimal,
Expand Down Expand Up @@ -36,86 +36,140 @@ pub fn to_mesh_api_operation(
})
}

pub fn to_mesh_api_operation_fee(
mapping_context: &MappingContext,
database: &StateManagerDatabase<impl ReadableRocks>,
index: i64,
status: &MeshApiOperationStatus,
account_address: &GlobalAddress,
resource_address: &ResourceAddress,
amount: Decimal,
fee_payment_type: FeePaymentBalanceChangeType,
) -> Result<models::Operation, MappingError> {
// 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<models::TransactionIdentifier, MappingError> {
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<impl ReadableRocks>,
state_version: StateVersion,
transaction_identifiers: &CommittedTransactionIdentifiers,
requested_transaction_identifier: Option<&str>,
) -> Result<(Vec<models::Operation>, 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<Vec<models::Operation>, 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<impl ReadableRocks>,
fee_source: &FeeSource,
) -> Result<IndexMap<GlobalAddress, Decimal>, 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)
}
5 changes: 5 additions & 0 deletions core-rust/mesh-api-server/src/mesh_api/conversions/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MappingError> for ResponseError {
Expand Down
28 changes: 20 additions & 8 deletions core-rust/mesh-api-server/src/mesh_api/handlers/block.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use models::transaction_identifier;

use crate::prelude::*;

pub(crate) async fn handle_block(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 15 additions & 1 deletion core-rust/mesh-api-server/src/mesh_api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use crate::prelude::*;
pub(crate) enum MeshApiOperationTypes {
Withdraw,
Deposit,
// LockFee,
FeePayment,
FeeDistributed,
TipDistributed,
RoyaltyDistributed,
// Mint,
// Burn,
}
Expand Down Expand Up @@ -37,3 +40,14 @@ impl From<MeshApiOperationStatus> for models::OperationStatus {
Self::new(value.to_string(), successful)
}
}

impl From<FeePaymentBalanceChangeType> 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,
}
}
}

0 comments on commit efc0360

Please sign in to comment.