architect_api/accounts/
account.rs

1//! Accounts represent physical external accounts, mapped by the cpty
2//! (only partially user-defined, when the cpty can't disambiguate).
3//! There isn't a dichotomy between "internal" and "external" accounts--
4//! internal Architect subaccounting should be accomplished via Labels,
5//! and account re-labeling or grouping should be done via AccountGroups.
6//!
7//! If a mislabeling occurs, e.g. use a set of credentials that claim to
8//! map to the same account, but don't in actuality, reconciliation
9//! errors will be raised by Folio.
10
11use crate::{json_schema_is_string, Str};
12use anyhow::{bail, Result};
13use derive_more::{Deref, Display};
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use std::str::FromStr;
17use uuid::Uuid;
18
19pub type AccountId = Uuid;
20
21#[derive(
22    Debug,
23    Display,
24    Deref,
25    Clone,
26    Copy,
27    PartialEq,
28    Eq,
29    PartialOrd,
30    Ord,
31    Serialize,
32    Deserialize,
33)]
34#[cfg_attr(feature = "graphql", derive(juniper::GraphQLScalar))]
35#[cfg_attr(feature = "graphql", graphql(transparent))]
36pub struct AccountName(Str);
37
38json_schema_is_string!(AccountName);
39
40impl AccountName {
41    /// Constructor that codifies some attempt at standard naming convention
42    pub fn new(
43        cpty_name: impl AsRef<str>,
44        cpty_account_id: impl AsRef<str>,
45    ) -> Result<Self> {
46        let name = format!("{}:{}", cpty_name.as_ref(), cpty_account_id.as_ref());
47        Ok(Self(Str::try_from(name)?))
48    }
49
50    pub fn cpty_name(&self) -> Option<&str> {
51        self.0.split_once(':').map(|(c, _)| c)
52    }
53
54    pub fn cpty_account_id(&self) -> Option<&str> {
55        self.0.split_once(':').map(|(_, c)| c)
56    }
57}
58
59impl FromStr for AccountName {
60    type Err = anyhow::Error;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        if s.contains(':') {
64            Ok(Self(Str::try_from(s)?))
65        } else {
66            bail!("invalid account name: {}", s);
67        }
68    }
69}
70
71#[cfg(feature = "postgres-types")]
72impl postgres_types::ToSql for AccountName {
73    postgres_types::to_sql_checked!();
74
75    fn to_sql(
76        &self,
77        ty: &postgres_types::Type,
78        out: &mut bytes::BytesMut,
79    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
80        self.0.as_str().to_sql(ty, out)
81    }
82
83    fn accepts(ty: &postgres_types::Type) -> bool {
84        String::accepts(ty)
85    }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
89#[serde(untagged)]
90pub enum AccountIdOrName {
91    Id(AccountId),
92    Name(AccountName),
93}
94
95json_schema_is_string!(AccountIdOrName);
96
97impl std::str::FromStr for AccountIdOrName {
98    type Err = anyhow::Error;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        if let Ok(id) = AccountId::from_str(s) {
102            Ok(Self::Id(id))
103        } else {
104            Ok(Self::Name(AccountName::from_str(s)?))
105        }
106    }
107}
108
109#[derive(
110    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
111)]
112#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
113pub struct Account {
114    pub id: AccountId,
115    pub name: AccountName,
116}
117
118pub trait AsAccount {
119    fn as_account(&self) -> Account;
120}
121
122/// Set of flags for account permissions
123#[derive(
124    Debug,
125    Default,
126    Clone,
127    Copy,
128    Serialize,
129    Deserialize,
130    PartialEq,
131    Eq,
132    PartialOrd,
133    Ord,
134    JsonSchema,
135)]
136#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
137pub struct AccountPermissions {
138    pub list: bool,            // know about the account's existence
139    pub view: bool,            // know the account's holdings and activity
140    pub trade: bool,           // trade on the account, any position effect
141    pub reduce_or_close: bool, // trade on the account only if reducing or closing
142    pub set_limits: bool,      // set limits on the account
143}
144
145impl AccountPermissions {
146    pub fn all() -> Self {
147        Self {
148            list: true,
149            view: true,
150            trade: true,
151            reduce_or_close: true,
152            set_limits: true,
153        }
154    }
155
156    pub fn none() -> Self {
157        Self {
158            list: false,
159            view: false,
160            trade: false,
161            reduce_or_close: false,
162            set_limits: false,
163        }
164    }
165
166    pub fn is_none(&self) -> bool {
167        !self.list
168            && !self.view
169            && !self.trade
170            && !self.reduce_or_close
171            && !self.set_limits
172    }
173
174    pub fn read_only() -> Self {
175        Self {
176            list: true,
177            view: true,
178            trade: false,
179            reduce_or_close: false,
180            set_limits: false,
181        }
182    }
183
184    pub fn list(&self) -> bool {
185        self.list
186    }
187
188    pub fn view(&self) -> bool {
189        self.view
190    }
191
192    pub fn trade(&self) -> bool {
193        self.trade
194    }
195
196    pub fn reduce_or_close(&self) -> bool {
197        self.reduce_or_close
198    }
199
200    pub fn set_limits(&self) -> bool {
201        self.set_limits
202    }
203
204    pub fn display(&self) -> String {
205        let mut allowed = vec![];
206        let mut denied = vec![];
207        macro_rules! sift {
208            ($perm:ident) => {
209                if self.$perm {
210                    allowed.push(stringify!($perm));
211                } else {
212                    denied.push(stringify!($perm));
213                }
214            };
215        }
216        sift!(list);
217        sift!(view);
218        sift!(trade);
219        sift!(reduce_or_close);
220        sift!(set_limits);
221        format!("allow({}) deny({})", allowed.join(", "), denied.join(", "))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_account_id_or_name_json() {
231        let id: AccountId = "aa0fc734-0da2-4168-8712-4c0b67f01c59".parse().unwrap();
232        let name: AccountName = AccountName::new("COINBASE", "TEST").unwrap();
233
234        // Test AccountId serialization
235        let id_spec = AccountIdOrName::Id(id);
236        insta::assert_json_snapshot!(id_spec, @r#""aa0fc734-0da2-4168-8712-4c0b67f01c59""#);
237
238        // Test AccountId deserialization
239        let id_json = r#""aa0fc734-0da2-4168-8712-4c0b67f01c59""#;
240        let id_deserialized: AccountIdOrName = serde_json::from_str(id_json).unwrap();
241        assert_eq!(id_spec, id_deserialized);
242
243        // Test name serialization
244        let name_spec = AccountIdOrName::Name(name);
245        insta::assert_json_snapshot!(name_spec, @r#""COINBASE:TEST""#);
246
247        // Test name deserialization
248        let name_json = r#""COINBASE:TEST""#;
249        let name_deserialized: AccountIdOrName = serde_json::from_str(name_json).unwrap();
250        assert_eq!(name_spec, name_deserialized);
251    }
252}