penumbra_sdk_dex/
trading_pair.rs

1use anyhow::anyhow;
2use ark_ff::ToConstraintField;
3use ark_r1cs_std::prelude::{AllocVar, EqGadget};
4use ark_relations::r1cs::SynthesisError;
5use decaf377::Fq;
6use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
7use serde::{Deserialize, Serialize};
8use std::{
9    fmt::{self, Display, Formatter},
10    str::FromStr,
11};
12
13use penumbra_sdk_asset::asset::{self, AssetIdVar, Unit, REGISTRY};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
16#[serde(try_from = "pb::DirectedTradingPair", into = "pb::DirectedTradingPair")]
17pub struct DirectedTradingPair {
18    pub start: asset::Id,
19    pub end: asset::Id,
20}
21
22impl DirectedTradingPair {
23    pub fn new(start: asset::Id, end: asset::Id) -> Self {
24        Self { start, end }
25    }
26
27    pub fn to_canonical(&self) -> TradingPair {
28        TradingPair::new(self.start, self.end)
29    }
30
31    pub fn flip(&self) -> DirectedTradingPair {
32        DirectedTradingPair {
33            start: self.end,
34            end: self.start,
35        }
36    }
37}
38
39impl DomainType for DirectedTradingPair {
40    type Proto = pb::DirectedTradingPair;
41}
42
43impl From<DirectedTradingPair> for pb::DirectedTradingPair {
44    fn from(tp: DirectedTradingPair) -> Self {
45        Self {
46            start: Some(tp.start.into()),
47            end: Some(tp.end.into()),
48        }
49    }
50}
51
52impl TryFrom<pb::DirectedTradingPair> for DirectedTradingPair {
53    type Error = anyhow::Error;
54    fn try_from(tp: pb::DirectedTradingPair) -> anyhow::Result<Self> {
55        Ok(Self {
56            start: tp
57                .start
58                .ok_or_else(|| anyhow::anyhow!("missing directed trading pair start"))?
59                .try_into()?,
60            end: tp
61                .end
62                .ok_or_else(|| anyhow::anyhow!("missing directed trading pair end"))?
63                .try_into()?,
64        })
65    }
66}
67
68impl From<DirectedTradingPair> for TradingPair {
69    fn from(pair: DirectedTradingPair) -> Self {
70        pair.to_canonical()
71    }
72}
73
74/// The canonical representation of a tuple of asset [`Id`]s.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
76#[serde(try_from = "pb::TradingPair", into = "pb::TradingPair")]
77pub struct TradingPair {
78    pub(crate) asset_1: asset::Id,
79    pub(crate) asset_2: asset::Id,
80}
81
82impl TradingPair {
83    pub fn new(a: asset::Id, b: asset::Id) -> Self {
84        if a < b {
85            Self {
86                asset_1: a,
87                asset_2: b,
88            }
89        } else {
90            Self {
91                asset_1: b,
92                asset_2: a,
93            }
94        }
95    }
96
97    pub fn asset_1(&self) -> asset::Id {
98        self.asset_1
99    }
100
101    pub fn asset_2(&self) -> asset::Id {
102        self.asset_2
103    }
104
105    /// Convert the trading pair to bytes.
106    pub(crate) fn to_bytes(self) -> [u8; 64] {
107        let mut result: [u8; 64] = [0; 64];
108        result[0..32].copy_from_slice(&self.asset_1.0.to_bytes());
109        result[32..64].copy_from_slice(&self.asset_2.0.to_bytes());
110        result
111    }
112}
113
114impl TryFrom<[u8; 64]> for TradingPair {
115    type Error = anyhow::Error;
116    fn try_from(bytes: [u8; 64]) -> anyhow::Result<Self> {
117        let asset_1_bytes = &bytes[0..32];
118        let asset_2_bytes = &bytes[32..64];
119        let asset_1 = asset_1_bytes
120            .try_into()
121            .map_err(|_| anyhow::anyhow!("invalid asset_1 bytes in TradingPair"))?;
122        let asset_2 = asset_2_bytes
123            .try_into()
124            .map_err(|_| anyhow::anyhow!("invalid asset_2 bytes in TradingPair"))?;
125        let trading_pair = TradingPair::new(asset_1, asset_2);
126        let result = Self { asset_1, asset_2 };
127        if trading_pair != result {
128            anyhow::bail!("non-canonical trading pair");
129        }
130        Ok(result)
131    }
132}
133
134/// Represents a trading pair in R1CS.
135pub struct TradingPairVar {
136    pub asset_1: asset::AssetIdVar,
137    pub asset_2: asset::AssetIdVar,
138}
139
140impl TradingPairVar {
141    /// This allocates a `TradingPairVar` without checking it is canonically ordered.
142    ///
143    /// # Safety
144    ///
145    /// Does _not_ ensure that asset_1 <= asset_2, as is the case for `TradingPair`.
146    pub fn new_variable_unchecked(
147        cs: impl Into<ark_relations::r1cs::Namespace<Fq>>,
148        f: impl FnOnce() -> Result<TradingPair, SynthesisError>,
149        mode: ark_r1cs_std::prelude::AllocationMode,
150    ) -> Result<Self, SynthesisError> {
151        let ns = cs.into();
152        let cs = ns.cs();
153        let trading_pair = f()?;
154        let asset_1 = AssetIdVar::new_variable(cs.clone(), || Ok(trading_pair.asset_1()), mode)?;
155        let asset_2 = AssetIdVar::new_variable(cs, || Ok(trading_pair.asset_2()), mode)?;
156        // Note: We do not check that the trading pair is canonically encoded.
157        Ok(Self { asset_1, asset_2 })
158    }
159}
160
161impl ToConstraintField<Fq> for TradingPair {
162    fn to_field_elements(&self) -> Option<Vec<Fq>> {
163        let mut public_inputs = Vec::new();
164        public_inputs.extend(
165            Fq::from(self.asset_1().0)
166                .to_field_elements()
167                .expect("Fq types are Bls12-377 field members"),
168        );
169        public_inputs.extend(
170            Fq::from(self.asset_2().0)
171                .to_field_elements()
172                .expect("Fq types are Bls12-377 field members"),
173        );
174        Some(public_inputs)
175    }
176}
177
178impl EqGadget<Fq> for TradingPairVar {
179    fn is_eq(&self, other: &Self) -> Result<ark_r1cs_std::prelude::Boolean<Fq>, SynthesisError> {
180        let asset_1_eq = self.asset_1.is_eq(&other.asset_1);
181        let asset_2_eq = self.asset_2.is_eq(&other.asset_2);
182        asset_1_eq.and(asset_2_eq)
183    }
184}
185
186impl DomainType for TradingPair {
187    type Proto = pb::TradingPair;
188}
189
190impl From<TradingPair> for pb::TradingPair {
191    fn from(tp: TradingPair) -> Self {
192        Self {
193            asset_1: Some(tp.asset_1.into()),
194            asset_2: Some(tp.asset_2.into()),
195        }
196    }
197}
198
199impl TryFrom<pb::TradingPair> for TradingPair {
200    type Error = anyhow::Error;
201    fn try_from(tp: pb::TradingPair) -> anyhow::Result<Self> {
202        let asset_1 = tp
203            .asset_1
204            .ok_or_else(|| anyhow::anyhow!("missing trading pair asset1"))?
205            .try_into()?;
206        let asset_2 = tp
207            .asset_2
208            .ok_or_else(|| anyhow::anyhow!("missing trading pair asset2"))?
209            .try_into()?;
210        let trading_pair = TradingPair::new(asset_1, asset_2);
211        Ok(trading_pair)
212    }
213}
214
215impl FromStr for TradingPair {
216    type Err = anyhow::Error;
217
218    /// Takes an input of the form DENOM1:DENOM2,
219    /// splits on the `:` (erroring if there is more than one `:`),
220    /// parses the first and second halves using `asset::REGISTRY.parse_unit`,
221    /// then computes the asset IDs and then the canonically-ordered trading pair.
222    fn from_str(s: &str) -> anyhow::Result<Self> {
223        let parts: Vec<&str> = s.split(':').collect();
224
225        if parts.len() != 2 {
226            Err(anyhow!("invalid trading pair string"))
227        } else {
228            let denom_1 = REGISTRY.parse_unit(parts[0]);
229            let denom_2 = REGISTRY.parse_unit(parts[1]);
230            Ok(Self::new(denom_1.id(), denom_2.id()))
231        }
232    }
233}
234
235impl FromStr for DirectedTradingPair {
236    type Err = anyhow::Error;
237
238    /// Takes an input of the form DENOM1:DENOM2,
239    /// splits on the `:` (erroring if there is more than one `:`),
240    /// parses the first and second halves using `asset::REGISTRY.parse_unit`,
241    /// then computes the asset IDs and then the canonically-ordered trading pair.
242    fn from_str(s: &str) -> anyhow::Result<Self> {
243        let parts: Vec<&str> = s.split(':').collect();
244
245        if parts.len() != 2 {
246            Err(anyhow!("invalid trading pair string"))
247        } else {
248            let denom_1 = REGISTRY.parse_unit(parts[0]);
249            let denom_2 = REGISTRY.parse_unit(parts[1]);
250            Ok(Self::new(denom_1.id(), denom_2.id()))
251        }
252    }
253}
254
255/// Produce an output string of the form ASSET_ID1:ASSET_ID2
256/// TODO: this mismatches the `FromStr` impl which uses denominations.
257/// The asset ID is more canonical than a base denom so I think that's okay,
258/// but the `FromStr` impl should probably be able to handle asset IDs as well.
259impl fmt::Display for TradingPair {
260    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261        write!(f, "{}:{}", self.asset_1, self.asset_2)
262    }
263}
264
265/// A directed tuple of `Unit`s, similar to a `DirectedTradingPair` but embedding
266/// useful denom data.
267#[derive(Clone, Debug)]
268pub struct DirectedUnitPair {
269    pub start: Unit,
270    pub end: Unit,
271}
272
273impl DirectedUnitPair {
274    pub fn new(start: Unit, end: Unit) -> Self {
275        Self { start, end }
276    }
277
278    pub fn to_canonical_string(&self) -> String {
279        if self.match_canonical_ordering() {
280            self.to_string()
281        } else {
282            self.flip().to_string()
283        }
284    }
285
286    pub fn match_canonical_ordering(&self) -> bool {
287        self.start.id() > self.end.id()
288    }
289
290    pub fn into_directed_trading_pair(&self) -> DirectedTradingPair {
291        DirectedTradingPair {
292            start: self.start.id(),
293            end: self.end.id(),
294        }
295    }
296
297    pub fn flip(&self) -> Self {
298        DirectedUnitPair {
299            start: self.end.clone(),
300            end: self.start.clone(),
301        }
302    }
303}
304
305impl FromStr for DirectedUnitPair {
306    type Err = anyhow::Error;
307
308    /// Takes an input of the form DENOM1:DENOM2,
309    /// splits on the `:` (erroring if there is more than one `:`),
310    /// parses the first and second halves using `asset::REGISTRY.parse_unit`,
311    /// then forms a `Market` i.e. which is a directed tuple of `Units`.
312    fn from_str(s: &str) -> anyhow::Result<Self> {
313        let parts: Vec<&str> = s.split(':').collect();
314
315        if parts.len() != 2 {
316            Err(anyhow!("invalid market string"))
317        } else {
318            let denom_1 = REGISTRY.parse_unit(parts[0]);
319            let denom_2 = REGISTRY.parse_unit(parts[1]);
320            Ok(Self::new(denom_1, denom_2))
321        }
322    }
323}
324
325impl Display for DirectedUnitPair {
326    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
327        write!(f, "{}:{}", self.start, self.end)
328    }
329}