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("The response did not contain the requested IAVL inclusion proof")]
96    MissingProof,
97
98    #[error("Failed to compress provided wasm code - {0}")]
99    WasmCompressionError(io::Error),
100
101    #[error("Logs returned from the validator were malformed")]
102    MalformedLogString,
103
104    #[error(
105    "Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
106    )]
107    BroadcastTxErrorCheckTx {
108        hash: Hash,
109        height: Option<block::Height>,
110        code: u32,
111        raw_log: String,
112    },
113
114    #[error(
115    "Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
116    )]
117    BroadcastTxErrorDeliverTx {
118        hash: Hash,
119        height: Option<block::Height>,
120        code: u32,
121        raw_log: String,
122    },
123
124    #[error("The provided gas price is malformed")]
125    MalformedGasPrice,
126
127    #[error("Failed to estimate gas price for the transaction")]
128    GasEstimationFailure,
129
130    #[error("Abci query failed with code {code} - {log}")]
131    AbciError {
132        code: u32,
133        log: String,
134        pretty_log: Option<String>,
135    },
136
137    #[error("Unsupported account type: {type_url}")]
138    UnsupportedAccountType { type_url: String },
139
140    #[error("{coin_representation} is not a valid Cosmos Coin")]
141    MalformedCoin { coin_representation: String },
142
143    #[error("This account does not have BaseAccount information available to it")]
144    NoBaseAccountInformationAvailable,
145
146    #[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())]
147    BroadcastTimeout { hash: Hash, timeout: Duration },
148
149    #[error("Cosmwasm std error: {0}")]
150    CosmwasmStdError(#[from] cosmwasm_std::StdError),
151
152    #[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")]
153    UnexpectedBech32Prefix { got: String, expected: String },
154
155    #[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")]
156    UnexpectedNumberOfMsgResponses { got: usize },
157
158    #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")]
159    MalformedResponseData { got: usize, expected: usize },
160
161    #[error(
162        "one of the extension query for {contract} failed with the following message: {message}"
163    )]
164    ExtensionQueryFailure { contract: String, message: String },
165}
166
167impl NyxdError {
168    pub fn extension_query_failure(
169        contract: impl Into<String>,
170        message: impl Into<String>,
171    ) -> Self {
172        NyxdError::ExtensionQueryFailure {
173            contract: contract.into(),
174            message: message.into(),
175        }
176    }
177}
178
179// The purpose of parsing the abci query result is that we want to generate the `pretty_log` if
180// possible.
181pub fn parse_abci_query_result(query_result: AbciQuery) -> Result<AbciQuery, NyxdError> {
182    match query_result.code {
183        AbciCode::Ok => Ok(query_result),
184        AbciCode::Err(code) => Err(NyxdError::AbciError {
185            code: code.into(),
186            log: query_result.log.clone(),
187            pretty_log: try_parse_abci_log(&query_result.log),
188        }),
189    }
190}
191
192// Some of the error strings returned by the query are a bit too technical to present to the
193// enduser. So we special case some commonly encountered errors.
194fn try_parse_abci_log(log: &str) -> Option<String> {
195    if log.contains("Maximum amount of locked coins has already been pledged") {
196        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())
197    } else {
198        None
199    }
200}
201
202impl NyxdError {
203    pub fn is_tendermint_response_timeout(&self) -> bool {
204        match &self {
205            NyxdError::TendermintErrorRpc(TendermintRpcError(
206                TendermintRpcErrorDetail::Response(err),
207                _,
208            )) => {
209                let response = &err.source;
210                if response.code() == Code::InternalError {
211                    // 0.34 (and earlier) versions of tendermint seemed to be using phrase "timed out waiting ..."
212                    // (https://github.com/tendermint/tendermint/blob/v0.34.13/rpc/core/mempool.go#L124)
213                    // while 0.35+ has "timeout waiting for ..."
214                    // https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/internal/rpc/core/mempool.go#L99
215                    // note that as of the time of writing this comment (08.10.2021), the most recent version
216                    // of cosmos-sdk (v0.44.1) uses tendermint 0.34.13
217                    if let Some(data) = response.data() {
218                        data.contains("timed out") || data.contains("timeout")
219                    } else {
220                        false
221                    }
222                } else {
223                    false
224                }
225            }
226            _ => false,
227        }
228    }
229
230    pub fn is_tendermint_response_duplicate(&self) -> bool {
231        match &self {
232            NyxdError::TendermintErrorRpc(TendermintRpcError(
233                TendermintRpcErrorDetail::Response(err),
234                _,
235            )) => {
236                let response = &err.source;
237                if response.code() == Code::InternalError {
238                    // this particular error message seems to be unchanged between 0.34 and newer versions
239                    // https://github.com/tendermint/tendermint/blob/v0.34.13/mempool/errors.go#L10
240                    // https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/types/mempool.go#L10
241                    if let Some(data) = response.data() {
242                        data.contains("tx already exists in cache")
243                    } else {
244                        false
245                    }
246                } else {
247                    false
248                }
249            }
250            _ => false,
251        }
252    }
253
254    /// Returns `true` if the error indicates the queried block height has been
255    /// pruned by the RPC node (its history no longer reaches that low).
256    pub fn is_block_pruned(&self) -> bool {
257        match &self {
258            NyxdError::TendermintErrorRpc(TendermintRpcError(
259                TendermintRpcErrorDetail::Response(err),
260                _,
261            )) => {
262                let response = &err.source;
263                if response.code() == Code::InternalError {
264                    // e.g. "height 14862522 is not available, lowest height is 16853136"
265                    if let Some(data) = response.data() {
266                        data.contains("is not available") && data.contains("lowest height")
267                    } else {
268                        false
269                    }
270                } else {
271                    false
272                }
273            }
274            _ => false,
275        }
276    }
277
278    pub fn unavailable_contract_address<S: Into<String>>(contract_type: S) -> Self {
279        NyxdError::NoContractAddressAvailable(contract_type.into())
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn rpc_internal_error(data: &str) -> NyxdError {
288        let response = ResponseError::new(Code::InternalError, Some(data.to_string()));
289        NyxdError::TendermintErrorRpc(TendermintRpcError::response(response))
290    }
291
292    #[test]
293    fn detects_pruned_block_error() {
294        let err = rpc_internal_error("height 14862522 is not available, lowest height is 16853136");
295        assert!(err.is_block_pruned());
296    }
297
298    #[test]
299    fn ignores_other_internal_errors() {
300        assert!(
301            !rpc_internal_error("timed out waiting for tx to be included in a block")
302                .is_block_pruned()
303        );
304    }
305
306    #[test]
307    fn ignores_non_rpc_errors() {
308        assert!(!NyxdError::NoContractAddressAvailable("mixnet".to_string()).is_block_pruned());
309    }
310}