Current section
Files
Jump to
Current section
Files
lib/remit_md/errors.ex
defmodule RemitMd.Error do
@moduledoc """
Error type returned by all remit.md SDK operations.
Every error has a machine-readable `code`, a human-readable `message`,
a `doc_url` pointing to the relevant documentation, and optional `context`
with additional structured data.
"""
defexception [:code, :message, :doc_url, :context]
@base_doc "https://remit.md/docs/errors"
# ─── Error codes (match TS SDK errors.ts - all 28 canonical codes) ────────
# Auth errors
@doc "EIP-712 signature verification failed."
def invalid_signature, do: "INVALID_SIGNATURE"
@doc "EIP-712 signature verification failed (legacy alias)."
def signature_invalid, do: "INVALID_SIGNATURE"
@doc "Nonce has already been used."
def nonce_reused, do: "NONCE_REUSED"
@doc "Nonce has already been used (legacy alias)."
def nonce_replay, do: "NONCE_REUSED"
@doc "The request timestamp is outside the acceptable window."
def timestamp_expired, do: "TIMESTAMP_EXPIRED"
@doc "API key or EIP-712 signature is missing or invalid."
def unauthorized, do: "UNAUTHORIZED"
# Balance / funds
@doc "The invoking agent has insufficient USDC balance."
def insufficient_balance, do: "INSUFFICIENT_BALANCE"
@doc "Transaction amount is below minimum."
def below_minimum, do: "BELOW_MINIMUM"
# Escrow errors
@doc "Escrow not found."
def escrow_not_found, do: "ESCROW_NOT_FOUND"
@doc "This invoice already has a funded escrow."
def escrow_already_funded, do: "ESCROW_ALREADY_FUNDED"
@doc "Escrow has expired."
def escrow_expired, do: "ESCROW_EXPIRED"
# Invoice errors
@doc "Invoice is malformed or invalid."
def invalid_invoice, do: "INVALID_INVOICE"
@doc "An invoice with this ID already exists."
def duplicate_invoice, do: "DUPLICATE_INVOICE"
@doc "Payer and payee are the same address."
def self_payment, do: "SELF_PAYMENT"
@doc "Payment type is not valid for this invoice."
def invalid_payment_type, do: "INVALID_PAYMENT_TYPE"
# Tab errors
@doc "Tab has reached its spending limit."
def tab_depleted, do: "TAB_DEPLETED"
@doc "Tab has expired."
def tab_expired, do: "TAB_EXPIRED"
@doc "Tab not found."
def tab_not_found, do: "TAB_NOT_FOUND"
# Stream errors
@doc "Stream not found."
def stream_not_found, do: "STREAM_NOT_FOUND"
@doc "Streaming rate exceeds the maximum allowed."
def rate_exceeds_cap, do: "RATE_EXCEEDS_CAP"
# Bounty errors
@doc "Bounty has expired."
def bounty_expired, do: "BOUNTY_EXPIRED"
@doc "Bounty has already been awarded."
def bounty_claimed, do: "BOUNTY_CLAIMED"
@doc "Bounty has reached maximum submission attempts."
def bounty_max_attempts, do: "BOUNTY_MAX_ATTEMPTS"
@doc "Bounty not found."
def bounty_not_found, do: "BOUNTY_NOT_FOUND"
# Chain errors
@doc "Invoice chain does not match wallet chain."
def chain_mismatch, do: "CHAIN_MISMATCH"
@doc "Chain ID is unsupported or mismatched."
def chain_unsupported, do: "CHAIN_UNSUPPORTED"
# Rate limiting
@doc "Request rate limit exceeded."
def rate_limited, do: "RATE_LIMITED"
# Cancellation errors
@doc "Cannot cancel after claim start."
def cancel_blocked_claim_start, do: "CANCEL_BLOCKED_CLAIM_START"
@doc "Cannot cancel while evidence is pending review."
def cancel_blocked_evidence, do: "CANCEL_BLOCKED_EVIDENCE"
# Protocol errors
@doc "SDK version is not compatible with this API version."
def version_mismatch, do: "VERSION_MISMATCH"
@doc "A network error occurred connecting to the API."
def network_error, do: "NETWORK_ERROR"
# Generic errors (kept for backward compat)
@doc "Recipient address is invalid or not checksummed."
def invalid_address, do: "INVALID_ADDRESS"
@doc "Amount is below the 1 micro-USDC minimum or exceeds the maximum."
def invalid_amount, do: "INVALID_AMOUNT"
@doc "The requested resource (escrow, tab, etc.) was not found."
def not_found, do: "NOT_FOUND"
@doc "The resource is in the wrong state for this operation."
def invalid_state, do: "INVALID_STATE"
@doc "The tab or stream has expired."
def expired, do: "EXPIRED"
@doc "The escrow or bounty milestone validation failed."
def validation_failed, do: "VALIDATION_FAILED"
@doc "On-chain transaction failed or reverted."
def transaction_failed, do: "TRANSACTION_FAILED"
@doc "Spending limit set by the operator has been reached."
def spending_limit_exceeded, do: "SPENDING_LIMIT_EXCEEDED"
@doc "The operation is not permitted in the current context."
def forbidden, do: "FORBIDDEN"
@doc "Service is temporarily unavailable."
def service_unavailable, do: "SERVICE_UNAVAILABLE"
@doc "An unclassified server error occurred."
def server_error, do: "SERVER_ERROR"
@doc "Contract call failed (on-chain operation)."
def contract_error, do: "CONTRACT_ERROR"
# ─── Error code map (for from_code/2 factory) ───────────────────────────────
@error_codes %{
"INVALID_SIGNATURE" => true,
"NONCE_REUSED" => true,
"TIMESTAMP_EXPIRED" => true,
"UNAUTHORIZED" => true,
"INSUFFICIENT_BALANCE" => true,
"BELOW_MINIMUM" => true,
"ESCROW_NOT_FOUND" => true,
"ESCROW_ALREADY_FUNDED" => true,
"ESCROW_EXPIRED" => true,
"INVALID_INVOICE" => true,
"DUPLICATE_INVOICE" => true,
"SELF_PAYMENT" => true,
"INVALID_PAYMENT_TYPE" => true,
"TAB_DEPLETED" => true,
"TAB_EXPIRED" => true,
"TAB_NOT_FOUND" => true,
"STREAM_NOT_FOUND" => true,
"RATE_EXCEEDS_CAP" => true,
"BOUNTY_EXPIRED" => true,
"BOUNTY_CLAIMED" => true,
"BOUNTY_MAX_ATTEMPTS" => true,
"BOUNTY_NOT_FOUND" => true,
"CHAIN_MISMATCH" => true,
"CHAIN_UNSUPPORTED" => true,
"RATE_LIMITED" => true,
"CANCEL_BLOCKED_CLAIM_START" => true,
"CANCEL_BLOCKED_EVIDENCE" => true,
"VERSION_MISMATCH" => true
}
@doc "Check if a code string is a known canonical error code."
def known_code?(code), do: Map.has_key?(@error_codes, code)
# ─── Constructor helpers ───────────────────────────────────────────────────
@doc """
Build a `RemitMd.Error` struct.
iex> RemitMd.Error.new(RemitMd.Error.not_found(), "Escrow abc123 not found")
%RemitMd.Error{code: "NOT_FOUND", message: "Escrow abc123 not found", ...}
"""
def new(code, message, opts \\ []) do
slug = code |> String.downcase() |> String.replace("_", "-")
%__MODULE__{
code: code,
message: message,
doc_url: Keyword.get(opts, :doc_url, "#{@base_doc}##{slug}"),
context: Keyword.get(opts, :context)
}
end
@doc """
Build a `RemitMd.Error` from an API error code and message string.
"""
def from_code(code, message \\ nil) do
msg = message || "Error: #{code}"
new(code, msg)
end
@impl true
def message(%__MODULE__{code: code, message: msg, doc_url: url}) do
"[#{code}] #{msg} - #{url}"
end
end