1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use ark_ff::Zero;
use decaf377::Fr;
use penumbra_asset::{balance, Balance, Value};
use penumbra_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
use serde::{Deserialize, Serialize};

use crate::{
    lp::{position, LpNft, Reserves},
    TradingPair,
};

use super::action::PositionWithdraw;

/// A planned [`PositionWithdraw`](PositionWithdraw).
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(
    try_from = "pb::PositionWithdrawPlan",
    into = "pb::PositionWithdrawPlan"
)]
pub struct PositionWithdrawPlan {
    pub reserves: Reserves,
    pub position_id: position::Id,
    pub pair: TradingPair,
    pub sequence: u64,
    pub rewards: Vec<Value>,
}

impl PositionWithdrawPlan {
    /// Convenience method to construct the [`PositionWithdraw`] described by this [`PositionWithdrawPlan`].
    pub fn position_withdraw(&self) -> PositionWithdraw {
        PositionWithdraw {
            position_id: self.position_id,
            reserves_commitment: self.reserves_commitment(),
            sequence: self.sequence,
        }
    }

    pub fn reserves_commitment(&self) -> balance::Commitment {
        let mut reserves_balance = self.reserves.balance(&self.pair);
        for reward in &self.rewards {
            reserves_balance += *reward;
        }
        reserves_balance.commit(Fr::zero())
    }

    pub fn balance(&self) -> Balance {
        // PositionWithdraw outputs will correspond to the final reserves
        // and a PositionWithdraw token.
        // Spends will be the PositionClose token.
        let mut balance = self.reserves.balance(&self.pair);

        // We consume a token of self.sequence-1 and produce one of self.sequence.
        // We treat -1 as "closed", the previous state.
        balance -= if self.sequence == 0 {
            Value {
                amount: 1u64.into(),
                asset_id: LpNft::new(self.position_id, position::State::Closed).asset_id(),
            }
        } else {
            Value {
                amount: 1u64.into(),
                asset_id: LpNft::new(
                    self.position_id,
                    position::State::Withdrawn {
                        sequence: self.sequence - 1,
                    },
                )
                .asset_id(),
            }
        };
        balance += Value {
            amount: 1u64.into(),
            asset_id: LpNft::new(
                self.position_id,
                position::State::Withdrawn {
                    sequence: self.sequence,
                },
            )
            .asset_id(),
        };

        balance
    }
}

impl DomainType for PositionWithdrawPlan {
    type Proto = pb::PositionWithdrawPlan;
}

impl From<PositionWithdrawPlan> for pb::PositionWithdrawPlan {
    fn from(msg: PositionWithdrawPlan) -> Self {
        Self {
            reserves: Some(msg.reserves.into()),
            position_id: Some(msg.position_id.into()),
            pair: Some(msg.pair.into()),
            sequence: msg.sequence,
            rewards: msg.rewards.into_iter().map(Into::into).collect(),
        }
    }
}

impl TryFrom<pb::PositionWithdrawPlan> for PositionWithdrawPlan {
    type Error = anyhow::Error;
    fn try_from(msg: pb::PositionWithdrawPlan) -> Result<Self, Self::Error> {
        Ok(Self {
            reserves: msg
                .reserves
                .ok_or_else(|| anyhow::anyhow!("missing reserves"))?
                .try_into()?,
            position_id: msg
                .position_id
                .ok_or_else(|| anyhow::anyhow!("missing position_id"))?
                .try_into()?,
            pair: msg
                .pair
                .ok_or_else(|| anyhow::anyhow!("missing pair"))?
                .try_into()?,
            sequence: msg.sequence,
            rewards: msg
                .rewards
                .into_iter()
                .map(TryInto::try_into)
                .collect::<Result<_, _>>()?,
        })
    }
}