Files
nym/nym-api/src/ecash/error.rs
T
Drazen Urch 90e9e3cff8 Domain fronting integration (#5974)
* feat: unify HTTP client creation and enable domain fronting

Enhanced the base nym_http_api_client to reduce fragmentation and enable domain fronting:

- Added SerializationFormat enum for explicit JSON/bincode choice (no auto-detection)
- Added from_network() method to create clients from NymNetworkDetails with domain fronting
- Added with_bincode() builder method for explicit serialization configuration
- Set Accept header based on serialization preference
- Added deprecation paths for NymApiClient wrapper and nym_api::Client re-export
- Enabled domain fronting support via network defaults feature

This is part of a broader effort to consolidate HTTP client implementations across the codebase,
reducing ~500 lines of wrapper code and providing automatic domain fronting for censorship resistance.

* feat: migrate NymApiClient usage to unified HTTP client

- Wire up domain fronting configuration in NymNetworkDetails
- Implement NymApiClientExt trait for base nym_http_api_client::Client
- Migrate direct NymApiClient usage in multiple components:
  - nym-network-monitor
  - verloc measurements
  - connection tester
  - coconut/ecash client
  - validator rewarder
- Add Copy derive to ApiUrlConst to enable iteration
- Update error handling and Display implementations

This enables automatic domain fronting for all Nym API calls via the configured CDN front hosts.

* fix: resolve all compilation errors after NymApiClient migration

- Add missing nym-http-api-client dependencies to multiple crates
- Add NymApiClientExt trait imports where needed
- Fix type mismatches from NymApiClient to unified Client
- Add error conversions for NymAPIError in various error enums
- Implement missing trait methods (get_current_rewarded_set, get_all_basic_nodes_with_metadata, get_all_described_nodes)
- Fix type conversions for RewardedSetResponse in network monitor
- Update all API client instantiation to use new unified HTTP client

* feat: complete migration to unified HTTP client and fix all compilation errors

- Added missing NymApiClientExt trait methods (get_all_expanded_nodes, change_base_urls)
- Fixed all compilation errors across the workspace
- Updated nym-node to use unified client instead of deprecated NymApiClient
- Fixed type conversions for RewardedSetResponse → EpochRewardedSet
- Added nym-http-api-client dependency where needed
- Updated all examples and documentation to use new client API

* fix: provide all API URLs for automatic failover in endpoint rotation

Previously, when rotating API endpoints, only a single URL was provided to the
HTTP client, defeating the purpose of having multiple URLs for resilience.

Changes:
- NymApiTopologyProvider now provides all URLs in rotated order when switching endpoints
- NymApisClient similarly provides all URLs starting from the working endpoint
- Added clarifying comments for broadcast/exhaustive query methods where single URLs are intentionally used
- This enables the HTTP client's built-in failover mechanism while maintaining endpoint rotation behavior

The fix ensures that if the primary endpoint fails, the client can automatically
failover to alternative endpoints without manual intervention, improving overall
network resilience.

* Update common/client-core/src/client/base_client/mod.rs

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>

* Remove error generics, address PR comments

* Explicit warning on missing fronting configuration

* Assorted CI fixes

* Registry proc-macro

* Rename macro

* Syn workspace version

* Where do we need to put inventory

* Ergonomics and call sites, incept the builder

* fix: Address critical issues in client configuration registry implementation

- Fixed HeaderMapInit parsing bug that would cause compilation errors
- Added comprehensive documentation with usage examples and DSL reference
- Improved error handling with better error messages for invalid headers
- Added test coverage for both macro and registry functionality
- Added debug inspection capabilities for registered configurations
- Fixed module name conflicts in tests by using separate modules

All tests now passing:
- 7 macro tests validating DSL parsing and code generation
- 4 registry tests verifying configuration collection and application

* Use default value for the ports until api is deployed

* Feature/improved http error (#6025)

* use display impl for urls

* feat: attempt to add more details to reqwest errors

* temporarily restored GenericRequestFailure variant

* another restoration

* cleanup

* Some debug tooling, and default timeout fix

* Fix user-agent override

* Fix various wasm things

---------

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Bogdan-Ștefan Neacşu <bogdan@nymtech.net>
2025-09-15 14:32:15 +02:00

255 lines
9.0 KiB
Rust

// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::NymApiStorageError;
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId};
use nym_credentials_interface::UnknownTicketType;
use nym_crypto::asymmetric::{
ed25519::{Ed25519RecoveryError, SignatureError},
x25519::KeyRecoveryError,
};
use nym_dkg::error::DkgError;
use nym_dkg::Threshold;
use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::AccountId;
use thiserror::Error;
use time::OffsetDateTime;
pub type Result<T, E = EcashError> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum EcashError {
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("this instance is running without on-chain signing capabilities so no transactions can be sent")]
ChainSignerNotEnabled,
#[error("this operation couldn't be completed as this nym-api is not an active ecash signer")]
NotASigner,
#[error("the provided expiration date is too late")]
ExpirationDateTooLate,
#[error("the provided expiration date is too early")]
ExpirationDateTooEarly,
#[error("attempted to request too many partial ticketbooks at once. got {requested}, but the maximum allowed is {max}")]
RequestTooBig { requested: usize, max: usize },
#[error("the provided expiration date is malformed")]
MalformedExpirationDate { raw: String },
#[error("coconut api query failure: {0}")]
CoconutApiError(#[from] EcashApiError),
#[error("nym api query failure: {0}")]
NymApiError(Box<NymAPIError>),
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),
#[error("could not parse Ed25519 data: {0}")]
Ed25519ParseError(#[from] Ed25519RecoveryError),
#[error("could not parse X25519 data: {0}")]
X25519ParseError(#[from] KeyRecoveryError),
#[error("nyxd error: {0}")]
NyxdError(#[from] NyxdError),
#[error("validator client error: {0}")]
ValidatorClientError(#[from] nym_validator_client::ValidatorClientError),
#[error("Compact ecash internal error - {0}")]
CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError),
#[error("Account linked to this public key has been blacklisted")]
BlacklistedAccount,
#[error("signature didn't verify correctly")]
SignatureVerificationError(#[from] SignatureError),
#[error("storage error: {0}")]
StorageError(#[from] NymApiStorageError),
#[error("credentials error: {0}")]
CredentialsError(#[from] nym_credentials::error::Error),
#[error("DKG error: {0}")]
DkgError(#[from] DkgError),
#[error("failed to recover assigned node index: {reason}")]
NodeIndexRecoveryError { reason: String },
#[error("unrecoverable state: {reason}")]
UnrecoverableState { reason: String },
#[error("DKG has not finished yet in order to derive the coconut key")]
KeyPairNotDerivedYet,
#[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")]
MissingDkgState { epoch_id: EpochId },
#[error("a new iteration of DKG is currently in progress. all ticket issuance is halted until that's completed")]
DkgInProgress,
#[error(
"the node index value for epoch {epoch_id} is not available - are you sure we are a dealer?"
)]
UnavailableAssignedIndex { epoch_id: EpochId },
#[error("the receiver index value for epoch {epoch_id} is not available - are you sure we are a receiver?")]
UnavailableReceiverIndex { epoch_id: EpochId },
#[error("the threshold value for epoch {epoch_id} is not available")]
UnavailableThreshold { epoch_id: EpochId },
#[error("the proposal id value for epoch {epoch_id} is not available")]
UnavailableProposalId { epoch_id: EpochId },
#[error("could not find dealing chunk {chunk_index} for dealing {dealing_index} from dealer {dealer} for epoch {epoch_id} on the chain!")]
MissingDealingChunk {
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
chunk_index: ChunkIndex,
},
#[error("could not find ecash deposit associated with id {deposit_id}")]
NonExistentDeposit { deposit_id: DepositId },
#[error("the provided request digest does not match the hash of attached serial numbers")]
MismatchedRequestDigest,
#[error("one of the attached tickets {serial_number_bs58} has not been verified before")]
TicketNotVerified { serial_number_bs58: String },
#[error("the provided ticket(s) redemption proposal is invalid: {source}")]
RedemptionProposalFailure {
#[from]
source: RedemptionError,
},
#[error("this gateway hasn't submitted any tickets for verification")]
NotTicketsProvided,
#[error("this gateway is attempting to redeem its tickets too often. last redemption happened on {last_redemption}. the earliest next permitted redemption will be on {next_allowed}")]
TooFrequentRedemption {
last_redemption: OffsetDateTime,
next_allowed: OffsetDateTime,
},
#[error(
"could not sign the data for epoch {requested}. our current key is for epoch {available}"
)]
InvalidSigningKeyEpoch {
requested: EpochId,
available: EpochId,
},
#[error("could not obtain enough shares for aggregation. got {shares} shares whilst the threshold is {threshold}")]
InsufficientNumberOfShares { threshold: Threshold, shares: usize },
#[error(transparent)]
UnknownTicketBookType(#[from] UnknownTicketType),
#[error("could not find issued ticketbook associated with deposit {deposit_id}")]
UnavailableTicketbook { deposit_id: DepositId },
#[error("could not generate merkle proof for the provided deposits")]
MerkleProofGenerationFailure,
}
impl From<NymAPIError> for EcashError {
fn from(e: NymAPIError) -> Self {
EcashError::NymApiError(Box::new(e))
}
}
// impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError {
// fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> {
// let err_msg = self.to_string();
// Response::build()
// .header(ContentType::Plain)
// .sized_body(err_msg.len(), Cursor::new(err_msg))
// .status(Status::BadRequest)
// .ok()
// }
// }
//
// impl OpenApiResponderInner for EcashError {
// fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
// let mut responses = Responses::default();
// ensure_status_code_exists(&mut responses, 400);
// Ok(responses)
// }
// }
#[derive(Debug, Error)]
pub enum RedemptionError {
#[error("failed to retrieve proposal {proposal_id} from the chain")]
ProposalRetrievalFailure { proposal_id: u64 },
#[error(
"the proposal {proposal_id} has invalid title. got {received} but expected {}",
BATCH_REDEMPTION_PROPOSAL_TITLE
)]
InvalidProposalTitle { proposal_id: u64, received: String },
#[error("the proposal {proposal_id} has invalid description. got {received} but expected {expected}")]
InvalidProposalDescription {
proposal_id: u64,
received: String,
expected: String,
},
#[error("the proposal {proposal_id} is still pending")]
StillPending { proposal_id: u64 },
#[error("the proposal {proposal_id} has already been executed")]
AlreadyExecuted { proposal_id: u64 },
#[error("the proposal {proposal_id} has already been rejected")]
AlreadyRejected { proposal_id: u64 },
#[error("the proposal {proposal_id} has already been passed")]
AlreadyPassed { proposal_id: u64 },
#[error("the proposal {proposal_id} was proposed by an unexpected address {received}. expected the ecash contract at {expected}")]
InvalidProposer {
proposal_id: u64,
received: String,
expected: AccountId,
},
#[error(
"the proposal {proposal_id} did not contain exactly a single contract execution message"
)]
TooManyMessages { proposal_id: u64 },
#[error("the proposal {proposal_id} did not contain the correct redemption execution message")]
InvalidMessage { proposal_id: u64 },
#[error("the proposal {proposal_id} has not been made against the expected e-cash contract")]
InvalidContract { proposal_id: u64 },
#[error("the proposal {proposal_id} proposes redemption of tickets for gateway {proposed}, but the request has been sent by {received}")]
InvalidRedemptionTarget {
proposal_id: u64,
proposed: String,
received: String,
},
#[error("the proposal {proposal_id} proposes redemption of {proposed} tickets, but the request has been sent for {received} instead")]
InvalidRedemptionTicketCount {
proposal_id: u64,
proposed: u16,
received: u16,
},
}