Skip to main content

sdk_4mica/
error.rs

1use crate::contract::Core4Mica;
2use alloy::contract as alloy_contract;
3use alloy::primitives::{Address, Bytes};
4use anyhow::Error;
5use crypto::hex::DecodeHexError;
6use reqwest::StatusCode;
7use rpc::ApiClientError;
8use serde_json::Value;
9use thiserror::Error;
10use url::ParseError;
11
12#[derive(Error, Debug)]
13pub enum ConfigError {
14    #[error("invalid config value: {0}")]
15    InvalidValue(String),
16    #[error("missing config: {0}")]
17    Missing(String),
18}
19
20#[derive(Error, Debug)]
21pub enum AuthError {
22    #[error("invalid auth URL: {0}")]
23    InvalidUrl(#[from] ParseError),
24    #[error("auth request failed: {0}")]
25    Transport(#[from] reqwest::Error),
26    #[error("failed to decode auth response: {0}")]
27    Decode(#[from] serde_json::Error),
28    #[error("auth server returned {status}: {message}")]
29    Api { status: StatusCode, message: String },
30    #[error("signing failed: {0}")]
31    Signing(String),
32    #[error("auth config is missing")]
33    MissingConfig,
34    #[error("refresh token not available")]
35    MissingRefreshToken,
36    #[error("auth state error: {0}")]
37    Internal(String),
38}
39
40impl From<AuthError> for ApiClientError {
41    fn from(val: AuthError) -> Self {
42        let status = match &val {
43            AuthError::Api { status, .. } => *status,
44            AuthError::InvalidUrl(_) | AuthError::MissingConfig => StatusCode::BAD_REQUEST,
45            AuthError::MissingRefreshToken => StatusCode::UNAUTHORIZED,
46            AuthError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE,
47            AuthError::Decode(_) => StatusCode::BAD_GATEWAY,
48            AuthError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
49            AuthError::Signing(_) => StatusCode::UNAUTHORIZED,
50        };
51
52        match val {
53            AuthError::InvalidUrl(err) => ApiClientError::InvalidUrl(err),
54            AuthError::Transport(err) => ApiClientError::Transport(err),
55            AuthError::Decode(err) => ApiClientError::Decode(err),
56            AuthError::Api { status, message } => ApiClientError::Api { status, message },
57            other => ApiClientError::Api {
58                status,
59                message: other.to_string(),
60            },
61        }
62    }
63}
64
65#[derive(Error, Debug)]
66pub enum ClientError {
67    #[error("client RPC error: {0}")]
68    Rpc(String),
69
70    #[error("client provider error: {0}")]
71    Provider(String),
72
73    #[error("client initialization error: {0}")]
74    Initialization(String),
75}
76
77#[derive(Debug, Error)]
78pub enum SignPaymentError {
79    #[error("invalid params: {0}")]
80    InvalidParams(String),
81    #[error("address mismatch: signer={signer:?} != claims.user_address={claims}")]
82    AddressMismatch { signer: Address, claims: String },
83    #[error("invalid user address in claims")]
84    InvalidUserAddress,
85    #[error("invalid recipient address in claims")]
86    InvalidRecipientAddress,
87    #[error("failed to sign the payment: {0}")]
88    Failed(String),
89
90    #[error(transparent)]
91    Rpc(#[from] ApiClientError),
92}
93
94#[derive(Debug, Error)]
95pub enum RemunerateError {
96    #[error("invalid params: {0}")]
97    InvalidParams(String),
98    #[error("failed to decode guarantee claims hex")]
99    ClaimsHex(#[source] Error),
100    #[error("failed to decode guarantee claims")]
101    ClaimsDecode(#[source] Error),
102    #[error("failed to convert guarantee claims into contract type")]
103    GuaranteeConversion(#[source] Error),
104    #[error("failed to decode signature hex")]
105    SignatureHex(#[source] DecodeHexError),
106    #[error("failed to decode BLS signature")]
107    SignatureDecode(#[source] Error),
108    #[error("tab not yet overdue")]
109    TabNotYetOverdue,
110    #[error("tab expired")]
111    TabExpired,
112    #[error("tab previously remunerated")]
113    TabPreviouslyRemunerated,
114    #[error("tab already paid")]
115    TabAlreadyPaid,
116    #[error("invalid signature")]
117    InvalidSignature,
118    #[error("double spending detected")]
119    DoubleSpendingDetected,
120    #[error("invalid recipient")]
121    InvalidRecipient,
122    #[error("amount is zero")]
123    AmountZero,
124    #[error("transfer failed")]
125    TransferFailed,
126    #[error("certificate verification failed: {0}")]
127    CertificateInvalid(#[source] Error),
128    #[error("certificate signature mismatch before submission")]
129    CertificateMismatch,
130    #[error("guarantee version mismatch: expected {expected}, got {actual}")]
131    GuaranteeVersionMismatch { expected: u64, actual: u64 },
132    #[error("guarantee domain mismatch")]
133    GuaranteeDomainMismatch,
134    #[error("unsupported guarantee version: {0}")]
135    UnsupportedGuaranteeVersion(u64),
136    #[error("invalid min validation score")]
137    InvalidMinValidationScore,
138    #[error("invalid validation chain id")]
139    InvalidValidationChainId,
140    #[error("untrusted validation registry: {0}")]
141    UntrustedValidationRegistry(Address),
142    #[error("validation subject hash mismatch")]
143    ValidationSubjectHashMismatch,
144    #[error("validation request hash mismatch")]
145    ValidationRequestHashMismatch,
146    #[error("validation lookup failed")]
147    ValidationLookupFailed,
148    #[error("validation pending")]
149    ValidationPending,
150    #[error("validation score too low")]
151    ValidationScoreTooLow,
152    #[error("validation validator mismatch")]
153    ValidationValidatorMismatch,
154    #[error("validation agent mismatch")]
155    ValidationAgentMismatch,
156    #[error("validation tag mismatch")]
157    ValidationTagMismatch,
158
159    #[error(transparent)]
160    Client(#[from] ClientError),
161
162    #[error("unknown revert (selector {selector:#x})")]
163    UnknownRevert { selector: u32, data: Vec<u8> },
164    #[error("provider/transport error: {0}")]
165    Transport(String),
166}
167
168#[derive(Debug, Error)]
169pub enum FinalizeWithdrawalError {
170    #[error("invalid params: {0}")]
171    InvalidParams(String),
172    #[error("no withdrawal requested")]
173    NoWithdrawalRequested,
174    #[error("grace period not elapsed")]
175    GracePeriodNotElapsed,
176    #[error("transfer failed")]
177    TransferFailed,
178
179    #[error(transparent)]
180    Client(#[from] ClientError),
181
182    #[error("unknown revert (selector {selector:#x})")]
183    UnknownRevert { selector: u32, data: Vec<u8> },
184    #[error("provider/transport error: {0}")]
185    Transport(String),
186}
187
188#[derive(Debug, Error)]
189pub enum RequestWithdrawalError {
190    #[error("invalid params: {0}")]
191    InvalidParams(String),
192    #[error("amount is zero")]
193    AmountZero,
194    #[error("insufficient available")]
195    InsufficientAvailable,
196
197    #[error(transparent)]
198    Client(#[from] ClientError),
199
200    #[error("unknown revert (selector {selector:#x})")]
201    UnknownRevert { selector: u32, data: Vec<u8> },
202    #[error("provider/transport error: {0}")]
203    Transport(String),
204}
205
206#[derive(Debug, Error)]
207pub enum CancelWithdrawalError {
208    #[error("invalid params: {0}")]
209    InvalidParams(String),
210    #[error("no withdrawal requested")]
211    NoWithdrawalRequested,
212
213    #[error(transparent)]
214    Client(#[from] ClientError),
215
216    #[error("unknown revert (selector {selector:#x})")]
217    UnknownRevert { selector: u32, data: Vec<u8> },
218    #[error("provider/transport error: {0}")]
219    Transport(String),
220}
221
222#[derive(Debug, Error)]
223pub enum DepositError {
224    #[error("invalid params: {0}")]
225    InvalidParams(String),
226    #[error("amount is zero")]
227    AmountZero,
228
229    #[error(transparent)]
230    Client(#[from] ClientError),
231
232    #[error("unknown revert (selector {selector:#x})")]
233    UnknownRevert { selector: u32, data: Vec<u8> },
234    #[error("provider/transport error: {0}")]
235    Transport(String),
236}
237
238#[derive(Debug, Error)]
239pub enum ApproveErc20Error {
240    #[error("invalid params: {0}")]
241    InvalidParams(String),
242
243    #[error(transparent)]
244    Client(#[from] ClientError),
245
246    #[error("unknown revert (selector {selector:#x})")]
247    UnknownRevert { selector: u32, data: Vec<u8> },
248    #[error("provider/transport error: {0}")]
249    Transport(String),
250}
251
252#[derive(Debug, Error)]
253pub enum PayTabError {
254    #[error("invalid params: {0}")]
255    InvalidParams(String),
256    #[error("invalid asset")]
257    InvalidAsset,
258
259    #[error(transparent)]
260    Client(#[from] ClientError),
261
262    #[error("unknown revert (selector {selector:#x})")]
263    UnknownRevert { selector: u32, data: Vec<u8> },
264    #[error("provider/transport error: {0}")]
265    Transport(String),
266}
267
268#[derive(Debug, Error)]
269pub enum GetUserError {
270    #[error("unknown revert (selector {selector:#x})")]
271    UnknownRevert { selector: u32, data: Vec<u8> },
272    #[error("provider/transport error: {0}")]
273    Transport(String),
274}
275
276#[derive(Debug, Error)]
277pub enum TabPaymentStatusError {
278    #[error("unknown revert (selector {selector:#x})")]
279    UnknownRevert { selector: u32, data: Vec<u8> },
280    #[error("provider/transport error: {0}")]
281    Transport(String),
282}
283
284#[derive(Debug, Error)]
285pub enum CreateTabError {
286    #[error("invalid params: {0}")]
287    InvalidParams(String),
288
289    #[error(transparent)]
290    Rpc(#[from] ApiClientError),
291}
292
293#[derive(Debug, Error)]
294pub enum IssuePaymentGuaranteeError {
295    #[error("invalid params: {0}")]
296    InvalidParams(String),
297
298    #[error(transparent)]
299    Rpc(#[from] ApiClientError),
300}
301
302#[derive(Debug, Error)]
303pub enum RecipientQueryError {
304    #[error(transparent)]
305    Rpc(#[from] ApiClientError),
306}
307
308#[derive(Debug, Error)]
309pub enum VerifyGuaranteeError {
310    #[error("invalid BLS certificate")]
311    InvalidCertificate(#[source] Error),
312    #[error("certificate signature mismatch")]
313    CertificateMismatch,
314    #[error("guarantee version mismatch: expected {expected}, got {actual}")]
315    GuaranteeVersionMismatch { expected: u64, actual: u64 },
316    #[error("guarantee domain mismatch")]
317    GuaranteeDomainMismatch,
318    #[error("unsupported guarantee version: {0}")]
319    UnsupportedGuaranteeVersion(u64),
320}
321
322#[derive(Debug, Error)]
323pub enum X402Error {
324    #[error("invalid scheme: {0}")]
325    InvalidScheme(String),
326    #[error("invalid x402 version: {0}")]
327    InvalidVersion(String),
328    #[error("invalid facilitator url: {0}")]
329    InvalidFacilitatorUrl(String),
330    #[error("failed to resolve tab endpoint: {0}")]
331    TabResolutionFailed(String),
332    #[error("failed to encode payment envelope: {0}")]
333    EncodeEnvelope(String),
334    #[error("invalid paymentRequirements.extra: {0}")]
335    InvalidExtra(String),
336    #[error("invalid number for field {field}: {source}")]
337    InvalidNumber { field: String, source: Error },
338    #[error("user mismatch in paymentRequirements: found {found}, expected {expected}")]
339    UserMismatch { found: String, expected: String },
340    #[error("settlement failed with status {status}: {body}")]
341    SettlementFailed { status: StatusCode, body: Value },
342    #[error(transparent)]
343    Signing(#[from] SignPaymentError),
344    #[error(transparent)]
345    Http(#[from] reqwest::Error),
346}
347
348/// Minimal context for a revert we could decode from the contract call.
349#[derive(Debug, Clone)]
350struct RevertDetails {
351    selector: u32,
352    data: Vec<u8>,
353}
354
355impl RevertDetails {
356    fn from_error(e: &alloy_contract::Error) -> Option<Self> {
357        e.as_revert_data().map(|bytes: Bytes| {
358            let data = bytes.to_vec();
359            let selector = if data.len() >= 4 {
360                u32::from_be_bytes([data[0], data[1], data[2], data[3]])
361            } else {
362                0
363            };
364            Self { selector, data }
365        })
366    }
367}
368
369const INVALID_MIN_VALIDATION_SCORE_SELECTOR: u32 = 0x940e8e0e;
370const INVALID_VALIDATION_CHAIN_ID_SELECTOR: u32 = 0xabe8d799;
371const UNTRUSTED_VALIDATION_REGISTRY_SELECTOR: u32 = 0x6098bbe0;
372const VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR: u32 = 0xd7201f6e;
373const VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR: u32 = 0x95ce60ab;
374const VALIDATION_LOOKUP_FAILED_SELECTOR: u32 = 0x105163d4;
375const VALIDATION_PENDING_SELECTOR: u32 = 0x860263f8;
376const VALIDATION_SCORE_TOO_LOW_SELECTOR: u32 = 0xf44670f9;
377const VALIDATION_VALIDATOR_MISMATCH_SELECTOR: u32 = 0x9e8eb320;
378const VALIDATION_AGENT_MISMATCH_SELECTOR: u32 = 0xe474a924;
379const VALIDATION_TAG_MISMATCH_SELECTOR: u32 = 0x0604e144;
380
381trait ContractErrorTarget {
382    fn from_unknown_revert(revert: RevertDetails) -> Self;
383    fn from_transport(err: alloy_contract::Error) -> Self;
384}
385
386fn map_contract_error<T, F>(error: alloy_contract::Error, map_decoded: F) -> T
387where
388    T: ContractErrorTarget,
389    F: FnOnce(Core4Mica::Core4MicaErrors) -> Option<T>,
390{
391    if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>()
392        && let Some(mapped) = map_decoded(decoded)
393    {
394        return mapped;
395    }
396
397    if let Some(revert) = RevertDetails::from_error(&error) {
398        return T::from_unknown_revert(revert);
399    }
400
401    T::from_transport(error)
402}
403
404macro_rules! impl_from_alloy_error {
405    ($target:ty, { $($contract_err:pat => $target_err:expr),* $(,)? }) => {
406        impl From<alloy_contract::Error> for $target {
407            fn from(e: alloy_contract::Error) -> Self {
408                map_contract_error(e, |decoded| match decoded {
409                    $(
410                        $contract_err => Some($target_err),
411                    )*
412                    _ => None,
413                })
414            }
415        }
416    };
417    ($target:ty) => {
418        impl From<alloy_contract::Error> for $target {
419            fn from(e: alloy_contract::Error) -> Self {
420                map_contract_error(e, |_| None)
421            }
422        }
423    };
424}
425
426macro_rules! impl_contract_error_target {
427    ($target:ty) => {
428        impl ContractErrorTarget for $target {
429            fn from_unknown_revert(revert: RevertDetails) -> Self {
430                Self::UnknownRevert {
431                    selector: revert.selector,
432                    data: revert.data,
433                }
434            }
435
436            fn from_transport(err: alloy_contract::Error) -> Self {
437                Self::Transport(err.to_string())
438            }
439        }
440    };
441}
442
443impl_contract_error_target!(RemunerateError);
444impl_contract_error_target!(FinalizeWithdrawalError);
445impl_contract_error_target!(RequestWithdrawalError);
446impl_contract_error_target!(CancelWithdrawalError);
447impl_contract_error_target!(DepositError);
448impl_contract_error_target!(ApproveErc20Error);
449impl_contract_error_target!(PayTabError);
450impl_contract_error_target!(GetUserError);
451impl_contract_error_target!(TabPaymentStatusError);
452
453impl From<alloy_contract::Error> for RemunerateError {
454    fn from(error: alloy_contract::Error) -> Self {
455        if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>() {
456            match decoded {
457                Core4Mica::Core4MicaErrors::TabNotYetOverdue(_) => return Self::TabNotYetOverdue,
458                Core4Mica::Core4MicaErrors::TabExpired(_) => return Self::TabExpired,
459                Core4Mica::Core4MicaErrors::TabPreviouslyRemunerated(_) => {
460                    return Self::TabPreviouslyRemunerated;
461                }
462                Core4Mica::Core4MicaErrors::TabAlreadyPaid(_) => return Self::TabAlreadyPaid,
463                Core4Mica::Core4MicaErrors::InvalidSignature(_) => return Self::InvalidSignature,
464                Core4Mica::Core4MicaErrors::DoubleSpendingDetected(_) => {
465                    return Self::DoubleSpendingDetected;
466                }
467                Core4Mica::Core4MicaErrors::InvalidRecipient(_) => return Self::InvalidRecipient,
468                Core4Mica::Core4MicaErrors::AmountZero(_) => return Self::AmountZero,
469                Core4Mica::Core4MicaErrors::TransferFailed(_) => return Self::TransferFailed,
470                Core4Mica::Core4MicaErrors::InvalidGuaranteeDomain(_) => {
471                    return Self::GuaranteeDomainMismatch;
472                }
473                Core4Mica::Core4MicaErrors::UnsupportedGuaranteeVersion(err) => {
474                    return Self::UnsupportedGuaranteeVersion(err.version);
475                }
476                _ => {}
477            }
478        }
479
480        if let Some(revert) = RevertDetails::from_error(&error) {
481            return match revert.selector {
482                INVALID_MIN_VALIDATION_SCORE_SELECTOR => Self::InvalidMinValidationScore,
483                INVALID_VALIDATION_CHAIN_ID_SELECTOR => Self::InvalidValidationChainId,
484                UNTRUSTED_VALIDATION_REGISTRY_SELECTOR => decode_address_argument(&revert.data)
485                    .map(Self::UntrustedValidationRegistry)
486                    .unwrap_or(Self::UnknownRevert {
487                        selector: revert.selector,
488                        data: revert.data,
489                    }),
490                VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR => Self::ValidationSubjectHashMismatch,
491                VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR => Self::ValidationRequestHashMismatch,
492                VALIDATION_LOOKUP_FAILED_SELECTOR => Self::ValidationLookupFailed,
493                VALIDATION_PENDING_SELECTOR => Self::ValidationPending,
494                VALIDATION_SCORE_TOO_LOW_SELECTOR => Self::ValidationScoreTooLow,
495                VALIDATION_VALIDATOR_MISMATCH_SELECTOR => Self::ValidationValidatorMismatch,
496                VALIDATION_AGENT_MISMATCH_SELECTOR => Self::ValidationAgentMismatch,
497                VALIDATION_TAG_MISMATCH_SELECTOR => Self::ValidationTagMismatch,
498                _ => Self::UnknownRevert {
499                    selector: revert.selector,
500                    data: revert.data,
501                },
502            };
503        }
504
505        Self::Transport(error.to_string())
506    }
507}
508
509fn decode_address_argument(data: &[u8]) -> Option<Address> {
510    if data.len() < 36 {
511        return None;
512    }
513
514    let mut address = [0u8; 20];
515    address.copy_from_slice(&data[16..36]);
516    Some(Address::from(address))
517}
518
519impl_from_alloy_error!(FinalizeWithdrawalError, {
520    Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
521    Core4Mica::Core4MicaErrors::GracePeriodNotElapsed(_) => Self::GracePeriodNotElapsed,
522    Core4Mica::Core4MicaErrors::TransferFailed(_) => Self::TransferFailed,
523});
524
525impl_from_alloy_error!(RequestWithdrawalError, {
526    Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
527    Core4Mica::Core4MicaErrors::InsufficientAvailable(_) => Self::InsufficientAvailable,
528});
529
530impl_from_alloy_error!(CancelWithdrawalError, {
531    Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
532});
533
534impl_from_alloy_error!(DepositError, {
535    Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
536});
537
538impl_from_alloy_error!(PayTabError, {
539    Core4Mica::Core4MicaErrors::InvalidAsset(_) => Self::InvalidAsset,
540});
541
542impl_from_alloy_error!(ApproveErc20Error);
543
544impl_from_alloy_error!(GetUserError);
545
546impl_from_alloy_error!(TabPaymentStatusError);