penumbra_sdk_shielded_pool/
ics20_withdrawal.rs1use ibc_types::core::{channel::ChannelId, channel::PortId, client::Height as IbcHeight};
2use penumbra_sdk_asset::{
3 asset::{self, Metadata},
4 Balance, Value,
5};
6use penumbra_sdk_keys::Address;
7use penumbra_sdk_num::Amount;
8use penumbra_sdk_proto::{
9 penumbra::core::component::ibc::v1::{self as pb, FungibleTokenPacketData},
10 DomainType,
11};
12use penumbra_sdk_txhash::{EffectHash, EffectingData};
13use serde::{Deserialize, Serialize};
14use std::str::FromStr;
15
16#[cfg(feature = "component")]
17use penumbra_sdk_ibc::component::packet::{IBCPacket, Unchecked};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(try_from = "pb::Ics20Withdrawal", into = "pb::Ics20Withdrawal")]
21pub struct Ics20Withdrawal {
22 pub amount: Amount,
24 pub denom: asset::Metadata,
25 pub destination_chain_address: String,
27 pub return_address: Address,
30 pub timeout_height: IbcHeight,
36 pub timeout_time: u64,
38 pub source_channel: ChannelId,
40
41 pub use_compat_address: bool,
44
45 pub ics20_memo: String,
49 pub use_transparent_address: bool,
51}
52
53#[cfg(feature = "component")]
54impl From<Ics20Withdrawal> for IBCPacket<Unchecked> {
55 fn from(withdrawal: Ics20Withdrawal) -> Self {
56 Self::new(
57 PortId::transfer(),
58 withdrawal.source_channel.clone(),
59 withdrawal.timeout_height,
60 withdrawal.timeout_time,
61 withdrawal.packet_data(),
62 )
63 }
64}
65
66impl Ics20Withdrawal {
67 pub fn value(&self) -> Value {
68 Value {
69 amount: self.amount,
70 asset_id: self.denom.id(),
71 }
72 }
73
74 pub fn balance(&self) -> Balance {
75 -Balance::from(self.value())
76 }
77
78 pub fn packet_data(&self) -> Vec<u8> {
79 let ftpd: FungibleTokenPacketData = self.clone().into();
80
81 serde_json::to_vec(&ftpd).expect("can serialize FungibleTokenPacketData as JSON")
83 }
84
85 pub fn validate(&self) -> anyhow::Result<()> {
87 if self.timeout_time == 0 {
88 anyhow::bail!("timeout time must be non-zero");
89 }
90
91 if self.timeout_time % 60_000_000_000 != 0 {
94 anyhow::bail!(
95 "withdrawal timeout timestamp {} is not rounded to one minute",
96 self.timeout_time
97 );
98 }
99
100 Ok(())
104 }
105}
106
107impl EffectingData for Ics20Withdrawal {
108 fn effect_hash(&self) -> EffectHash {
109 EffectHash::from_proto_effecting_data(&self.to_proto())
110 }
111}
112
113impl DomainType for Ics20Withdrawal {
114 type Proto = pb::Ics20Withdrawal;
115}
116
117#[allow(deprecated)]
118impl From<Ics20Withdrawal> for pb::Ics20Withdrawal {
119 fn from(w: Ics20Withdrawal) -> Self {
120 pb::Ics20Withdrawal {
121 amount: Some(w.amount.into()),
122 denom: Some(w.denom.base_denom().into()),
123 destination_chain_address: w.destination_chain_address,
124 return_address: Some(w.return_address.into()),
125 timeout_height: Some(w.timeout_height.into()),
126 timeout_time: w.timeout_time,
127 source_channel: w.source_channel.to_string(),
128 use_compat_address: w.use_compat_address,
129 ics20_memo: w.ics20_memo.to_string(),
130 use_transparent_address: w.use_transparent_address,
131 }
132 }
133}
134
135#[allow(deprecated)]
136impl TryFrom<pb::Ics20Withdrawal> for Ics20Withdrawal {
137 type Error = anyhow::Error;
138 fn try_from(s: pb::Ics20Withdrawal) -> Result<Self, Self::Error> {
139 Ok(Self {
140 amount: s
141 .amount
142 .ok_or_else(|| anyhow::anyhow!("missing amount"))?
143 .try_into()?,
144 denom: Metadata::default_for(
145 &s.denom
146 .ok_or_else(|| anyhow::anyhow!("missing denom metadata"))?
147 .try_into()?,
148 )
149 .ok_or_else(|| anyhow::anyhow!("could not generate default denom metadata"))?,
150 destination_chain_address: s.destination_chain_address,
151 return_address: s
152 .return_address
153 .ok_or_else(|| anyhow::anyhow!("missing sender"))?
154 .try_into()?,
155 timeout_height: s
156 .timeout_height
157 .ok_or_else(|| anyhow::anyhow!("missing timeout height"))?
158 .try_into()?,
159 timeout_time: s.timeout_time,
160 source_channel: ChannelId::from_str(&s.source_channel)?,
161 use_compat_address: s.use_compat_address,
162 ics20_memo: s.ics20_memo,
163 use_transparent_address: s.use_transparent_address,
164 })
165 }
166}
167
168impl From<Ics20Withdrawal> for pb::FungibleTokenPacketData {
169 fn from(w: Ics20Withdrawal) -> Self {
170 let ordinary_return_address = w.return_address.to_string();
171
172 let return_address = if w.use_transparent_address {
173 w.return_address
174 .encode_as_transparent_address()
175 .unwrap_or_else(|| ordinary_return_address)
176 } else {
177 ordinary_return_address
178 };
179
180 pb::FungibleTokenPacketData {
181 amount: w.value().amount.to_string(),
182 denom: w.denom.to_string(),
183 receiver: w.destination_chain_address,
184 sender: return_address,
185 memo: w.ics20_memo,
186 }
187 }
188}