diff --git a/justfile b/justfile index 1f3c3101..e55a7e2c 100644 --- a/justfile +++ b/justfile @@ -24,8 +24,8 @@ fmt-check: taplo: taplo fmt -test: - cargo +{{ RUST_STABLE }} test +test *ADDITIONAL_ARGS: + cargo +{{ RUST_STABLE }} test {{ ADDITIONAL_ARGS }} clippy: cargo +{{ RUST_STABLE }} clippy diff --git a/orm/src/transactions.rs b/orm/src/transactions.rs index 74a2db61..b4eed492 100644 --- a/orm/src/transactions.rs +++ b/orm/src/transactions.rs @@ -55,7 +55,8 @@ impl From for TransactionKindDb { TransactionKind::ShieldingTransfer(_) => Self::ShieldingTransfer, TransactionKind::MixedTransfer(_) => Self::MixedTransfer, TransactionKind::IbcMsg(_) => Self::IbcMsgTransfer, - TransactionKind::IbcTrasparentTransfer(_) => { + TransactionKind::IbcSendTrasparentTransfer(_) + | TransactionKind::IbcRecvTrasparentTransfer(_) => { Self::IbcTransparentTransfer } TransactionKind::IbcShieldingTransfer(_) => { diff --git a/shared/src/block.rs b/shared/src/block.rs index d0fa9282..d877f402 100644 --- a/shared/src/block.rs +++ b/shared/src/block.rs @@ -211,7 +211,8 @@ impl Block { vec![] } } - TransactionKind::IbcTrasparentTransfer((_, transfer)) + TransactionKind::IbcSendTrasparentTransfer((_, transfer)) + | TransactionKind::IbcRecvTrasparentTransfer((_, transfer)) | TransactionKind::IbcShieldingTransfer((_, transfer)) | TransactionKind::IbcUnshieldingTransfer((_, transfer)) => { let sources = transfer @@ -678,7 +679,11 @@ impl Block { }) .iter() .filter_map(|tx| match &tx.kind { - TransactionKind::IbcTrasparentTransfer(( + TransactionKind::IbcSendTrasparentTransfer(( + Token::Ibc(ibc_token), + _, + )) + | TransactionKind::IbcRecvTrasparentTransfer(( Token::Ibc(ibc_token), _, )) @@ -830,7 +835,8 @@ impl Block { }) .collect() } - TransactionKind::IbcTrasparentTransfer((token, data)) => { + TransactionKind::IbcSendTrasparentTransfer((token, data)) + | TransactionKind::IbcRecvTrasparentTransfer((token, data)) => { [&data.sources, &data.targets] .iter() .flat_map(|transfer_changes| { diff --git a/shared/src/block_result.rs b/shared/src/block_result.rs index 80290ebb..504178fe 100644 --- a/shared/src/block_result.rs +++ b/shared/src/block_result.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use bigdecimal::BigDecimal; use namada_core::masp::MaspTxId; use namada_core::token::Amount as NamadaAmount; -use namada_events::extend::ReadFromEventAttributes; +use namada_events::extend::{InnerTxHash, ReadFromEventAttributes}; use namada_ibc::IbcTxDataHash; use namada_ibc::apps::transfer::types::packet::PacketData as Ics20PacketData; use namada_tx::IndexedTx; @@ -60,6 +60,7 @@ pub struct BlockResult { #[derive(Debug, Clone)] pub struct Event { pub kind: EventKind, + pub inner_tx_hash: Option, pub attributes: Option, } @@ -197,6 +198,34 @@ pub struct IbcPacket { pub data: String, } +impl IbcPacket { + pub fn as_fungible_token_packet(&self) -> Option { + let packet_data: Ics20PacketData = + serde_json::from_str(&self.data).ok()?; + let ibc_amount: NamadaAmount = + packet_data.token.amount.try_into().ok()?; + + Some(FungibleTokenPacket { + memo: packet_data.memo.to_string(), + sender: packet_data.sender.to_string(), + receiver: packet_data.receiver.to_string(), + denom: packet_data.token.denom.to_string(), + amount: Amount::from(ibc_amount).into(), + }) + } + + pub fn id(&self) -> String { + format!( + "{}/{}/{}/{}/{}", + self.dest_port, + self.dest_channel, + self.source_port, + self.source_channel, + self.sequence + ) + } +} + #[derive(Debug, Clone, Default)] pub struct FungibleTokenPacket { pub sender: String, @@ -393,20 +422,11 @@ impl TxAttributesType { _ => return None, }; - let packet_data: Ics20PacketData = - serde_json::from_str(&packet.data).ok()?; - let ibc_amount: NamadaAmount = - packet_data.token.amount.try_into().ok()?; - - let ics20_packet = FungibleTokenPacket { - memo: packet_data.memo.to_string(), - sender: packet_data.sender.to_string(), - receiver: packet_data.receiver.to_string(), - denom: packet_data.token.denom.to_string(), - amount: Amount::from(ibc_amount).into(), - }; - - Some((action, Some(packet), Cow::Owned(ics20_packet))) + Some(( + action, + Some(packet), + Cow::Owned(packet.as_fungible_token_packet()?), + )) } } @@ -430,7 +450,12 @@ impl From for BlockResult { ); let attributes = TxAttributesType::deserialize(&kind, &raw_attributes); - Event { kind, attributes } + let inner_tx_hash = try_parse_inner_tx_hash(&raw_attributes); + Event { + kind, + attributes, + inner_tx_hash, + } }) .collect::>(); let end_events = value @@ -451,7 +476,12 @@ impl From for BlockResult { ); let attributes = TxAttributesType::deserialize(&kind, &raw_attributes); - Event { kind, attributes } + let inner_tx_hash = try_parse_inner_tx_hash(&raw_attributes); + Event { + kind, + attributes, + inner_tx_hash, + } }) .collect::>(); Self { @@ -561,6 +591,14 @@ impl BlockResult { } } +fn try_parse_inner_tx_hash( + raw_attributes: &BTreeMap, +) -> Option { + InnerTxHash::read_opt_from_event_attributes(raw_attributes) + .expect("parsing the inner tx hash shouldn't fail") + .map(|hash| Id::Hash(hash.to_string().to_lowercase())) +} + #[cfg(test)] mod tests { use super::*; @@ -594,6 +632,7 @@ mod tests { Some(Event { kind, + inner_tx_hash: None, attributes: parsed_attributes, }) }) @@ -604,6 +643,7 @@ mod tests { events.remove(0), Event { kind: EventKind::FungibleTokenPacket, + inner_tx_hash: None, attributes: Some( TxAttributesType::FungibleTokenPacket { is_ack: true, diff --git a/shared/src/transaction.rs b/shared/src/transaction.rs index dfc1fd86..a3b8be24 100644 --- a/shared/src/transaction.rs +++ b/shared/src/transaction.rs @@ -77,7 +77,8 @@ pub enum TransactionKind { MixedTransfer(Option), /// Generic, non-transfer, IBC messages IbcMsg(Option>), - IbcTrasparentTransfer((crate::token::Token, TransferData)), + IbcRecvTrasparentTransfer((crate::token::Token, TransferData)), + IbcSendTrasparentTransfer((crate::token::Token, TransferData)), IbcShieldingTransfer((crate::token::Token, TransferData)), IbcUnshieldingTransfer((crate::token::Token, TransferData)), Bond(Option), @@ -376,18 +377,27 @@ impl InnerTransaction { && (wrapper_tx_succeeded || masp_fee_payment || !atomic_batch) } + pub fn is_sent_ibc(&self) -> bool { + matches!( + self.kind, + TransactionKind::IbcSendTrasparentTransfer(_) + | TransactionKind::IbcUnshieldingTransfer(_) + ) + } + pub fn is_ibc(&self) -> bool { matches!( self.kind, TransactionKind::IbcMsg(_) - | TransactionKind::IbcTrasparentTransfer(_) + | TransactionKind::IbcRecvTrasparentTransfer(_) + | TransactionKind::IbcSendTrasparentTransfer(_) | TransactionKind::IbcUnshieldingTransfer(_) | TransactionKind::IbcShieldingTransfer(_) ) } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Fee { pub gas: String, pub gas_used: Option, @@ -649,7 +659,7 @@ fn extract_masp_transaction( } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct IbcSequence { pub sequence_number: String, pub source_port: String, diff --git a/shared/src/utils.rs b/shared/src/utils.rs index d03d29b2..d2d60801 100644 --- a/shared/src/utils.rs +++ b/shared/src/utils.rs @@ -252,7 +252,7 @@ pub fn transfer_to_ibc_tx_kind( transfer_data, ))) } else { - Ok(TransactionKind::IbcTrasparentTransfer(( + Ok(TransactionKind::IbcRecvTrasparentTransfer(( token_id, transfer_data, ))) @@ -311,7 +311,7 @@ pub fn transfer_to_ibc_tx_kind( transfer_data, ))) } else { - Ok(TransactionKind::IbcTrasparentTransfer(( + Ok(TransactionKind::IbcSendTrasparentTransfer(( token_id, transfer_data, ))) diff --git a/transactions/src/services/tx.rs b/transactions/src/services/tx.rs index 51b79d5a..e519e2aa 100644 --- a/transactions/src/services/tx.rs +++ b/transactions/src/services/tx.rs @@ -1,9 +1,12 @@ use bigdecimal::BigDecimal; +use namada_sdk::address::Address; +use namada_sdk::hash::Hash; use namada_sdk::ibc::core::channel::types::acknowledgement::AcknowledgementStatus; use namada_sdk::ibc::core::channel::types::msgs::PacketMsg; use namada_sdk::ibc::core::handler::types::msgs::MsgEnvelope; use shared::block_result::{BlockResult, TxAttributesType}; use shared::gas::GasEstimation; +use shared::id::Id; use shared::transaction::{ IbcAck, IbcAckStatus, IbcSequence, IbcTokenAction, InnerTransaction, TransactionKind, WrapperTransaction, ibc_denom_received, ibc_denom_sent, @@ -45,18 +48,18 @@ pub fn get_ibc_packets( block_results: &BlockResult, txs: &[(WrapperTransaction, Vec)], ) -> Vec { - let mut ibc_txs: Vec<_> = txs.iter().rev().fold( - Default::default(), - |mut acc, (wrapper_tx, inner_txs)| { - // Extract successful ibc transactions from each batch - for inner_tx in inner_txs { - if inner_tx.is_ibc() && inner_tx.was_successful(wrapper_tx) { - acc.push(inner_tx.tx_id.to_owned()) + let mut legacy_extracted_id_tx_ids = + txs.iter().flat_map(|(wrapper_tx, inner_txs)| { + inner_txs.iter().filter_map(|inner_tx| { + // Extract successful ibc transactions from each batch + if inner_tx.is_sent_ibc() && inner_tx.was_successful(wrapper_tx) + { + Some(inner_tx.tx_id.to_owned()) + } else { + None } - } - acc - }, - ); + }) + }); block_results .end_events @@ -71,9 +74,41 @@ pub fn get_ibc_packets( source_channel: packet.source_channel.clone(), dest_channel: packet.dest_channel.clone(), timeout: packet.timeout_timestamp, - tx_id: ibc_txs - .pop() - .expect("Ibc ack should have a corresponding tx."), + tx_id: { + if let Some(id) = event.inner_tx_hash.as_ref() { + // the id was in the event. this should + // be the case 99% of the times, unless + // we're crawling through the history + // of some older namada version, or + // we encounter a pgf funding tx + id.clone() + } else if packet + .as_fungible_token_packet() + .is_some_and(|ics20_packet| { + matches!( + ics20_packet.sender.parse().ok(), + Some(Address::Internal(_)) + ) + }) + { + // this packet was sent by an internal address, + // most likely the pgf (for pgf funding via + // IBC). there is no inner tx id in this + // case, let's add the hash of the packet + // id, as a workaround. + Id::Hash( + Hash::sha256(packet.id()) + .to_string() + .to_lowercase(), + ) + } else { + // this handles older namada versions + legacy_extracted_id_tx_ids.next().expect( + "Ibc sent packet should have a \ + corresponding tx", + ) + } + }, }), _ => None, } @@ -171,7 +206,8 @@ pub fn get_gas_estimates( let notes = tx.notes; gas_estimate.increase_mixed_transfer(notes) } - TransactionKind::IbcTrasparentTransfer(_) => { + TransactionKind::IbcSendTrasparentTransfer(_) + | TransactionKind::IbcRecvTrasparentTransfer(_) => { gas_estimate.increase_ibc_transparent_transfer() } TransactionKind::Bond(_) => gas_estimate.increase_bond(), @@ -227,3 +263,132 @@ pub fn get_gas_estimates( }) .collect() } + +#[cfg(test)] +mod tests { + use namada_sdk::address::PGF; + use namada_sdk::ibc::apps::transfer::types::PrefixedCoin; + use namada_sdk::ibc::apps::transfer::types::packet::PacketData as Ics20PacketData; + use shared::block_result::{ + Event, EventKind, IbcCorePacketKind, IbcPacket, + }; + use shared::ser::{AccountsMap, TransferData}; + use shared::token::Token; + use shared::transaction::TransactionExitStatus; + + use super::*; + + fn mock_block_result( + inner_tx_hash: Option<&str>, + with_packet_sender: Option, + ) -> BlockResult { + BlockResult { + end_events: vec![Event { + kind: EventKind::IbcCore(IbcCorePacketKind::Send), + inner_tx_hash: inner_tx_hash + .map(|hash| Id::Hash(hash.to_string())), + attributes: Some(TxAttributesType::SendPacket(IbcPacket { + source_port: "transfer".to_string(), + dest_port: "transfer".to_string(), + source_channel: "channel-0".to_string(), + dest_channel: "channel-0".to_string(), + timeout_timestamp: 0, + timeout_height: String::new(), + sequence: "1".to_string(), + data: with_packet_sender + .map(|sender| { + serde_json::to_string(&Ics20PacketData { + token: PrefixedCoin { + denom: "eatshit".parse().unwrap(), + amount: "1234".parse().unwrap(), + }, + sender: sender.into(), + receiver: "a1aaaa".to_string().into(), + memo: String::new().into(), + }) + .unwrap() + }) + .unwrap_or_default(), + })), + }], + ..Default::default() + } + } + + #[test] + fn test_get_ibc_packets() { + let expected_seq = |tx_id| IbcSequence { + sequence_number: "1".to_string(), + source_port: "transfer".to_string(), + dest_port: "transfer".to_string(), + source_channel: "channel-0".to_string(), + dest_channel: "channel-0".to_string(), + timeout: 0, + tx_id, + }; + + // get ibc seq just from the events + inner tx hash + let block_result = mock_block_result(Some("deadbeef"), None); + assert_eq!( + get_ibc_packets(&block_result, &[]), + vec![expected_seq(Id::Hash("deadbeef".to_string()))], + ); + + // protocol transfer, there is no inner tx hash + let block_result = mock_block_result(None, Some(PGF.to_string())); + assert_eq!( + get_ibc_packets(&block_result, &[]), + vec![expected_seq(Id::Hash( + Hash::sha256("transfer/channel-0/transfer/channel-0/1") + .to_string() + .to_lowercase() + ))], + ); + + // no inner tx hash in the event, get it from the provided tx slice + let block_result = mock_block_result(None, Some("a1aaaa".to_string())); + let wrapper = WrapperTransaction { + exit_code: TransactionExitStatus::Applied, + tx_id: Id::Hash("eatshit".to_string()), + index: 0, + fee: Default::default(), + atomic: false, + block_height: 0, + total_signatures: 0, + size: 0, + }; + let inner1 = InnerTransaction { + tx_id: Id::Hash("deadbeef".to_string()), + wrapper_id: Id::Hash("eatshit".to_string()), + index: 0, + kind: TransactionKind::IbcSendTrasparentTransfer(( + Token::Native(Id::Hash("aabbcc".to_string())), + TransferData { + sources: AccountsMap(Default::default()), + targets: AccountsMap(Default::default()), + shielded_section_hash: None, + }, + )), + data: None, + extra_sections: Default::default(), + memo: None, + notes: 0, + exit_code: TransactionExitStatus::Applied, + }; + let inner2 = InnerTransaction { + kind: TransactionKind::IbcRecvTrasparentTransfer(( + Token::Native(Id::Hash("aabbcc".to_string())), + TransferData { + sources: AccountsMap(Default::default()), + targets: AccountsMap(Default::default()), + shielded_section_hash: None, + }, + )), + ..inner1.clone() + }; + assert_eq!( + get_ibc_packets(&block_result, &[(wrapper, vec![inner1, inner2])]), + vec![expected_seq(Id::Hash("deadbeef".to_string()))], + ); + } +}