1use std::fmt;
4use std::time::Duration;
5
6use alloy::primitives::{Address, B256, Bytes};
7use alloy::sol_types::{Panic, Revert, SolError, SolInterface};
8use alloy::transports::{RpcError, TransportError, TransportErrorKind};
9use serde::{Deserialize, Serialize};
10
11use crate::artifacts::ParalockContract;
12use crate::bindings::{
13 ITIP20Errors::ITIP20ErrorsErrors, ITIP20Factory::ITIP20FactoryErrors, ITIP20Issuer::ITIP20IssuerErrors,
14 MockUSD::MockUSDErrors, ParalockAdaptorEscrow::ParalockAdaptorEscrowErrors, ParalockHTLC::ParalockHTLCErrors,
15};
16use crate::swap::EscrowStatus;
17
18#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum EvmError {
25 #[error("invalid configuration: {0}")]
27 Config(String),
28 #[error("invalid input: {0}")]
30 InvalidInput(String),
31 #[error("RPC error during {method}{}: {message}", code.map(|c| format!(" (code {c})")).unwrap_or_default())]
33 Rpc {
34 method: &'static str,
36 code: Option<i64>,
38 message: String,
40 transient: bool,
42 },
43 #[error("chain id mismatch: configured {expected}, but the RPC node reports {actual}")]
45 ChainIdMismatch {
46 expected: u64,
48 actual: u64,
50 },
51 #[error("invalid RPC response: {0}")]
53 InvalidResponse(String),
54 #[error("{}", reverted_message(hash.as_ref(), revert.as_ref()))]
56 TransactionReverted {
57 hash: Option<B256>,
59 revert: Option<RevertReason>,
61 },
62 #[error("transaction {hash}{} was not mined within {timeout:?}", nonce_suffix(*nonce))]
66 ReceiptTimeout {
67 hash: B256,
69 nonce: Option<u64>,
71 timeout: Duration,
73 },
74 #[error("broadcast of transaction {hash} (nonce {nonce}) may have failed: {message}")]
78 BroadcastUnknown {
79 hash: B256,
81 nonce: u64,
83 message: String,
85 },
86 #[error("nonce {nonce} of transaction {hash} is already used by a mined transaction")]
89 NonceUsed {
90 hash: B256,
92 nonce: u64,
94 },
95 #[error("invalid receipt for transaction {hash}: {message}")]
97 InvalidReceipt {
98 hash: B256,
100 message: String,
102 },
103 #[error("{}", code_mismatch_message(address, *expected, *empty))]
106 CodeMismatch {
107 address: Address,
109 expected: ParalockContract,
111 empty: bool,
113 },
114 #[error("timed out after {timeout:?} waiting for {what}")]
116 Timeout {
117 what: String,
119 timeout: Duration,
121 },
122 #[error("this client is read-only: configure a signer to send transactions")]
124 ReadOnly,
125 #[error("signer error: {0}")]
127 Signer(String),
128 #[error("invalid claim signature: {0}")]
130 Signature(#[from] SignatureError),
131 #[error("claim signature recovers to {recovered}, not the swap's signer {expected}")]
133 WrongSigner {
134 expected: Address,
136 recovered: Address,
138 },
139 #[error("preimage does not hash to {hashlock}")]
141 PreimageMismatch {
142 hashlock: B256,
144 },
145 #[error("{key} is {status:?} but no Locked event was found from block {from_block}")]
148 LockEventMissing {
149 key: B256,
151 status: EscrowStatus,
153 from_block: u64,
155 },
156 #[error("{key} is settled but its Claimed/Refunded event is not indexed yet")]
158 SettlementNotIndexed {
159 key: B256,
161 },
162 #[error("estimated max fee per gas {estimated} exceeds the configured cap {cap}")]
164 FeeCapExceeded {
165 estimated: u128,
167 cap: u128,
169 },
170 #[error(
172 "chain {chain_id} is {name}: deploy_paralock_contracts only deploys to local chains (it creates a mintable \
173 test stablecoin). Use contracts/script/Deploy.s.sol"
174 )]
175 PublicNetwork {
176 chain_id: u64,
178 name: &'static str,
180 },
181}
182
183impl EvmError {
184 pub fn is_transient(&self) -> bool {
190 match self {
191 Self::Rpc { transient, .. } => *transient,
192 Self::ReceiptTimeout { .. }
193 | Self::BroadcastUnknown { .. }
194 | Self::Timeout { .. }
195 | Self::SettlementNotIndexed { .. }
196 | Self::LockEventMissing { .. } => true,
197 _ => false,
198 }
199 }
200
201 pub fn tx_hash(&self) -> Option<B256> {
209 match self {
210 Self::ReceiptTimeout { hash, .. }
211 | Self::BroadcastUnknown { hash, .. }
212 | Self::NonceUsed { hash, .. }
213 | Self::InvalidReceipt { hash, .. } => Some(*hash),
214 Self::TransactionReverted { hash, .. } => *hash,
215 _ => None,
216 }
217 }
218
219 pub fn nonce(&self) -> Option<u64> {
221 match self {
222 Self::ReceiptTimeout { nonce, .. } => *nonce,
223 Self::BroadcastUnknown { nonce, .. } | Self::NonceUsed { nonce, .. } => Some(*nonce),
224 _ => None,
225 }
226 }
227
228 pub fn revert_name(&self) -> Option<&str> {
230 match self {
231 Self::TransactionReverted { revert: Some(revert), .. } => revert.name(),
232 _ => None,
233 }
234 }
235
236 pub fn revert(&self) -> Option<&RevertReason> {
238 match self {
239 Self::TransactionReverted { revert, .. } => revert.as_ref(),
240 _ => None,
241 }
242 }
243}
244
245fn code_mismatch_message(address: &Address, expected: ParalockContract, empty: bool) -> String {
246 let what = if empty { " (it has no code)" } else { "" };
247 format!("{address} does not hold the {} runtime code of this build{what}", expected.name())
248}
249
250fn nonce_suffix(nonce: Option<u64>) -> String {
251 nonce.map(|n| format!(" (nonce {n})")).unwrap_or_default()
252}
253
254fn reverted_message(hash: Option<&B256>, revert: Option<&RevertReason>) -> String {
255 let what = match hash {
256 Some(hash) => format!("transaction {hash} reverted"),
257 None => "transaction would revert (simulation)".to_owned(),
258 };
259 match revert {
260 Some(revert) => format!("{what}: {revert}"),
261 None => what,
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
267#[non_exhaustive]
268pub enum SignatureError {
269 #[error("invalid signature recovery value {0}")]
271 InvalidRecovery(u64),
272 #[error("signature s is out of range")]
274 SOutOfRange,
275 #[error("signature r is out of range")]
277 ROutOfRange,
278 #[error("signature is not canonical (v must be 27 or 28 and s low)")]
280 NotCanonical,
281 #[error("signature does not recover to a public key")]
283 Unrecoverable,
284}
285
286#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289#[non_exhaustive]
290pub enum RevertSource {
291 Htlc,
293 AdaptorEscrow,
295 Token,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq)]
301pub struct RevertReason {
302 pub data: Bytes,
304 pub name: Option<&'static str>,
307 pub source: Option<RevertSource>,
309 pub message: String,
311}
312
313impl RevertReason {
314 pub fn decode(data: Bytes, prefer: Option<RevertSource>) -> Self {
318 let order: [RevertSource; 3] = match prefer {
319 Some(RevertSource::AdaptorEscrow) => [RevertSource::AdaptorEscrow, RevertSource::Htlc, RevertSource::Token],
320 Some(RevertSource::Token) => [RevertSource::Token, RevertSource::Htlc, RevertSource::AdaptorEscrow],
321 _ => [RevertSource::Htlc, RevertSource::AdaptorEscrow, RevertSource::Token],
322 };
323 for source in order {
324 let decoded = match source {
325 RevertSource::Htlc => describe::<ParalockHTLCErrors>(&data, ParalockHTLCErrors::name_by_selector),
326 RevertSource::AdaptorEscrow => {
327 describe::<ParalockAdaptorEscrowErrors>(&data, ParalockAdaptorEscrowErrors::name_by_selector)
328 }
329 RevertSource::Token => describe::<ITIP20ErrorsErrors>(&data, ITIP20ErrorsErrors::name_by_selector)
330 .or_else(|| describe::<ITIP20FactoryErrors>(&data, ITIP20FactoryErrors::name_by_selector))
331 .or_else(|| describe::<ITIP20IssuerErrors>(&data, ITIP20IssuerErrors::name_by_selector))
332 .or_else(|| describe::<MockUSDErrors>(&data, MockUSDErrors::name_by_selector)),
333 };
334 if let Some((name, message)) = decoded {
335 return Self { data, name: Some(name), source: Some(source), message };
336 }
337 }
338 if let Ok(revert) = Revert::abi_decode_validate(&data) {
339 let message = format!("Error({:?})", revert.reason);
340 return Self { data, name: Some("Error"), source: None, message };
341 }
342 if let Ok(panic) = Panic::abi_decode_validate(&data) {
343 let message = format!("Panic({:#x})", panic.code);
344 return Self { data, name: Some("Panic"), source: None, message };
345 }
346 let message = if data.is_empty() { "no revert data".to_owned() } else { format!("unknown revert data {data}") };
347 Self { data, name: None, source: None, message }
348 }
349
350 pub fn name(&self) -> Option<&str> {
352 self.name
353 }
354
355 pub fn is(&self, name: &str) -> bool {
357 self.name == Some(name)
358 }
359
360 pub fn htlc_error(&self) -> Option<ParalockHTLCErrors> {
362 ParalockHTLCErrors::abi_decode_validate(&self.data).ok()
363 }
364
365 pub fn adaptor_error(&self) -> Option<ParalockAdaptorEscrowErrors> {
367 ParalockAdaptorEscrowErrors::abi_decode_validate(&self.data).ok()
368 }
369}
370
371impl fmt::Display for RevertReason {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 f.write_str(&self.message)
374 }
375}
376
377fn describe<E: SolInterface + fmt::Debug>(
379 data: &[u8],
380 name_by_selector: fn([u8; 4]) -> Option<&'static str>,
381) -> Option<(&'static str, String)> {
382 let selector: [u8; 4] = data.get(..4)?.try_into().ok()?;
383 if !E::valid_selector(selector) {
384 return None;
385 }
386 let decoded = E::abi_decode_validate(data).ok()?;
387 let name = name_by_selector(selector)?;
388 let debug = format!("{decoded:?}");
390 let inner = debug
391 .strip_prefix(name)
392 .and_then(|rest| rest.strip_prefix('('))
393 .and_then(|rest| rest.strip_suffix(')'))
394 .map(str::to_owned)
395 .unwrap_or(debug);
396 Some((name, inner))
397}
398
399#[derive(Clone, Debug)]
401pub(crate) struct ErrorContext {
402 pub redact: Vec<String>,
404}
405
406impl ErrorContext {
407 pub(crate) fn redact(&self, message: &str) -> String {
408 let mut out = message.to_owned();
409 for secret in &self.redact {
410 if !secret.is_empty() {
411 out = out.replace(secret.as_str(), "<rpc-url>");
412 }
413 }
414 out
415 }
416
417 pub(crate) fn rpc(&self, method: &'static str, err: TransportError) -> EvmError {
419 let code = match &err {
420 RpcError::ErrorResp(payload) => Some(payload.code),
421 _ => None,
422 };
423 EvmError::Rpc { method, code, message: self.redact(&err.to_string()), transient: is_transient(&err) }
424 }
425
426 pub(crate) fn call(&self, method: &'static str, err: TransportError, source: Option<RevertSource>) -> EvmError {
429 if let Some(data) = revert_data(&err) {
430 return EvmError::TransactionReverted { hash: None, revert: Some(RevertReason::decode(data, source)) };
431 }
432 self.rpc(method, err)
433 }
434}
435
436pub(crate) fn revert_data(err: &TransportError) -> Option<Bytes> {
438 let RpcError::ErrorResp(payload) = err else { return None };
439 if let Some(data) = payload.as_revert_data() {
440 return Some(data);
441 }
442 if payload.code == 3 {
444 let raw = payload.data.as_ref()?;
445 let text: String = serde_json::from_str(raw.get()).ok()?;
446 let hex = text.strip_prefix("0x")?;
447 return alloy::hex::decode(hex).ok().map(Bytes::from);
448 }
449 if payload.message.contains("revert") {
450 return Some(Bytes::new());
451 }
452 None
453}
454
455pub(crate) fn is_transient(err: &TransportError) -> bool {
458 use alloy::transports::RpcError as E;
459 match err {
460 E::Transport(kind) => match kind {
461 TransportErrorKind::HttpError(http) => http.status == 429 || (500..=599).contains(&http.status),
462 TransportErrorKind::HttpErrorWithRetryAfter { error, .. } => {
463 error.status == 429 || (500..=599).contains(&error.status)
464 }
465 TransportErrorKind::MissingBatchResponse(_) | TransportErrorKind::BackendGone => true,
466 TransportErrorKind::Custom(_) => true,
468 _ => false,
469 },
470 E::ErrorResp(payload) => is_retryable_payload(payload),
471 E::NullResp => true,
472 E::DeserError { text, .. } => {
473 let trimmed = text.trim_start();
475 trimmed.starts_with('<') || trimmed.is_empty()
476 }
477 _ => false,
478 }
479}
480
481pub(crate) fn is_retryable_payload(payload: &alloy::rpc::json_rpc::ErrorPayload) -> bool {
485 (payload.is_retry_err() || (payload.code == -32603 && payload.message.contains("timeout")))
486 && !mentions_range_limit(&payload.message)
487}
488
489const RANGE_REFUSALS: [&str; 22] = [
492 "block range",
493 "range too large",
494 "range is too large",
495 "range too wide",
496 "range is too wide",
497 "range too big",
498 "range is too big",
499 "too many blocks",
500 "too many results",
501 "too many logs",
502 "query returned more than",
503 "limit exceeded",
504 "exceed max",
505 "exceeds max",
506 "exceed the max",
507 "exceeds the max",
508 "exceed limit",
509 "exceeds limit",
510 "exceed the limit",
511 "exceeds the limit",
512 "response size",
513 "payload too large",
514];
515
516pub(crate) fn is_range_refusal(err: &TransportError) -> bool {
521 match err {
522 RpcError::ErrorResp(_) => true,
523 RpcError::Transport(kind) => match kind.as_http_error() {
524 Some(http) if http.status == 400 || http.status == 413 => true,
525 Some(http) if http.status == 429 => false,
526 _ => mentions_range_limit(&err.to_string()),
527 },
528 _ => mentions_range_limit(&err.to_string()),
529 }
530}
531
532fn mentions_range_limit(message: &str) -> bool {
533 let message = message.to_lowercase();
534 !message.contains("rate limit") && RANGE_REFUSALS.iter().any(|phrase| message.contains(phrase))
535}
536
537#[cfg(test)]
538mod tests {
539 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
540
541 use super::*;
542 use crate::bindings::{ParalockAdaptorEscrow, ParalockHTLC};
543 use alloy::primitives::U256;
544
545 #[test]
546 fn decodes_custom_errors_with_their_names() {
547 let data = Bytes::from(ParalockHTLC::SwapExpired { expiry: 1_800_003_600 }.abi_encode());
548 let revert = RevertReason::decode(data.clone(), Some(RevertSource::Htlc));
549 assert_eq!(revert.name(), Some("SwapExpired"));
550 assert_eq!(revert.source, Some(RevertSource::Htlc));
551 assert!(revert.message.contains("1800003600"), "{}", revert.message);
552 assert!(matches!(revert.htlc_error(), Some(ParalockHTLCErrors::SwapExpired(_))));
553
554 let unit = RevertReason::decode(Bytes::from(ParalockHTLC::NotSender {}.abi_encode()), None);
555 assert_eq!(unit.name(), Some("NotSender"));
556 assert_eq!(unit.message, "NotSender");
557
558 let malleable = Bytes::from(ParalockAdaptorEscrow::MalleableSignature {}.abi_encode());
559 let revert = RevertReason::decode(malleable, Some(RevertSource::AdaptorEscrow));
560 assert_eq!(revert.name(), Some("MalleableSignature"));
561 assert!(matches!(revert.adaptor_error(), Some(ParalockAdaptorEscrowErrors::MalleableSignature(_))));
562 }
563
564 #[test]
565 fn shared_selectors_resolve_to_the_preferred_contract() {
566 let data = Bytes::from(ParalockAdaptorEscrow::SwapNotFound { id: B256::repeat_byte(1) }.abi_encode());
567 let as_adaptor = RevertReason::decode(data.clone(), Some(RevertSource::AdaptorEscrow));
568 assert_eq!(as_adaptor.source, Some(RevertSource::AdaptorEscrow));
569 let as_htlc = RevertReason::decode(data, Some(RevertSource::Htlc));
570 assert_eq!(as_htlc.source, Some(RevertSource::Htlc));
571 assert_eq!(as_htlc.name(), Some("SwapNotFound"));
572 }
573
574 #[test]
575 fn decodes_token_errors_strings_panics_and_garbage() {
576 let tip20 = crate::bindings::ITIP20Errors::InsufficientBalance {
577 available: U256::from(1),
578 required: U256::from(2),
579 token: Address::repeat_byte(0x20),
580 };
581 let revert = RevertReason::decode(Bytes::from(tip20.abi_encode()), Some(RevertSource::Htlc));
582 assert_eq!(revert.name(), Some("InsufficientBalance"));
583 assert_eq!(revert.source, Some(RevertSource::Token));
584
585 let string = Revert::from("nope").abi_encode();
586 let revert = RevertReason::decode(Bytes::from(string), None);
587 assert_eq!(revert.name(), Some("Error"));
588 assert!(revert.message.contains("nope"));
589
590 let panic = Panic { code: U256::from(0x11) }.abi_encode();
591 assert_eq!(RevertReason::decode(Bytes::from(panic), None).name(), Some("Panic"));
592
593 for garbage in [&b""[..], &[0xde, 0xad], &[0xde, 0xad, 0xbe, 0xef, 1, 2, 3]] {
594 let revert = RevertReason::decode(Bytes::copy_from_slice(garbage), None);
595 assert_eq!(revert.name(), None);
596 }
597 let mut truncated = ParalockHTLC::SwapExpired { expiry: 1 }.abi_encode();
599 truncated.truncate(10);
600 assert_eq!(RevertReason::decode(Bytes::from(truncated), None).name(), None);
601 }
602
603 #[test]
604 fn redacts_the_rpc_url() {
605 let ctx = ErrorContext { redact: vec!["https://rpc.example/key123".to_owned()] };
606 let msg = ctx.redact("error sending request for url (https://rpc.example/key123): connection refused");
607 assert!(!msg.contains("key123"));
608 assert!(msg.contains("<rpc-url>"));
609 }
610
611 #[test]
612 fn error_messages() {
613 let err = EvmError::TransactionReverted {
614 hash: None,
615 revert: Some(RevertReason::decode(
616 Bytes::from(ParalockHTLC::SwapSettled { hashlock: B256::ZERO }.abi_encode()),
617 None,
618 )),
619 };
620 assert_eq!(err.revert_name(), Some("SwapSettled"));
621 assert!(err.to_string().contains("simulation"));
622 assert!(err.to_string().contains("SwapSettled"));
623 assert_eq!(SignatureError::SOutOfRange.to_string(), "signature s is out of range");
624 assert_eq!(SignatureError::InvalidRecovery(29).to_string(), "invalid signature recovery value 29");
625 }
626
627 #[test]
628 fn errors_after_a_broadcast_carry_the_hash_and_nonce() {
629 let hash = B256::repeat_byte(0xab);
630 let timeout = EvmError::ReceiptTimeout { hash, nonce: Some(7), timeout: Duration::from_secs(5) };
631 assert_eq!((timeout.tx_hash(), timeout.nonce()), (Some(hash), Some(7)));
632 assert!(timeout.is_transient());
633 let text = timeout.to_string();
634 assert!(text.contains(&hash.to_string()) && text.contains("nonce 7"), "{text}");
635 let unknown_nonce = EvmError::ReceiptTimeout { hash, nonce: None, timeout: Duration::from_secs(5) };
636 assert_eq!(unknown_nonce.nonce(), None);
637 assert!(!unknown_nonce.to_string().contains("nonce"), "{unknown_nonce}");
638
639 let uncertain = EvmError::BroadcastUnknown { hash, nonce: 3, message: "connection reset".into() };
640 assert_eq!((uncertain.tx_hash(), uncertain.nonce()), (Some(hash), Some(3)));
641 assert!(uncertain.is_transient());
642 assert!(uncertain.to_string().contains(&hash.to_string()));
643
644 let used = EvmError::NonceUsed { hash, nonce: 3 };
645 assert_eq!((used.tx_hash(), used.nonce(), used.is_transient()), (Some(hash), Some(3), false));
646 let mined = EvmError::TransactionReverted { hash: Some(hash), revert: None };
647 assert_eq!(mined.tx_hash(), Some(hash));
648 assert!(mined.to_string().contains(&hash.to_string()));
649 let receipt = EvmError::InvalidReceipt { hash, message: "no status".into() };
650 assert_eq!(receipt.tx_hash(), Some(hash));
651 assert_eq!(EvmError::TransactionReverted { hash: None, revert: None }.tx_hash(), None);
653 assert_eq!(EvmError::ReadOnly.tx_hash(), None);
654 assert_eq!(EvmError::FeeCapExceeded { estimated: 2, cap: 1 }.nonce(), None);
655
656 let code =
657 EvmError::CodeMismatch { address: Address::repeat_byte(1), expected: ParalockContract::Htlc, empty: true };
658 assert!(code.to_string().contains("ParalockHTLC") && code.to_string().contains("no code"), "{code}");
659 }
660
661 #[test]
662 fn range_refusals_are_told_apart_from_failures() {
663 use alloy::rpc::json_rpc::ErrorPayload;
664 let http = |status: u16, body: &str| TransportErrorKind::http_error(status, body.to_owned());
665 let rpc_error = |code: i64, message: &str| {
666 TransportError::ErrorResp(ErrorPayload { code, message: message.to_owned().into(), data: None })
667 };
668 assert!(is_range_refusal(&http(413, "")));
670 assert!(is_range_refusal(&http(413, "<html><body>413 Request Entity Too Large</body></html>")));
671 assert!(is_range_refusal(&http(400, "Bad Request")));
672 assert!(is_range_refusal(&http(503, "query returned more than 10000 results")));
673 assert!(is_range_refusal(&http(500, "eth_getLogs block range is too large, max is 1k")));
674 assert!(is_range_refusal(&rpc_error(-32005, "query returned more than 10000 results")));
675 assert!(is_range_refusal(&rpc_error(-32602, "invalid params")));
676 let text = TransportError::deser_err(
677 serde_json::from_str::<serde_json::Value>("nope").unwrap_err(),
678 "Log response size exceeded. You can make eth_getLogs requests with up to a 2K block range",
679 );
680 assert!(is_range_refusal(&text));
681 assert!(is_range_refusal(&TransportErrorKind::custom_str("exceeds the max range of 10000 blocks")));
682 assert!(!is_range_refusal(&TransportErrorKind::custom_str("error sending request: connection refused")));
684 assert!(!is_range_refusal(&http(429, "rate limit exceeded")));
685 assert!(!is_range_refusal(&http(500, "internal server error")));
686 assert!(!is_range_refusal(&http(401, "invalid API key")));
687 assert!(!is_range_refusal(&TransportErrorKind::custom_str("daily rate limit exceeded")));
688 assert!(!is_range_refusal(&TransportError::NullResp));
689 }
690
691 #[test]
692 fn range_refusals_are_not_retried_as_rate_limits() {
693 use alloy::rpc::json_rpc::ErrorPayload;
694 let payload = |code: i64, message: &str| ErrorPayload { code, message: message.to_owned().into(), data: None };
695 assert!(is_retryable_payload(&payload(-32005, "project ID request rate exceeded")));
696 assert!(is_retryable_payload(&payload(429, "Too Many Requests")));
697 assert!(is_retryable_payload(&payload(-32603, "request timeout")));
698 assert!(!is_retryable_payload(&payload(-32005, "query returned more than 10000 results")));
699 assert!(!is_retryable_payload(&payload(-32000, "execution reverted")));
700 let refusal = TransportError::ErrorResp(payload(-32005, "query returned more than 10000 results"));
701 assert!(!is_transient(&refusal) && is_range_refusal(&refusal));
702 }
703}