From b902f26e7dd6e5534e3b8406164939cf6c5f57de Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 1 Oct 2024 12:53:58 -0700 Subject: [PATCH 1/4] pindexer: ibc: define transfer table --- crates/bin/pindexer/src/ibc/ibc.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 crates/bin/pindexer/src/ibc/ibc.sql diff --git a/crates/bin/pindexer/src/ibc/ibc.sql b/crates/bin/pindexer/src/ibc/ibc.sql new file mode 100644 index 0000000000..bc41d099ae --- /dev/null +++ b/crates/bin/pindexer/src/ibc/ibc.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS ibc_transfer ( + id SERIAL PRIMARY KEY, + -- The AssetID of whatever is being transferred. + asset BYTE NOT NULL, + -- The amount being transf + amount NUMERIC(39, 0) NOT NULL, + -- The address on the penumbra side. + -- + -- This may be the sender or the receiver, depending on if this inflow or outflow. + penumbra_addr BYTEA NOT NULL, + -- The address on the other side. + foreign_addr TEXT NOT NULL, + -- What kind of transfer this is. + kind TEXT NOT NULL CHECK (kind IN ('inbound', 'outbound', 'refund_timeout', 'refund_error')) +); From f2a4421411ef0bf989eeade0e18d49bd0d101b4e Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 1 Oct 2024 13:30:16 -0700 Subject: [PATCH 2/4] pindexer: ibc: define events --- crates/bin/pindexer/src/ibc/mod.rs | 107 +++++++++++++++++++++++++++++ crates/bin/pindexer/src/lib.rs | 1 + 2 files changed, 108 insertions(+) create mode 100644 crates/bin/pindexer/src/ibc/mod.rs diff --git a/crates/bin/pindexer/src/ibc/mod.rs b/crates/bin/pindexer/src/ibc/mod.rs new file mode 100644 index 0000000000..3857da3435 --- /dev/null +++ b/crates/bin/pindexer/src/ibc/mod.rs @@ -0,0 +1,107 @@ +use anyhow::anyhow; +use cometindex::ContextualizedEvent; +use penumbra_asset::Value; +use penumbra_keys::Address; +use penumbra_proto::{ + core::component::shielded_pool::v1::{ + self as pb, event_outbound_fungible_token_refund::Reason as RefundReason, + }, + event::ProtoEvent as _, +}; + +/// The kind of event we might care about. +#[derive(Clone, Copy, Debug)] +enum EventKind { + InboundTransfer, + OutboundTransfer, + OutboundRefund, +} + +impl EventKind { + fn tag(&self) -> &'static str { + match self { + Self::InboundTransfer => { + "penumbra.core.component.shielded_pool.v1.EventInboundFungibleTokenTransfer" + } + Self::OutboundTransfer => { + "penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenTransfer" + } + Self::OutboundRefund => { + "penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenRefund" + } + } + } +} + +impl TryFrom<&str> for EventKind { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + for kind in [ + Self::InboundTransfer, + Self::OutboundTransfer, + Self::OutboundRefund, + ] { + if kind.tag() == value { + return Ok(kind); + } + } + Err(anyhow!("unexpected event kind: {value}")) + } +} + +/// Represents the event data that we care about. +#[derive(Debug, Clone)] +enum Event { + InboundTransfer { + receiver: Address, + sender: String, + value: Value, + }, + OutboundTransfer { + sender: Address, + receiver: String, + value: Value, + }, + OutboundRefund { + sender: Address, + receiver: String, + value: Value, + reason: RefundReason, + }, +} + +impl TryFrom<&ContextualizedEvent> for Event { + type Error = anyhow::Error; + + fn try_from(event: &ContextualizedEvent) -> Result { + match EventKind::try_from(event.event.kind.as_str())? { + EventKind::InboundTransfer => { + let pe = pb::EventInboundFungibleTokenTransfer::from_event(&event.event)?; + Ok(Self::InboundTransfer { + receiver: pe.receiver.ok_or(anyhow!("missing receiver"))?.try_into()?, + sender: pe.sender, + value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?, + }) + } + EventKind::OutboundTransfer => { + let pe = pb::EventOutboundFungibleTokenTransfer::from_event(&event.event)?; + Ok(Self::OutboundTransfer { + sender: pe.sender.ok_or(anyhow!("missing sender"))?.try_into()?, + receiver: pe.receiver, + value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?, + }) + } + EventKind::OutboundRefund => { + let pe = pb::EventOutboundFungibleTokenRefund::from_event(&event.event)?; + let reason = pe.reason(); + Ok(Self::OutboundRefund { + sender: pe.sender.ok_or(anyhow!("missing sender"))?.try_into()?, + receiver: pe.receiver, + value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?, + reason, + }) + } + } + } +} diff --git a/crates/bin/pindexer/src/lib.rs b/crates/bin/pindexer/src/lib.rs index 2e17584475..42e319d134 100644 --- a/crates/bin/pindexer/src/lib.rs +++ b/crates/bin/pindexer/src/lib.rs @@ -4,6 +4,7 @@ mod indexer_ext; pub use indexer_ext::IndexerExt; pub mod block; pub mod dex; +pub mod ibc; pub mod shielded_pool; mod sql; pub mod stake; From f600ccb2d8d159487634d88a601e602389199299 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 1 Oct 2024 14:03:41 -0700 Subject: [PATCH 3/4] pindexer: ibc: implement indexing for ibc transfers --- crates/bin/pindexer/src/ibc/ibc.sql | 4 +- crates/bin/pindexer/src/ibc/mod.rs | 116 ++++++++++++++++++++++++- crates/bin/pindexer/src/indexer_ext.rs | 1 + 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/crates/bin/pindexer/src/ibc/ibc.sql b/crates/bin/pindexer/src/ibc/ibc.sql index bc41d099ae..5a5b9e6989 100644 --- a/crates/bin/pindexer/src/ibc/ibc.sql +++ b/crates/bin/pindexer/src/ibc/ibc.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS ibc_transfer ( id SERIAL PRIMARY KEY, -- The AssetID of whatever is being transferred. - asset BYTE NOT NULL, + asset BYTEA NOT NULL, -- The amount being transf amount NUMERIC(39, 0) NOT NULL, -- The address on the penumbra side. @@ -11,5 +11,5 @@ CREATE TABLE IF NOT EXISTS ibc_transfer ( -- The address on the other side. foreign_addr TEXT NOT NULL, -- What kind of transfer this is. - kind TEXT NOT NULL CHECK (kind IN ('inbound', 'outbound', 'refund_timeout', 'refund_error')) + kind TEXT NOT NULL CHECK (kind IN ('inbound', 'outbound', 'refund_timeout', 'refund_error', 'refund_other')) ); diff --git a/crates/bin/pindexer/src/ibc/mod.rs b/crates/bin/pindexer/src/ibc/mod.rs index 3857da3435..dae1050fac 100644 --- a/crates/bin/pindexer/src/ibc/mod.rs +++ b/crates/bin/pindexer/src/ibc/mod.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use cometindex::ContextualizedEvent; +use cometindex::{async_trait, AppView, ContextualizedEvent, PgTransaction}; use penumbra_asset::Value; use penumbra_keys::Address; use penumbra_proto::{ @@ -8,6 +8,7 @@ use penumbra_proto::{ }, event::ProtoEvent as _, }; +use sqlx::PgPool; /// The kind of event we might care about. #[derive(Clone, Copy, Debug)] @@ -105,3 +106,116 @@ impl TryFrom<&ContextualizedEvent> for Event { } } } + +/// The database's view of a transfer. +#[derive(Debug)] +struct DatabaseTransfer { + penumbra_addr: Address, + foreign_addr: String, + negate: bool, + value: Value, + kind: &'static str, +} + +impl Event { + fn db_transfer(self) -> DatabaseTransfer { + match self { + Event::InboundTransfer { + receiver, + sender, + value, + } => DatabaseTransfer { + penumbra_addr: receiver, + foreign_addr: sender, + negate: false, + value, + kind: "inbound", + }, + Event::OutboundTransfer { + sender, + receiver, + value, + } => DatabaseTransfer { + penumbra_addr: sender, + foreign_addr: receiver, + negate: true, + value, + kind: "outbound", + }, + Event::OutboundRefund { + sender, + receiver, + value, + reason, + } => DatabaseTransfer { + penumbra_addr: sender, + foreign_addr: receiver, + negate: false, + value, + kind: match reason { + RefundReason::Unspecified => "refund_other", + RefundReason::Timeout => "refund_timeout", + RefundReason::Error => "refund_error", + }, + }, + } + } +} + +async fn init_db(dbtx: &mut PgTransaction<'_>) -> anyhow::Result<()> { + for statement in include_str!("ibc.sql").split(";") { + sqlx::query(statement).execute(dbtx.as_mut()).await?; + } + Ok(()) +} + +async fn create_transfer( + dbtx: &mut PgTransaction<'_>, + transfer: DatabaseTransfer, +) -> anyhow::Result<()> { + sqlx::query("INSERT INTO ibc_transfer VALUES (DEFAULT, $1, $6::NUMERIC(39, 0) * $2::NUMERIC(39, 0), $3, $4, $5)") + .bind(transfer.value.asset_id.to_bytes()) + .bind(transfer.value.amount.to_string()) + .bind(transfer.penumbra_addr.to_vec()) + .bind(transfer.foreign_addr) + .bind(transfer.kind) + .bind(if transfer.negate { -1i32 } else { 1i32 }) + .execute(dbtx.as_mut()) + .await?; + Ok(()) +} + +#[derive(Debug)] +pub struct Component {} + +impl Component { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl AppView for Component { + async fn init_chain( + &self, + dbtx: &mut PgTransaction, + _app_state: &serde_json::Value, + ) -> anyhow::Result<()> { + init_db(dbtx).await + } + + fn is_relevant(&self, type_str: &str) -> bool { + EventKind::try_from(type_str).is_ok() + } + + #[tracing::instrument(skip_all, fields(height = event.block_height, name = event.event.kind.as_str()))] + async fn index_event( + &self, + dbtx: &mut PgTransaction, + event: &ContextualizedEvent, + _src_db: &PgPool, + ) -> anyhow::Result<()> { + let transfer = Event::try_from(event)?.db_transfer(); + create_transfer(dbtx, transfer).await + } +} diff --git a/crates/bin/pindexer/src/indexer_ext.rs b/crates/bin/pindexer/src/indexer_ext.rs index 8ee1939318..6a7c5e8208 100644 --- a/crates/bin/pindexer/src/indexer_ext.rs +++ b/crates/bin/pindexer/src/indexer_ext.rs @@ -12,5 +12,6 @@ impl IndexerExt for cometindex::Indexer { .with_index(crate::governance::GovernanceProposals {}) .with_index(crate::dex::Component::new()) .with_index(crate::supply::Component::new()) + .with_index(crate::ibc::Component::new()) } } From 51e3cd312c2425b37d90137dfc1dd8e8a1ea810f Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Thu, 3 Oct 2024 16:03:54 -0700 Subject: [PATCH 4/4] pindexer: ibc: store height in transfers --- crates/bin/pindexer/src/ibc/ibc.sql | 2 ++ crates/bin/pindexer/src/ibc/mod.rs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bin/pindexer/src/ibc/ibc.sql b/crates/bin/pindexer/src/ibc/ibc.sql index 5a5b9e6989..66e6c40a5e 100644 --- a/crates/bin/pindexer/src/ibc/ibc.sql +++ b/crates/bin/pindexer/src/ibc/ibc.sql @@ -1,5 +1,7 @@ CREATE TABLE IF NOT EXISTS ibc_transfer ( id SERIAL PRIMARY KEY, + -- The height that this transfer happened at. + height BIGINT NOT NULL, -- The AssetID of whatever is being transferred. asset BYTEA NOT NULL, -- The amount being transf diff --git a/crates/bin/pindexer/src/ibc/mod.rs b/crates/bin/pindexer/src/ibc/mod.rs index dae1050fac..9bc6d795c4 100644 --- a/crates/bin/pindexer/src/ibc/mod.rs +++ b/crates/bin/pindexer/src/ibc/mod.rs @@ -171,15 +171,17 @@ async fn init_db(dbtx: &mut PgTransaction<'_>) -> anyhow::Result<()> { async fn create_transfer( dbtx: &mut PgTransaction<'_>, + height: u64, transfer: DatabaseTransfer, ) -> anyhow::Result<()> { - sqlx::query("INSERT INTO ibc_transfer VALUES (DEFAULT, $1, $6::NUMERIC(39, 0) * $2::NUMERIC(39, 0), $3, $4, $5)") + sqlx::query("INSERT INTO ibc_transfer VALUES (DEFAULT, $7, $1, $6::NUMERIC(39, 0) * $2::NUMERIC(39, 0), $3, $4, $5)") .bind(transfer.value.asset_id.to_bytes()) .bind(transfer.value.amount.to_string()) .bind(transfer.penumbra_addr.to_vec()) .bind(transfer.foreign_addr) .bind(transfer.kind) .bind(if transfer.negate { -1i32 } else { 1i32 }) + .bind(i64::try_from(height)?) .execute(dbtx.as_mut()) .await?; Ok(()) @@ -216,6 +218,6 @@ impl AppView for Component { _src_db: &PgPool, ) -> anyhow::Result<()> { let transfer = Event::try_from(event)?.db_transfer(); - create_transfer(dbtx, transfer).await + create_transfer(dbtx, event.block_height, transfer).await } }