use crate::nyxd::cosmwasm_client::types::ContractCodeId;
use crate::signing::direct_wallet::DirectSecp256k1HdWalletError;
use cosmrs::tendermint::Hash;
use cosmrs::{
tendermint::{abci::Code as AbciCode, block},
AccountId,
};
use std::{io, time::Duration};
use tendermint_rpc::endpoint::abci_query::AbciQuery;
use thiserror::Error;
pub use cosmrs::tendermint::error::Error as TendermintError;
pub use tendermint_rpc::{
error::{Error as TendermintRpcError, ErrorDetail as TendermintRpcErrorDetail},
response_error::{Code, ResponseError},
};
#[derive(Debug, Error)]
pub enum NyxdError {
#[error("No contract address is available to perform the call: {0}")]
NoContractAddressAvailable(String),
#[error(transparent)]
WalletError(#[from] DirectSecp256k1HdWalletError),
#[error("There was an issue on the cosmrs side: {0}")]
CosmrsError(#[from] cosmrs::Error),
#[error("There was an issue on the cosmrs side: {0}")]
CosmrsErrorReport(#[from] cosmrs::ErrorReport),
#[error("cosmwasm event not found")]
ComswasmEventNotFound,
#[error("cosmwasm attribute not found")]
ComswasmAttributeNotFound,
#[error("Failed to derive account address")]
AccountDerivationError,
#[error("Address {0} was not found in the wallet")]
SigningAccountNotFound(AccountId),
#[error("Failed to sign raw transaction")]
SigningFailure,
#[error("{0} is not a valid tx hash")]
InvalidTxHash(String),
#[error("Tendermint RPC request failed - {0}")]
TendermintErrorRpc(#[from] TendermintRpcError),
#[error("tendermint library failure: {0}")]
TendermintError(#[from] TendermintError),
#[error("Failed when attempting to serialize data ({0})")]
SerializationError(String),
#[error("Failed when attempting to deserialize data ({0})")]
DeserializationError(String),
#[error("Failed when attempting to encode our protobuf data - {0}")]
ProtobufEncodingError(#[from] prost::EncodeError),
#[error("Failed to decode our protobuf data - {0}")]
ProtobufDecodingError(#[from] prost::DecodeError),
#[error("Account {0} does not exist on the chain")]
NonExistentAccountError(AccountId),
#[error("Failed on json serialization/deserialization - {0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Account {0} is not a valid account address")]
MalformedAccountAddress(String),
#[error("Account {0} has an invalid associated public key")]
InvalidPublicKey(AccountId),
#[error("Queried contract (code_id: {0}) did not have any code information attached")]
NoCodeInformation(ContractCodeId),
#[error("Queried contract (address: {0}) did not have any contract information attached")]
NoContractInformation(AccountId),
#[error("Contract contains invalid operations in its history")]
InvalidContractHistoryOperation,
#[error("Block has an invalid height (either negative or larger than i64::MAX")]
InvalidHeight,
#[error("Failed to compress provided wasm code - {0}")]
WasmCompressionError(io::Error),
#[error("Logs returned from the validator were malformed")]
MalformedLogString,
#[error(
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorCheckTx {
hash: Hash,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
#[error(
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorDeliverTx {
hash: Hash,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
#[error("The provided gas price is malformed")]
MalformedGasPrice,
#[error("Failed to estimate gas price for the transaction")]
GasEstimationFailure,
#[error("Abci query failed with code {code} - {log}")]
AbciError {
code: u32,
log: String,
pretty_log: Option<String>,
},
#[error("Unsupported account type: {type_url}")]
UnsupportedAccountType { type_url: String },
#[error("{coin_representation} is not a valid Cosmos Coin")]
MalformedCoin { coin_representation: String },
#[error("This account does not have BaseAccount information available to it")]
NoBaseAccountInformationAvailable,
#[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())]
BroadcastTimeout { hash: Hash, timeout: Duration },
#[error("Cosmwasm std error: {0}")]
CosmwasmStdError(#[from] cosmwasm_std::StdError),
#[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")]
UnexpectedBech32Prefix { got: String, expected: String },
#[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")]
UnexpectedNumberOfMsgResponses { got: usize },
#[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")]
MalformedResponseData { got: usize, expected: usize },
#[error(
"one of the extension query for {contract} failed with the following message: {message}"
)]
ExtensionQueryFailure { contract: String, message: String },
}
impl NyxdError {
pub fn extension_query_failure(
contract: impl Into<String>,
message: impl Into<String>,
) -> Self {
NyxdError::ExtensionQueryFailure {
contract: contract.into(),
message: message.into(),
}
}
}
pub fn parse_abci_query_result(query_result: AbciQuery) -> Result<AbciQuery, NyxdError> {
match query_result.code {
AbciCode::Ok => Ok(query_result),
AbciCode::Err(code) => Err(NyxdError::AbciError {
code: code.into(),
log: query_result.log.clone(),
pretty_log: try_parse_abci_log(&query_result.log),
}),
}
}
fn try_parse_abci_log(log: &str) -> Option<String> {
if log.contains("Maximum amount of locked coins has already been pledged") {
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())
} else {
None
}
}
impl NyxdError {
pub fn is_tendermint_response_timeout(&self) -> bool {
match &self {
NyxdError::TendermintErrorRpc(TendermintRpcError(
TendermintRpcErrorDetail::Response(err),
_,
)) => {
let response = &err.source;
if response.code() == Code::InternalError {
if let Some(data) = response.data() {
data.contains("timed out") || data.contains("timeout")
} else {
false
}
} else {
false
}
}
_ => false,
}
}
pub fn is_tendermint_response_duplicate(&self) -> bool {
match &self {
NyxdError::TendermintErrorRpc(TendermintRpcError(
TendermintRpcErrorDetail::Response(err),
_,
)) => {
let response = &err.source;
if response.code() == Code::InternalError {
if let Some(data) = response.data() {
data.contains("tx already exists in cache")
} else {
false
}
} else {
false
}
}
_ => false,
}
}
pub fn is_block_pruned(&self) -> bool {
match &self {
NyxdError::TendermintErrorRpc(TendermintRpcError(
TendermintRpcErrorDetail::Response(err),
_,
)) => {
let response = &err.source;
if response.code() == Code::InternalError {
if let Some(data) = response.data() {
data.contains("is not available") && data.contains("lowest height")
} else {
false
}
} else {
false
}
}
_ => false,
}
}
pub fn unavailable_contract_address<S: Into<String>>(contract_type: S) -> Self {
NyxdError::NoContractAddressAvailable(contract_type.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rpc_internal_error(data: &str) -> NyxdError {
let response = ResponseError::new(Code::InternalError, Some(data.to_string()));
NyxdError::TendermintErrorRpc(TendermintRpcError::response(response))
}
#[test]
fn detects_pruned_block_error() {
let err = rpc_internal_error("height 14862522 is not available, lowest height is 16853136");
assert!(err.is_block_pruned());
}
#[test]
fn ignores_other_internal_errors() {
assert!(
!rpc_internal_error("timed out waiting for tx to be included in a block")
.is_block_pruned()
);
}
#[test]
fn ignores_non_rpc_errors() {
assert!(!NyxdError::NoContractAddressAvailable("mixnet".to_string()).is_block_pruned());
}
}