penumbra_sdk_dex/lp/
plan.rs

1use ark_ff::Zero;
2use decaf377::Fr;
3use penumbra_sdk_asset::{balance, Balance, Value};
4use penumbra_sdk_keys::{
5    keys::FullViewingKey, symmetric::POSITION_METADATA_NONCE_SIZE_BYTES, PositionMetadataKey,
6};
7use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    lp::{metadata::PositionMetadata, position, position::Position, LpNft, Reserves},
12    TradingPair,
13};
14
15use super::action::{PositionOpen, PositionWithdraw};
16
17/// A planned [`PositionOpen`](PositionOpen).
18#[derive(Clone, Debug, Deserialize, Serialize)]
19#[serde(try_from = "pb::PositionOpenPlan", into = "pb::PositionOpenPlan")]
20pub struct PositionOpenPlan {
21    pub position: Position,
22    pub metadata: Option<PositionMetadata>,
23}
24
25impl PositionOpenPlan {
26    /// Convenience method to construct the [`PositionOpen`] described by this [`PositionOpenPlan`].
27    ///
28    /// If the nonce is not provided, it will be derived from the position id.
29    pub fn position_open(
30        &self,
31        fvk: &FullViewingKey,
32        nonce: Option<&[u8; POSITION_METADATA_NONCE_SIZE_BYTES]>,
33    ) -> PositionOpen {
34        let pmk = PositionMetadataKey::derive(fvk.outgoing());
35        let nonce = nonce.copied().unwrap_or_else(|| {
36            let out: [u8; POSITION_METADATA_NONCE_SIZE_BYTES] = self.position.id().0
37                [..POSITION_METADATA_NONCE_SIZE_BYTES]
38                .try_into()
39                .expect("position id is 32 bytes");
40            out
41        });
42        let encrypted_metadata = self.metadata.map(|m| m.encrypt(&pmk, &nonce));
43        PositionOpen {
44            position: self.position.clone(),
45            encrypted_metadata,
46        }
47    }
48
49    pub fn balance(&self) -> Balance {
50        let opened_position_nft = Value {
51            amount: 1u64.into(),
52            asset_id: LpNft::new(self.position.id(), position::State::Opened).asset_id(),
53        };
54
55        let reserves = self.position.reserves.balance(&self.position.phi.pair);
56
57        // The action consumes the reserves and produces an LP NFT
58        Balance::from(opened_position_nft) - reserves
59    }
60}
61
62impl DomainType for PositionOpenPlan {
63    type Proto = pb::PositionOpenPlan;
64}
65
66impl From<PositionOpenPlan> for pb::PositionOpenPlan {
67    fn from(msg: PositionOpenPlan) -> Self {
68        Self {
69            position: Some(msg.position.into()),
70            metadata: msg.metadata.map(|x| x.into()),
71        }
72    }
73}
74
75impl TryFrom<pb::PositionOpenPlan> for PositionOpenPlan {
76    type Error = anyhow::Error;
77    fn try_from(msg: pb::PositionOpenPlan) -> Result<Self, Self::Error> {
78        Ok(Self {
79            position: msg
80                .position
81                .ok_or_else(|| anyhow::anyhow!("missing position"))?
82                .try_into()?,
83            metadata: msg.metadata.map(|x| x.try_into()).transpose()?,
84        })
85    }
86}
87
88/// A planned [`PositionWithdraw`](PositionWithdraw).
89#[derive(Clone, Debug, Deserialize, Serialize)]
90#[serde(
91    try_from = "pb::PositionWithdrawPlan",
92    into = "pb::PositionWithdrawPlan"
93)]
94pub struct PositionWithdrawPlan {
95    pub reserves: Reserves,
96    pub position_id: position::Id,
97    pub pair: TradingPair,
98    pub sequence: u64,
99    pub rewards: Vec<Value>,
100}
101
102impl PositionWithdrawPlan {
103    /// Convenience method to construct the [`PositionWithdraw`] described by this [`PositionWithdrawPlan`].
104    pub fn position_withdraw(&self) -> PositionWithdraw {
105        PositionWithdraw {
106            position_id: self.position_id,
107            reserves_commitment: self.reserves_commitment(),
108            sequence: self.sequence,
109        }
110    }
111
112    pub fn reserves_commitment(&self) -> balance::Commitment {
113        let mut reserves_balance = self.reserves.balance(&self.pair);
114        for reward in &self.rewards {
115            reserves_balance += *reward;
116        }
117        reserves_balance.commit(Fr::zero())
118    }
119
120    pub fn balance(&self) -> Balance {
121        // PositionWithdraw outputs will correspond to the final reserves
122        // and a PositionWithdraw token.
123        // Spends will be the PositionClose token.
124        let mut balance = self.reserves.balance(&self.pair);
125
126        // We consume a token of self.sequence-1 and produce one of self.sequence.
127        // We treat -1 as "closed", the previous state.
128        balance -= if self.sequence == 0 {
129            Value {
130                amount: 1u64.into(),
131                asset_id: LpNft::new(self.position_id, position::State::Closed).asset_id(),
132            }
133        } else {
134            Value {
135                amount: 1u64.into(),
136                asset_id: LpNft::new(
137                    self.position_id,
138                    position::State::Withdrawn {
139                        sequence: self.sequence - 1,
140                    },
141                )
142                .asset_id(),
143            }
144        };
145        balance += Value {
146            amount: 1u64.into(),
147            asset_id: LpNft::new(
148                self.position_id,
149                position::State::Withdrawn {
150                    sequence: self.sequence,
151                },
152            )
153            .asset_id(),
154        };
155
156        balance
157    }
158}
159
160impl DomainType for PositionWithdrawPlan {
161    type Proto = pb::PositionWithdrawPlan;
162}
163
164impl From<PositionWithdrawPlan> for pb::PositionWithdrawPlan {
165    fn from(msg: PositionWithdrawPlan) -> Self {
166        Self {
167            reserves: Some(msg.reserves.into()),
168            position_id: Some(msg.position_id.into()),
169            pair: Some(msg.pair.into()),
170            sequence: msg.sequence,
171            rewards: msg.rewards.into_iter().map(Into::into).collect(),
172        }
173    }
174}
175
176impl TryFrom<pb::PositionWithdrawPlan> for PositionWithdrawPlan {
177    type Error = anyhow::Error;
178    fn try_from(msg: pb::PositionWithdrawPlan) -> Result<Self, Self::Error> {
179        Ok(Self {
180            reserves: msg
181                .reserves
182                .ok_or_else(|| anyhow::anyhow!("missing reserves"))?
183                .try_into()?,
184            position_id: msg
185                .position_id
186                .ok_or_else(|| anyhow::anyhow!("missing position_id"))?
187                .try_into()?,
188            pair: msg
189                .pair
190                .ok_or_else(|| anyhow::anyhow!("missing pair"))?
191                .try_into()?,
192            sequence: msg.sequence,
193            rewards: msg
194                .rewards
195                .into_iter()
196                .map(TryInto::try_into)
197                .collect::<Result<_, _>>()?,
198        })
199    }
200}