This document is the single source of truth for every #[contracterror] enum
across the Fluxapay contracts. Each error surfaces to callers as
Error(Contract, #<code>); use the tables below to look up what a numeric
code means, what typically causes it, and how to fix it.
sdk/src/index.ts exports FLUXAPAY_CONTRACT_ERROR_MAP, which mirrors the
Core (PaymentProcessor / RefundManager) table below — the only
contract whose error map is safe to expose as a single flat code -> name
map in the SDK (see the note on code collisions at the end of this
document). scripts/check-error-map-sync.ts parses the Rust source and this
document on every CI run to catch drift between them.
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | Unauthorized |
Caller lacks the required role/permission. | Calling an admin/merchant/oracle-only function from an unauthorized address. | Grant the correct role via access_control, or call from an authorized address. |
| 2 | PaymentAlreadyExists |
A payment with this payment_id was already created. |
Reusing a payment_id (often a retry without a fresh ID). |
Generate a new unique payment_id, or fetch the existing payment instead. |
| 3 | PaymentExpired |
The payment's expires_at has passed. |
Verifying/paying after the payment window closed. | Create a new payment; consider extending duration_secs for long-lived flows. |
| 4 | InvalidPaymentId |
payment_id failed format validation (length/charset). |
ID shorter than 3 or longer than 64 chars, or contains disallowed characters. | Use 3–64 chars of [a-zA-Z0-9_-] only. |
| 8 | RefundAlreadyProcessed |
The refund is not in a state that can be processed again. | Calling approve/reject/process on a refund that's already Completed or Rejected. |
Check get_refund status before acting; this is typically not retryable. |
| 9 | DisputeNotFound |
No dispute exists with the given ID. | Typo'd dispute ID, or dispute was never created. | Verify the dispute ID via get_dispute/list endpoints. |
| 12 | DisputeAlreadyResolved |
Dispute has already reached a final resolution. | Voting/resolving a dispute twice. | No action needed — the dispute outcome is final. |
| 14 | PaymentAlreadyProcessed |
Payment is not Confirmed/Overpaid, so refund/settlement isn't allowed. |
Refunding a Pending, PartiallyPaid, or already-refunded payment. |
Wait for confirmation, or check payment.status first. |
| 15 | AccessControlError |
An underlying access-control check failed (see AccessControlError table). |
Role/admin operation failed in the shared access-control module. | Inspect the wrapped AccessControlError variant for specifics. |
| 16 | RefundExceedsPayment |
Sum of non-rejected refunds would exceed the original payment amount. | Requesting a refund larger than the remaining refundable balance. | Request an amount ≤ payment.amount - sum(non-rejected refunds). |
| 17 | ContractPaused |
The contract is globally paused. | An admin invoked the emergency pause. | Wait for an admin to unpause, or contact the operator. |
| 18 | RateLimitExceeded |
Caller exceeded a configured rate limit. | Too many requests/payments in a short window. | Back off and retry after the rate-limit window resets. |
| 19 | RefundCancelled |
The refund was cancelled and cannot be processed. | Acting on a refund after it was explicitly cancelled. | Create a new refund request if still eligible. |
| 20 | UnsupportedToken |
The token address is not an accepted asset. | Using a token not configured for this deployment. | Use one of the supported token addresses for this environment. |
| 21 | AmountBelowMin |
Amount is below the configured minimum. | Payment/stake amount too small. | Increase the amount to at least the configured minimum. |
| 22 | AmountAboveMax |
Amount exceeds the configured maximum. | Payment/stake amount too large. | Reduce the amount, or split into multiple payments. |
| 23 | InvalidExpiry |
expires_at is invalid (e.g. in the past, or before start_time). |
Passing a stale or malformed timestamp. | Pass a future Unix timestamp greater than "now". |
| 24 | InvalidSettlement |
Settlement parameters/signatures failed validation. | Malformed or mismatched collaborative-settlement data. | Re-derive settlement data per docs/ settlement guide and retry. |
| 25 | DuplicateIdempotencyKey |
client_token was already used for a different payment. |
Retrying a request with the same idempotency key but different params. | Reuse the key only for byte-identical retries, or mint a new key. |
| 26 | InvalidAddress |
An address argument failed validation. | Passing a malformed or zero address. | Pass a valid Stellar/Soroban Address. |
| 27 | ArbitrageDetected |
Swap path forms a circular/arbitrage route. | DEX router path validation rejected the route. | Use a direct, non-circular swap path. |
| 28 | SwapPathInvalid |
DEX swap path or quoted return failed validation. | Malformed path, or quote inconsistent with path. | Re-quote the path via the DEX router before submitting. |
| 29 | OraclePriceDeviation |
DEX quote deviates too far from the oracle reference price. | Stale quote, or thin liquidity causing slippage. | Re-quote closer to execution time, or reduce trade size. |
| 30 | SubscriptionInGracePeriod |
Subscription payment failed but is within its retry grace period. | Informational — not a terminal failure. | No action required; the daemon will retry automatically. |
| 31 | SubscriptionRetryExhausted |
Subscription exhausted all retries and is now cancelled. | Payment method kept failing through all retry attempts. | Re-subscribe with a valid, funded payment method. |
| 32 | InvalidResumeTimestamp |
Resume timestamp is in the past or otherwise invalid. | Resuming a paused subscription/stream with a bad timestamp. | Pass a resume timestamp ≥ current ledger time. |
| 33 | MerchantAuthError |
An underlying MerchantAuthError occurred (see that table). |
Pre-authorization pull failed a sub-check. | Inspect the wrapped MerchantAuthError variant for specifics. |
| 34 | TierVolumeLimitExceeded |
Merchant exceeded their KYC tier's monthly volume cap. | Processing volume exceeded the tier's AmountLimits. |
Request a tier upgrade, or wait for the monthly window to reset. |
| 35 | BatchTooLarge |
Batch payment request exceeds the supported maximum size. | Submitting too many payments in one batch call. | Split into smaller batches under the documented max size. |
| 36 | RefundExpired |
The refund request has expired (30-day window) and cannot be processed. | Approving/rejecting a refund after expiry_at. |
Requester must create a new refund request. |
| 37 | InsufficientArbitrators |
Not enough arbitrators are available to vote on a dispute. | Dispute arbitration pool too small. | Register additional arbitrators before opening disputes. |
| 38 | ArbitrationVotingThresholdNotMet |
Dispute voting threshold hasn't been reached yet. | Resolving a dispute before enough arbitrators voted. | Wait for more votes before attempting resolution. |
| 39 | FeeProposalNotReady |
Fee proposal hasn't matured past the required 7-day timelock. | Applying a fee change before the timelock elapses. | Wait until 7 days after the proposal was created. |
| 40 | InvalidEvidenceFormat |
Dispute evidence is not a valid IPFS multihash (CIDv0/CIDv1). | Passing an arbitrary string instead of a CID. | Pass a valid IPFS CIDv0/CIDv1 hash as evidence. |
| 41 | InvalidSettlementSignature |
One or both collaborative-settlement signatures are invalid. | Signature doesn't match the expected signer/payload. | Re-sign the settlement payload with the correct key. |
| 42 | RefundCooldownNotElapsed |
Refund requested before the post-confirmation cooldown elapsed. | Requesting a refund immediately after payment confirmation. | Wait until confirmed_at + refund_cooldown_secs has passed. |
| 43 | Reentrancy |
Reentrancy detected in process_refund_internal/settle_payment. |
Nested/reentrant contract call during a guarded operation. | Not user-fixable — indicates a caller bug; avoid recursive invocations. |
| 44 | NoFeeProposal |
No active fee-change proposal exists. | Trying to apply/cancel a proposal that was never created. | Create a fee proposal first via the appropriate admin call. |
| 45 | StaleOracleRate |
FX oracle rate is stale or unavailable. | Oracle hasn't been updated within the staleness threshold. | Wait for (or trigger) an oracle rate update before retrying. |
| 46 | LinkExpired / InsufficientTreasuryBalance |
Payment link has expired or treasury balance is below the requested withdrawal — both variants share code 46 in the source (see "Known discriminant collisions" below). | Using an expired payment link, or withdrawing more than the treasury holds. | Check docs/error-codes.md's collision note; disambiguate via the calling context (link vs. treasury operation). |
| 47 | MetadataValueTooLong |
A metadata value exceeds 256 characters. | Oversized value in the metadata map. |
Shorten the value to ≤ 256 characters. |
| 48 | UpgradeFailed |
Contract upgrade rejected the new WASM hash. | Invalid or incompatible upgrade payload. | Verify the WASM hash and upgrade authorization, then retry. |
| 49 | MetadataTooLarge |
Metadata map has more than 20 keys. | Attaching too many metadata fields to a payment. | Reduce to ≤ 20 keys, or store extra data off-chain. |
| 50 | InvalidMemoType |
memo_type is not one of Text, Id, Hash, Return. |
Typo or unsupported memo type string. | Use exactly one of the four supported memo types. |
| 51 | MemoTooLong |
Text memo exceeds the 28-byte Stellar limit. | Memo text too long for a Stellar MEMO_TEXT. |
Shorten the memo to ≤ 28 bytes. |
| 52 | InvalidMemoId |
Id-type memo is not parseable as a u64. |
Passing a non-numeric string as an Id memo. |
Pass a valid unsigned 64-bit integer as a string. |
| 53 | PayerNotWhitelisted |
Payer address is not on the merchant's customer whitelist. | Merchant has whitelist mode enabled and payer isn't listed. | Merchant must add the payer via the whitelist management call. |
| 54 | DisputeRateLimitExceeded / LinkMaxUsesReached / DirectTransferNotDisputable / MaxRetriesExceeded / InvalidStatusTransition |
Five distinct variants share code 54 in the source (see "Known discriminant collisions" below). | Any of: too many disputes opened; a payment link hit max_uses; disputing a direct_transfer payment; exceeding the 3-hop retry chain; an invalid status transition. |
Disambiguate via the calling context — which operation returned the error tells you which variant applies. |
| 55 | RateDeviationExceeded |
FX oracle rate deviation exceeds the configured limit. | Requested rate differs too much from the oracle's tracked rate. | Re-quote against the current oracle rate before retrying. |
| 404 | PaymentNotFound |
No payment exists with the given payment_id. |
Typo'd ID, or payment was never created. | Verify the ID via get_payment / listing endpoints. |
| 405 | RefundNotFound |
No refund exists with the given refund_id. |
Typo'd ID, or refund was never created. | Verify the ID via get_refund / get_payment_refunds. |
| 406 | InvalidAmount |
Amount is zero, negative, or otherwise invalid. | Passing a non-positive amount to a payment/refund call. | Pass a strictly positive i128 amount. |
The Rust source currently assigns the same numeric value to multiple
Error variants in two places:
- Code 46:
LinkExpiredandInsufficientTreasuryBalance - Code 54:
DisputeRateLimitExceeded,LinkMaxUsesReached,DirectTransferNotDisputable,MaxRetriesExceeded,InvalidStatusTransition
This means a bare Error(Contract, #54) cannot be mapped to a single name
with certainty — callers must disambiguate using which operation raised the
error. FLUXAPAY_CONTRACT_ERROR_MAP picks the first-declared variant name
for these two codes as a best-effort default. Giving each variant a unique
discriminant in fluxapay/src/lib.rs::Error would resolve this; that change
is out of scope here since it alters the contract's wire-level error codes.
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | Unauthorized |
Caller does not hold the required role. | Calling a role-gated function without that role. | Have an admin grant the role first. |
| 2 | RoleAlreadyGranted |
The account already holds this role. | Granting a role that's already active. | No action needed — the role is already in effect. |
| 3 | RoleNotGranted |
The account does not hold this role. | Revoking/checking a role the account never had. | Confirm the role assignment via a role-query call. |
| 4 | CannotRenounceAdmin |
The sole admin attempted to renounce their own role. | Removing the last remaining admin. | Transfer admin to another address first. |
| 5 | InvalidAdmin |
Proposed admin address is invalid. | Passing a zero/malformed address as new admin. | Pass a valid, distinct Address. |
| 6 | RevocationCooldownActive |
A role revocation cooldown is still active. | Re-revoking before the cooldown window elapsed. | Wait for the cooldown to expire before retrying. |
| 7 | NoPendingRevocation |
No pending revocation exists to act on. | Confirming/cancelling a revocation that was never proposed. | Propose the revocation first. |
| 8 | RecoveryKeyNotSet |
No recovery key configured for this account. | Attempting account recovery without a registered recovery key. | Register a recovery key before relying on recovery flows. |
| 9 | ProposalNotFound |
No admin proposal exists with the given ID/nonce. | Voting on a proposal that doesn't exist. | Verify the proposal nonce before voting. |
| 10 | ProposalAlreadyVoted |
This account already voted on the proposal. | Double-voting on the same admin proposal. | Each account may vote once per proposal. |
| 11 | ProposalExpired |
The proposal's voting window has closed. | Voting/executing after the proposal expired. | Create a new proposal. |
| 12 | ProposalThresholdNotMet |
Not enough approvals to execute the proposal. | Executing a multi-sig admin action before quorum. | Collect additional approvals before executing. |
| 13 | PendingAdminTransfer |
An admin transfer is already pending. | Proposing a new transfer while one is in flight. | Wait for the pending transfer to complete or expire. |
| 14 | InvalidRecovery |
Recovery attempt failed validation. | Wrong recovery key or malformed recovery payload. | Retry with the correct registered recovery key. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | StreamNotFound |
No stream exists with the given ID. | Typo'd stream ID, or stream was never created. | Verify the ID via a stream-lookup call. |
| 2 | Unauthorized |
Caller is not the sender of the stream. | A non-sender tried to modify/cancel the stream. | Only the stream's sender may perform this action. |
| 3 | RateNotDecreased |
New rate must be strictly less than the current rate. | Attempting to increase a stream's rate (disallowed by design). | Only decrease the rate; increases require a new stream. |
| 4 | InvalidRate |
Rate cannot be zero or negative. | Passing a non-positive rate_per_second. |
Pass a strictly positive rate. |
| 5 | StreamAlreadyExists |
A stream with that ID already exists. | Reusing a stream ID. | Use a new, unique stream ID. |
| 6 | InvalidDeposit |
Deposit must be positive. | Creating a stream with a zero/negative deposit. | Pass a strictly positive deposit amount. |
| 7 | StreamNotActive |
Stream is not active. | Withdrawing from/modifying a cancelled or finished stream. | No action possible — the stream has ended. |
| 8 | DestinationNotSet |
No destination configured for a permissionless withdrawal. | Calling permissionless withdraw before setting a destination. | Sender must configure a withdrawal destination first. |
| 9 | ContractPaused |
The contract is globally paused. | An admin invoked the emergency pause. | Wait for an admin to unpause. |
| 10 | MilestoneNotApproved |
Distributions are locked until the sender approves milestones. | Withdrawing before the sender approved the current milestone. | Sender must call the milestone-approval function first. |
| 11 | WithdrawalInProgress |
Withdrawal already in progress (reentrancy guard). | A second withdrawal call while one is still executing. | Wait for the in-flight withdrawal to complete, then retry. |
| 12 | RateBelowMinimum |
New rate is below the minimum allowed rate for this stream. | Decreasing the rate past the configured min_rate_per_second floor. |
Choose a rate ≥ the stream's minimum (DoS-protection floor). |
| 13 | StreamNotPaused |
Stream is not paused. | Resuming a stream that was never paused. | Confirm stream status before calling resume. |
| 14 | InvalidReceiver |
Receiver address cannot equal the sender address. | Creating a stream that pays back to its own sender. | Use a different address for the receiver. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | RateNotFound |
No rate is recorded for the requested currency pair. | Querying a pair the oracle hasn't been given a rate for. | Have the oracle publish a rate for that pair first. |
| 2 | RateStale |
The recorded rate is older than the staleness threshold. | Oracle hasn't updated the rate recently enough. | Wait for (or trigger) a fresh oracle update. |
| 3 | Unauthorized |
Caller is not an authorized oracle/admin. | Calling an oracle-only update function without the role. | Grant the oracle role, or call from an authorized address. |
| 4 | BatchTooLarge / RateDeviationExceeded |
Batch rate update exceeds the max of 20 pairs, or rate deviation exceeds the configured limit — both variants share code 4 in the source. | Submitting > 20 pairs in one batch update, or a rate too far from the previous value. | Split batch updates to ≤ 20 pairs; check deviation limits separately. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | MerchantAlreadyExists |
A merchant with this ID is already registered. | Re-registering the same merchant ID. | Use update_merchant instead, or a new merchant ID. |
| 2 | MerchantNotFound |
No merchant exists with the given ID. | Typo'd merchant ID, or merchant never registered. | Verify the ID via a merchant-lookup call. |
| 3 | Unauthorized |
Caller lacks permission for this merchant operation. | Non-admin/non-owner calling a gated merchant function. | Call from the merchant owner or an admin address. |
| 4 | NotVerified |
Merchant has not completed KYC verification. | Performing an action gated behind verification. | Complete KYC verification for the merchant first. |
| 5 | AdminAlreadySet |
The registry has already been initialized with an admin. | Calling initialize a second time. |
initialize is one-time only; use admin-management calls instead. |
| 6 | PayoutAddressNotWhitelisted |
Payout address is not on the merchant's approved list. | Withdrawing to an address that wasn't whitelisted. | Whitelist the payout address before withdrawing to it. |
| 7 | WhitelistModeRequiresBusinessTier |
Only Business-tier merchants may enable whitelist mode. | Enabling customer whitelist mode below Business tier. | Upgrade the merchant to Business tier first. |
| 8 | PayerNotWhitelisted |
Payer is not in the merchant's customer whitelist. | Whitelist mode is on and the payer isn't listed. | Merchant must add the payer to the whitelist. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | AuthorizationNotFound |
No pre-authorization exists for this (customer, merchant) pair. | Pulling funds without a prior grant. | Customer must grant a pre-authorization first. |
| 2 | AuthorizationInactive |
The authorization has been revoked or is inactive. | Pulling against a revoked/inactive authorization. | Customer must grant a new authorization. |
| 3 | LimitExceeded |
Requested pull exceeds the remaining period limit. | Pulling more than limit_per_period allows in the current window. |
Pull a smaller amount, or wait for the next period. |
| 4 | InvalidAmount |
Amount must be positive. | Passing a zero/negative pull amount. | Pass a strictly positive amount. |
| 5 | Unauthorized |
Caller is not the authorized merchant. | Some other address attempting to pull funds. | Only the merchant named in the authorization may pull. |
| 6 | AuthorizationAlreadyExists |
An authorization already exists for this pair. | Granting a second authorization without revoking the first. | Revoke the existing authorization before granting a new one. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | SwapFailed |
The underlying DEX swap execution failed. | Router/pool call reverted. | Retry with a fresh quote, or a different path. |
| 2 | InvalidPath |
The swap path is malformed or unsupported. | Empty path, or a hop the router doesn't support. | Use a valid, router-supported swap path. |
| 3 | InsufficientLiquidity |
Not enough liquidity to fill the requested swap. | Trade size too large for the pool's depth. | Reduce trade size, or split across multiple swaps. |
| 4 | SlippageExceeded |
Output amount fell below the minimum acceptable. | Price moved between quote and execution. | Increase slippage tolerance, or re-quote closer to execution. |
| 5 | PriceImpactExceeded |
Trade's price impact exceeds the configured guard. | Trade size too large relative to pool depth. | Reduce trade size to stay within the price-impact guard. |
| 6 | NoOutputAmount |
Swap would return zero output. | Degenerate/zero-value swap request. | Increase the input amount. |
| 7 | Refunded |
Swap failed and input was refunded via fallback logic. | Router fallback path triggered after a failed swap. | Informational — funds were returned; retry if desired. |
| Code | Name | Description | Common Cause | Remediation |
|---|---|---|---|---|
| 1 | Unauthorized |
Caller is not authorized to use this session key. | Executing a payload with a session key you don't own. | Use a session key granted to your account. |
| 2 | SessionNotFound |
No session exists for the given (account, session_key) pair. | Typo'd session key, or session was never granted. | Grant a session key before attempting execution. |
| 3 | SessionExpired |
The session key has passed its expiry time. | Executing after the session's validity window closed. | Grant a new session key. |
| 4 | InvalidPayload |
The execution payload failed validation. | Malformed or empty payload/hash. | Pass a well-formed payload matching the expected schema. |
Generated for #458. Keep this table in sync with the #[contracterror] enums it documents — scripts/check-error-map-sync.ts checks FLUXAPAY_CONTRACT_ERROR_MAP (Core table) against fluxapay/src/lib.rs on every CI run.