penumbra_sdk_auction/auction/
dutch.rs

1use std::num::NonZeroU64;
2
3use anyhow::anyhow;
4use penumbra_sdk_asset::{asset, Value};
5use penumbra_sdk_dex::lp::position::{self};
6use penumbra_sdk_num::Amount;
7use penumbra_sdk_proto::{core::component::auction::v1 as pb, DomainType};
8use serde::{Deserialize, Serialize};
9
10use crate::auction::AuctionId;
11
12pub mod actions;
13pub use actions::{ActionDutchAuctionEnd, ActionDutchAuctionSchedule, ActionDutchAuctionWithdraw};
14
15pub const DUTCH_AUCTION_DOMAIN_SEP: &[u8] = b"penumbra_DA_nft";
16
17/// A deployed Dutch Auction, containing an immutable description
18/// and stateful data about its current state.
19#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
20#[serde(try_from = "pb::DutchAuction", into = "pb::DutchAuction")]
21pub struct DutchAuction {
22    pub description: DutchAuctionDescription,
23    pub state: DutchAuctionState,
24}
25
26/* Protobuf impls for `DutchAuction` */
27impl DomainType for DutchAuction {
28    type Proto = pb::DutchAuction;
29}
30
31impl From<DutchAuction> for pb::DutchAuction {
32    fn from(domain: DutchAuction) -> Self {
33        pb::DutchAuction {
34            description: Some(domain.description.into()),
35            state: Some(domain.state.into()),
36        }
37    }
38}
39
40impl TryFrom<pb::DutchAuction> for DutchAuction {
41    type Error = anyhow::Error;
42
43    fn try_from(msg: pb::DutchAuction) -> Result<Self, Self::Error> {
44        Ok(DutchAuction {
45            description: msg
46                .description
47                .ok_or_else(|| anyhow!("DutchAuction is missing description"))?
48                .try_into()?,
49            state: msg
50                .state
51                .ok_or_else(|| anyhow!("DutchAuction is missing a state field"))?
52                .try_into()?,
53        })
54    }
55}
56/* ********************************** */
57
58/// A description of the immutable parts of a dutch auction.
59#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
60#[serde(
61    try_from = "pb::DutchAuctionDescription",
62    into = "pb::DutchAuctionDescription"
63)]
64pub struct DutchAuctionDescription {
65    pub input: Value,
66    pub output_id: asset::Id,
67    pub max_output: Amount,
68    pub min_output: Amount,
69    pub start_height: u64,
70    pub end_height: u64,
71    pub step_count: u64,
72    pub nonce: [u8; 32],
73}
74
75impl DutchAuctionDescription {
76    /// Compute the unique identifier for the auction description.
77    pub fn id(&self) -> AuctionId {
78        let mut state = blake2b_simd::Params::default()
79            .personal(DUTCH_AUCTION_DOMAIN_SEP)
80            .to_state();
81
82        state.update(&self.nonce);
83        state.update(&self.input.asset_id.to_bytes());
84        state.update(&self.input.amount.to_le_bytes());
85        state.update(&self.max_output.to_le_bytes());
86        state.update(&self.start_height.to_le_bytes());
87        state.update(&self.end_height.to_le_bytes());
88        state.update(&self.step_count.to_le_bytes());
89
90        let hash = state.finalize();
91        let mut bytes = [0; 32];
92        bytes[0..32].copy_from_slice(&hash.as_bytes()[0..32]);
93        AuctionId(bytes)
94    }
95}
96
97/* Protobuf impls */
98impl DomainType for DutchAuctionDescription {
99    type Proto = pb::DutchAuctionDescription;
100}
101
102impl From<DutchAuctionDescription> for pb::DutchAuctionDescription {
103    fn from(domain: DutchAuctionDescription) -> Self {
104        Self {
105            input: Some(domain.input.into()),
106            output_id: Some(domain.output_id.into()),
107            max_output: Some(domain.max_output.into()),
108            min_output: Some(domain.min_output.into()),
109            start_height: domain.start_height,
110            end_height: domain.end_height,
111            step_count: domain.step_count,
112            nonce: domain.nonce.as_slice().to_vec(),
113        }
114    }
115}
116
117impl TryFrom<pb::DutchAuctionDescription> for DutchAuctionDescription {
118    type Error = anyhow::Error;
119
120    fn try_from(msg: pb::DutchAuctionDescription) -> Result<Self, Self::Error> {
121        let d = DutchAuctionDescription {
122            input: msg
123                .input
124                .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing input"))?
125                .try_into()?,
126            output_id: msg
127                .output_id
128                .ok_or_else(|| {
129                    anyhow!("DutchAuctionDescription message is missing an output identifier")
130                })?
131                .try_into()?,
132            max_output: msg
133                .max_output
134                .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing max output"))?
135                .try_into()?,
136            min_output: msg
137                .min_output
138                .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing min output"))?
139                .try_into()?,
140            start_height: msg.start_height,
141            end_height: msg.end_height,
142            step_count: msg.step_count,
143            nonce: msg.nonce.as_slice().try_into()?,
144        };
145        Ok(d)
146    }
147}
148/* ********************************** */
149
150/// A stateful description of a dutch auction, recording its state (via a sequence number),
151/// the current position id associated to it (if any), and its amount IO.
152/// # State
153/// We record the state of the dutch auction via an untyped `u64` instead of an enum.
154/// This futureproof support for auction types that have a richer state machine e.g. allows
155/// claiming a withdrawn auction multiple times, burning and minting a new withdrawn auction
156/// with an incremented sequence number.
157///
158/// For Dutch auctions:
159///
160///   ┌───┐            ┌───┐             ┌───┐
161///   │ 0 │───Closed──▶│ 1 │──Withdrawn─▶│ 2 │
162///   └───┘            └───┘             └───┘
163///     ▲                                     
164///     │                                     
165///  Opened                                   
166///     │                                     
167///
168#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
169#[serde(try_from = "pb::DutchAuctionState", into = "pb::DutchAuctionState")]
170pub struct DutchAuctionState {
171    pub sequence: u64,
172    pub current_position: Option<position::Id>,
173    pub next_trigger: Option<NonZeroU64>,
174    pub input_reserves: Amount,
175    pub output_reserves: Amount,
176}
177
178/* Protobuf impls for `DutchAuctionState` */
179impl DomainType for DutchAuctionState {
180    type Proto = pb::DutchAuctionState;
181}
182
183impl From<DutchAuctionState> for pb::DutchAuctionState {
184    fn from(domain: DutchAuctionState) -> Self {
185        Self {
186            seq: domain.sequence,
187            current_position: domain.current_position.map(Into::into),
188            next_trigger: domain.next_trigger.map_or(0u64, Into::into),
189            input_reserves: Some(domain.input_reserves.into()),
190            output_reserves: Some(domain.output_reserves.into()),
191        }
192    }
193}
194
195impl TryFrom<pb::DutchAuctionState> for DutchAuctionState {
196    type Error = anyhow::Error;
197
198    fn try_from(msg: pb::DutchAuctionState) -> Result<Self, Self::Error> {
199        Ok(DutchAuctionState {
200            sequence: msg.seq,
201            current_position: msg.current_position.map(TryInto::try_into).transpose()?,
202            next_trigger: NonZeroU64::new(msg.next_trigger),
203            input_reserves: msg
204                .input_reserves
205                .ok_or_else(|| anyhow!("DutchAuctionState message is missing input reserves"))?
206                .try_into()?,
207            output_reserves: msg
208                .output_reserves
209                .ok_or_else(|| anyhow!("DutchAuctionState message is missing output reserves"))?
210                .try_into()?,
211        })
212    }
213}
214/* ********************************** */