penumbra_sdk_dex/lp/
action.rs

1use serde::{Deserialize, Serialize};
2
3use penumbra_sdk_asset::{balance, Balance, Value};
4use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
5use penumbra_sdk_txhash::{EffectHash, EffectingData};
6
7use super::{position, position::Position, LpNft};
8
9/// A transaction action that opens a new position.
10///
11/// This action's contribution to the transaction's value balance is to consume
12/// the initial reserves and contribute an opened position NFT.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(try_from = "pb::PositionOpen", into = "pb::PositionOpen")]
15pub struct PositionOpen {
16    /// Contains the data defining the position, sufficient to compute its `PositionId`.
17    ///
18    /// Positions are immutable, so the `PositionData` (and hence the `PositionId`)
19    /// are unchanged over the entire lifetime of the position.
20    pub position: Position,
21}
22
23impl EffectingData for PositionOpen {
24    fn effect_hash(&self) -> EffectHash {
25        // The position open action consists only of the position, which
26        // we consider effecting data.
27        EffectHash::from_proto_effecting_data(&self.to_proto())
28    }
29}
30
31impl PositionOpen {
32    /// Compute a commitment to the value this action contributes to its transaction.
33    pub fn balance(&self) -> Balance {
34        let opened_position_nft = Value {
35            amount: 1u64.into(),
36            asset_id: LpNft::new(self.position.id(), position::State::Opened).asset_id(),
37        };
38
39        let reserves = self.position.reserves.balance(&self.position.phi.pair);
40
41        // The action consumes the reserves and produces an LP NFT
42        Balance::from(opened_position_nft) - reserves
43    }
44}
45
46/// A transaction action that closes a position.
47///
48/// This action's contribution to the transaction's value balance is to consume
49/// an opened position NFT and contribute a closed position NFT.
50///
51/// Closing a position does not immediately withdraw funds, because Penumbra
52/// transactions (like any ZK transaction model) are early-binding: the prover
53/// must know the state transition they prove knowledge of, and they cannot know
54/// the final reserves with certainty until after the position has been deactivated.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(try_from = "pb::PositionClose", into = "pb::PositionClose")]
57pub struct PositionClose {
58    pub position_id: position::Id,
59}
60
61impl EffectingData for PositionClose {
62    fn effect_hash(&self) -> EffectHash {
63        EffectHash::from_proto_effecting_data(&self.to_proto())
64    }
65}
66
67impl PositionClose {
68    /// Compute the value this action contributes to its transaction.
69    pub fn balance(&self) -> Balance {
70        let opened_position_nft = Value {
71            amount: 1u64.into(),
72            asset_id: LpNft::new(self.position_id, position::State::Opened).asset_id(),
73        };
74
75        let closed_position_nft = Value {
76            amount: 1u64.into(),
77            asset_id: LpNft::new(self.position_id, position::State::Closed).asset_id(),
78        };
79
80        // The action consumes an opened position and produces a closed position.
81        Balance::from(closed_position_nft) - opened_position_nft
82    }
83}
84
85/// A transaction action that withdraws funds from a closed position.
86///
87/// This action's contribution to the transaction's value balance is to consume a
88/// closed position NFT and contribute a withdrawn position NFT, as well as all
89/// of the funds that were in the position at the time of closing.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(try_from = "pb::PositionWithdraw", into = "pb::PositionWithdraw")]
92pub struct PositionWithdraw {
93    pub position_id: position::Id,
94    /// A transparent (zero blinding factor) commitment to the position's final reserves and fees.
95    ///
96    /// The chain will check this commitment by recomputing it with the on-chain state.
97    pub reserves_commitment: balance::Commitment,
98    /// The sequence number of the withdrawal, allowing multiple withdrawals from the same position.
99    pub sequence: u64,
100}
101
102impl EffectingData for PositionWithdraw {
103    fn effect_hash(&self) -> EffectHash {
104        EffectHash::from_proto_effecting_data(&self.to_proto())
105    }
106}
107
108impl DomainType for PositionOpen {
109    type Proto = pb::PositionOpen;
110}
111
112impl From<PositionOpen> for pb::PositionOpen {
113    fn from(value: PositionOpen) -> Self {
114        Self {
115            position: Some(value.position.into()),
116        }
117    }
118}
119
120impl TryFrom<pb::PositionOpen> for PositionOpen {
121    type Error = anyhow::Error;
122
123    fn try_from(value: pb::PositionOpen) -> Result<Self, Self::Error> {
124        Ok(Self {
125            position: value
126                .position
127                .ok_or_else(|| anyhow::anyhow!("missing position"))?
128                .try_into()?,
129        })
130    }
131}
132
133impl DomainType for PositionClose {
134    type Proto = pb::PositionClose;
135}
136
137impl From<PositionClose> for pb::PositionClose {
138    fn from(value: PositionClose) -> Self {
139        Self {
140            position_id: Some(value.position_id.into()),
141        }
142    }
143}
144
145impl TryFrom<pb::PositionClose> for PositionClose {
146    type Error = anyhow::Error;
147
148    fn try_from(value: pb::PositionClose) -> Result<Self, Self::Error> {
149        Ok(Self {
150            position_id: value
151                .position_id
152                .ok_or_else(|| anyhow::anyhow!("missing position_id"))?
153                .try_into()?,
154        })
155    }
156}
157
158impl DomainType for PositionWithdraw {
159    type Proto = pb::PositionWithdraw;
160}
161
162impl From<PositionWithdraw> for pb::PositionWithdraw {
163    fn from(value: PositionWithdraw) -> Self {
164        Self {
165            position_id: Some(value.position_id.into()),
166            reserves_commitment: Some(value.reserves_commitment.into()),
167            sequence: value.sequence,
168        }
169    }
170}
171
172impl TryFrom<pb::PositionWithdraw> for PositionWithdraw {
173    type Error = anyhow::Error;
174
175    fn try_from(value: pb::PositionWithdraw) -> Result<Self, Self::Error> {
176        Ok(Self {
177            position_id: value
178                .position_id
179                .ok_or_else(|| anyhow::anyhow!("missing position_id"))?
180                .try_into()?,
181            reserves_commitment: value
182                .reserves_commitment
183                .ok_or_else(|| anyhow::anyhow!("missing balance_commitment"))?
184                .try_into()?,
185            sequence: value.sequence,
186        })
187    }
188}