use crate::contract::Core4Mica;
use alloy::contract as alloy_contract;
use alloy::primitives::{Address, Bytes};
use anyhow::Error;
use crypto::hex::DecodeHexError;
use reqwest::StatusCode;
use rpc::ApiClientError;
use serde_json::Value;
use thiserror::Error;
use url::ParseError;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("invalid config value: {0}")]
InvalidValue(String),
#[error("missing config: {0}")]
Missing(String),
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("invalid auth URL: {0}")]
InvalidUrl(#[from] ParseError),
#[error("auth request failed: {0}")]
Transport(#[from] reqwest::Error),
#[error("failed to decode auth response: {0}")]
Decode(#[from] serde_json::Error),
#[error("auth server returned {status}: {message}")]
Api { status: StatusCode, message: String },
#[error("signing failed: {0}")]
Signing(String),
#[error("auth config is missing")]
MissingConfig,
#[error("refresh token not available")]
MissingRefreshToken,
#[error("auth state error: {0}")]
Internal(String),
}
impl From<AuthError> for ApiClientError {
fn from(val: AuthError) -> Self {
let status = match &val {
AuthError::Api { status, .. } => *status,
AuthError::InvalidUrl(_) | AuthError::MissingConfig => StatusCode::BAD_REQUEST,
AuthError::MissingRefreshToken => StatusCode::UNAUTHORIZED,
AuthError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE,
AuthError::Decode(_) => StatusCode::BAD_GATEWAY,
AuthError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
AuthError::Signing(_) => StatusCode::UNAUTHORIZED,
};
match val {
AuthError::InvalidUrl(err) => ApiClientError::InvalidUrl(err),
AuthError::Transport(err) => ApiClientError::Transport(err),
AuthError::Decode(err) => ApiClientError::Decode(err),
AuthError::Api { status, message } => ApiClientError::Api { status, message },
other => ApiClientError::Api {
status,
message: other.to_string(),
},
}
}
}
#[derive(Error, Debug)]
pub enum ClientError {
#[error("client RPC error: {0}")]
Rpc(String),
#[error("client provider error: {0}")]
Provider(String),
#[error("client initialization error: {0}")]
Initialization(String),
}
#[derive(Debug, Error)]
pub enum SignPaymentError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("address mismatch: signer={signer:?} != claims.user_address={claims}")]
AddressMismatch { signer: Address, claims: String },
#[error("invalid user address in claims")]
InvalidUserAddress,
#[error("invalid recipient address in claims")]
InvalidRecipientAddress,
#[error("failed to sign the payment: {0}")]
Failed(String),
#[error(transparent)]
Rpc(#[from] ApiClientError),
}
#[derive(Debug, Error)]
pub enum RemunerateError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("failed to decode guarantee claims hex")]
ClaimsHex(#[source] Error),
#[error("failed to decode guarantee claims")]
ClaimsDecode(#[source] Error),
#[error("failed to convert guarantee claims into contract type")]
GuaranteeConversion(#[source] Error),
#[error("failed to decode signature hex")]
SignatureHex(#[source] DecodeHexError),
#[error("failed to decode BLS signature")]
SignatureDecode(#[source] Error),
#[error("tab not yet overdue")]
TabNotYetOverdue,
#[error("tab expired")]
TabExpired,
#[error("tab previously remunerated")]
TabPreviouslyRemunerated,
#[error("tab already paid")]
TabAlreadyPaid,
#[error("invalid signature")]
InvalidSignature,
#[error("double spending detected")]
DoubleSpendingDetected,
#[error("invalid recipient")]
InvalidRecipient,
#[error("amount is zero")]
AmountZero,
#[error("transfer failed")]
TransferFailed,
#[error("certificate verification failed: {0}")]
CertificateInvalid(#[source] Error),
#[error("certificate signature mismatch before submission")]
CertificateMismatch,
#[error("guarantee version mismatch: expected {expected}, got {actual}")]
GuaranteeVersionMismatch { expected: u64, actual: u64 },
#[error("guarantee domain mismatch")]
GuaranteeDomainMismatch,
#[error("unsupported guarantee version: {0}")]
UnsupportedGuaranteeVersion(u64),
#[error("invalid min validation score")]
InvalidMinValidationScore,
#[error("invalid validation chain id")]
InvalidValidationChainId,
#[error("untrusted validation registry: {0}")]
UntrustedValidationRegistry(Address),
#[error("validation subject hash mismatch")]
ValidationSubjectHashMismatch,
#[error("validation request hash mismatch")]
ValidationRequestHashMismatch,
#[error("validation lookup failed")]
ValidationLookupFailed,
#[error("validation pending")]
ValidationPending,
#[error("validation score too low")]
ValidationScoreTooLow,
#[error("validation validator mismatch")]
ValidationValidatorMismatch,
#[error("validation agent mismatch")]
ValidationAgentMismatch,
#[error("validation tag mismatch")]
ValidationTagMismatch,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum FinalizeWithdrawalError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("no withdrawal requested")]
NoWithdrawalRequested,
#[error("grace period not elapsed")]
GracePeriodNotElapsed,
#[error("transfer failed")]
TransferFailed,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum RequestWithdrawalError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("amount is zero")]
AmountZero,
#[error("insufficient available")]
InsufficientAvailable,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum CancelWithdrawalError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("no withdrawal requested")]
NoWithdrawalRequested,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum DepositError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("amount is zero")]
AmountZero,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum ApproveErc20Error {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum PayTabError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error("invalid asset")]
InvalidAsset,
#[error(transparent)]
Client(#[from] ClientError),
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum GetUserError {
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum TabPaymentStatusError {
#[error("unknown revert (selector {selector:#x})")]
UnknownRevert { selector: u32, data: Vec<u8> },
#[error("provider/transport error: {0}")]
Transport(String),
}
#[derive(Debug, Error)]
pub enum CreateTabError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error(transparent)]
Rpc(#[from] ApiClientError),
}
#[derive(Debug, Error)]
pub enum IssuePaymentGuaranteeError {
#[error("invalid params: {0}")]
InvalidParams(String),
#[error(transparent)]
Rpc(#[from] ApiClientError),
}
#[derive(Debug, Error)]
pub enum RecipientQueryError {
#[error(transparent)]
Rpc(#[from] ApiClientError),
}
#[derive(Debug, Error)]
pub enum VerifyGuaranteeError {
#[error("invalid BLS certificate")]
InvalidCertificate(#[source] Error),
#[error("certificate signature mismatch")]
CertificateMismatch,
#[error("guarantee version mismatch: expected {expected}, got {actual}")]
GuaranteeVersionMismatch { expected: u64, actual: u64 },
#[error("guarantee domain mismatch")]
GuaranteeDomainMismatch,
#[error("unsupported guarantee version: {0}")]
UnsupportedGuaranteeVersion(u64),
}
#[derive(Debug, Error)]
pub enum X402Error {
#[error("invalid scheme: {0}")]
InvalidScheme(String),
#[error("invalid x402 version: {0}")]
InvalidVersion(String),
#[error("invalid facilitator url: {0}")]
InvalidFacilitatorUrl(String),
#[error("failed to resolve tab endpoint: {0}")]
TabResolutionFailed(String),
#[error("failed to encode payment envelope: {0}")]
EncodeEnvelope(String),
#[error("invalid paymentRequirements.extra: {0}")]
InvalidExtra(String),
#[error("invalid number for field {field}: {source}")]
InvalidNumber { field: String, source: Error },
#[error("user mismatch in paymentRequirements: found {found}, expected {expected}")]
UserMismatch { found: String, expected: String },
#[error("settlement failed with status {status}: {body}")]
SettlementFailed { status: StatusCode, body: Value },
#[error(transparent)]
Signing(#[from] SignPaymentError),
#[error(transparent)]
Http(#[from] reqwest::Error),
}
#[derive(Debug, Clone)]
struct RevertDetails {
selector: u32,
data: Vec<u8>,
}
impl RevertDetails {
fn from_error(e: &alloy_contract::Error) -> Option<Self> {
e.as_revert_data().map(|bytes: Bytes| {
let data = bytes.to_vec();
let selector = if data.len() >= 4 {
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
} else {
0
};
Self { selector, data }
})
}
}
const INVALID_MIN_VALIDATION_SCORE_SELECTOR: u32 = 0x940e8e0e;
const INVALID_VALIDATION_CHAIN_ID_SELECTOR: u32 = 0xabe8d799;
const UNTRUSTED_VALIDATION_REGISTRY_SELECTOR: u32 = 0x6098bbe0;
const VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR: u32 = 0xd7201f6e;
const VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR: u32 = 0x95ce60ab;
const VALIDATION_LOOKUP_FAILED_SELECTOR: u32 = 0x105163d4;
const VALIDATION_PENDING_SELECTOR: u32 = 0x860263f8;
const VALIDATION_SCORE_TOO_LOW_SELECTOR: u32 = 0xf44670f9;
const VALIDATION_VALIDATOR_MISMATCH_SELECTOR: u32 = 0x9e8eb320;
const VALIDATION_AGENT_MISMATCH_SELECTOR: u32 = 0xe474a924;
const VALIDATION_TAG_MISMATCH_SELECTOR: u32 = 0x0604e144;
trait ContractErrorTarget {
fn from_unknown_revert(revert: RevertDetails) -> Self;
fn from_transport(err: alloy_contract::Error) -> Self;
}
fn map_contract_error<T, F>(error: alloy_contract::Error, map_decoded: F) -> T
where
T: ContractErrorTarget,
F: FnOnce(Core4Mica::Core4MicaErrors) -> Option<T>,
{
if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>()
&& let Some(mapped) = map_decoded(decoded)
{
return mapped;
}
if let Some(revert) = RevertDetails::from_error(&error) {
return T::from_unknown_revert(revert);
}
T::from_transport(error)
}
macro_rules! impl_from_alloy_error {
($target:ty, { $($contract_err:pat => $target_err:expr),* $(,)? }) => {
impl From<alloy_contract::Error> for $target {
fn from(e: alloy_contract::Error) -> Self {
map_contract_error(e, |decoded| match decoded {
$(
$contract_err => Some($target_err),
)*
_ => None,
})
}
}
};
($target:ty) => {
impl From<alloy_contract::Error> for $target {
fn from(e: alloy_contract::Error) -> Self {
map_contract_error(e, |_| None)
}
}
};
}
macro_rules! impl_contract_error_target {
($target:ty) => {
impl ContractErrorTarget for $target {
fn from_unknown_revert(revert: RevertDetails) -> Self {
Self::UnknownRevert {
selector: revert.selector,
data: revert.data,
}
}
fn from_transport(err: alloy_contract::Error) -> Self {
Self::Transport(err.to_string())
}
}
};
}
impl_contract_error_target!(RemunerateError);
impl_contract_error_target!(FinalizeWithdrawalError);
impl_contract_error_target!(RequestWithdrawalError);
impl_contract_error_target!(CancelWithdrawalError);
impl_contract_error_target!(DepositError);
impl_contract_error_target!(ApproveErc20Error);
impl_contract_error_target!(PayTabError);
impl_contract_error_target!(GetUserError);
impl_contract_error_target!(TabPaymentStatusError);
impl From<alloy_contract::Error> for RemunerateError {
fn from(error: alloy_contract::Error) -> Self {
if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>() {
match decoded {
Core4Mica::Core4MicaErrors::TabNotYetOverdue(_) => return Self::TabNotYetOverdue,
Core4Mica::Core4MicaErrors::TabExpired(_) => return Self::TabExpired,
Core4Mica::Core4MicaErrors::TabPreviouslyRemunerated(_) => {
return Self::TabPreviouslyRemunerated;
}
Core4Mica::Core4MicaErrors::TabAlreadyPaid(_) => return Self::TabAlreadyPaid,
Core4Mica::Core4MicaErrors::InvalidSignature(_) => return Self::InvalidSignature,
Core4Mica::Core4MicaErrors::DoubleSpendingDetected(_) => {
return Self::DoubleSpendingDetected;
}
Core4Mica::Core4MicaErrors::InvalidRecipient(_) => return Self::InvalidRecipient,
Core4Mica::Core4MicaErrors::AmountZero(_) => return Self::AmountZero,
Core4Mica::Core4MicaErrors::TransferFailed(_) => return Self::TransferFailed,
Core4Mica::Core4MicaErrors::InvalidGuaranteeDomain(_) => {
return Self::GuaranteeDomainMismatch;
}
Core4Mica::Core4MicaErrors::UnsupportedGuaranteeVersion(err) => {
return Self::UnsupportedGuaranteeVersion(err.version);
}
_ => {}
}
}
if let Some(revert) = RevertDetails::from_error(&error) {
return match revert.selector {
INVALID_MIN_VALIDATION_SCORE_SELECTOR => Self::InvalidMinValidationScore,
INVALID_VALIDATION_CHAIN_ID_SELECTOR => Self::InvalidValidationChainId,
UNTRUSTED_VALIDATION_REGISTRY_SELECTOR => decode_address_argument(&revert.data)
.map(Self::UntrustedValidationRegistry)
.unwrap_or(Self::UnknownRevert {
selector: revert.selector,
data: revert.data,
}),
VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR => Self::ValidationSubjectHashMismatch,
VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR => Self::ValidationRequestHashMismatch,
VALIDATION_LOOKUP_FAILED_SELECTOR => Self::ValidationLookupFailed,
VALIDATION_PENDING_SELECTOR => Self::ValidationPending,
VALIDATION_SCORE_TOO_LOW_SELECTOR => Self::ValidationScoreTooLow,
VALIDATION_VALIDATOR_MISMATCH_SELECTOR => Self::ValidationValidatorMismatch,
VALIDATION_AGENT_MISMATCH_SELECTOR => Self::ValidationAgentMismatch,
VALIDATION_TAG_MISMATCH_SELECTOR => Self::ValidationTagMismatch,
_ => Self::UnknownRevert {
selector: revert.selector,
data: revert.data,
},
};
}
Self::Transport(error.to_string())
}
}
fn decode_address_argument(data: &[u8]) -> Option<Address> {
if data.len() < 36 {
return None;
}
let mut address = [0u8; 20];
address.copy_from_slice(&data[16..36]);
Some(Address::from(address))
}
impl_from_alloy_error!(FinalizeWithdrawalError, {
Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
Core4Mica::Core4MicaErrors::GracePeriodNotElapsed(_) => Self::GracePeriodNotElapsed,
Core4Mica::Core4MicaErrors::TransferFailed(_) => Self::TransferFailed,
});
impl_from_alloy_error!(RequestWithdrawalError, {
Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
Core4Mica::Core4MicaErrors::InsufficientAvailable(_) => Self::InsufficientAvailable,
});
impl_from_alloy_error!(CancelWithdrawalError, {
Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
});
impl_from_alloy_error!(DepositError, {
Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
});
impl_from_alloy_error!(PayTabError, {
Core4Mica::Core4MicaErrors::InvalidAsset(_) => Self::InvalidAsset,
});
impl_from_alloy_error!(ApproveErc20Error);
impl_from_alloy_error!(GetUserError);
impl_from_alloy_error!(TabPaymentStatusError);