Skip to main content

polyoxide_core/
session_signer.rs

1//! Venue scopes a Deposit Wallet session key can be authorized for.
2//!
3//! Shared by `polyoxide-clob` (which lists them) and `polyoxide-relay` (which
4//! requests them). The venue adds venues over time, so an unknown string is
5//! kept as [`SessionSignerScope::Other`] rather than rejected.
6
7use std::fmt;
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11/// Whether a key acting for a Deposit Wallet is the wallet's owner or an
12/// authorized session key.
13///
14/// A session key's signatures, for orders and for relayer batches alike, are
15/// wrapped in an extra ERC-6492-style envelope naming the session signer; an
16/// owner's are not. Both are EOAs distinct from the wallet, so the role cannot
17/// be inferred from addresses and is stated explicitly.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum DepositWalletRole {
20    /// The EOA that owns the Deposit Wallet; the wallet's address is derived from this key.
21    Owner,
22    /// A key the owner authorized through `authorizeSessionSigner`; it can trade but not withdraw.
23    SessionKey,
24}
25
26/// A trading venue a session key may act on.
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum SessionSignerScope {
30    /// The central limit order book (`"CLOB"`).
31    Clob,
32    /// Combos RFQ trading (`"COMBOSRFQ"`).
33    CombosRfq,
34    /// Every current and future venue (`"ALL"`). Must be requested alone.
35    All,
36    /// A scope this crate does not know yet, kept verbatim.
37    ///
38    /// Build scopes with [`Self::from_wire`] or [`std::str::FromStr::from_str`],
39    /// not by constructing `Other` directly: `Other("ALL")` serialises like
40    /// [`Self::All`] but does not compare equal to it.
41    Other(String),
42}
43
44impl SessionSignerScope {
45    /// The wire spelling.
46    pub fn as_str(&self) -> &str {
47        match self {
48            Self::Clob => "CLOB",
49            Self::CombosRfq => "COMBOSRFQ",
50            Self::All => "ALL",
51            Self::Other(s) => s,
52        }
53    }
54
55    /// Parse a wire spelling; anything unrecognised becomes [`Self::Other`].
56    pub fn from_wire(s: &str) -> Self {
57        match s {
58            "CLOB" => Self::Clob,
59            "COMBOSRFQ" => Self::CombosRfq,
60            "ALL" => Self::All,
61            other => Self::Other(other.to_string()),
62        }
63    }
64}
65
66impl fmt::Display for SessionSignerScope {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(self.as_str())
69    }
70}
71
72impl std::str::FromStr for SessionSignerScope {
73    type Err = std::convert::Infallible;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        Ok(Self::from_wire(s))
77    }
78}
79
80impl Serialize for SessionSignerScope {
81    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
82        serializer.serialize_str(self.as_str())
83    }
84}
85
86impl<'de> Deserialize<'de> for SessionSignerScope {
87    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
88        let s = String::deserialize(deserializer)?;
89        Ok(Self::from_wire(&s))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn known_scopes_round_trip_their_wire_strings() {
99        for (scope, wire) in [
100            (SessionSignerScope::Clob, "\"CLOB\""),
101            (SessionSignerScope::CombosRfq, "\"COMBOSRFQ\""),
102            (SessionSignerScope::All, "\"ALL\""),
103        ] {
104            assert_eq!(serde_json::to_string(&scope).unwrap(), wire);
105            assert_eq!(
106                serde_json::from_str::<SessionSignerScope>(wire).unwrap(),
107                scope
108            );
109        }
110    }
111
112    #[test]
113    fn unknown_scopes_are_preserved_not_rejected() {
114        let scope: SessionSignerScope = serde_json::from_str("\"PERPS\"").unwrap();
115        assert_eq!(scope, SessionSignerScope::Other("PERPS".into()));
116        assert_eq!(serde_json::to_string(&scope).unwrap(), "\"PERPS\"");
117        assert_eq!(scope.to_string(), "PERPS");
118    }
119
120    #[test]
121    fn deposit_wallet_role_is_copy_and_distinguishes_the_two_roles() {
122        let owner = DepositWalletRole::Owner;
123        let copy = owner;
124        assert_eq!(owner, copy);
125        assert_ne!(DepositWalletRole::Owner, DepositWalletRole::SessionKey);
126    }
127}