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#[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 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
134pub struct TradingPairVar {
136 pub asset_1: asset::AssetIdVar,
137 pub asset_2: asset::AssetIdVar,
138}
139
140impl TradingPairVar {
141 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 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 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 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
255impl 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#[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 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}