nym_validator_client/coconut/
mod.rs

1// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
5use crate::nyxd::error::NyxdError;
6use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
7use nym_coconut_dkg_common::verification_key::ContractVKShare;
8use nym_compact_ecash::error::CompactEcashError;
9use nym_compact_ecash::{Base58, VerificationKeyAuth};
10use std::fmt::{Display, Formatter};
11use thiserror::Error;
12use url::Url;
13
14// TODO: it really doesn't feel like this should live in this crate.
15#[derive(Clone)]
16pub struct EcashApiClient {
17    pub api_client: nym_http_api_client::Client,
18    pub verification_key: VerificationKeyAuth,
19    pub node_id: NodeIndex,
20    pub cosmos_address: cosmrs::AccountId,
21}
22
23impl Display for EcashApiClient {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        write!(
26            f,
27            "[id: {}] {} @ ({})",
28            self.node_id,
29            self.cosmos_address,
30            self.api_client
31                .base_urls()
32                .iter()
33                .map(|url| url.to_string())
34                .collect::<Vec<String>>()
35                .join(", ")
36        )
37    }
38}
39
40// TODO: this should be using the coconut error
41// (which is in different crate; perhaps this client should be moved there?)
42
43#[derive(Debug, Error)]
44pub enum EcashApiError {
45    // TODO: ask @BN whether this is a correct error message
46    #[error("the provided key share hasn't been verified")]
47    UnverifiedShare,
48
49    #[error("failed to query the contract: {source}")]
50    ContractQueryFailure {
51        #[from]
52        source: NyxdError,
53    },
54
55    #[error("the provided announce address is malformed: {source}")]
56    MalformedAnnounceAddress {
57        #[from]
58        source: url::ParseError,
59    },
60
61    #[error("the provided verification key is malformed: {source}")]
62    MalformedVerificationKey {
63        #[from]
64        source: CompactEcashError,
65    },
66
67    #[error("failed to create API client: {0}")]
68    ClientError(String),
69
70    #[error("the provided account address is malformed: {source}")]
71    MalformedAccountAddress {
72        #[from]
73        source: cosmrs::ErrorReport,
74    },
75
76    #[error("nym api error")]
77    NymApi {
78        #[from]
79        source: crate::ValidatorClientError,
80    },
81}
82
83impl TryFrom<ContractVKShare> for EcashApiClient {
84    type Error = EcashApiError;
85
86    fn try_from(share: ContractVKShare) -> Result<Self, Self::Error> {
87        if !share.verified {
88            return Err(EcashApiError::UnverifiedShare);
89        }
90
91        let url_address = Url::parse(&share.announce_address)?;
92
93        // The NymApiClient constructed here uses the default (hickory DoT/DoH) resolver because
94        // this EcashApiClient is used by both client and non-client applications.
95        //
96        // In non-client applications this resolver can cause warning logs about H2 connection
97        // failure. This indicates that the long lived https connection was closed by the remote
98        // peer and the resolver will have to reconnect. It should not impact actual functionality
99        let api_client = nym_http_api_client::Client::builder(url_address)
100            .map_err(|e| EcashApiError::ClientError(e.to_string()))?
101            .build()
102            .map_err(|e| EcashApiError::ClientError(e.to_string()))?;
103
104        Ok(EcashApiClient {
105            api_client,
106            verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
107            node_id: share.node_index,
108            cosmos_address: share.owner.as_str().parse()?,
109        })
110    }
111}
112
113pub async fn all_ecash_api_clients<C>(
114    client: &C,
115    epoch_id: EpochId,
116) -> Result<Vec<EcashApiClient>, EcashApiError>
117where
118    C: DkgQueryClient + Sync + Send,
119{
120    // TODO: this will error out if there's an invalid share out there. is that what we want?
121    client
122        .get_all_verification_key_shares(epoch_id)
123        .await?
124        .into_iter()
125        .map(TryInto::try_into)
126        .collect::<Result<Vec<_>, _>>()
127
128    // ... if not, let's switch to the below:
129    // client
130    //     .get_all_verification_key_shares(epoch_id)
131    //     .await?
132    //     .into_iter()
133    //     .filter_map(TryInto::try_into)
134    //     .collect::<Result<Vec<_>, _>>()
135}