Skip to main content

nym_validator_client/nyxd/
error.rs

1// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::nyxd::cosmwasm_client::types::ContractCodeId;
5use crate::signing::direct_wallet::DirectSecp256k1HdWalletError;
6use cosmrs::tendermint::Hash;
7use cosmrs::{
8    tendermint::{abci::Code as AbciCode, block},
9    AccountId,
10};
11use std::{io, time::Duration};
12use tendermint_rpc::endpoint::abci_query::AbciQuery;
13use thiserror::Error;
14
15pub use cosmrs::tendermint::error::Error as TendermintError;
16pub use tendermint_rpc::{
17    error::{Error as TendermintRpcError, ErrorDetail as TendermintRpcErrorDetail},
18    response_error::{Code, ResponseError},
19};
20
21#[derive(Debug, Error)]
22pub enum NyxdError {
23    #[error("No contract address is available to perform the call: {0}")]
24    NoContractAddressAvailable(String),
25
26    #[error(transparent)]
27    WalletError(#[from] DirectSecp256k1HdWalletError),
28
29    #[error("There was an issue on the cosmrs side: {0}")]
30    CosmrsError(#[from] cosmrs::Error),
31
32    #[error("There was an issue on the cosmrs side: {0}")]
33    CosmrsErrorReport(#[from] cosmrs::ErrorReport),
34
35    #[error("cosmwasm event not found")]
36    ComswasmEventNotFound,
37
38    #[error("cosmwasm attribute not found")]
39    ComswasmAttributeNotFound,
40
41    #[error("Failed to derive account address")]
42    AccountDerivationError,
43
44    #[error("Address {0} was not found in the wallet")]
45    SigningAccountNotFound(AccountId),
46
47    #[error("Failed to sign raw transaction")]
48    SigningFailure,
49
50    #[error("{0} is not a valid tx hash")]
51    InvalidTxHash(String),
52
53    #[error("Tendermint RPC request failed - {0}")]
54    TendermintErrorRpc(#[from] TendermintRpcError),
55
56    #[error("tendermint library failure: {0}")]
57    TendermintError(#[from] TendermintError),
58
59    #[error("Failed when attempting to serialize data ({0})")]
60    SerializationError(String),
61
62    #[error("Failed when attempting to deserialize data ({0})")]
63    DeserializationError(String),
64
65    #[error("Failed when attempting to encode our protobuf data - {0}")]
66    ProtobufEncodingError(#[from] prost::EncodeError),
67
68    #[error("Failed to decode our protobuf data - {0}")]
69    ProtobufDecodingError(#[from] prost::DecodeError),
70
71    #[error("Account {0} does not exist on the chain")]
72    NonExistentAccountError(AccountId),
73
74    #[error("Failed on json serialization/deserialization - {0}")]
75    SerdeJsonError(#[from] serde_json::Error),
76
77    #[error("Account {0} is not a valid account address")]
78    MalformedAccountAddress(String),
79
80    #[error("Account {0} has an invalid associated public key")]
81    InvalidPublicKey(AccountId),
82
83    #[error("Queried contract (code_id: {0}) did not have any code information attached")]
84    NoCodeInformation(ContractCodeId),
85
86    #[error("Queried contract (address: {0}) did not have any contract information attached")]
87    NoContractInformation(AccountId),
88
89    #[error("Contract contains invalid operations in its history")]
90    InvalidContractHistoryOperation,
91
92    #[error("Block has an invalid height (either negative or larger than i64::MAX")]
93    InvalidHeight,
94
95    #[error("Failed to compress provided wasm code - {0}")]
96    WasmCompressionError(io::Error),
97
98    #[error("Logs returned from the validator were malformed")]
99    MalformedLogString,
100
101    #[error(
102    "Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
103    )]
104    BroadcastTxErrorCheckTx {
105        hash: Hash,
106        height: Option<block::Height>,
107        code: u32,
108        raw_log: String,
109    },
110
111    #[error(
112    "Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
113    )]
114    BroadcastTxErrorDeliverTx {
115        hash: Hash,
116        height: Option<block::Height>,
117        code: u32,
118        raw_log: String,
119    },
120
121    #[error("The provided gas price is malformed")]
122    MalformedGasPrice,
123
124    #[error("Failed to estimate gas price for the transaction")]
125    GasEstimationFailure,
126
127    #[error("Abci query failed with code {code} - {log}")]
128    AbciError {
129        code: u32,
130        log: String,
131        pretty_log: Option<String>,
132    },
133
134    #[error("Unsupported account type: {type_url}")]
135    UnsupportedAccountType { type_url: String },
136
137    #[error("{coin_representation} is not a valid Cosmos Coin")]
138    MalformedCoin { coin_representation: String },
139
140    #[error("This account does not have BaseAccount information available to it")]
141    NoBaseAccountInformationAvailable,
142
143    #[error("Transaction with ID {hash} has been submitted but not yet found on the chain. You might want to check for it later. There was a total wait of {} seconds", .timeout.as_secs())]
144    BroadcastTimeout { hash: Hash, timeout: Duration },
145
146    #[error("Cosmwasm std error: {0}")]
147    CosmwasmStdError(#[from] cosmwasm_std::StdError),
148
149    #[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")]
150    UnexpectedBech32Prefix { got: String, expected: String },
151
152    #[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")]
153    UnexpectedNumberOfMsgResponses { got: usize },
154
155    #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")]
156    MalformedResponseData { got: usize, expected: usize },
157
158    #[error(
159        "one of the extension query for {contract} failed with the following message: {message}"
160    )]
161    ExtensionQueryFailure { contract: String, message: String },
162}
163
164impl NyxdError {
165    pub fn extension_query_failure(
166        contract: impl Into<String>,
167        message: impl Into<String>,
168    ) -> Self {
169        NyxdError::ExtensionQueryFailure {
170            contract: contract.into(),
171            message: message.into(),
172        }
173    }
174}
175
176// The purpose of parsing the abci query result is that we want to generate the `pretty_log` if
177// possible.
178pub fn parse_abci_query_result(query_result: AbciQuery) -> Result<AbciQuery, NyxdError> {
179    match query_result.code {
180        AbciCode::Ok => Ok(query_result),
181        AbciCode::Err(code) => Err(NyxdError::AbciError {
182            code: code.into(),
183            log: query_result.log.clone(),
184            pretty_log: try_parse_abci_log(&query_result.log),
185        }),
186    }
187}
188
189// Some of the error strings returned by the query are a bit too technical to present to the
190// enduser. So we special case some commonly encountered errors.
191fn try_parse_abci_log(log: &str) -> Option<String> {
192    if log.contains("Maximum amount of locked coins has already been pledged") {
193        Some("Maximum amount of locked tokens has already been used. You can only use up to 10% of your locked tokens for bonding and delegating.".to_string())
194    } else {
195        None
196    }
197}
198
199impl NyxdError {
200    pub fn is_tendermint_response_timeout(&self) -> bool {
201        match &self {
202            NyxdError::TendermintErrorRpc(TendermintRpcError(
203                TendermintRpcErrorDetail::Response(err),
204                _,
205            )) => {
206                let response = &err.source;
207                if response.code() == Code::InternalError {
208                    // 0.34 (and earlier) versions of tendermint seemed to be using phrase "timed out waiting ..."
209                    // (https://github.com/tendermint/tendermint/blob/v0.34.13/rpc/core/mempool.go#L124)
210                    // while 0.35+ has "timeout waiting for ..."
211                    // https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/internal/rpc/core/mempool.go#L99
212                    // note that as of the time of writing this comment (08.10.2021), the most recent version
213                    // of cosmos-sdk (v0.44.1) uses tendermint 0.34.13
214                    if let Some(data) = response.data() {
215                        data.contains("timed out") || data.contains("timeout")
216                    } else {
217                        false
218                    }
219                } else {
220                    false
221                }
222            }
223            _ => false,
224        }
225    }
226
227    pub fn is_tendermint_response_duplicate(&self) -> bool {
228        match &self {
229            NyxdError::TendermintErrorRpc(TendermintRpcError(
230                TendermintRpcErrorDetail::Response(err),
231                _,
232            )) => {
233                let response = &err.source;
234                if response.code() == Code::InternalError {
235                    // this particular error message seems to be unchanged between 0.34 and newer versions
236                    // https://github.com/tendermint/tendermint/blob/v0.34.13/mempool/errors.go#L10
237                    // https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/types/mempool.go#L10
238                    if let Some(data) = response.data() {
239                        data.contains("tx already exists in cache")
240                    } else {
241                        false
242                    }
243                } else {
244                    false
245                }
246            }
247            _ => false,
248        }
249    }
250
251    /// Returns `true` if the error indicates the queried block height has been
252    /// pruned by the RPC node (its history no longer reaches that low).
253    pub fn is_block_pruned(&self) -> bool {
254        match &self {
255            NyxdError::TendermintErrorRpc(TendermintRpcError(
256                TendermintRpcErrorDetail::Response(err),
257                _,
258            )) => {
259                let response = &err.source;
260                if response.code() == Code::InternalError {
261                    // e.g. "height 14862522 is not available, lowest height is 16853136"
262                    if let Some(data) = response.data() {
263                        data.contains("is not available") && data.contains("lowest height")
264                    } else {
265                        false
266                    }
267                } else {
268                    false
269                }
270            }
271            _ => false,
272        }
273    }
274
275    pub fn unavailable_contract_address<S: Into<String>>(contract_type: S) -> Self {
276        NyxdError::NoContractAddressAvailable(contract_type.into())
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    fn rpc_internal_error(data: &str) -> NyxdError {
285        let response = ResponseError::new(Code::InternalError, Some(data.to_string()));
286        NyxdError::TendermintErrorRpc(TendermintRpcError::response(response))
287    }
288
289    #[test]
290    fn detects_pruned_block_error() {
291        let err = rpc_internal_error("height 14862522 is not available, lowest height is 16853136");
292        assert!(err.is_block_pruned());
293    }
294
295    #[test]
296    fn ignores_other_internal_errors() {
297        assert!(
298            !rpc_internal_error("timed out waiting for tx to be included in a block")
299                .is_block_pruned()
300        );
301    }
302
303    #[test]
304    fn ignores_non_rpc_errors() {
305        assert!(!NyxdError::NoContractAddressAvailable("mixnet".to_string()).is_block_pruned());
306    }
307}