xand_api_proto/proto_models/
mod.rs

1//! Server-agnostic wire types and business representations for the xand-api-client.
2//!
3//! This module does not take any dependencies on any runtime specifics.
4//!
5//! If you are a caller of `xand-api` (IE: Some external service like the trustee or member--api),
6//! you should depend exclusively on the `xand-api-client` package, which re-exports the things you will
7//! need from this module.
8
9#![forbid(unsafe_code)]
10
11pub mod cidr_block;
12mod correlation_id;
13pub mod errors;
14pub mod serde_hex;
15mod transaction_id;
16
17pub use cidr_block::{CidrBlock, CidrBlockParseErr};
18pub use correlation_id::{CorrelationId, CorrelationIdError};
19#[cfg(feature = "runtime-conversions")]
20pub use substrate_mapping::{
21    hash_substrate_encodeable, test_helpers as substrate_test_helpers, to_platform::*,
22    TxnConversionError,
23};
24pub use transaction_id::{TransactionId, TransactionIdError};
25// pub use xand_runtime_models::{CidrBlock, CidrBlockParseErr, ProposalStage};
26
27use chrono::{DateTime, LocalResult, TimeZone, Utc};
28use derive_more::Constructor;
29pub mod public_key;
30pub use errors::EncryptionError;
31pub use public_key::PublicKey;
32use serde::{Deserialize, Serialize};
33use snafu::{ResultExt, Snafu};
34use std::{
35    any::type_name,
36    collections::HashMap,
37    convert::{TryFrom, TryInto},
38    fmt::{Debug, Display, Error, Formatter},
39    str::FromStr,
40};
41use strum::{EnumString, ToString};
42use wasm_hash_verifier::XandHash;
43use xand_address::Address;
44
45/// Default page size for paginated results.
46pub const DEFAULT_PAGE_SIZE: u32 = 50;
47
48/// A Encryption key representing the public portion of some private key, and hence a (potential)
49/// sender or receiver of encrypted messages.
50///
51/// Currently, this means a Base58 encoded string
52#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
53pub struct EncryptionKey([u8; 32]);
54
55impl EncryptionKey {
56    pub fn as_bytes(&self) -> &[u8] {
57        &self.0
58    }
59}
60
61impl Display for EncryptionKey {
62    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
63        let encoded_key = bs58::encode(self.0).into_string();
64        Display::fmt(&encoded_key, f)
65    }
66}
67
68impl TryFrom<String> for EncryptionKey {
69    type Error = EncryptionKeyError;
70
71    fn try_from(value: String) -> Result<Self, Self::Error> {
72        let key = bs58::decode(&value).into_vec().context(Base58Encode)?;
73        EncryptionKey::try_from(key.as_slice())
74    }
75}
76
77impl FromStr for EncryptionKey {
78    type Err = EncryptionKeyError;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        s.to_owned().try_into()
82    }
83}
84
85impl TryFrom<&[u8]> for EncryptionKey {
86    type Error = EncryptionKeyError;
87
88    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
89        if slice.len() != 32 {
90            Err(EncryptionKeyError::InvalidKeyLength)
91        } else {
92            let mut bytes: [u8; 32] = [0; 32];
93            bytes.copy_from_slice(slice);
94            Ok(EncryptionKey(bytes))
95        }
96    }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq, Serialize, snafu::Snafu)]
100pub enum EncryptionKeyError {
101    #[snafu(display("Encryption key wasn't proper base 58: {:?}", source))]
102    Base58Encode {
103        #[serde(serialize_with = "xand_utils::snafu_extensions::debug_serialize")]
104        source: bs58::decode::Error,
105    },
106    #[snafu(display("Invalid encryption key length, expected 32 bytes"))]
107    InvalidKeyLength,
108}
109
110/// A chain-implementation agnostic representation of the status of a transaction/extrinsic/whatever
111/// that a client has submitted (or tried to submit) to the network.
112///
113/// From's and to's are implemented for the most common conversion scenarios.
114#[derive(Serialize, Deserialize, Debug, EnumString, Hash, PartialEq, Eq, Clone, ToString)]
115pub enum TransactionStatus {
116    /// The chain and/or validator has never seen the transaction (or refuses to admit it has)
117    Unknown,
118    /// The chain and/or validator has seen the transaction but has not made a decision about it
119    Pending,
120    /// The chain and/or validator has decided the transaction is invalid. The parameter is the
121    /// reason for rejection.
122    Invalid(String),
123    /// The transaction is successfully committed to the chain
124    Committed,
125    /// The transaction is finalized
126    Finalized,
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130pub struct Transaction {
131    pub signer_address: Address,
132    pub transaction_id: TransactionId,
133    pub status: TransactionStatus,
134    pub txn: XandTransaction,
135    pub timestamp: DateTime<Utc>,
136}
137
138impl Transaction {
139    pub fn is_financial_event(&self) -> bool {
140        matches!(
141            self.txn,
142            XandTransaction::CreateRequest(_)
143                | XandTransaction::CashConfirmation(_)
144                | XandTransaction::RedeemRequest(_)
145                | XandTransaction::RedeemFulfillment(_)
146                | XandTransaction::Send(_)
147        )
148    }
149
150    pub fn from_xand_txn(
151        id: TransactionId,
152        txn: XandTransaction,
153        signer: Address,
154        status: TransactionStatus,
155        timestamp: DateTime<Utc>,
156    ) -> Self {
157        Self {
158            transaction_id: id,
159            signer_address: signer,
160            status,
161            txn,
162            timestamp,
163        }
164    }
165
166    // These "get_xxx" methods that return an option are fragile and undesirable, they
167    // exist because the member api flattens the fields of transactions to one depth level.
168
169    /// If the transaction involves an amount of money, return that amount in minor units
170    pub fn get_amount(&self) -> Option<u64> {
171        match &self.txn {
172            XandTransaction::Send(data) => Some(data.amount_in_minor_unit),
173            XandTransaction::CreateRequest(data) => Some(data.amount_in_minor_unit),
174            XandTransaction::RedeemRequest(data) => Some(data.amount_in_minor_unit),
175            _ => None,
176        }
177    }
178
179    /// If the transaction involves a correlation id, return it.
180    pub fn get_correlation_id(&self) -> Option<&[u8]> {
181        match &self.txn {
182            XandTransaction::CreateRequest(data) => Some(data.correlation_id.as_bytes()),
183            XandTransaction::RedeemRequest(data) => Some(data.correlation_id.as_bytes()),
184            XandTransaction::CreateCancellation(data) => Some(data.correlation_id.as_bytes()),
185            XandTransaction::CashConfirmation(data) => Some(data.correlation_id.as_bytes()),
186            XandTransaction::RedeemFulfillment(data) => Some(data.correlation_id.as_bytes()),
187            _ => None,
188        }
189    }
190
191    /// If the transaction involves some target address for the result of the operation, return it
192    pub fn get_destination_addr(&self) -> Option<Address> {
193        match &self.txn {
194            XandTransaction::RegisterMember(data) => Some(data.address.clone()),
195            XandTransaction::SetTrust(data) => Some(data.address.clone()),
196            XandTransaction::Send(data) => Some(data.destination_account.clone()),
197            XandTransaction::CreateRequest(_) => Some(self.signer_address.clone()),
198            _ => None,
199        }
200    }
201
202    /// If the transaction contains bank account data, return it.
203    pub fn get_bank_info(&self) -> Option<&BankAccountInfo> {
204        match &self.txn {
205            XandTransaction::CreateRequest(data) => Some(&data.account),
206            XandTransaction::RedeemRequest(data) => Some(&data.account),
207            _ => None,
208        }
209    }
210
211    pub fn timestamp_from_unix_time_millis(t: i64) -> Option<DateTime<Utc>> {
212        match Utc.timestamp_millis_opt(t) {
213            LocalResult::None => None,
214            LocalResult::Single(date_time) => Some(date_time),
215            // According to the documentation (https://docs.rs/chrono/latest/chrono/offset/trait.TimeZone.html#method.timestamp_millis_opt),
216            // this variant will never be returned
217            LocalResult::Ambiguous(_, _) => None,
218        }
219    }
220}
221
222#[derive(
223    Clone,
224    Debug,
225    Hash,
226    PartialEq,
227    Eq,
228    Serialize,
229    Deserialize,
230    strum::Display,
231    strum::EnumDiscriminants,
232)]
233/// Chain-agnostic versions of all of our transaction types wrapped up in an enum
234#[serde(tag = "serde_operation")]
235#[strum_discriminants(name(TransactionType))]
236pub enum XandTransaction {
237    RegisterMember(RegisterAccountAsMember),
238    RemoveMember(RemoveMember),
239    ExitMember(ExitMember),
240    SetTrust(SetTrustNodeId),
241    SetLimitedAgent(SetLimitedAgentId),
242    SetValidatorEmissionRate(SetValidatorEmissionRate),
243    SetMemberEncryptionKey(SetMemberEncKey),
244    SetTrustEncryptionKey(SetTrustEncKey),
245    SetPendingCreateRequestExpire(SetPendingCreateRequestExpire),
246    Send(Send),
247    CreateRequest(PendingCreateRequest),
248    CashConfirmation(CashConfirmation),
249    CreateCancellation(CreateCancellation),
250    RedeemCancellation(RedeemCancellation),
251    RedeemRequest(PendingRedeemRequest),
252    RedeemFulfillment(RedeemFulfillment),
253    AddAuthorityKey(AddAuthorityKey),
254    RemoveAuthorityKey(RemoveAuthorityKey),
255    AllowlistCidrBlock(AllowlistCidrBlock),
256    RemoveAllowlistCidrBlock(RemoveAllowlistCidrBlock),
257    RootAllowlistCidrBlock(RootAllowlistCidrBlock),
258    RootRemoveAllowlistCidrBlock(RootRemoveAllowlistCidrBlock),
259    SubmitProposal(SubmitProposal),
260    VoteProposal(VoteProposal),
261    RegisterSessionKeys(RegisterSessionKeys),
262    RuntimeUpgrade(RuntimeUpgrade),
263    WithdrawFromNetwork(WithdrawFromNetwork),
264}
265#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
266pub struct WithdrawFromNetwork {}
267
268#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct RegisterSessionKeys {
271    /// Stringified version of the ss58 session key for block production
272    pub block_production_pubkey: Address,
273    /// Stringified version of the ss58 session key for block finalization
274    pub block_finalization_pubkey: Address,
275}
276
277#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct RegisterAccountAsMember {
280    /// Stringified version of the ss58 address to register
281    pub address: Address,
282
283    /// Public encryption key
284    pub encryption_key: PublicKey,
285}
286
287#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct RemoveMember {
290    /// Stringified version of the ss58 address to mark for removal
291    pub address: Address,
292}
293
294#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct ExitMember {
297    /// Stringified version of the ss58 address to remove
298    pub address: Address,
299}
300
301#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct SetTrustNodeId {
304    /// Stringified version of the ss58 address to become the trust
305    pub address: Address,
306
307    /// Public encryption key
308    pub encryption_key: PublicKey,
309}
310
311#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct SetLimitedAgentId {
314    /// Optional stringified version of the ss58 address to become the limited agent
315    pub address: Option<Address>,
316}
317
318#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct SetValidatorEmissionRate {
321    /// Amount paid, in minor units, per "emission period" (number of blocks defined below)
322    pub minor_units_per_emission: u64,
323    /// Interval (number of blocks produced) between each payout
324    pub block_quota: u32,
325}
326
327#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct SetMemberEncKey {
330    /// The new encryption key to set for the member
331    pub key: PublicKey,
332}
333
334#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct Blockstamp {
337    pub block_number: u64,
338    pub unix_timestamp_ms: i64,
339    pub is_stale: bool,
340}
341
342#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct TotalIssuance {
345    /// The total claims in circulation on the network.
346    pub total_issued: u64,
347
348    pub blockstamp: Blockstamp,
349}
350
351#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct SetTrustEncKey {
354    /// The new encryption key to set for the trust
355    pub key: PublicKey,
356}
357
358#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct SetPendingCreateRequestExpire {
361    /// Value of new timeout in milliseconds
362    pub expire_in_milliseconds: u64,
363}
364
365#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct Send {
368    /// Stringified version of the public key claims will be sent to
369    pub destination_account: Address,
370    /// Amount to send in cents
371    pub amount_in_minor_unit: u64,
372}
373
374#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
375pub struct BankAccountId {
376    pub routing_number: String,
377    pub account_number: String,
378}
379
380/// Some bank account information associated with create and redeem requests. Depending on where in their
381/// lifecycle they are, the information may or may not be encrypted yet.
382#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
383pub enum BankAccountInfo {
384    Unencrypted(BankAccountId),
385    Encrypted(EncryptionError),
386}
387
388#[derive(Debug, Snafu, Serialize)]
389pub enum BankAccountConversionErr {
390    #[snafu(display("Problem (de)serializing unencrypted bank info: {:?}", source))]
391    #[snafu(context(false))]
392    BadUnencryptedJson {
393        #[snafu(source(from(serde_json::Error, Box::new)))]
394        #[serde(serialize_with = "xand_utils::snafu_extensions::debug_serialize")]
395        source: Box<serde_json::Error>,
396    },
397    #[snafu(display("Encrypted bank info is too large"))]
398    EncryptedPayloadTooLarge,
399    #[snafu(display("Encrypted payload is invalid"))]
400    #[snafu(context(false))]
401    BadEncryptedPayload {
402        #[serde(serialize_with = "xand_utils::snafu_extensions::debug_serialize")]
403        source: Box<dyn std::error::Error + std::marker::Send + Sync>,
404    },
405    #[snafu(display("Failed to convert from bytes: {}", message))]
406    ByteConversionError { message: String },
407}
408
409impl From<BankAccountId> for BankAccountInfo {
410    fn from(id: BankAccountId) -> Self {
411        Self::Unencrypted(id)
412    }
413}
414
415pub struct ConversionFailure {
416    pub value: String,
417    pub original_type: &'static str,
418    pub target_type: &'static str,
419}
420
421pub fn convert_type<T, U>(v: T) -> Result<U, ConversionFailure>
422where
423    T: Copy + Debug,
424    U: TryFrom<T>,
425{
426    v.try_into().map_err(|_| ConversionFailure {
427        value: format!("{:?}", v),
428        original_type: type_name::<T>(),
429        target_type: type_name::<U>(),
430    })
431}
432
433#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
434#[serde(rename_all = "camelCase")]
435pub struct PendingCreateRequest {
436    /// Amount of the pending create request, in USD cents
437    pub amount_in_minor_unit: u64,
438    /// An id uniquely identifying this pending create request
439    pub correlation_id: CorrelationId,
440    /// The (possibly decrypted) info about the bank account which will/did provide funds for
441    /// this request
442    pub account: BankAccountInfo,
443    /// ID of the corresponding fulfilling or cancellation transaction if known.
444    /// It will always be `None` when submitting a `PendingCreateRequest`.
445    pub completing_transaction: Option<CreateRequestCompletion>,
446    // TODO: Expire time should to be exposed here, but isn't actually needed by anything at the
447    //   moment
448}
449
450#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct PendingRedeemRequest {
453    /// Amount of the pending redeem request, in USD cents
454    pub amount_in_minor_unit: u64,
455    /// An id uniquely identifying this pending redeem request
456    pub correlation_id: CorrelationId,
457    /// The (possibly decrypted) info about the bank account to which the trust should/did
458    /// deposit funds
459    pub account: BankAccountInfo,
460    /// ID of the corresponding fulfilling or cancellation transaction if known.
461    /// It will always be `None` when submitting a `PendingRedeemRequest`.
462    pub completing_transaction: Option<RedeemRequestCompletion>,
463}
464
465#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct RedeemFulfillment {
468    /// Correlation id of the redeem request to mark as fulfilled
469    pub correlation_id: CorrelationId,
470}
471
472#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct CashConfirmation {
475    /// Correlation id of the create request to mark as fulfilled
476    pub correlation_id: CorrelationId,
477}
478
479#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct CreateCancellation {
482    /// Correlation id of the create request to mark as cancelled
483    pub correlation_id: CorrelationId,
484    /// The reason for the cancellation
485    pub reason: CreateCancellationReason,
486}
487
488#[derive(
489    Clone,
490    Debug,
491    Hash,
492    PartialEq,
493    Eq,
494    Serialize,
495    Deserialize,
496    strum::Display,
497    strum::EnumString,
498    strum::IntoStaticStr,
499)]
500pub enum CreateCancellationReason {
501    /// The pending create request existed for too long without any fiat transfer
502    Expired,
503    /// The bank account information could not be acted upon by the trust
504    InvalidData,
505    /// The bank could not be found
506    BankNotFound,
507}
508
509#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct RedeemCancellation {
512    /// Correlation id of the redeem request to mark as cancelled
513    pub correlation_id: CorrelationId,
514    /// The reason for the cancellation
515    pub reason: RedeemCancellationReason,
516}
517
518#[derive(
519    Clone,
520    Debug,
521    Hash,
522    PartialEq,
523    Eq,
524    Serialize,
525    Deserialize,
526    strum::Display,
527    strum::EnumString,
528    strum::IntoStaticStr,
529)]
530pub enum RedeemCancellationReason {
531    /// The bank account information could not be acted upon by the trust
532    InvalidData,
533    /// The bank account is not allowed by the trustee
534    AccountNotAllowed,
535}
536
537#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct AddAuthorityKey {
540    /// Stringified version of the validator's signing pubkey (ss58 address from sr25519 pubkey)
541    pub account_id: Address,
542}
543
544#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
545#[serde(rename_all = "camelCase")]
546pub struct RemoveAuthorityKey {
547    /// Stringified version of the validator's signing pubkey (ss58 address from sr25519 pubkey)
548    pub account_id: Address,
549}
550
551#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
552#[serde(rename_all = "camelCase")]
553pub struct AllowlistCidrBlock {
554    pub cidr_block: CidrBlock,
555}
556
557#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
558#[serde(rename_all = "camelCase")]
559pub struct RemoveAllowlistCidrBlock {
560    pub cidr_block: CidrBlock,
561}
562
563#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
564#[serde(rename_all = "camelCase")]
565pub struct RootAllowlistCidrBlock {
566    pub account: Address,
567    pub cidr_block: CidrBlock,
568}
569
570#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
571#[serde(rename_all = "camelCase")]
572pub struct RootRemoveAllowlistCidrBlock {
573    pub account: Address,
574    pub cidr_block: CidrBlock,
575}
576
577#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
578#[serde(rename_all = "camelCase")]
579pub struct SubmitProposal {
580    pub proposed_action: AdministrativeTransaction,
581}
582
583#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
584#[serde(rename_all = "camelCase")]
585pub struct VoteProposal {
586    pub id: u32,
587    pub vote: bool,
588}
589
590#[derive(Clone, Debug, Hash, PartialEq, Eq, Constructor, Serialize, Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub struct RuntimeUpgrade {
593    pub code: Vec<u8>,
594    pub xand_hash: XandHash,
595    pub wait_blocks: u32,
596}
597
598#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
599pub enum CreateRequestCompletion {
600    Confirmation(TransactionId),
601    Cancellation(TransactionId),
602    Expiration(TransactionId),
603}
604
605#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
606pub enum RedeemRequestCompletion {
607    Confirmation(TransactionId),
608    Cancellation(TransactionId),
609}
610
611#[derive(Clone, Debug, Default)]
612pub struct TransactionFilter {
613    pub addresses: Vec<Address>,
614    pub types: Vec<TransactionType>,
615    pub start_time: Option<DateTime<Utc>>,
616    pub end_time: Option<DateTime<Utc>>,
617}
618
619#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
620pub enum AdministrativeTransaction {
621    RegisterAccountAsMember(RegisterAccountAsMember),
622    SetTrust(SetTrustNodeId),
623    SetLimitedAgent(SetLimitedAgentId),
624    SetValidatorEmissionRate(SetValidatorEmissionRate),
625    AddAuthorityKey(AddAuthorityKey),
626    RemoveAuthorityKey(RemoveAuthorityKey),
627    RootAllowlistCidrBlock(RootAllowlistCidrBlock),
628    RootRemoveAllowlistCidrBlock(RootRemoveAllowlistCidrBlock),
629    RemoveMember(RemoveMember),
630    RuntimeUpgrade(RuntimeUpgrade),
631}
632
633impl AdministrativeTransaction {
634    pub fn from_transaction(t: XandTransaction) -> Option<Self> {
635        match t {
636            XandTransaction::RegisterMember(x) => {
637                Some(AdministrativeTransaction::RegisterAccountAsMember(x))
638            }
639            XandTransaction::RemoveMember(x) => Some(AdministrativeTransaction::RemoveMember(x)),
640            XandTransaction::SetTrust(x) => Some(AdministrativeTransaction::SetTrust(x)),
641            XandTransaction::SetLimitedAgent(x) => {
642                Some(AdministrativeTransaction::SetLimitedAgent(x))
643            }
644            XandTransaction::SetValidatorEmissionRate(x) => {
645                Some(AdministrativeTransaction::SetValidatorEmissionRate(x))
646            }
647            XandTransaction::AddAuthorityKey(x) => {
648                Some(AdministrativeTransaction::AddAuthorityKey(x))
649            }
650            XandTransaction::RemoveAuthorityKey(x) => {
651                Some(AdministrativeTransaction::RemoveAuthorityKey(x))
652            }
653            XandTransaction::AllowlistCidrBlock(_) => None,
654            XandTransaction::RemoveAllowlistCidrBlock(_) => None,
655            XandTransaction::RootAllowlistCidrBlock(x) => {
656                Some(AdministrativeTransaction::RootAllowlistCidrBlock(x))
657            }
658            XandTransaction::RootRemoveAllowlistCidrBlock(x) => {
659                Some(AdministrativeTransaction::RootRemoveAllowlistCidrBlock(x))
660            }
661            XandTransaction::RuntimeUpgrade(x) => {
662                Some(AdministrativeTransaction::RuntimeUpgrade(x))
663            }
664            // These transactions are performed directly by the requesting validator (i.e. root
665            // permissions are not required) so there are no corresponding administrative
666            // transactions
667            XandTransaction::SubmitProposal(_) => None,
668            XandTransaction::VoteProposal(_) => None,
669            XandTransaction::SetMemberEncryptionKey(_) => None,
670            XandTransaction::SetTrustEncryptionKey(_) => None,
671            XandTransaction::SetPendingCreateRequestExpire(_) => None,
672            XandTransaction::Send(_) => None,
673            XandTransaction::CreateRequest(_) => None,
674            XandTransaction::CashConfirmation(_) => None,
675            XandTransaction::CreateCancellation(_) => None,
676            XandTransaction::RedeemCancellation(_) => None,
677            XandTransaction::RedeemRequest(_) => None,
678            XandTransaction::RedeemFulfillment(_) => None,
679            XandTransaction::RegisterSessionKeys(_) => None,
680            XandTransaction::ExitMember(_) => None,
681            XandTransaction::WithdrawFromNetwork(_) => None,
682        }
683    }
684}
685
686#[derive(Clone, Debug, Eq, PartialEq)]
687pub struct Proposal {
688    /// Unique proposal id
689    pub id: u32,
690    /// Map of voters to their votes, true is yes, false is no
691    pub votes: HashMap<Address, bool>,
692    /// Address of party that created proposal
693    pub proposer: Address,
694    /// Blockchain block on which proposal will expire and be removed
695    pub expiration_block_id: u32,
696    /// The action that will be executed if this proposal is accepted
697    pub proposed_action: AdministrativeTransaction,
698    /// This proposal's current state
699    pub status: ProposalStage,
700}
701
702/// Represents the stage of a proposal. Proposals can only ever change stage once, from `Proposed`
703/// to another stage. All other stages are final, and any subsequent changes should be considered
704/// a bug.
705#[derive(Clone, Debug, Eq, PartialEq)]
706#[cfg_attr(
707    feature = "serialization",
708    derive(serde::Deserialize, serde::Serialize)
709)]
710pub enum ProposalStage {
711    Proposed,
712    Accepted,
713    Rejected,
714    Invalid,
715}
716
717impl core::default::Default for ProposalStage {
718    fn default() -> Self {
719        ProposalStage::Proposed
720    }
721}
722
723#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
724pub struct ValidatorEmissionRate {
725    pub minor_units_per_emission: u64,
726    pub block_quota: u32,
727}
728
729#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
730pub struct GetValidatorEmissionProgress {
731    pub address: Address,
732}
733
734#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
735pub struct ValidatorEmissionProgress {
736    pub effective_emission_rate: ValidatorEmissionRate,
737    pub blocks_completed_progress: u32,
738}
739
740#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
741pub enum HealthStatus {
742    Unhealthy,
743    Healthy,
744    Syncing,
745}
746
747#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)]
748pub struct HealthResponse {
749    pub status: HealthStatus,
750    pub current_block: u64,
751    pub elapsed_blocks_since_startup: u64,
752    pub elapsed_time_since_startup_millis: i64,
753    pub best_known_block: u64,
754}
755
756// TODO: Consider removing; much of this is probably duplicated in xand-models.
757
758#[cfg(test)]
759mod test {
760    use super::*;
761    use chrono::Utc;
762    use std::str::FromStr;
763    use xand_address::Address;
764
765    fn example_addr() -> Address {
766        Address::from_str("5Hh9Gq21Ns4Knd6CjzjMymK6HeW9yYfxdMfhMoDyA8geHVbJ").unwrap()
767    }
768
769    fn example_encryption_key() -> PublicKey {
770        Default::default()
771    }
772
773    fn from_xand_txn(
774        id: TransactionId,
775        txn: XandTransaction,
776        signer: Address,
777        status: TransactionStatus,
778    ) -> Transaction {
779        let timestamp = Utc.with_ymd_and_hms(2020, 2, 5, 6, 0, 0).unwrap();
780        Transaction::from_xand_txn(id, txn, signer, status, timestamp)
781    }
782
783    fn fake_account() -> BankAccountInfo {
784        let id = BankAccountId {
785            account_number: "123".to_string(),
786            routing_number: "456".to_string(),
787        };
788        id.into()
789    }
790
791    #[test]
792    pub fn transaction_size() {
793        // This is just a good sanity check to make sure we don't accidentally make these really big
794        assert!(std::mem::size_of::<Transaction>() < 300);
795    }
796
797    #[test]
798    pub fn transaction_status_from_str() {
799        let status_str = TransactionStatus::Unknown.to_string();
800
801        let result = TransactionStatus::from_str(&status_str).unwrap();
802
803        assert_eq!(result.to_string(), status_str);
804    }
805
806    #[test]
807    pub fn transaction_is_financial_event_create_request() {
808        let transaction = from_xand_txn(
809            TransactionId::default(),
810            XandTransaction::CreateRequest(PendingCreateRequest {
811                account: fake_account(),
812                amount_in_minor_unit: 42,
813                correlation_id: CorrelationId::gen_random(),
814                completing_transaction: None,
815            }),
816            example_addr(),
817            TransactionStatus::Pending,
818        );
819
820        assert!(transaction.is_financial_event());
821    }
822
823    #[test]
824    pub fn transaction_is_financial_event_cash_confirmation() {
825        let transaction = from_xand_txn(
826            TransactionId::from_str(
827                "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
828            )
829            .unwrap(),
830            XandTransaction::CashConfirmation(CashConfirmation {
831                correlation_id: CorrelationId::gen_random(),
832            }),
833            example_addr(),
834            TransactionStatus::Committed,
835        );
836
837        assert!(transaction.is_financial_event());
838    }
839
840    #[test]
841    pub fn transaction_is_financial_event_redeem_request() {
842        let transaction = from_xand_txn(
843            TransactionId::default(),
844            XandTransaction::RedeemRequest(PendingRedeemRequest {
845                account: fake_account(),
846                amount_in_minor_unit: 42,
847                correlation_id: CorrelationId::gen_random(),
848                completing_transaction: None,
849            }),
850            example_addr(),
851            TransactionStatus::Committed,
852        );
853
854        assert!(transaction.is_financial_event());
855    }
856
857    #[test]
858    pub fn transaction_is_financial_event_redeem_fulfillment() {
859        let transaction = from_xand_txn(
860            TransactionId::from_str(
861                "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
862            )
863            .unwrap(),
864            XandTransaction::RedeemFulfillment(RedeemFulfillment {
865                correlation_id: CorrelationId::gen_random(),
866            }),
867            example_addr(),
868            TransactionStatus::Committed,
869        );
870
871        assert!(transaction.is_financial_event());
872    }
873
874    #[test]
875    pub fn transaction_is_financial_event_send() {
876        let transaction = from_xand_txn(
877            TransactionId::default(),
878            XandTransaction::Send(Send {
879                amount_in_minor_unit: 42,
880                destination_account: example_addr(),
881            }),
882            example_addr(),
883            TransactionStatus::Pending,
884        );
885        assert!(transaction.is_financial_event());
886    }
887
888    #[test]
889    pub fn transaction_is_financial_event_some_non_financial() {
890        let transaction = from_xand_txn(
891            TransactionId::default(),
892            XandTransaction::RegisterMember(RegisterAccountAsMember {
893                address: example_addr(),
894                encryption_key: example_encryption_key(),
895            }),
896            example_addr(),
897            TransactionStatus::Pending,
898        );
899        assert!(!transaction.is_financial_event());
900    }
901}