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#[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#[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#[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}