Skip to content
markpaper

src/signer/errors.ts

v0.2.1 · 3.7 KB

Download file
// Classification of exchange / sidecar errors (knowledge base: orders.md §9-10, rate-limits.md §1).
//
// Codes observed live (2026-08-20..22):
//   23000  "Too Many Requests!: L1Address ratelimit reached ... 40 requests per 60 second is allowed"
//          - the write window; should not happen with a working limiter (it means something is not
//          counted: leverage? cancels? a second process on the address?). The Python SDK raises it as
//          an exception, so the sidecar returns it as HTTP 502 with the text: match the CODE IN THE TEXT.
//   21706  base_amount below min_base_amount (skip opens/partials before sending; bump a full close);
//   21734  "too far from the mark" - structural, remember the order key for minutes.
// Every other code is unobserved; texts arrive as the SDK's `err` string.

export type SignerErrorKind =
  | 'rate_limited'
  | 'below_min_base_amount'
  | 'far_from_mark'
  | 'timeout'
  | 'unauthorized'
  | 'bad_request'
  | 'unknown';

export interface SignerErrorInfo {
  kind: SignerErrorKind;
  /** Exchange error code found in the text, when any. */
  code: number | undefined;
  /** Parsed from a 23000 text: the limit the exchange states (read it from the answer, not from a constant). */
  rateLimit: { requests: number; windowSec: number } | undefined;
  /** Trimmed original text. */
  message: string;
}

export const ERROR_CODE_RATE_LIMITED = 23000;
export const ERROR_CODE_BELOW_MIN_BASE_AMOUNT = 21706;
export const ERROR_CODE_FAR_FROM_MARK = 21734;

const CODE_JSON_RE = /"code"\s*:\s*(\d{4,6})\b/;
const CODE_BARE_RE = /\b(?:code|error)\s*[=:]?\s*(\d{4,6})\b/i;
const KNOWN_CODE_RE = /\b(23000|21706|21734)\b/;
const RATE_TEXT_RE = /(\d+)\s+requests?\s+per\s+(\d+)\s+second/i;

/** Extracts a numeric error code from an error text (`{"code":23000,...}`, `code=21706`, or a bare known code). */
export function extractErrorCode(text: string): number | undefined {
  const m = CODE_JSON_RE.exec(text) ?? CODE_BARE_RE.exec(text) ?? KNOWN_CODE_RE.exec(text);
  return m ? Number(m[1]) : undefined;
}

/**
 * Classifies an error text (and optionally the sidecar's HTTP status) into a {@link SignerErrorKind}.
 * Matching is by code in the text, then by wording; the HTTP status only distinguishes the sidecar's
 * own 400 / 401 / 504 answers.
 */
export function interpretSignerError(text: string | undefined | null, httpStatus?: number): SignerErrorInfo {
  const message = (text ?? '').trim();
  const code = extractErrorCode(message);
  const rate = RATE_TEXT_RE.exec(message);
  const rateLimit = rate ? { requests: Number(rate[1]), windowSec: Number(rate[2]) } : undefined;

  let kind: SignerErrorKind = 'unknown';
  if (code === ERROR_CODE_RATE_LIMITED || /ratelimit reached|too many requests/i.test(message) || rateLimit)
    kind = 'rate_limited';
  else if (code === ERROR_CODE_BELOW_MIN_BASE_AMOUNT) kind = 'below_min_base_amount';
  else if (code === ERROR_CODE_FAR_FROM_MARK || /too far from the mark/i.test(message)) kind = 'far_from_mark';
  else if (httpStatus === 504 || /^timeout$/i.test(message)) kind = 'timeout';
  else if (httpStatus === 401 || /^unauthorized$/i.test(message)) kind = 'unauthorized';
  else if (httpStatus === 400 || /^bad request/i.test(message)) kind = 'bad_request';

  return { kind, code, rateLimit, message };
}

/** True when the order key should be remembered as "far from mark" (code 21734). */
export function isFarFromMarkError(text: string | undefined | null): boolean {
  return interpretSignerError(text).kind === 'far_from_mark';
}

/** True when the exchange reported the write window (code 23000): the local limiter under-counts something. */
export function isExchangeRateLimitError(text: string | undefined | null): boolean {
  return interpretSignerError(text).kind === 'rate_limited';
}
All files