From e16700fd89640028c6135690ae951cf8bd2d76e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Wed, 2 Oct 2024 14:18:48 -0700 Subject: [PATCH] feat: events for ibc transfers (#4874) ## Describe your changes This adds specific events related to fungible token transfers, recording them as they happen in the shielded pool. First of all, this is a much more legible and readily consumable event around actual IBC events we care about, which is reason enough to have this, imo, but also, there's currently a flaw in the whole event system in that we don't have access, through events only, to the actual acknowledgement data for a packet. In particular, we can't tell using the raw ibc events if a packet was acked successfully, finalizing a transfer, or unsuccessfully, causing the transfer to be refunded. Knowing which of the two matters a lot, and will cause queries like "how much of this asset has been locked in the shielded pool" to not return the right result by quite a bit. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Event addition only --- .../shielded-pool/src/component/transfer.rs | 104 ++- .../core/component/shielded-pool/src/event.rs | 63 +- ...enumbra.core.component.shielded_pool.v1.rs | 146 ++++ ...a.core.component.shielded_pool.v1.serde.rs | 646 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 642967 -> 646138 bytes .../shielded_pool/v1/shielded_pool.proto | 55 ++ 6 files changed, 1004 insertions(+), 10 deletions(-) diff --git a/crates/core/component/shielded-pool/src/component/transfer.rs b/crates/core/component/shielded-pool/src/component/transfer.rs index d75df18540..18308e82ca 100644 --- a/crates/core/component/shielded-pool/src/component/transfer.rs +++ b/crates/core/component/shielded-pool/src/component/transfer.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ component::{AssetRegistry, NoteManager}, - Ics20Withdrawal, + event, Ics20Withdrawal, }; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -21,9 +21,11 @@ use ibc_types::{ transfer::acknowledgement::TokenTransferAcknowledgement, }; use penumbra_asset::{asset, asset::Metadata, Value}; +use penumbra_ibc::component::ChannelStateReadExt; use penumbra_keys::Address; use penumbra_num::Amount; use penumbra_proto::{ + core::component::shielded_pool::v1::FungibleTokenTransferPacketMetadata, penumbra::core::component::ibc::v1::FungibleTokenPacketData, StateReadProto, StateWriteProto, }; use penumbra_sct::CommitmentSource; @@ -116,6 +118,23 @@ pub trait Ics20TransferWriteExt: StateWrite { ), new_value_balance, ); + self.record_proto(event::outbound_fungible_token_transfer( + Value { + amount: withdrawal.amount, + asset_id: withdrawal.denom.id(), + }, + &withdrawal.return_address, + withdrawal.destination_chain_address.clone(), + FungibleTokenTransferPacketMetadata { + channel: withdrawal.source_channel.0.clone(), + sequence: self + .get_send_sequence( + &withdrawal.source_channel, + &checked_packet.source_port(), + ) + .await?, + }, + )); } else { // receiver is the source, burn utxos @@ -149,6 +168,23 @@ pub trait Ics20TransferWriteExt: StateWrite { ), new_value_balance, ); + self.record_proto(event::outbound_fungible_token_transfer( + Value { + amount: withdrawal.amount, + asset_id: withdrawal.denom.id(), + }, + &withdrawal.return_address, + withdrawal.destination_chain_address.clone(), + FungibleTokenTransferPacketMetadata { + channel: withdrawal.source_channel.0.clone(), + sequence: self + .get_send_sequence( + &withdrawal.source_channel, + &checked_packet.source_port(), + ) + .await?, + }, + )); } self.send_packet_execute(checked_packet).await; @@ -352,6 +388,15 @@ async fn recv_transfer_packet_inner( state_key::ics20_value_balance::by_asset_id(&msg.packet.chan_on_b, &denom.id()), new_value_balance, ); + state.record_proto(event::inbound_fungible_token_transfer( + value, + packet_data.sender.clone(), + &receiver_address, + FungibleTokenTransferPacketMetadata { + channel: msg.packet.chan_on_a.0.clone(), + sequence: msg.packet.sequence.0, + }, + )); } else { // create new denom: // @@ -403,13 +448,26 @@ async fn recv_transfer_packet_inner( state_key::ics20_value_balance::by_asset_id(&msg.packet.chan_on_b, &denom.id()), new_value_balance, ); + state.record_proto(event::inbound_fungible_token_transfer( + value, + packet_data.sender.clone(), + &receiver_address, + FungibleTokenTransferPacketMetadata { + channel: msg.packet.chan_on_a.0.clone(), + sequence: msg.packet.sequence.0, + }, + )); } Ok(()) } // see: https://github.com/cosmos/ibc/blob/8326e26e7e1188b95c32481ff00348a705b23700/spec/app/ics-020-fungible-token-transfer/README.md?plain=1#L297 -async fn refund_tokens(mut state: S, packet: &Packet) -> Result<()> { +async fn refund_tokens( + mut state: S, + packet: &Packet, + reason: event::FungibleTokenRefundReason, +) -> Result<()> { let packet_data: FungibleTokenPacketData = serde_json::from_slice(packet.data.as_slice())?; let denom: asset::Metadata = packet_data // CRITICAL: verify that this denom is validated in upstream timeout handling .denom @@ -469,6 +527,17 @@ async fn refund_tokens(mut state: S, packet: &Packet) -> Result<( state_key::ics20_value_balance::by_asset_id(&packet.chan_on_a, &denom.id()), new_value_balance, ); + state.record_proto(event::outbound_fungible_token_refund( + value, + &receiver, // note, this comes from packet_data.sender + packet_data.receiver.clone(), + reason, + // Use the destination channel, i.e. our name for it, to be consistent across events. + FungibleTokenTransferPacketMetadata { + channel: packet.chan_on_b.0.clone(), + sequence: packet.sequence.0, + }, + )); } else { let value_balance: Amount = state .get(&state_key::ics20_value_balance::by_asset_id( @@ -497,6 +566,17 @@ async fn refund_tokens(mut state: S, packet: &Packet) -> Result<( state_key::ics20_value_balance::by_asset_id(&packet.chan_on_a, &denom.id()), new_value_balance, ); + // note, order flipped relative to the event. + state.record_proto(event::outbound_fungible_token_refund( + value, + &receiver, // note, this comes from packet_data.sender + packet_data.receiver.clone(), + reason, + FungibleTokenTransferPacketMetadata { + channel: packet.chan_on_b.0.clone(), + sequence: packet.sequence.0, + }, + )); } Ok(()) @@ -535,9 +615,13 @@ impl AppHandlerExecute for Ics20Transfer { async fn timeout_packet_execute(mut state: S, msg: &MsgTimeout) -> Result<()> { // timeouts may fail due to counterparty chains sending transfers of u128-1 - refund_tokens(&mut state, &msg.packet) - .await - .context("able to timeout packet")?; + refund_tokens( + &mut state, + &msg.packet, + event::FungibleTokenRefundReason::Timeout, + ) + .await + .context("able to timeout packet")?; Ok(()) } @@ -552,9 +636,13 @@ impl AppHandlerExecute for Ics20Transfer { // in the case where a counterparty chain acknowledges a packet with an error, // for example due to a middleware processing issue or other behavior, // the funds should be unescrowed back to the packet sender. - refund_tokens(&mut state, &msg.packet) - .await - .context("unable to refund packet acknowledgement")?; + refund_tokens( + &mut state, + &msg.packet, + event::FungibleTokenRefundReason::Error, + ) + .await + .context("unable to refund packet acknowledgement")?; } Ok(()) diff --git a/crates/core/component/shielded-pool/src/event.rs b/crates/core/component/shielded-pool/src/event.rs index 3280655f1b..f13f1398c0 100644 --- a/crates/core/component/shielded-pool/src/event.rs +++ b/crates/core/component/shielded-pool/src/event.rs @@ -1,7 +1,12 @@ +use penumbra_asset::Value; +use penumbra_keys::Address; +use penumbra_proto::core::component::shielded_pool::v1::{ + event_outbound_fungible_token_refund::Reason, EventInboundFungibleTokenTransfer, + EventOutboundFungibleTokenRefund, EventOutboundFungibleTokenTransfer, EventOutput, EventSpend, + FungibleTokenTransferPacketMetadata, +}; use penumbra_sct::Nullifier; -use penumbra_proto::core::component::shielded_pool::v1::{EventOutput, EventSpend}; - use crate::NotePayload; // These are sort of like the proto/domain type From impls, because @@ -18,3 +23,57 @@ pub fn output(note_payload: &NotePayload) -> EventOutput { note_commitment: Some(note_payload.note_commitment.into()), } } + +pub fn outbound_fungible_token_transfer( + value: Value, + sender: &Address, + receiver: String, + meta: FungibleTokenTransferPacketMetadata, +) -> EventOutboundFungibleTokenTransfer { + EventOutboundFungibleTokenTransfer { + value: Some(value.into()), + sender: Some(sender.into()), + receiver, + meta: Some(meta), + } +} + +#[derive(Clone, Copy, Debug)] +pub enum FungibleTokenRefundReason { + Timeout, + Error, +} + +pub fn outbound_fungible_token_refund( + value: Value, + sender: &Address, + receiver: String, + reason: FungibleTokenRefundReason, + meta: FungibleTokenTransferPacketMetadata, +) -> EventOutboundFungibleTokenRefund { + let reason = match reason { + FungibleTokenRefundReason::Timeout => Reason::Timeout, + FungibleTokenRefundReason::Error => Reason::Error, + }; + EventOutboundFungibleTokenRefund { + value: Some(value.into()), + sender: Some(sender.into()), + receiver, + reason: reason as i32, + meta: Some(meta), + } +} + +pub fn inbound_fungible_token_transfer( + value: Value, + sender: String, + receiver: &Address, + meta: FungibleTokenTransferPacketMetadata, +) -> EventInboundFungibleTokenTransfer { + EventInboundFungibleTokenTransfer { + value: Some(value.into()), + sender, + receiver: Some(receiver.into()), + meta: Some(meta), + } +} diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index 31d9776a20..1362033de8 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -732,6 +732,152 @@ impl ::prost::Name for AssetMetadataByIdsResponse { ) } } +/// Metadata about the packet associated with the transfer. +/// +/// This allows identifying which specific packet is associated with the transfer. +/// Implicitly, both ports are going to be "transfer". +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FungibleTokenTransferPacketMetadata { + /// The identifier for the channel on *this* chain. + #[prost(string, tag = "1")] + pub channel: ::prost::alloc::string::String, + /// Sequence number for the packet. + #[prost(uint64, tag = "2")] + pub sequence: u64, +} +impl ::prost::Name for FungibleTokenTransferPacketMetadata { + const NAME: &'static str = "FungibleTokenTransferPacketMetadata"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventOutboundFungibleTokenTransfer { + /// The value being transferred out of the chain. + #[prost(message, optional, tag = "1")] + pub value: ::core::option::Option, + /// The sending address on chain. + #[prost(message, optional, tag = "2")] + pub sender: ::core::option::Option, + /// The receiving address, which we don't assume anything about. + #[prost(string, tag = "3")] + pub receiver: ::prost::alloc::string::String, + #[prost(message, optional, tag = "4")] + pub meta: ::core::option::Option, +} +impl ::prost::Name for EventOutboundFungibleTokenTransfer { + const NAME: &'static str = "EventOutboundFungibleTokenTransfer"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventOutboundFungibleTokenRefund { + /// The value being refunded. + #[prost(message, optional, tag = "1")] + pub value: ::core::option::Option, + /// The sender being refunded. + #[prost(message, optional, tag = "2")] + pub sender: ::core::option::Option, + /// The address that attempted to receive the funds. + #[prost(string, tag = "3")] + pub receiver: ::prost::alloc::string::String, + /// Why the refund is happening. + #[prost(enumeration = "event_outbound_fungible_token_refund::Reason", tag = "4")] + pub reason: i32, + /// This will be the metadata for the packet for the transfer being refunded. + /// + /// This allows linking a refund to the transfer. + #[prost(message, optional, tag = "5")] + pub meta: ::core::option::Option, +} +/// Nested message and enum types in `EventOutboundFungibleTokenRefund`. +pub mod event_outbound_fungible_token_refund { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Reason { + /// No particular reason. + Unspecified = 0, + /// The transfer timed out. + Timeout = 1, + /// The transfer was acknowledged with an error. + Error = 2, + } + impl Reason { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Reason::Unspecified => "REASON_UNSPECIFIED", + Reason::Timeout => "REASON_TIMEOUT", + Reason::Error => "REASON_ERROR", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REASON_UNSPECIFIED" => Some(Self::Unspecified), + "REASON_TIMEOUT" => Some(Self::Timeout), + "REASON_ERROR" => Some(Self::Error), + _ => None, + } + } + } +} +impl ::prost::Name for EventOutboundFungibleTokenRefund { + const NAME: &'static str = "EventOutboundFungibleTokenRefund"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventInboundFungibleTokenTransfer { + /// The value being transferred in. + #[prost(message, optional, tag = "1")] + pub value: ::core::option::Option, + /// The sender on the counterparty chain. + #[prost(string, tag = "2")] + pub sender: ::prost::alloc::string::String, + /// The receiver on this chain. + #[prost(message, optional, tag = "3")] + pub receiver: ::core::option::Option, + #[prost(message, optional, tag = "4")] + pub meta: ::core::option::Option, +} +impl ::prost::Name for EventInboundFungibleTokenTransfer { + const NAME: &'static str = "EventInboundFungibleTokenTransfer"; + const PACKAGE: &'static str = "penumbra.core.component.shielded_pool.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.shielded_pool.v1.{}", Self::NAME + ) + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod query_service_client { diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index a4df2af9c1..85e3edf22e 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -494,6 +494,537 @@ impl<'de> serde::Deserialize<'de> for EventBroadcastClue { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventBroadcastClue", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventInboundFungibleTokenTransfer { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + if !self.sender.is_empty() { + len += 1; + } + if self.receiver.is_some() { + len += 1; + } + if self.meta.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventInboundFungibleTokenTransfer", len)?; + if let Some(v) = self.value.as_ref() { + struct_ser.serialize_field("value", v)?; + } + if !self.sender.is_empty() { + struct_ser.serialize_field("sender", &self.sender)?; + } + if let Some(v) = self.receiver.as_ref() { + struct_ser.serialize_field("receiver", v)?; + } + if let Some(v) = self.meta.as_ref() { + struct_ser.serialize_field("meta", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventInboundFungibleTokenTransfer { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "value", + "sender", + "receiver", + "meta", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Value, + Sender, + Receiver, + Meta, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "value" => Ok(GeneratedField::Value), + "sender" => Ok(GeneratedField::Sender), + "receiver" => Ok(GeneratedField::Receiver), + "meta" => Ok(GeneratedField::Meta), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventInboundFungibleTokenTransfer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventInboundFungibleTokenTransfer") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + let mut sender__ = None; + let mut receiver__ = None; + let mut meta__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Value => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("value")); + } + value__ = map_.next_value()?; + } + GeneratedField::Sender => { + if sender__.is_some() { + return Err(serde::de::Error::duplicate_field("sender")); + } + sender__ = Some(map_.next_value()?); + } + GeneratedField::Receiver => { + if receiver__.is_some() { + return Err(serde::de::Error::duplicate_field("receiver")); + } + receiver__ = map_.next_value()?; + } + GeneratedField::Meta => { + if meta__.is_some() { + return Err(serde::de::Error::duplicate_field("meta")); + } + meta__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventInboundFungibleTokenTransfer { + value: value__, + sender: sender__.unwrap_or_default(), + receiver: receiver__, + meta: meta__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventInboundFungibleTokenTransfer", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EventOutboundFungibleTokenRefund { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + if self.sender.is_some() { + len += 1; + } + if !self.receiver.is_empty() { + len += 1; + } + if self.reason != 0 { + len += 1; + } + if self.meta.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenRefund", len)?; + if let Some(v) = self.value.as_ref() { + struct_ser.serialize_field("value", v)?; + } + if let Some(v) = self.sender.as_ref() { + struct_ser.serialize_field("sender", v)?; + } + if !self.receiver.is_empty() { + struct_ser.serialize_field("receiver", &self.receiver)?; + } + if self.reason != 0 { + let v = event_outbound_fungible_token_refund::Reason::try_from(self.reason) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.reason)))?; + struct_ser.serialize_field("reason", &v)?; + } + if let Some(v) = self.meta.as_ref() { + struct_ser.serialize_field("meta", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventOutboundFungibleTokenRefund { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "value", + "sender", + "receiver", + "reason", + "meta", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Value, + Sender, + Receiver, + Reason, + Meta, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "value" => Ok(GeneratedField::Value), + "sender" => Ok(GeneratedField::Sender), + "receiver" => Ok(GeneratedField::Receiver), + "reason" => Ok(GeneratedField::Reason), + "meta" => Ok(GeneratedField::Meta), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventOutboundFungibleTokenRefund; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenRefund") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + let mut sender__ = None; + let mut receiver__ = None; + let mut reason__ = None; + let mut meta__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Value => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("value")); + } + value__ = map_.next_value()?; + } + GeneratedField::Sender => { + if sender__.is_some() { + return Err(serde::de::Error::duplicate_field("sender")); + } + sender__ = map_.next_value()?; + } + GeneratedField::Receiver => { + if receiver__.is_some() { + return Err(serde::de::Error::duplicate_field("receiver")); + } + receiver__ = Some(map_.next_value()?); + } + GeneratedField::Reason => { + if reason__.is_some() { + return Err(serde::de::Error::duplicate_field("reason")); + } + reason__ = Some(map_.next_value::()? as i32); + } + GeneratedField::Meta => { + if meta__.is_some() { + return Err(serde::de::Error::duplicate_field("meta")); + } + meta__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventOutboundFungibleTokenRefund { + value: value__, + sender: sender__, + receiver: receiver__.unwrap_or_default(), + reason: reason__.unwrap_or_default(), + meta: meta__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenRefund", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for event_outbound_fungible_token_refund::Reason { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "REASON_UNSPECIFIED", + Self::Timeout => "REASON_TIMEOUT", + Self::Error => "REASON_ERROR", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for event_outbound_fungible_token_refund::Reason { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "REASON_UNSPECIFIED", + "REASON_TIMEOUT", + "REASON_ERROR", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = event_outbound_fungible_token_refund::Reason; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "REASON_UNSPECIFIED" => Ok(event_outbound_fungible_token_refund::Reason::Unspecified), + "REASON_TIMEOUT" => Ok(event_outbound_fungible_token_refund::Reason::Timeout), + "REASON_ERROR" => Ok(event_outbound_fungible_token_refund::Reason::Error), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for EventOutboundFungibleTokenTransfer { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + if self.sender.is_some() { + len += 1; + } + if !self.receiver.is_empty() { + len += 1; + } + if self.meta.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenTransfer", len)?; + if let Some(v) = self.value.as_ref() { + struct_ser.serialize_field("value", v)?; + } + if let Some(v) = self.sender.as_ref() { + struct_ser.serialize_field("sender", v)?; + } + if !self.receiver.is_empty() { + struct_ser.serialize_field("receiver", &self.receiver)?; + } + if let Some(v) = self.meta.as_ref() { + struct_ser.serialize_field("meta", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventOutboundFungibleTokenTransfer { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "value", + "sender", + "receiver", + "meta", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Value, + Sender, + Receiver, + Meta, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "value" => Ok(GeneratedField::Value), + "sender" => Ok(GeneratedField::Sender), + "receiver" => Ok(GeneratedField::Receiver), + "meta" => Ok(GeneratedField::Meta), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventOutboundFungibleTokenTransfer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenTransfer") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + let mut sender__ = None; + let mut receiver__ = None; + let mut meta__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Value => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("value")); + } + value__ = map_.next_value()?; + } + GeneratedField::Sender => { + if sender__.is_some() { + return Err(serde::de::Error::duplicate_field("sender")); + } + sender__ = map_.next_value()?; + } + GeneratedField::Receiver => { + if receiver__.is_some() { + return Err(serde::de::Error::duplicate_field("receiver")); + } + receiver__ = Some(map_.next_value()?); + } + GeneratedField::Meta => { + if meta__.is_some() { + return Err(serde::de::Error::duplicate_field("meta")); + } + meta__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventOutboundFungibleTokenTransfer { + value: value__, + sender: sender__, + receiver: receiver__.unwrap_or_default(), + meta: meta__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenTransfer", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for EventOutput { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1341,6 +1872,121 @@ impl<'de> serde::Deserialize<'de> for FmdParameters { deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.FmdParameters", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for FungibleTokenTransferPacketMetadata { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.channel.is_empty() { + len += 1; + } + if self.sequence != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.FungibleTokenTransferPacketMetadata", len)?; + if !self.channel.is_empty() { + struct_ser.serialize_field("channel", &self.channel)?; + } + if self.sequence != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("sequence", ToString::to_string(&self.sequence).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FungibleTokenTransferPacketMetadata { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "channel", + "sequence", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Channel, + Sequence, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "channel" => Ok(GeneratedField::Channel), + "sequence" => Ok(GeneratedField::Sequence), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FungibleTokenTransferPacketMetadata; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.shielded_pool.v1.FungibleTokenTransferPacketMetadata") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut channel__ = None; + let mut sequence__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Channel => { + if channel__.is_some() { + return Err(serde::de::Error::duplicate_field("channel")); + } + channel__ = Some(map_.next_value()?); + } + GeneratedField::Sequence => { + if sequence__.is_some() { + return Err(serde::de::Error::duplicate_field("sequence")); + } + sequence__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(FungibleTokenTransferPacketMetadata { + channel: channel__.unwrap_or_default(), + sequence: sequence__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.shielded_pool.v1.FungibleTokenTransferPacketMetadata", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GenesisContent { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 5127703ec3ff420ed8031f1a9a75fde97737de06..a1b2955a571836ef3b777301c5b908cafb93b694 100644 GIT binary patch delta 2768 zcma)8O>7ip81B3~yWRcxD?_`5{p0H*F0@c027*RFNJ>H?RF)bJYRGi=Yj^B+W}TU( zX*{g)Vm#@=WD`Olgb+|F5;ZMBVoC@hV7z)XA>rUnqbG?6pYNNQWr-*p=J4#i@ALjW z?>pN*S9-Km`tUpLy@&eZ=NfAdeXs9b`tqT6EL|$T%{Cos+SAr#T}(JL!k%zV+nW;Z zn`UK3_(z0qR!!e*_bsQg2Qw_`iT9hru88%-wJmzNgNvULD~_EKwm;tVC!MBUeJX4F z!Ef>1sogB$3A-xX_R#O~(#s(*K<|o*uuchgeGK%eat9kG(`S*(dL6&7*mkn=C z=u<;HnRr>b6>r5D|L^R|VhRaAEw^3Ilbgj7t}s2vR@trIc}!)u|5@=y%gU)7I(JHp zca=Eu{3RZ0FZ@X(2PO_585@6VqE9pW*vj(Qf#c&xPmYz#BFe-PE}wE-nK`f2OxqT9 z?$~^| zJ5-^k%z9IxKdMpTSX@!%a$e3LZ?(;{p{nvh6AuUS1?B zW;Uu|@|?&(X0_@H&y%g}Zeflzki$M5lF1q`(qKM<;J!$Fh`rHLEG%CNyXA~YCuL4yGi`x1ry1w`yiL4VQ2mV}py zc7&P5kT>Aw2*UR=y*L~}WxRb<%_eUou29dda5i~ZUZLa`wi*@X^&TU!K=duelqWD= zFHr9)R$>YHS`P0jmaLT-NJSwEUc($bK$y<+Z96g8w7n(2qMLZh1n`wBP(B&vdv2V7Vp-rt3C{(8jcdS7zd04 zi2{OpF_I`C#<3_RI;@EC^1760P9+|E@xq1p7qQeQjMJ@PK57ZCM*_{0T*B*8psy=t z6mUc8bEjgCMDN#3NYVGjY{N%CaGk=>saiJqiSs&zNKJww&}3d|@`lu8OI{pc#JeFy zxlP3bx=GZZDex1udBqkKfUC)CW}_i&sJ_E3H4F2IJBB{oq|}*0E1C#jP9q zGW4GL0JT_N3uCO;y|yE))EboZIG>z z^pSffb3M^g*#zsBCmpFTN9zWN)R!q-w}O$lP54p;r?3Fa+mzg@j&MP0>W9I5_o#?B$xISQzyUr E2Q#pUOaK4? delta 87 zcmeyhU48m|^@c5sSx;E5RCC!(chqN+oIdY3i}Lo>PZ$Hmwmbc23}xhe=FP<}#LB=h kW$OZ_`>O4;^_YN|8Hibcm=%cGfS4VKIkwN%V!Z diff --git a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index 02251a7800..f93427e8e8 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -275,3 +275,58 @@ message AssetMetadataByIdsResponse { // A single asset metadata streamed from the node. core.asset.v1.Metadata denom_metadata = 1; } + +// Metadata about the packet associated with the transfer. +// +// This allows identifying which specific packet is associated with the transfer. +// Implicitly, both ports are going to be "transfer". +message FungibleTokenTransferPacketMetadata { + // The identifier for the channel on *this* chain. + string channel = 1; + // Sequence number for the packet. + uint64 sequence = 2; +} + +message EventOutboundFungibleTokenTransfer { + // The value being transferred out of the chain. + core.asset.v1.Value value = 1; + // The sending address on chain. + core.keys.v1.Address sender = 2; + // The receiving address, which we don't assume anything about. + string receiver = 3; + FungibleTokenTransferPacketMetadata meta = 4; +} + +message EventOutboundFungibleTokenRefund { + enum Reason { + // No particular reason. + REASON_UNSPECIFIED = 0; + // The transfer timed out. + REASON_TIMEOUT = 1; + // The transfer was acknowledged with an error. + REASON_ERROR = 2; + } + + // The value being refunded. + core.asset.v1.Value value = 1; + // The sender being refunded. + core.keys.v1.Address sender = 2; + // The address that attempted to receive the funds. + string receiver = 3; + // Why the refund is happening. + Reason reason = 4; + // This will be the metadata for the packet for the transfer being refunded. + // + // This allows linking a refund to the transfer. + FungibleTokenTransferPacketMetadata meta = 5; +} + +message EventInboundFungibleTokenTransfer { + // The value being transferred in. + core.asset.v1.Value value = 1; + // The sender on the counterparty chain. + string sender = 2; + // The receiver on this chain. + core.keys.v1.Address receiver = 3; + FungibleTokenTransferPacketMetadata meta = 4; +}