use serde_json::Value;
type ErrorEnum = (u64, &'static str, &'static [(&'static str, &'static str)]);
const ABI_ERROR_ENUMS: &[ErrorEnum] = &[
(
537125673719950211,
"upgradability::errors::SetProxyOwnerError",
&[("CannotUninitialize", "Cannot uninitialize proxy owner")],
),
(
821289540733930261,
"contract_schema::trade_account::CallerError",
&[("InvalidCaller", "Caller is not authorized for this operation")],
),
(
1043998670105365804,
"contract_schema::order_book::OrderCancelError",
&[
("NotOrderOwner", "You can only cancel your own orders"),
("TraderNotBlacklisted", "Trader is not blacklisted"),
("NoBlacklist", "No blacklist configured for this market"),
],
),
(
2735857006735158246,
"contract_schema::trade_account::SessionError",
&[
("SessionInThePast", "Session expiry is in the past. Create a new session."),
("NoApprovedContractIdsProvided", "Session must include at least one approved contract"),
],
),
(
4755763688038835574,
"contract_schema::order_book::FeeError",
&[("NoFeesAvailable", "No fees to collect")],
),
(
4997665884103701952,
"pausable::errors::PauseError",
&[
("Paused", "Market is paused"),
("NotPaused", "Market is not paused"),
],
),
(
5347491661573165298,
"contract_schema::whitelist::WhitelistError",
&[
("TraderAlreadyWhitelisted", "Account is already whitelisted"),
("TraderNotWhitelisted", "Account is not whitelisted"),
],
),
(
8930260739195532515,
"contract_schema::order_book::OrderBookInitializationError",
&[
("InvalidAsset", "Invalid asset configuration (admin)"),
("InvalidDecimals", "Invalid decimals configuration (admin)"),
("InvalidPriceWindow", "Invalid price window (admin)"),
("InvalidPricePrecision", "Invalid price precision (admin)"),
("OwnerNotSet", "Owner not set (admin)"),
("InvalidMinOrder", "Invalid minimum order (admin)"),
],
),
(
9305944841695250538,
"contract_schema::register::TradeAccountRegistryError",
&[
("OwnerAlreadyHasTradeAccount", "This wallet already has a trade account"),
("TradeAccountNotRegistered", "Trade account not found. Call setup_account() first."),
("TradeAccountAlreadyHasReferer", "Referral code already set for this account"),
],
),
(
11035215306127844569,
"contract_schema::trade_account::SignerError",
&[
("InvalidSigner", "Signature doesn't match the session signer"),
("ProxyOwnerIsContract", "Contract IDs cannot be used as proxy owners"),
],
),
(
12033795032676640771,
"contract_schema::order_book::OrderCreationError",
&[
("InvalidOrderArgs", "Order arguments are invalid"),
("InvalidInputAmount", "Input amount doesn\u{2019}t match price \u{00d7} quantity. Check your balance."),
("InvalidAsset", "Wrong asset for this market"),
("PriceExceedsRange", "Price is outside the allowed range for this market"),
("PricePrecision", "Price doesn\u{2019}t align with the market\u{2019}s tick size. Use Market.scale_price()."),
("InvalidHeapPrices", "Internal order book state error. Retry the order."),
("FractionalPrice", "price \u{00d7} quantity must be divisible by 10^base_decimals. Use Market.adjust_quantity()."),
("OrderNotFilled", "FillOrKill order could not be fully filled. Try a smaller quantity or use Spot."),
("OrderPartiallyFilled", "PostOnly order would cross the spread. Use a lower buy price or higher sell price."),
("TraderNotWhiteListed", "Account not whitelisted. Call whitelist_account() first."),
("TraderBlackListed", "Account is blacklisted and cannot trade on this market"),
("InvalidMarketOrder", "Market orders are not supported on this order book"),
("InvalidMarketOrderArgs", "Invalid arguments for bounded market order"),
],
),
(
12825652816513834595,
"ownership::errors::InitializationError",
&[("CannotReinitialized", "Contract already initialized")],
),
(
13517258236389385817,
"contract_schema::blacklist::BlacklistError",
&[
("TraderAlreadyBlacklisted", "Account is already blacklisted"),
("TraderNotBlacklisted", "Account is not blacklisted"),
],
),
(
14509209538366790003,
"std::crypto::signature_error::SignatureError",
&[
("UnrecoverablePublicKey", "Could not recover public key from signature"),
("InvalidPublicKey", "Public key is invalid"),
("InvalidSignature", "Signature verification failed"),
("InvalidOperation", "Invalid cryptographic operation"),
],
),
(
14888260448086063780,
"contract_schema::trade_account::WithdrawError",
&[
("AmountIsZero", "Withdrawal amount must be greater than zero"),
("NotEnoughBalance", "Insufficient balance for withdrawal"),
],
),
(
17376141311665587813,
"src5::AccessError",
&[("NotOwner", "Caller is not the contract owner")],
),
(
17909535172322737929,
"contract_schema::trade_account::NonceError",
&[("InvalidNonce", "Nonce is stale or out of sequence. Refresh the nonce and retry.")],
),
];
const SIGNAL_CONSTANTS: &[(u64, &str)] = &[
(0xFFFF_FFFF_FFFF_0000, "FAILED_REQUIRE"),
(0xFFFF_FFFF_FFFF_0001, "FAILED_TRANSFER_TO_ADDRESS"),
(0xFFFF_FFFF_FFFF_0003, "FAILED_ASSERT_EQ"),
(0xFFFF_FFFF_FFFF_0004, "FAILED_ASSERT"),
(0xFFFF_FFFF_FFFF_0005, "FAILED_ASSERT_NE"),
(0xFFFF_FFFF_FFFF_0006, "REVERT_WITH_LOG"),
];
fn format_error(enum_name: &str, variant: &str, description: &str) -> String {
let short_name = enum_name.rsplit("::").next().unwrap_or(enum_name);
format!("{short_name}::{variant} \u{2014} {description}")
}
fn variant_to_qualified(variant: &str) -> Option<String> {
for &(_, enum_name, variants) in ABI_ERROR_ENUMS {
for &(v, desc) in variants {
if v == variant {
return Some(format_error(enum_name, v, desc));
}
}
}
None
}
fn extract_log_result_error(text: &str) -> Option<String> {
let mut result: Option<&str> = None;
let mut offset = 0;
while offset < text.len() {
if let Some(pos) = text[offset..].find("Ok(\\\"") {
let start = offset + pos + 5; if let Some(end_rel) = text[start..].find("\\\"") {
let name = &text[start..start + end_rel];
if !name.is_empty() && !name.contains(' ') && variant_to_qualified(name).is_some() {
result = Some(name);
}
offset = start + end_rel + 2;
continue;
}
}
if let Some(pos) = text[offset..].find("Ok(\"") {
let start = offset + pos + 4; if let Some(end_rel) = text[start..].find("\")") {
let name = &text[start..start + end_rel];
if !name.is_empty() && !name.contains(' ') && variant_to_qualified(name).is_some() {
result = Some(name);
}
offset = start + end_rel + 2;
continue;
}
}
break;
}
result.and_then(variant_to_qualified)
}
fn extract_logdata_error(text: &str) -> Option<String> {
let revert_idx = text.rfind("Revert {")?;
let logdata_idx = text[..revert_idx].rfind("LogData {")?;
let logdata_block = &text[logdata_idx..revert_idx];
let rb_idx = logdata_block.find("rb:")?;
let after_rb = &logdata_block[rb_idx + 3..];
let after_rb = after_rb.trim_start();
let digits: String = after_rb
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if digits.is_empty() {
return None;
}
let log_id: u64 = digits.parse().ok()?;
let (_, enum_name, variants) = ABI_ERROR_ENUMS.iter().find(|(id, _, _)| *id == log_id)?;
let bytes_marker = "Bytes(";
let data_idx = logdata_block.find(bytes_marker)?;
let hex_start = data_idx + bytes_marker.len();
let hex_end = logdata_block[hex_start..].find(')')? + hex_start;
let hex_str = &logdata_block[hex_start..hex_end];
if hex_str.len() < 16 {
return None;
}
let discriminant = u64::from_str_radix(&hex_str[..16], 16).ok()? as usize;
if discriminant < variants.len() {
let (variant_name, desc) = variants[discriminant];
Some(format_error(enum_name, variant_name, desc))
} else {
Some(format!("{enum_name}::unknown(discriminant={discriminant})"))
}
}
fn extract_panic_reason(text: &str) -> Option<String> {
let marker = "PanicInstruction {";
let start = text.find(marker)?;
let after = &text[start + marker.len()..];
let reason_pos = after.find("reason:")?;
let name_start = &after[reason_pos + "reason:".len()..];
let name: String = name_start
.trim_start()
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.is_empty() {
None
} else {
Some(name)
}
}
fn extract_revert_codes(text: &str) -> Vec<u64> {
let mut codes = Vec::new();
let mut offset = 0;
while let Some(start_rel) = text[offset..].find("Revert(") {
let start = offset + start_rel + "Revert(".len();
let digits: String = text[start..]
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if !digits.is_empty()
&& text[start + digits.len()..]
.chars()
.next()
.is_some_and(|c| c == ')')
{
if let Ok(v) = digits.parse::<u64>() {
codes.push(v);
}
}
offset = start;
}
offset = 0;
while let Some(start_rel) = text[offset..].find("Revert {") {
let block_start = offset + start_rel;
let brace_end = text[block_start..].find('}');
if let Some(ra_rel) = text[block_start..].find("ra:") {
let brace_end_abs = brace_end.map(|e| block_start + e);
let ra_abs = block_start + ra_rel;
if brace_end_abs.is_none() || ra_abs < brace_end_abs.unwrap() {
let after_ra = &text[ra_abs + 3..];
let after_ra = after_ra.trim_start();
let digits: String = after_ra
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if let Ok(v) = digits.parse::<u64>() {
codes.push(v);
}
}
}
offset = block_start + "Revert {".len();
}
codes
}
fn recognize_signal(text: &str) -> Option<&'static str> {
for code in extract_revert_codes(text) {
for &(signal_val, signal_name) in SIGNAL_CONSTANTS {
if code == signal_val {
return Some(signal_name);
}
}
}
None
}
pub(crate) fn augment_revert_reason(
message: &str,
reason: &str,
receipts: Option<&Value>,
) -> String {
let receipts_text = match receipts {
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| v.to_string()),
None => String::new(),
};
let context = format!("{message}\n{reason}\n{receipts_text}");
if let Some(decoded) = extract_log_result_error(&context) {
return decoded;
}
if let Some(decoded) = extract_logdata_error(&context) {
return decoded;
}
let signal = recognize_signal(&context);
if let Some(panic) = extract_panic_reason(&context) {
return panic;
}
if let Some(err_idx) = context.find("and error:") {
let after = context[err_idx + "and error:".len()..].trim_start();
let summary = if let Some(receipts_idx) = after.find(", receipts:") {
after[..receipts_idx].trim()
} else {
&after[..after.len().min(200)]
};
if !summary.is_empty() {
return summary.to_string();
}
}
if let Some(signal_name) = signal {
return format!("{signal_name} (specific error unknown \u{2014} check .receipts)");
}
if reason.len() > 200 {
return format!(
"{}... (truncated, full receipts on .receipts)",
&reason[..200]
);
}
reason.to_string()
}
#[cfg(test)]
mod tests {
use super::augment_revert_reason;
const REALISTIC_REASON: &str = concat!(
"Failed to process SessionCallPayload { actions: [MarketActions { actions: ",
"[SettleBalance, CreateOrder { side: Buy }] }] } with error: ",
"Transaction abc123 failed with logs: LogResult { results: ",
"[Ok(\"IncrementNonceEvent { nonce: 2752 }\"), ",
"Ok(\"SessionContractCallEvent { nonce: 2751 }\"), ",
"Ok(\"SessionContractCallEvent { nonce: 2751 }\"), ",
"Ok(\"OrderCreatedEvent { quantity: 1000000, price: 2129980000000 }\"), ",
"Ok(\"OrderMatchedEvent { quantity: 1000000, price: 2129320000000 }\"), ",
"Ok(\"FeesCollectedEvent { base_fees: 100, quote_fees: 0 }\"), ",
"Ok(\"OrderPartiallyFilled\")] } ",
"and error: transaction reverted: Revert(18446744073709486086), ",
"receipts: [Call { id: 0000, to: f155, amount: 0 }, ",
"LogData { id: f155, ra: 0, rb: 2261086600904378517, ptr: 67108286, len: 8, ",
"digest: abc, data: Some(Bytes(0000000000000000)) }, ",
"LogData { id: 2a78, ra: 0, rb: 12033795032676640771, ptr: 67100980, len: 8, ",
"digest: 4c0e, data: Some(Bytes(0000000000000008)) }, ",
"Revert { id: 2a78, ra: 18446744073709486086 }, ",
"ScriptResult { result: Revert }]"
);
#[test]
fn test_extracts_error_from_log_result() {
let decoded =
augment_revert_reason("Failed to process transaction", REALISTIC_REASON, None);
assert_eq!(
decoded,
"OrderCreationError::OrderPartiallyFilled \u{2014} PostOnly order would cross the spread. Use a lower buy price or higher sell price."
);
}
#[test]
fn test_log_result_with_escaped_quotes() {
let reason = concat!(
"LogResult { results: [Ok(\\\"IncrementNonceEvent\\\"), ",
"Ok(\\\"TraderNotWhiteListed\\\")] }"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(
decoded,
"OrderCreationError::TraderNotWhiteListed \u{2014} Account not whitelisted. Call whitelist_account() first."
);
}
#[test]
fn test_log_result_ignores_non_error_entries() {
let reason = concat!(
"LogResult { results: [Ok(\"IncrementNonceEvent\"), ",
"Ok(\"OrderCreatedEvent\"), Ok(\"NotEnoughBalance\")] }"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(
decoded,
"WithdrawError::NotEnoughBalance \u{2014} Insufficient balance for withdrawal"
);
}
#[test]
fn test_extracts_error_from_logdata_receipt() {
let reason = concat!(
"receipts: [LogData { id: abc, ra: 0, rb: 12033795032676640771, ",
"ptr: 100, len: 8, digest: def, data: Some(Bytes(0000000000000008)) }, ",
"Revert { id: abc, ra: 18446744073709486086 }]"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(
decoded,
"OrderCreationError::OrderPartiallyFilled \u{2014} PostOnly order would cross the spread. Use a lower buy price or higher sell price."
);
}
#[test]
fn test_logdata_discriminant_zero() {
let reason = concat!(
"LogData { id: x, ra: 0, rb: 12033795032676640771, ",
"ptr: 0, len: 8, digest: y, data: Some(Bytes(0000000000000000)) }, ",
"Revert { id: x, ra: 18446744073709486086 }"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(
decoded,
"OrderCreationError::InvalidOrderArgs \u{2014} Order arguments are invalid"
);
}
#[test]
fn test_logdata_withdraw_error() {
let reason = concat!(
"LogData { id: x, ra: 0, rb: 14888260448086063780, ",
"ptr: 0, len: 8, digest: y, data: Some(Bytes(0000000000000001)) }, ",
"Revert { id: x, ra: 18446744073709486000 }"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(
decoded,
"WithdrawError::NotEnoughBalance \u{2014} Insufficient balance for withdrawal"
);
}
#[test]
fn test_logdata_unknown_log_id_falls_through() {
let reason = concat!(
"LogData { id: x, ra: 0, rb: 9999999999999999999, ",
"ptr: 0, len: 8, digest: y, data: Some(Bytes(0000000000000000)) }, ",
"Revert { id: x, ra: 18446744073709486086 }"
);
let decoded = augment_revert_reason("msg", reason, None);
assert!(
decoded.contains("REVERT_WITH_LOG"),
"expected REVERT_WITH_LOG, got: {decoded}"
);
}
#[test]
fn test_recognizes_failed_require_signal() {
let reason = "Revert(18446744073709486080)"; let decoded = augment_revert_reason("msg", reason, None);
assert!(
decoded.contains("FAILED_REQUIRE"),
"expected FAILED_REQUIRE, got: {decoded}"
);
}
#[test]
fn test_recognizes_revert_with_log_signal() {
let reason = "Revert(18446744073709486086)"; let decoded = augment_revert_reason("msg", reason, None);
assert!(
decoded.contains("REVERT_WITH_LOG"),
"expected REVERT_WITH_LOG, got: {decoded}"
);
}
#[test]
fn test_non_signal_revert_code_falls_through() {
let decoded = augment_revert_reason("msg", "Revert(42)", None);
assert_eq!(decoded, "Revert(42)");
}
#[test]
fn test_extracts_panic_reason() {
let reason = concat!(
"receipts: [Panic { id: abc, reason: PanicInstruction ",
"{ reason: NotEnoughBalance, instruction: CALL {} }, pc: 123 }]"
);
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(decoded, "NotEnoughBalance");
}
#[test]
fn test_extracts_and_error_summary() {
let reason = "lots of noise and error: transaction reverted: SomeError, receipts: [...]";
let decoded = augment_revert_reason("msg", reason, None);
assert_eq!(decoded, "transaction reverted: SomeError");
}
#[test]
fn test_leaves_reason_unchanged_when_no_patterns() {
let decoded = augment_revert_reason("plain error", "some reason", None);
assert_eq!(decoded, "some reason");
}
#[test]
fn test_truncates_long_reason() {
let reason = "x".repeat(500);
let decoded = augment_revert_reason("error", &reason, None);
assert!(decoded.len() < 300);
assert!(decoded.contains("truncated"));
}
#[test]
fn test_receipts_json_searched() {
let receipts =
serde_json::from_str::<serde_json::Value>(r#"[{"note": "Ok(\"InvalidNonce\")"}]"#)
.unwrap();
let decoded = augment_revert_reason("msg", "", Some(&receipts));
assert_eq!(
decoded,
"NonceError::InvalidNonce \u{2014} Nonce is stale or out of sequence. Refresh the nonce and retry."
);
}
#[test]
fn test_priority_log_result_over_logdata() {
let decoded =
augment_revert_reason("Failed to process transaction", REALISTIC_REASON, None);
assert!(
decoded.contains("OrderPartiallyFilled"),
"expected OrderPartiallyFilled, got: {decoded}"
);
}
}