Skip to main content

qorechain/tx/
errors.rs

1//! Transaction error decoding: map an ABCI `(code, codespace, raw_log)` triple
2//! to a typed [`QoreTxError`] with a human-readable reason, mirroring the
3//! TS / Go SDKs.
4
5use std::fmt;
6
7/// A decoded transaction error: a non-zero ABCI result code from `CheckTx` or
8/// `DeliverTx`, mapped (where possible) to a human-readable reason.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct QoreTxError {
11    /// The non-zero ABCI result code.
12    pub code: u32,
13    /// The module that produced the error (e.g. `"sdk"`, `"bank"`, `"amm"`). An
14    /// empty codespace means the root SDK codespace.
15    pub codespace: String,
16    /// The mapped human-readable description, or a generic fallback when the
17    /// `(codespace, code)` pair is not recognized.
18    pub reason: String,
19    /// The chain's `raw_log` string for the failed tx (may be empty).
20    pub raw_log: String,
21    /// The failed tx hash, when known.
22    pub tx_hash: String,
23}
24
25impl fmt::Display for QoreTxError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        let cs = if self.codespace.is_empty() {
28            "sdk"
29        } else {
30            &self.codespace
31        };
32        write!(f, "tx failed: code {} ({}/{})", self.code, cs, self.reason)?;
33        if !self.tx_hash.is_empty() {
34            write!(f, " tx {}", self.tx_hash)?;
35        }
36        if !self.raw_log.is_empty() {
37            write!(f, ": {}", self.raw_log)?;
38        }
39        Ok(())
40    }
41}
42
43impl std::error::Error for QoreTxError {}
44
45/// Maps an ABCI `(code, codespace, raw_log)` triple to a [`QoreTxError`].
46///
47/// A zero `code` returns `None` (success). The codespace selects the code table:
48/// the empty / `"sdk"` codespace uses the root SDK table; a module codespace
49/// uses its per-module table, with a generic fallback when the specific code is
50/// unmapped.
51pub fn decode_tx_error(code: u32, codespace: &str, raw_log: &str) -> Option<QoreTxError> {
52    if code == 0 {
53        return None;
54    }
55    Some(QoreTxError {
56        code,
57        codespace: codespace.to_string(),
58        reason: reason_for(code, codespace).to_string(),
59        raw_log: raw_log.to_string(),
60        tx_hash: String::new(),
61    })
62}
63
64/// The canonical descriptions for the root (`"sdk"`) codespace ABCI codes.
65fn sdk_reason(code: u32) -> Option<&'static str> {
66    Some(match code {
67        1 => "internal error",
68        2 => "tx parse error",
69        3 => "invalid sequence",
70        4 => "unauthorized",
71        5 => "insufficient funds",
72        6 => "unknown request",
73        7 => "invalid address",
74        8 => "invalid pubkey",
75        9 => "unknown address",
76        10 => "invalid coins",
77        11 => "out of gas",
78        12 => "memo too large",
79        13 => "insufficient fee",
80        14 => "maximum number of signatures exceeded",
81        15 => "no signatures supplied",
82        16 => "failed to marshal JSON bytes",
83        17 => "failed to unmarshal JSON bytes",
84        18 => "invalid request",
85        19 => "tx already in mempool",
86        20 => "mempool is full",
87        21 => "tx too large",
88        22 => "key not found",
89        23 => "invalid account password",
90        24 => "invalid signature",
91        25 => "no concrete type registered",
92        26 => "unpacking protobuf message failed",
93        27 => "invalid gas adjustment",
94        28 => "invalid height",
95        29 => "invalid version",
96        30 => "invalid chain id",
97        31 => "invalid type",
98        32 => "tx timeout height",
99        33 => "unknown extension options",
100        35 => "invalid gas limit",
101        _ => return None,
102    })
103}
104
105/// The per-module codespace tables (Cosmos + QoreChain). Only the commonly
106/// surfaced codes are enumerated; unmapped codes fall back to a generic message.
107fn module_reason(codespace: &str, code: u32) -> Option<&'static str> {
108    let reason = match (codespace, code) {
109        ("bank", 2) => "no inputs to send transaction",
110        ("bank", 3) => "no outputs to send transaction",
111        ("bank", 4) => "sum inputs != sum outputs",
112        ("bank", 5) => "send transactions are disabled",
113        ("staking", 2) => "validator does not exist",
114        ("staking", 3) => "validator already exist for this operator address",
115        ("staking", 13) => "too many shares to undelegate",
116        ("staking", 15) => "insufficient delegation shares",
117        ("distribution", 2) => "no delegation distribution info",
118        ("distribution", 3) => "no validator distribution info",
119        ("distribution", 6) => "set withdraw address disabled",
120        ("gov", 2) => "unknown proposal",
121        ("gov", 3) => "inactive proposal",
122        ("gov", 4) => "already active proposal",
123        ("gov", 5) => "invalid proposal content",
124        ("authz", 2) => "authorization not found",
125        ("authz", 3) => "invalid expiration time",
126        ("feegrant", 2) => "fee limit exceeded",
127        ("feegrant", 3) => "fee allowance already exists",
128        ("feegrant", 4) => "fee allowance expired",
129        _ => return None,
130    };
131    Some(reason)
132}
133
134/// The QoreChain custom-module codespaces recognized by the decoder. A code that
135/// is not specifically mapped still resolves to `"unknown <codespace> error"`
136/// rather than the root `"unknown error"`.
137const QORE_CODESPACES: &[&str] = &[
138    "pqc",
139    "amm",
140    "bridge",
141    "rdk",
142    "multilayer",
143    "svm",
144    "lightnode",
145    "license",
146    "abstractaccount",
147    "crossvm",
148    "rlconsensus",
149];
150
151const KNOWN_MODULE_CODESPACES: &[&str] = &[
152    "bank",
153    "staking",
154    "distribution",
155    "gov",
156    "authz",
157    "feegrant",
158];
159
160fn reason_for(code: u32, codespace: &str) -> String {
161    if codespace.is_empty() || codespace == "sdk" {
162        return sdk_reason(code).unwrap_or("unknown error").to_string();
163    }
164    if let Some(reason) = module_reason(codespace, code) {
165        return reason.to_string();
166    }
167    if KNOWN_MODULE_CODESPACES.contains(&codespace) || QORE_CODESPACES.contains(&codespace) {
168        return format!("unknown {codespace} error");
169    }
170    format!("unknown {codespace} error")
171}