nym_api_requests/ecash/
models.rs

1// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::signable::SignedMessage;
5use cosmrs::AccountId;
6use nym_coconut_dkg_common::types::EpochId;
7use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
8use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
9use nym_compact_ecash::utils::try_deserialize_g1_projective;
10use nym_compact_ecash::{Bytable, G1Projective};
11use nym_credentials_interface::TicketType;
12use nym_credentials_interface::{
13    BlindedSignature, CompactEcashError, CredentialSpendingData, PublicKeyUser,
14    VerificationKeyAuth, WithdrawalRequest,
15};
16use nym_crypto::asymmetric::ed25519;
17use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof};
18use serde::{Deserialize, Serialize};
19use sha2::Digest;
20use std::collections::BTreeMap;
21use std::ops::Deref;
22use thiserror::Error;
23use time::{Date, OffsetDateTime};
24use utoipa::ToSchema;
25
26#[derive(Serialize, Deserialize, Clone, ToSchema)]
27pub struct VerifyEcashTicketBody {
28    /// The cryptographic material required for spending the underlying credential.
29    #[schema(value_type = String)]
30    pub credential: CredentialSpendingData,
31
32    /// Cosmos address of the sender of the credential
33    #[schema(value_type = String)]
34    pub gateway_cosmos_addr: AccountId,
35}
36
37#[derive(Serialize, Deserialize, Clone, ToSchema)]
38pub struct VerifyEcashCredentialBody {
39    /// The cryptographic material required for spending the underlying credential.
40    #[schema(value_type = openapi_schema::CredentialSpendingData)]
41    pub credential: CredentialSpendingData,
42
43    /// Cosmos address of the sender of the credential
44    #[schema(value_type = String)]
45    pub gateway_cosmos_addr: AccountId,
46
47    /// Multisig proposal for releasing funds for the provided bandwidth credential
48    pub proposal_id: u64,
49}
50
51impl VerifyEcashCredentialBody {
52    pub fn new(
53        credential: CredentialSpendingData,
54        gateway_cosmos_addr: AccountId,
55        proposal_id: u64,
56    ) -> VerifyEcashCredentialBody {
57        VerifyEcashCredentialBody {
58            credential,
59            gateway_cosmos_addr,
60            proposal_id,
61        }
62    }
63}
64
65/// Used exclusively as part of OpenAPI docs
66#[derive(ToSchema)]
67pub enum EcashTicketVerificationResult {
68    Ok(()),
69    EcashTicketVerificationRejection,
70}
71
72#[derive(Debug, Serialize, Deserialize, ToSchema)]
73pub struct EcashTicketVerificationResponse {
74    #[schema(value_type = EcashTicketVerificationResult)]
75    pub verified: Result<(), EcashTicketVerificationRejection>,
76}
77
78impl EcashTicketVerificationResponse {
79    pub fn reject(reason: EcashTicketVerificationRejection) -> Self {
80        EcashTicketVerificationResponse {
81            verified: Err(reason),
82        }
83    }
84}
85
86#[derive(Debug, Error, Serialize, Deserialize, ToSchema)]
87pub enum EcashTicketVerificationRejection {
88    #[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC"
89    )]
90    InvalidSpentDate {
91        #[serde(with = "crate::helpers::date_serde")]
92        #[schema(value_type = String, example = "1970-01-01")]
93        today: Date,
94        #[serde(with = "crate::helpers::date_serde")]
95        #[schema(value_type = String, example = "1970-01-01")]
96        yesterday: Date,
97        #[serde(with = "crate::helpers::date_serde")]
98        #[schema(value_type = String, example = "1970-01-01")]
99        received: Date,
100    },
101
102    #[error("this ticket has already been received before")]
103    ReplayedTicket,
104
105    #[error("this ticket has already been spent before")]
106    DoubleSpend,
107
108    #[error("failed to verify the provided ticket")]
109    InvalidTicket,
110
111    #[error(
112        "the received payment contained more than a single ticket. that's currently not supported"
113    )]
114    MultipleTickets,
115}
116
117//  All strings are base58 encoded representations of structs
118#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)]
119pub struct BlindSignRequestBody {
120    #[schema(value_type = openapi_schema::WithdrawalRequest)]
121    pub inner_sign_request: WithdrawalRequest,
122
123    /// the id of the associated deposit
124    pub deposit_id: u32,
125
126    /// Signature on the inner sign request and the tx hash
127    #[schema(value_type = String)]
128    pub signature: ed25519::Signature,
129
130    #[schema(value_type = openapi_schema::PublicKeyUser)]
131    pub ecash_pubkey: PublicKeyUser,
132
133    #[serde(with = "crate::helpers::date_serde")]
134    #[schema(value_type = String, example = "1970-01-01")]
135    pub expiration_date: Date,
136
137    #[schema(value_type = String)]
138    pub ticketbook_type: TicketType,
139}
140
141impl BlindSignRequestBody {
142    pub fn new(
143        inner_sign_request: WithdrawalRequest,
144        deposit_id: u32,
145        signature: ed25519::Signature,
146        ecash_pubkey: PublicKeyUser,
147        expiration_date: Date,
148        ticketbook_type: TicketType,
149    ) -> BlindSignRequestBody {
150        BlindSignRequestBody {
151            inner_sign_request,
152            deposit_id,
153            signature,
154            ecash_pubkey,
155            expiration_date,
156            ticketbook_type,
157        }
158    }
159
160    pub fn encode_join_commitments(&self) -> Vec<u8> {
161        self.inner_sign_request
162            .get_private_attributes_commitments()
163            .iter()
164            .flat_map(|c| c.to_byte_vec())
165            .collect()
166    }
167
168    pub fn try_decode_joined_commitments(
169        joined: &[u8],
170    ) -> Result<Vec<G1Projective>, CompactEcashError> {
171        if !joined.len().is_multiple_of(48) {
172            // that's not the most ideal error variant, but creating dedicated error type would have been an overkill
173            return Err(CompactEcashError::DeserializationLengthMismatch {
174                type_name: "joined commitments".to_string(),
175                expected: (joined.len() / 48) * 48,
176                actual: joined.len(),
177            });
178        };
179        let mut commitments = Vec::new();
180
181        for chunk in joined.chunks_exact(48) {
182            //SAFETY : we're taking chunks of 48 bytes
183            #[allow(clippy::unwrap_used)]
184            let bytes: &[u8; 48] = chunk.try_into().unwrap();
185            commitments.push(try_deserialize_g1_projective(bytes)?);
186        }
187
188        Ok(commitments)
189    }
190}
191
192#[derive(Debug, Serialize, Deserialize, ToSchema)]
193pub struct BlindedSignatureResponse {
194    #[schema(value_type = openapi_schema::BlindedSignature)]
195    pub blinded_signature: BlindedSignature,
196}
197
198impl BlindedSignatureResponse {
199    pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse {
200        BlindedSignatureResponse { blinded_signature }
201    }
202
203    pub fn to_base58_string(&self) -> String {
204        bs58::encode(&self.to_bytes()).into_string()
205    }
206
207    pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, CompactEcashError> {
208        let bytes = bs58::decode(val).into_vec()?;
209        Self::from_bytes(&bytes)
210    }
211
212    pub fn to_bytes(&self) -> Vec<u8> {
213        self.blinded_signature.to_bytes().to_vec()
214    }
215
216    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CompactEcashError> {
217        Ok(BlindedSignatureResponse {
218            blinded_signature: BlindedSignature::from_bytes(bytes)?,
219        })
220    }
221}
222
223#[derive(Serialize, Deserialize, ToSchema)]
224pub struct MasterVerificationKeyResponse {
225    #[schema(value_type = openapi_schema::VerificationKeyAuth)]
226    pub key: VerificationKeyAuth,
227}
228
229impl MasterVerificationKeyResponse {
230    pub fn new(key: VerificationKeyAuth) -> MasterVerificationKeyResponse {
231        MasterVerificationKeyResponse { key }
232    }
233}
234
235#[derive(Serialize, Deserialize, ToSchema)]
236pub struct VerificationKeyResponse {
237    #[schema(value_type = openapi_schema::VerificationKeyAuth)]
238    pub key: VerificationKeyAuth,
239}
240
241impl VerificationKeyResponse {
242    pub fn new(key: VerificationKeyAuth) -> VerificationKeyResponse {
243        VerificationKeyResponse { key }
244    }
245}
246
247#[derive(Serialize, Deserialize)]
248pub struct CosmosAddressResponse {
249    pub addr: AccountId,
250}
251
252impl CosmosAddressResponse {
253    pub fn new(addr: AccountId) -> CosmosAddressResponse {
254        CosmosAddressResponse { addr }
255    }
256}
257
258#[derive(Serialize, Deserialize, ToSchema)]
259pub struct PartialExpirationDateSignatureResponse {
260    pub epoch_id: u64,
261
262    #[serde(with = "crate::helpers::date_serde")]
263    #[schema(value_type = String, example = "1970-01-01")]
264    pub expiration_date: Date,
265    #[schema(value_type = openapi_schema::AnnotatedExpirationDateSignature)]
266    pub signatures: Vec<AnnotatedExpirationDateSignature>,
267}
268
269#[derive(Serialize, Deserialize, ToSchema)]
270pub struct PartialCoinIndicesSignatureResponse {
271    pub epoch_id: u64,
272    #[schema(value_type = openapi_schema::AnnotatedCoinIndexSignature)]
273    pub signatures: Vec<AnnotatedCoinIndexSignature>,
274}
275
276#[derive(Serialize, Deserialize, ToSchema)]
277pub struct AggregatedExpirationDateSignatureResponse {
278    pub epoch_id: u64,
279
280    #[serde(with = "crate::helpers::date_serde")]
281    #[schema(value_type = String, example = "1970-01-01")]
282    pub expiration_date: Date,
283
284    #[schema(value_type = Vec<openapi_schema::AnnotatedExpirationDateSignature>)]
285    pub signatures: Vec<AnnotatedExpirationDateSignature>,
286}
287
288#[derive(Serialize, Deserialize, ToSchema)]
289pub struct AggregatedCoinIndicesSignatureResponse {
290    pub epoch_id: u64,
291    #[schema(value_type = openapi_schema::Signature)]
292    pub signatures: Vec<AnnotatedCoinIndexSignature>,
293}
294
295/// duplicate types from `nym-compact-ecash`, but these do derive `ToSchema```
296pub mod openapi_schema {
297    #![allow(dead_code)]
298    use nym_compact_ecash::common_types::{G2Projective, Scalar};
299
300    use super::*;
301
302    #[derive(ToSchema)]
303    pub struct AnnotatedExpirationDateSignature {
304        pub signature: Signature,
305        pub expiration_timestamp: u32,
306        pub spending_timestamp: u32,
307    }
308
309    #[derive(ToSchema)]
310    pub struct AnnotatedCoinIndexSignature {
311        pub signature: Signature,
312        pub index: u64,
313    }
314
315    #[derive(ToSchema)]
316    pub struct Signature {
317        #[schema(value_type = String)]
318        pub(crate) h: G1Projective,
319        #[schema(value_type = String)]
320        pub(crate) s: G1Projective,
321    }
322
323    #[derive(ToSchema)]
324    pub struct PublicKeyUser {
325        #[schema(value_type = String)]
326        pub(crate) pk: G1Projective,
327    }
328
329    #[derive(ToSchema)]
330    pub struct BlindedSignature {
331        #[schema(value_type = String)]
332        pub h: G1Projective,
333        #[schema(value_type = String)]
334        pub c: G1Projective,
335    }
336
337    #[derive(ToSchema)]
338    pub struct VerificationKeyAuth {
339        #[schema(value_type = String)]
340        pub(crate) alpha: G2Projective,
341        #[schema(value_type = Vec<String>)]
342        pub(crate) beta_g1: Vec<G1Projective>,
343        #[schema(value_type = Vec<String>)]
344        pub(crate) beta_g2: Vec<G2Projective>,
345    }
346
347    #[derive(ToSchema)]
348    pub struct WithdrawalRequest {
349        #[schema(value_type = String)]
350        joined_commitment_hash: G1Projective,
351        #[schema(value_type = String)]
352        joined_commitment: G1Projective,
353        #[schema(value_type = Vec<String>)]
354        private_attributes_commitments: Vec<G1Projective>,
355        zk_proof: WithdrawalReqProof,
356    }
357
358    #[derive(ToSchema)]
359    pub struct WithdrawalReqProof {
360        #[schema(value_type = String)]
361        challenge: Scalar,
362        #[schema(value_type = String)]
363        response_opening: Scalar,
364        #[schema(value_type = Vec<String>)]
365        response_openings: Vec<Scalar>,
366        #[schema(value_type = Vec<String>)]
367        response_attributes: Vec<Scalar>,
368    }
369
370    #[derive(ToSchema)]
371    pub struct CredentialSpendingData {
372        pub payment: Payment,
373
374        #[schema(value_type = [u8; 72], format = Binary)]
375        pub pay_info: PayInfo,
376
377        pub spend_date: Date,
378
379        // pub value: u64,
380        /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
381        pub epoch_id: u64,
382    }
383
384    #[derive(ToSchema)]
385    pub struct Payment {
386        #[schema(value_type = String)]
387        pub kappa: G2Projective,
388        #[schema(value_type = String)]
389        pub kappa_e: G2Projective,
390        pub sig: Signature,
391
392        pub sig_exp: Signature,
393        #[schema(value_type = Vec<String>)]
394        pub kappa_k: Vec<G2Projective>,
395        pub omega: Vec<Signature>,
396        #[schema(value_type = Vec<String>)]
397        pub ss: Vec<G1Projective>,
398        #[schema(value_type = Vec<String>)]
399        pub tt: Vec<G1Projective>,
400        #[schema(value_type = Vec<String>)]
401        pub aa: Vec<G1Projective>,
402        pub spend_value: u64,
403        #[schema(value_type = String)]
404        pub cc: G1Projective,
405        pub t_type: u8,
406        pub zk_proof: SpendProof,
407    }
408
409    #[derive(PartialEq, Eq, Debug, Clone, Copy, ToSchema)]
410    pub struct PayInfo {
411        #[schema(content_encoding = "base16")]
412        pub pay_info_bytes: [u8; 72],
413    }
414
415    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
416    pub struct SpendProof {
417        #[schema(value_type = String)]
418        challenge: Scalar,
419        #[schema(value_type = String)]
420        response_r: Scalar,
421        #[schema(value_type = String)]
422        response_r_e: Scalar,
423        #[schema(value_type = Vec<String>)]
424        responses_r_k: Vec<Scalar>,
425        #[schema(value_type = Vec<String>)]
426        responses_l: Vec<Scalar>,
427        #[schema(value_type = Vec<String>)]
428        responses_o_a: Vec<Scalar>,
429        #[schema(value_type = String)]
430        response_o_c: Scalar,
431        #[schema(value_type = Vec<String>)]
432        responses_mu: Vec<Scalar>,
433        #[schema(value_type = Vec<String>)]
434        responses_o_mu: Vec<Scalar>,
435        #[schema(value_type = Vec<String>)]
436        responses_attributes: Vec<Scalar>,
437    }
438}
439
440#[derive(Clone, Serialize, Deserialize, Debug)]
441pub struct Pagination<T> {
442    /// last_key is the last value returned in the previous query.
443    /// it's used to indicate the start of the next (this) page.
444    /// the value itself is not included in the response.
445    pub last_key: Option<T>,
446
447    /// limit is the total number of results to be returned in the result page.
448    /// If left empty it will default to a value to be set by each app.
449    pub limit: Option<u32>,
450}
451
452#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)]
453pub struct SerialNumberWrapper(#[serde(with = "nym_serde_helpers::bs58")] Vec<u8>);
454
455impl Deref for SerialNumberWrapper {
456    type Target = Vec<u8>;
457    fn deref(&self) -> &Self::Target {
458        &self.0
459    }
460}
461
462impl AsRef<[u8]> for SerialNumberWrapper {
463    fn as_ref(&self) -> &[u8] {
464        self.0.as_ref()
465    }
466}
467
468impl From<Vec<u8>> for SerialNumberWrapper {
469    fn from(value: Vec<u8>) -> Self {
470        SerialNumberWrapper(value)
471    }
472}
473
474#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)]
475pub struct BatchRedeemTicketsBody {
476    #[serde(with = "nym_serde_helpers::bs58")]
477    pub digest: Vec<u8>,
478    pub included_serial_numbers: Vec<SerialNumberWrapper>,
479    pub proposal_id: u64,
480    #[schema(value_type = String)]
481    pub gateway_cosmos_addr: AccountId,
482}
483
484impl BatchRedeemTicketsBody {
485    pub fn make_digest<I, T>(serial_numbers: I) -> Vec<u8>
486    where
487        I: Iterator<Item = T>,
488        T: AsRef<[u8]>,
489    {
490        let mut hasher = sha2::Sha256::new();
491        for sn in serial_numbers {
492            hasher.update(sn)
493        }
494        hasher.finalize().to_vec()
495    }
496
497    pub fn new(
498        digest: Vec<u8>,
499        proposal_id: u64,
500        serial_numbers: Vec<impl Into<SerialNumberWrapper>>,
501        redeemer: AccountId,
502    ) -> Self {
503        BatchRedeemTicketsBody {
504            digest,
505            included_serial_numbers: serial_numbers.into_iter().map(Into::into).collect(),
506            proposal_id,
507            gateway_cosmos_addr: redeemer,
508        }
509    }
510
511    pub fn verify_digest(&self) -> bool {
512        Self::make_digest(self.included_serial_numbers.iter()) == self.digest
513    }
514}
515
516#[derive(Debug, Serialize, Deserialize, ToSchema)]
517pub struct EcashBatchTicketRedemptionResponse {
518    pub proposal_accepted: bool,
519}
520
521#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
522#[serde(rename_all = "camelCase")]
523pub struct SpentCredentialsResponse {
524    #[serde(with = "nym_serde_helpers::base64")]
525    #[schema(value_type = String)]
526    pub bitmap: Vec<u8>,
527}
528
529impl SpentCredentialsResponse {
530    pub fn new(bitmap: Vec<u8>) -> SpentCredentialsResponse {
531        SpentCredentialsResponse { bitmap }
532    }
533}
534
535pub type DepositId = u32;
536
537#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq)]
538#[serde(rename_all = "camelCase")]
539pub struct CommitedDeposit {
540    #[schema(value_type = u32)]
541    pub deposit_id: DepositId,
542    pub merkle_index: usize,
543}
544
545pub type IssuedTicketbooksDataRequest = SignedMessage<IssuedTicketbooksDataRequestBody>;
546pub type IssuedTicketbooksChallengeCommitmentRequest =
547    SignedMessage<IssuedTicketbooksChallengeCommitmentRequestBody>;
548
549pub type IssuedTicketbooksChallengeCommitmentResponse =
550    SignedMessage<IssuedTicketbooksChallengeCommitmentResponseBody>;
551pub type IssuedTicketbooksForResponse = SignedMessage<IssuedTicketbooksForResponseBody>;
552pub type IssuedTicketbooksDataResponse = SignedMessage<IssuedTicketbooksDataResponseBody>;
553
554mod maybe_merkle_root_serde {
555    use super::*;
556    use serde::de::Error;
557    use serde::{Deserializer, Serialize, Serializer};
558
559    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
560    where
561        D: Deserializer<'de>,
562    {
563        let inner = Option::<String>::deserialize(deserializer)?;
564        match inner {
565            Some(inner) => {
566                let decoded = hex::decode(inner).map_err(Error::custom)?;
567                let arr: [u8; 32] = decoded.as_slice().try_into().map_err(Error::custom)?;
568                Ok(Some(arr))
569            }
570            None => Ok(None),
571        }
572    }
573
574    pub(crate) fn serialize<S>(
575        maybe_root: &Option<[u8; 32]>,
576        serializer: S,
577    ) -> Result<S::Ok, S::Error>
578    where
579        S: Serializer,
580    {
581        maybe_root.map(hex::encode).serialize(serializer)
582    }
583}
584
585#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
586#[serde(rename_all = "camelCase")]
587pub struct IssuedTicketbooksForResponseBody {
588    #[serde(with = "crate::helpers::date_serde")]
589    #[schema(value_type = String, example = "1970-01-01")]
590    pub expiration_date: Date,
591    pub deposits: Vec<CommitedDeposit>,
592    #[serde(with = "maybe_merkle_root_serde")]
593    pub merkle_root: Option<[u8; 32]>,
594}
595
596impl IssuedTicketbooksForResponseBody {
597    pub fn merkle_root_hex(&self) -> Option<String> {
598        self.merkle_root.map(hex::encode)
599    }
600}
601
602#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
603#[serde(rename_all = "camelCase")]
604pub struct IssuedTicketbooksChallengeCommitmentRequestBody {
605    #[serde(with = "crate::helpers::date_serde")]
606    #[schema(value_type = String, example = "1970-01-01")]
607    pub expiration_date: Date,
608    #[schema(value_type = Vec<u32>)]
609    pub deposits: Vec<DepositId>,
610}
611
612impl IssuedTicketbooksChallengeCommitmentRequestBody {
613    pub fn new(
614        expiration_date: Date,
615        deposits: Vec<DepositId>,
616    ) -> IssuedTicketbooksChallengeCommitmentRequestBody {
617        IssuedTicketbooksChallengeCommitmentRequestBody {
618            expiration_date,
619            deposits,
620        }
621    }
622}
623
624#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
625#[serde(rename_all = "camelCase")]
626pub struct IssuedTicketbooksChallengeCommitmentResponseBody {
627    #[serde(with = "crate::helpers::date_serde")]
628    #[schema(value_type = String, example = "1970-01-01")]
629    pub expiration_date: Date,
630
631    // keep the original request alongside the requester's signature
632    // to show that we returned the same date as we got asked for
633    // and haven't tampered with the content
634    pub original_request: IssuedTicketbooksChallengeCommitmentRequest,
635
636    /// Indicate the maximum number of entries that can be returned at once
637    /// when asking for ticketbook data
638    pub max_data_response_size: usize,
639
640    pub merkle_proof: IssuedTicketbooksFullMerkleProof,
641}
642
643#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
644#[serde(rename_all = "camelCase")]
645pub struct IssuedTicketbooksDataRequestBody {
646    #[serde(with = "crate::helpers::date_serde")]
647    #[schema(value_type = String, example = "1970-01-01")]
648    pub expiration_date: Date,
649    #[schema(value_type = Vec<u32>)]
650    pub deposits: Vec<DepositId>,
651}
652
653impl IssuedTicketbooksDataRequestBody {
654    pub fn new(
655        expiration_date: Date,
656        deposits: Vec<DepositId>,
657    ) -> IssuedTicketbooksDataRequestBody {
658        IssuedTicketbooksDataRequestBody {
659            expiration_date,
660            deposits,
661        }
662    }
663}
664
665#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
666#[serde(rename_all = "camelCase")]
667pub struct IssuedTicketbooksDataResponseBody {
668    #[serde(with = "crate::helpers::date_serde")]
669    #[schema(value_type = String, example = "1970-01-01")]
670    pub expiration_date: Date,
671
672    #[schema(value_type = BTreeMap<u32, IssuedTicketbook>)]
673    pub partial_ticketbooks: BTreeMap<DepositId, IssuedTicketbook>,
674
675    // keep the original request alongside the requester's signature
676    // to show that we returned the same deposits as we got asked for
677    // and haven't tampered with the content
678    pub original_request: IssuedTicketbooksDataRequest,
679}
680
681impl IssuedTicketbooksDataResponseBody {
682    // helpers so that library consumers wouldn't need to import all the required dependencies
683    pub fn try_get_partial_credential(
684        issued: &IssuedTicketbook,
685    ) -> Result<BlindedSignature, CompactEcashError> {
686        BlindedSignature::from_bytes(&issued.blinded_partial_credential)
687    }
688
689    pub fn try_get_private_attributes_commitments(
690        issued: &IssuedTicketbook,
691    ) -> Result<Vec<G1Projective>, CompactEcashError> {
692        BlindSignRequestBody::try_decode_joined_commitments(
693            &issued.joined_encoded_private_attributes_commitments,
694        )
695    }
696}
697
698#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
699#[serde(rename_all = "camelCase")]
700pub struct IssuedTicketbooksCountResponse {
701    pub issued: Vec<IssuedTicketbooksCount>,
702}
703
704#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
705#[serde(rename_all = "camelCase")]
706pub struct IssuedTicketbooksCount {
707    #[serde(with = "crate::helpers::date_serde")]
708    #[schema(value_type = String, example = "1970-01-01")]
709    pub issuance_date: Date,
710
711    #[serde(with = "crate::helpers::date_serde")]
712    #[schema(value_type = String, example = "1970-01-01")]
713    pub expiration_date: Date,
714
715    pub count: u32,
716}
717
718#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
719#[serde(rename_all = "camelCase")]
720pub struct IssuedTicketbooksOnCountResponse {
721    #[serde(with = "crate::helpers::date_serde")]
722    #[schema(value_type = String, example = "1970-01-01")]
723    pub issuance_date: Date,
724
725    pub total: usize,
726
727    pub issued: Vec<IssuedTicketbooksOnCount>,
728}
729
730#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
731#[serde(rename_all = "camelCase")]
732pub struct IssuedTicketbooksForCountResponse {
733    #[serde(with = "crate::helpers::date_serde")]
734    #[schema(value_type = String, example = "1970-01-01")]
735    pub expiration_date: Date,
736
737    pub total: usize,
738
739    pub issued: Vec<IssuedTicketbooksForCount>,
740}
741
742#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
743#[serde(rename_all = "camelCase")]
744pub struct IssuedTicketbooksOnCount {
745    #[serde(with = "crate::helpers::date_serde")]
746    #[schema(value_type = String, example = "1970-01-01")]
747    pub expiration_date: Date,
748
749    pub count: u32,
750}
751
752#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
753#[serde(rename_all = "camelCase")]
754pub struct IssuedTicketbooksForCount {
755    #[serde(with = "crate::helpers::date_serde")]
756    #[schema(value_type = String, example = "1970-01-01")]
757    pub issuance_date: Date,
758
759    pub count: u32,
760}
761
762pub type EcashSignerStatusResponse = SignedMessage<EcashSignerStatusResponseBody>;
763
764#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
765#[serde(rename_all = "camelCase")]
766// includes all pre-requisites for successful (assuming valid request) `/blind-sign`
767pub struct EcashSignerStatusResponseBody {
768    #[serde(with = "time::serde::rfc3339")]
769    #[schema(value_type = String)]
770    pub current_time: OffsetDateTime,
771
772    /// Current, perceived, dkg epoch id
773    pub dkg_ecash_epoch_id: EpochId,
774
775    /// Flag indicating whether the operator has explicitly disabled signer functionalities in the api
776    pub signer_disabled: bool,
777
778    /// Flag indicating whether this api thinks it's a valid ecash signer for the current epoch
779    pub is_ecash_signer: bool,
780
781    /// Flag indicating whether this api thinks it has valid signing keys.
782    /// It might be a valid signer that's not disabled, but the keys might have accidentally been
783    /// removed due to invalid data migration.
784    pub has_signing_keys: bool,
785}
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790    use nym_compact_ecash::scheme::keygen::KeyPairUser;
791    use nym_compact_ecash::withdrawal_request;
792    use nym_ecash_time::{ecash_today_date, EcashTime};
793    use rand_chacha::rand_core::SeedableRng;
794    use rand_chacha::ChaCha20Rng;
795
796    pub fn test_rng() -> ChaCha20Rng {
797        let dummy_seed = [42u8; 32];
798        ChaCha20Rng::from_seed(dummy_seed)
799    }
800
801    // had some issues with `Date` and serde...
802    // so might as well leave this unit test in case we do something to the helper
803    #[test]
804    fn aggregated_expiration_date_signature_responses_deserialises_correctly() {
805        let raw = r#"{"epoch_id":0,"expiration_date":"2024-08-03","signatures":[{"signature":{"h":"9379384af5236bffd7d6c533782d74863f892cf6e0fc8b4b64647e8bd5bccfe63df639a4a556d21826ab7ef54e4adafe","s":"83124c907935eba10c791cbc2cedf8714b68c6a266efead7bc4897c3b9652680bd151493689577ac790b9ccef1112f9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720137600},{"signature":{"h":"817b9fa0eef617ce751963d9c853df56fdf38a643b9ef8649b658d7ec3da61d49d9279f22509c66dd07051132a418c62","s":"af9fd22c2fde4cb36a0ee021c5a756f97f871ea404d639af1be845b277bed583f715d228d3556e79d15eba23a5b0d5e0"},"expiration_timestamp":1722643200,"spending_timestamp":1720224000},{"signature":{"h":"89f04f5ae1a1532551c2e17166775eac3c5ebba0198eff2919073efd733d6e17e0a6496e31061b0797f51b69d0fa17fc","s":"b752cdb2f4d8a4254d24d5773a8490de1ad84207cbe693638d1fdcbffd8426be96fae3586f19c570beb005bb6bf27b9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720310400},{"signature":{"h":"b1072da2612a6ebc0c02c7f673898dfea88eeeeb076a9dd2ab981d47a8176b87804ca6090cfb5b0c128f06adc6fdf0de","s":"8746a62f6333c3948f1fbb05543f0095cd44f610926c71f339511bf3bb97d98fd019d44c4a4dfc6710a9d5c718c5bfc2"},"expiration_timestamp":1722643200,"spending_timestamp":1720396800},{"signature":{"h":"8d266e7e56af7364d79c4190b24d5f0a601d7771c359815f642115123f5418d0304a46226d70e873b10a051f15178630","s":"a11d67c3293b224abb1256050031b32236484dc0f0fe66a9363d322e6b8c9b24719f4d4da0584d6106eb0f59fa6bb3c8"},"expiration_timestamp":1722643200,"spending_timestamp":1720483200},{"signature":{"h":"8ee31ef821949be316528b7b75b81baa6d0cf2846b615ff6d34e478f25746a07287ae6944279550879735740803c7922","s":"aa26b0df4445d8a10a226f1c70908451d211f787458c0743ba891c46bbbbde3bc59777f4edb9cabdd5fd181d772cfa43"},"expiration_timestamp":1722643200,"spending_timestamp":1720569600},{"signature":{"h":"b8fbacde3f9d507fb5bf6d5f96a33dd5c9c40cc9a062e92f9cdc6a2bddf8cbd873ffcc5112de816f98fbf47b74330abb","s":"a791aee82bdcf3cc4203ce657fbd2c3e9c2f0decd032c4ab91713d3cfd0ac57ebb9bb20e133d9a8a4ba1b8692a3b2706"},"expiration_timestamp":1722643200,"spending_timestamp":1720656000},{"signature":{"h":"b9ed48b47c1a1e6a1ae847ffaad01e0b7d22b2c9f9075beedf4924958ca044579f1e9ca18e268e2a0ef743c214934692","s":"a3e9191d0c3651c656a376e99f3d8c121a742ba55522765479ce866df8aaecc0281ae08003439debf7977f23e450a45f"},"expiration_timestamp":1722643200,"spending_timestamp":1720742400},{"signature":{"h":"a6f1fbdcc28953f318f067cde6e3d6ad0362fd46a501a74492be0884308cf306a7f30006181710f8883a380a8d4885f7","s":"9260d035ea97fc1b93d2db3802678005c95a4acdb0ce0c947cb0b9866ba4028a3b833d742f02ed0227ed33cee4ed17b4"},"expiration_timestamp":1722643200,"spending_timestamp":1720828800},{"signature":{"h":"95109fd35f7808084f224aa1fde645728c0c163751fe24d97e0002a7c38dc5a3a592dcb770893ddd25fc67f84e29e0a1","s":"b85279c4566aa365d208b57817ce96f1a1245895fbe25363202da89559fc454a2cfb09972fc3d1c468b96e8bc11b424c"},"expiration_timestamp":1722643200,"spending_timestamp":1720915200},{"signature":{"h":"8f4b7f93871d8f995a248d31664b02e1fe7107d67512f248a8f550d477846ca9f9862a5d44f5da458c6b83f4d8e62494","s":"b87e46a5d25de574eb4e16cf24199b2e0a658e0c1ef965b632165ed2d695637df76d373484dc93cec30e2f0a29ac91f8"},"expiration_timestamp":1722643200,"spending_timestamp":1721001600},{"signature":{"h":"b740491d3527f8fa0b8bed76d234d0737c09a049f653bb1e8a552b29f568aabd077604b08e7337d44f551fbda565e52e","s":"a08634a2560b3a79c8b56ff5387ef76c397c277810d78cc6dfac2161673df761d0ba61e6075e8a4781ec1b2ba7715b93"},"expiration_timestamp":1722643200,"spending_timestamp":1721088000},{"signature":{"h":"8d4b92fe48257d2574223fbe12afcdabd00db976e8d20faaddb5fb570373edb2a15af4ad9a7c10ceda3bb1304b0210aa","s":"8f8592c45a1cca753e5d0b04ffc2947fd27c7716f2751e632f2bc1bf0e73b179ce8080325951beb0886c5cf0fca79372"},"expiration_timestamp":1722643200,"spending_timestamp":1721174400},{"signature":{"h":"891d412e631041d07115cacf847575462d2389402a0e243a2f106b5e1c8f1431fa8e1aedc55cf943aef9fa8125d105a1","s":"ad2038ab95b823a8119bcc473be22101c4b58fd44ad375235c3664341fc617fe129702aec8765d8dc32ac845aec58a6a"},"expiration_timestamp":1722643200,"spending_timestamp":1721260800},{"signature":{"h":"84b97558e7889af167ba01ed608418ddabfc7fa71ba8f9cb045027f994fe9b5f9bb0ccc15822d1c73a37a48b041cd759","s":"adc370bc182ff9c7057591a3d4afcb21c714093e480374a07608c9f9d2baeff8c14fc5de3f0f457ea340e445ecc28487"},"expiration_timestamp":1722643200,"spending_timestamp":1721347200},{"signature":{"h":"b29b245acf66082c0f4e52cbf9db00ecb9cee070ddb1f5b56a9e85e2563dae04186ed03b7b590e0a9f4b4304ae1cd370","s":"8196445495ed1ee098bbe6083ece3522d4669e23dc4f3e2226e0e63294f1bc791e66380deedd21e5679e3019e93c7727"},"expiration_timestamp":1722643200,"spending_timestamp":1721433600},{"signature":{"h":"9750b9bdf1e86e8e4e6de44392f2160059da64838357a16e64155a5b7989fe7958ae95501e7c1f4039cf8b372ac3b214","s":"80707aa94af054fedcb4947809dab7a9317e303a12b7fb30858115c95bf8e46fec7ea9b0ce5097b75d8f7905c205f757"},"expiration_timestamp":1722643200,"spending_timestamp":1721520000},{"signature":{"h":"828be64b5cd147b5c43e68a2084cb77a762967d5621769136a0894c3d1ee50890412120d7c765cb3c835435d57ac4438","s":"956ca67f8f8591e72b8feeb53cbe52a0f69202313aa729c09760fb2a19b346fda1b7afd326822889fb228ca77a503072"},"expiration_timestamp":1722643200,"spending_timestamp":1721606400},{"signature":{"h":"83ac2c778f0b847af8a4bcc37bd5c2734544fe4427ee7ec5c4690738005cc805864fd5944266417ba2795d7333bd8c4a","s":"88936b7f61a0febfbedc8c0c40dad8f9c4eacb41d276ba00b7d03602ca69614faacca0d945713a272e9748dd6dd71f81"},"expiration_timestamp":1722643200,"spending_timestamp":1721692800},{"signature":{"h":"911c126da663478023efd1157586681937ac7f6c5a33b34a102b3611a63780b244ecdf05ecad09701e65ad20c5246f6a","s":"a1188f3b6ced776890b038e2d07b6af67f1a2c08f54a32d48dbfce61be3295e8ea910b6197b111a6ff16dd668eeda9f5"},"expiration_timestamp":1722643200,"spending_timestamp":1721779200},{"signature":{"h":"8c03c749b424e8fb3f26d51e45d21a7e9f59e6836f89f4a1f27b952e2bc8c038f3372f593dfb3bd284feb36918c2f194","s":"b20d3fd194f73142f0dfc68981519b76aa013e6743b2bdf63d41f3aa1e754d1f74c46012adf0332723f861e5aaa69dba"},"expiration_timestamp":1722643200,"spending_timestamp":1721865600},{"signature":{"h":"872de60a19f3ece61a70519b1f7a63476d13a20721ff00ae923a984bd710703af55112092e209970f074a8950b9e246e","s":"854906bbce5a0c2e47fa829441a03f5c3ac9357dc6d107547198f17505cd17852806e6d69b78264921a8c13c4a9fe50c"},"expiration_timestamp":1722643200,"spending_timestamp":1721952000},{"signature":{"h":"a2a57fd2b675fccccae34f9ff465f36638ee3809291c9b37b5830015a1a7c4d873528105f030a05efd16c6411672fe88","s":"b42f31e4ed05d3b026a62c5df97e04568c0c92d3fafaf95fa732ce69fbe7ee091ac025d04e3ce526b3474edaaf278398"},"expiration_timestamp":1722643200,"spending_timestamp":1722038400},{"signature":{"h":"b9be127e812602ac7a612897fd95e5869b360046993139590c82c2844654b6a5a72e9d74ed38eea6ee99ba0978382a3b","s":"b027db49bb9b0ec3931fc1cb4a6a739c968c45a59698d199056ebfcbeca7ddb363a123e55d05fbf5635d915be2a16ffb"},"expiration_timestamp":1722643200,"spending_timestamp":1722124800},{"signature":{"h":"8c40b2a5e17e1f9cdbe85eaaa419126374d35a255b8338474d442d0d40a4e10c3f2668f579155dfdad3fd403523683df","s":"a40cec4b00c9a975db6c307f25425f3a4d0e49eca2c05d5122630c54bd5d114f74e4abb78a7261752a9dfd4c020f8b91"},"expiration_timestamp":1722643200,"spending_timestamp":1722211200},{"signature":{"h":"b72821bc38167e7f422dcc669c19e0aeffb8247192c4bd50da1503b6aa28041bb5605598639bdb8b945dbc804fc852aa","s":"9443cad7c9d6194b601daf21cbc5fe3019d3350dd725f6d9e3ae366162f93e3b738779617f77ec146f0a05f9a40b602b"},"expiration_timestamp":1722643200,"spending_timestamp":1722297600},{"signature":{"h":"8fe36994ac7e92b5ad0869f2b1bfa3247aac27599459cce9d94cf84eeead8bba1ca4b294d16db3588a0eb9d9b28d4051","s":"8eae0ef0e966f9f473bd829f18e28cf9730447452040aaef114701f409ff237965f203f7394a7d61ca745a4b2f058d9b"},"expiration_timestamp":1722643200,"spending_timestamp":1722384000},{"signature":{"h":"a9f91a83a1db0c5609fcacd1290f1650e4e5ab98c3dd9f0b8fa1c54664c12ed17478ae6e598f0e900ace30addf1b0559","s":"801658058f8de9ba1c25518905196d7e4e0bc5e26a7a810c9dce3ac141f01c538c62808b4728ff683dee78670b2846f9"},"expiration_timestamp":1722643200,"spending_timestamp":1722470400},{"signature":{"h":"94ecd95c1d5ae03270079476aad6334dafdcf615157a6a34b05bb755a047bc2515e33a7e099f9fddc4eec3def8904cdf","s":"b59ff6ded76926a9ae97447ba00eac53d9aa0e1168218d990c5b97cc128a4f5a0c42f961f84cad8ad344630b2a15f3a9"},"expiration_timestamp":1722643200,"spending_timestamp":1722556800},{"signature":{"h":"ace778e70775d349c1e679e5ccef634a533738e7af8b51daa45ccbd920f976d15c4ba744d6ae8fdf711b84b20703cef6","s":"80581faceda2bc35b81240017e1ce8f121476c5ba4d75acddfc58ba6aaa2f12d626eaaab185548e6dd716b6e582740b8"},"expiration_timestamp":1722643200,"spending_timestamp":1722643200}]}"#;
806        let _: AggregatedExpirationDateSignatureResponse = serde_json::from_str(raw).unwrap();
807    }
808
809    #[test]
810    fn batch_redemption_body_roundtrip() {
811        let sn = vec![b"foomp".to_vec(), b"bar".to_vec()];
812        let gateway: AccountId = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf".parse().unwrap();
813        let digest = [42u8; 32].to_vec();
814
815        let req = BatchRedeemTicketsBody::new(digest, 69, sn, gateway);
816        let bytes = serde_json::to_vec(&req).unwrap();
817
818        let de: BatchRedeemTicketsBody = serde_json::from_slice(&bytes).unwrap();
819        assert_eq!(req, de);
820    }
821
822    #[test]
823    fn decoding_attribute_commitments() {
824        let mut rng = test_rng();
825        let keys = ed25519::KeyPair::new(&mut rng);
826        let dummy_sig = keys.private_key().sign("foomp");
827        let dummy_keypair = KeyPairUser::new();
828        let date = ecash_today_date();
829        let typ = TicketType::V1MixnetEntry;
830
831        let (inner, _) = withdrawal_request(
832            dummy_keypair.secret_key(),
833            date.ecash_unix_timestamp(),
834            typ.encode(),
835        )
836        .unwrap();
837
838        let dummy = BlindSignRequestBody {
839            inner_sign_request: inner,
840            deposit_id: 42,
841            signature: dummy_sig,
842            ecash_pubkey: dummy_keypair.public_key(),
843            expiration_date: date,
844            ticketbook_type: typ,
845        };
846
847        let good = dummy.encode_join_commitments();
848        let recovered = BlindSignRequestBody::try_decode_joined_commitments(&good).unwrap();
849        assert_eq!(
850            recovered,
851            dummy
852                .inner_sign_request
853                .get_private_attributes_commitments()
854        );
855
856        let mut bad1 = good.clone();
857        let mut bad2 = good.clone();
858        bad1.push(42);
859        bad2.pop().unwrap();
860
861        assert!(BlindSignRequestBody::try_decode_joined_commitments(&bad1).is_err());
862        assert!(BlindSignRequestBody::try_decode_joined_commitments(&bad2).is_err());
863    }
864}