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#[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);