pindexer/ibc/
mod.rs

1use anyhow::anyhow;
2use cometindex::{
3    async_trait,
4    index::{EventBatch, EventBatchContext},
5    AppView, ContextualizedEvent, PgTransaction,
6};
7use penumbra_sdk_asset::Value;
8use penumbra_sdk_keys::Address;
9use penumbra_sdk_proto::{
10    core::component::shielded_pool::v1::{
11        self as pb, event_outbound_fungible_token_refund::Reason as RefundReason,
12    },
13    event::ProtoEvent as _,
14};
15
16/// The kind of event we might care about.
17#[derive(Clone, Copy, Debug)]
18enum EventKind {
19    InboundTransfer,
20    OutboundTransfer,
21    OutboundRefund,
22}
23
24impl EventKind {
25    fn tag(&self) -> &'static str {
26        match self {
27            Self::InboundTransfer => {
28                "penumbra.core.component.shielded_pool.v1.EventInboundFungibleTokenTransfer"
29            }
30            Self::OutboundTransfer => {
31                "penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenTransfer"
32            }
33            Self::OutboundRefund => {
34                "penumbra.core.component.shielded_pool.v1.EventOutboundFungibleTokenRefund"
35            }
36        }
37    }
38}
39
40impl TryFrom<&str> for EventKind {
41    type Error = anyhow::Error;
42
43    fn try_from(value: &str) -> Result<Self, Self::Error> {
44        for kind in [
45            Self::InboundTransfer,
46            Self::OutboundTransfer,
47            Self::OutboundRefund,
48        ] {
49            if kind.tag() == value {
50                return Ok(kind);
51            }
52        }
53        Err(anyhow!("unexpected event kind: {value}"))
54    }
55}
56
57/// Represents the event data that we care about.
58#[derive(Debug, Clone)]
59enum Event {
60    InboundTransfer {
61        receiver: Address,
62        sender: String,
63        value: Value,
64    },
65    OutboundTransfer {
66        sender: Address,
67        receiver: String,
68        value: Value,
69    },
70    OutboundRefund {
71        sender: Address,
72        receiver: String,
73        value: Value,
74        reason: RefundReason,
75    },
76}
77
78impl TryFrom<ContextualizedEvent<'_>> for Event {
79    type Error = anyhow::Error;
80
81    fn try_from(event: ContextualizedEvent<'_>) -> Result<Self, Self::Error> {
82        match EventKind::try_from(event.event.kind.as_str())? {
83            EventKind::InboundTransfer => {
84                let pe = pb::EventInboundFungibleTokenTransfer::from_event(&event.event)?;
85                Ok(Self::InboundTransfer {
86                    receiver: pe.receiver.ok_or(anyhow!("missing receiver"))?.try_into()?,
87                    sender: pe.sender,
88                    value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?,
89                })
90            }
91            EventKind::OutboundTransfer => {
92                let pe = pb::EventOutboundFungibleTokenTransfer::from_event(&event.event)?;
93                Ok(Self::OutboundTransfer {
94                    sender: pe.sender.ok_or(anyhow!("missing sender"))?.try_into()?,
95                    receiver: pe.receiver,
96                    value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?,
97                })
98            }
99            EventKind::OutboundRefund => {
100                let pe = pb::EventOutboundFungibleTokenRefund::from_event(&event.event)?;
101                let reason = pe.reason();
102                Ok(Self::OutboundRefund {
103                    sender: pe.sender.ok_or(anyhow!("missing sender"))?.try_into()?,
104                    receiver: pe.receiver,
105                    value: pe.value.ok_or(anyhow!("missing value"))?.try_into()?,
106                    reason,
107                })
108            }
109        }
110    }
111}
112
113/// The database's view of a transfer.
114#[derive(Debug)]
115struct DatabaseTransfer {
116    penumbra_addr: Address,
117    foreign_addr: String,
118    negate: bool,
119    value: Value,
120    kind: &'static str,
121}
122
123impl Event {
124    fn db_transfer(self) -> DatabaseTransfer {
125        match self {
126            Event::InboundTransfer {
127                receiver,
128                sender,
129                value,
130            } => DatabaseTransfer {
131                penumbra_addr: receiver,
132                foreign_addr: sender,
133                negate: false,
134                value,
135                kind: "inbound",
136            },
137            Event::OutboundTransfer {
138                sender,
139                receiver,
140                value,
141            } => DatabaseTransfer {
142                penumbra_addr: sender,
143                foreign_addr: receiver,
144                negate: true,
145                value,
146                kind: "outbound",
147            },
148            Event::OutboundRefund {
149                sender,
150                receiver,
151                value,
152                reason,
153            } => DatabaseTransfer {
154                penumbra_addr: sender,
155                foreign_addr: receiver,
156                negate: false,
157                value,
158                kind: match reason {
159                    RefundReason::Unspecified => "refund_other",
160                    RefundReason::Timeout => "refund_timeout",
161                    RefundReason::Error => "refund_error",
162                },
163            },
164        }
165    }
166}
167
168async fn init_db(dbtx: &mut PgTransaction<'_>) -> anyhow::Result<()> {
169    for statement in include_str!("ibc.sql").split(";") {
170        sqlx::query(statement).execute(dbtx.as_mut()).await?;
171    }
172    Ok(())
173}
174
175async fn create_transfer(
176    dbtx: &mut PgTransaction<'_>,
177    height: u64,
178    transfer: DatabaseTransfer,
179) -> anyhow::Result<()> {
180    sqlx::query("INSERT INTO ibc_transfer VALUES (DEFAULT, $7, $1, $6::NUMERIC(39, 0) * $2::NUMERIC(39, 0), $3, $4, $5)")
181        .bind(transfer.value.asset_id.to_bytes())
182        .bind(transfer.value.amount.to_string())
183        .bind(transfer.penumbra_addr.to_vec())
184        .bind(transfer.foreign_addr)
185        .bind(transfer.kind)
186        .bind(if transfer.negate { -1i32 } else { 1i32 })
187        .bind(i64::try_from(height)?)
188        .execute(dbtx.as_mut())
189        .await?;
190    Ok(())
191}
192
193#[derive(Debug)]
194pub struct Component {}
195
196impl Component {
197    pub fn new() -> Self {
198        Self {}
199    }
200}
201
202#[async_trait]
203impl AppView for Component {
204    fn name(&self) -> String {
205        "ibc".to_string()
206    }
207
208    async fn init_chain(
209        &self,
210        dbtx: &mut PgTransaction,
211        _app_state: &serde_json::Value,
212    ) -> anyhow::Result<()> {
213        init_db(dbtx).await
214    }
215
216    async fn index_batch(
217        &self,
218        dbtx: &mut PgTransaction,
219        batch: EventBatch,
220        _ctx: EventBatchContext,
221    ) -> Result<(), anyhow::Error> {
222        for event in batch.events() {
223            let parsed = match Event::try_from(event) {
224                Ok(p) => p,
225                Err(_) => continue,
226            };
227            let transfer = parsed.db_transfer();
228            create_transfer(dbtx, event.block_height, transfer).await?;
229        }
230        Ok(())
231    }
232}