{"version":3,"file":"provider.mjs","names":[],"sources":["../../src/core/client.ts","../../src/core/provider.ts"],"sourcesContent":["/** HTTP client wrapper for Explorers providers */\n\nimport { ofetch } from \"ofetch\";\nimport { normalizeError } from \"./errors.js\";\nimport { version } from \"../version.js\";\n\nlet userAgent: string | undefined;\n\n/* Built on the first request so that loading the client evaluates nothing. */\nfunction agent(): string {\n  userAgent ??= `explorers/${version}`;\n  return userAgent;\n}\n\n/** Request metadata shared by the HTTP helpers. */\nexport interface ClientOptions {\n  timeout?: number;\n  headers?: Record<string, string>;\n  signal?: AbortSignal;\n  provider?: string;\n}\n\n/** Immutable view consumed by one HTTP request without freezing the public options DTO. */\nexport interface ClientRequestOptions {\n  readonly timeout?: number;\n  readonly headers?: Readonly<Record<string, string>>;\n  readonly signal?: AbortSignal;\n  readonly provider?: string;\n}\n\n/**\n * Remove trailing separators before provider paths are appended.\n *\n * @param {string} url - The `url` value.\n * @returns {string} The resulting value.\n */\nexport function normalizeBaseUrl(url: string): string {\n  return url.replace(/\\/+$/, \"\");\n}\n\nfunction parseJSON<T>(text: string | undefined): T {\n  if (text === undefined) return undefined as T;\n\n  type Reviver = (key: string, value: unknown, context: Readonly<{ source: string }>) => unknown;\n  const parseWithSource = JSON.parse as unknown as (value: string, reviver: Reviver) => unknown;\n\n  return parseWithSource(text, (_key, value, context) => {\n    if (\n      typeof value === \"number\" &&\n      !Number.isSafeInteger(value) &&\n      /^-?\\d+$/.test(context.source)\n    ) {\n      return context.source;\n    }\n    return value;\n  }) as T;\n}\n\n/**\n * Fetch JSON with Explorers headers and a 15-second default timeout.\n *\n * Transport failures are normalized before they leave this boundary.\n *\n * @param {string} url - The `url` value.\n * @param {ClientRequestOptions} options - Request metadata and cancellation.\n * @returns {Promise<T>} The resulting value.\n */\nexport async function getJSON<T>(url: string, options?: ClientRequestOptions): Promise<T> {\n  try {\n    const response = await ofetch.raw<string, \"text\">(url, {\n      method: \"GET\",\n      headers: {\n        Accept: \"application/json\",\n        \"User-Agent\": agent(),\n        ...options?.headers,\n      },\n      timeout: options?.timeout ?? 15_000,\n      signal: options?.signal,\n      retry: false,\n      responseType: \"text\",\n    });\n    return parseJSON<T>(response._data);\n  } catch (error) {\n    throw normalizeError(error, options?.provider, url);\n  }\n}\n\nexport async function postJSON<T>(\n  url: string,\n  body: unknown,\n  options?: ClientRequestOptions,\n): Promise<T> {\n  try {\n    const response = await ofetch.raw<string, \"text\">(url, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n        \"User-Agent\": agent(),\n        ...options?.headers,\n      },\n      body: JSON.stringify(body),\n      timeout: options?.timeout ?? 15_000,\n      signal: options?.signal,\n      retry: false,\n      responseType: \"text\",\n    });\n    return parseJSON<T>(response._data);\n  } catch (error) {\n    throw normalizeError(error, options?.provider, url);\n  }\n}\n\n/**\n * Build a query string while dropping parameters whose value is `undefined`.\n *\n * @example\n *   ```ts\n *   buildQuery({ page: 2, cursor: undefined }); // '?page=2'\n *   ```\n *\n * @param {Readonly<Record<string, string | number | undefined>>} params - The `params` value.\n * @returns {string} The resulting value.\n */\nexport function buildQuery(params: Readonly<Record<string, string | number | undefined>>): string {\n  const usp = new URLSearchParams();\n  for (const [key, value] of Object.entries(params)) {\n    if (value !== undefined) usp.set(key, String(value));\n  }\n  const s = usp.toString();\n  return s ? `?${s}` : \"\";\n}\n","import type {\n  Balance,\n  BlockInfo,\n  ChainKey,\n  ContractInfo,\n  GasData,\n  ProviderCapabilities,\n  TokenBalance,\n  TokenBalanceOptions,\n  TokenTransfer,\n  TokenTransferOptions,\n  Transaction,\n  TxHistoryOptions,\n  Utxo,\n} from \"./types.js\";\nimport { getJSON, postJSON } from \"./client.js\";\nimport type { ClientRequestOptions } from \"./client.js\";\nimport type { ProviderConfig } from \"./types.js\";\nimport { RateLimitError } from \"./errors.js\";\n\nconst RATE_LIMIT_RETRIES = 2;\nconst RATE_LIMIT_BASE_DELAY_MS = 1000;\nconst RATE_LIMIT_MAX_DELAY_MS = 30_000;\n\nfunction rateLimitMessage(value: unknown): string | undefined {\n  if (typeof value === \"string\") return value;\n  if (value !== null && typeof value === \"object\" && \"message\" in value) {\n    const message = value.message;\n    if (typeof message === \"string\") return message;\n  }\n  return undefined;\n}\n\nfunction throwIfRateLimited(data: unknown, provider: string): void {\n  if (data === null || typeof data !== \"object\") return;\n  const record = data as Record<string, unknown>;\n  for (const field of [\"message\", \"result\", \"error\"] as const) {\n    const message = rateLimitMessage(record[field]);\n    if (message !== undefined && /rate limit/i.test(message)) {\n      throw new RateLimitError(provider);\n    }\n  }\n}\n\nfunction rateLimitDelayMs(retryAfter: number | undefined, attempt: number): number {\n  const fromHeader = retryAfter === undefined ? undefined : retryAfter * 1000;\n  const delay = fromHeader ?? RATE_LIMIT_BASE_DELAY_MS * 2 ** attempt;\n  return Math.min(delay, RATE_LIMIT_MAX_DELAY_MS);\n}\n\nfunction abortReason(signal?: AbortSignal): Error {\n  if (signal?.reason instanceof Error) return signal.reason;\n  return new DOMException(\"This operation was aborted\", \"AbortError\");\n}\n\nasync function abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n  if (signal?.aborted) throw abortReason(signal);\n  if (ms <= 0) return;\n  await new Promise<void>((resolve, reject) => {\n    const timer = setTimeout(() => {\n      signal?.removeEventListener(\"abort\", onAbort);\n      resolve();\n    }, ms);\n    const onAbort = () => {\n      clearTimeout(timer);\n      reject(abortReason(signal));\n    };\n    signal?.addEventListener(\"abort\", onAbort, { once: true });\n  });\n}\n\nasync function withRateLimitRetry<T>(\n  operation: () => Promise<T>,\n  signal?: AbortSignal,\n): Promise<T> {\n  for (let attempt = 0; ; attempt += 1) {\n    try {\n      return await operation();\n    } catch (error) {\n      if (!(error instanceof RateLimitError) || attempt >= RATE_LIMIT_RETRIES) throw error;\n      await abortableDelay(rateLimitDelayMs(error.retryAfter, attempt), signal);\n    }\n  }\n}\n\n/**\n * Common API for block explorer backends.\n *\n * A provider holds backend configuration, not an address. Pass addresses to the relevant methods\n * and check `capabilities` before using optional operations.\n */\n// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- Optional methods stay absent at runtime.\nexport abstract class Provider {\n  private readonly timeout: number | undefined;\n\n  constructor(config: Readonly<ProviderConfig> = {}) {\n    this.timeout = config.timeout;\n  }\n\n  /**\n   * Registry key owned by the concrete class.\n   *\n   * @returns {string} The resulting value.\n   */\n  get name(): string {\n    return (this.constructor as ProviderConstructor).key;\n  }\n\n  /** Operations this provider can actually serve. */\n  abstract get capabilities(): ProviderCapabilities;\n\n  /** Fetch the native-token balance for an address. */\n  abstract getBalance(address: string, chain?: ChainKey): Promise<Balance>;\n\n  /** List transactions involving an address. */\n  abstract getTxHistory(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TxHistoryOptions>,\n  ): Promise<Transaction[]>;\n\n  /**\n   * Execute a provider-attributed GET request using the configured or per-request timeout.\n   *\n   * Retries HTTP 429 and JSON bodies that mention a rate limit, with backoff, before the error\n   * leaves. The HTTP client itself does not retry.\n   *\n   * @param {string} url - Request URL.\n   * @param {Omit<ClientRequestOptions, \"provider\">} options - Per-request headers, cancellation, and timeout override.\n   * @returns {Promise<T>} Parsed JSON body.\n   */\n  protected getJSON<T>(url: string, options?: Omit<ClientRequestOptions, \"provider\">): Promise<T> {\n    return withRateLimitRetry(async () => {\n      const data = await getJSON<T>(url, {\n        ...options,\n        timeout: options?.timeout ?? this.timeout,\n        provider: this.name,\n      });\n      throwIfRateLimited(data, this.name);\n      return data;\n    }, options?.signal);\n  }\n\n  /**\n   * Execute a provider-attributed JSON POST request using the configured timeout.\n   *\n   * Retries HTTP 429 and JSON bodies that mention a rate limit, with backoff, before the error\n   * leaves. Explorer POSTs are reads, so retrying them is safe.\n   *\n   * @param {string} url - Request URL.\n   * @param {unknown} body - JSON request body.\n   * @param {Omit<ClientRequestOptions, \"provider\">} options - Per-request headers, cancellation, and timeout override.\n   * @returns {Promise<T>} Parsed JSON body.\n   */\n  protected postJSON<T>(\n    url: string,\n    body: unknown,\n    options?: Omit<ClientRequestOptions, \"provider\">,\n  ): Promise<T> {\n    return withRateLimitRetry(async () => {\n      const data = await postJSON<T>(url, body, {\n        ...options,\n        timeout: options?.timeout ?? this.timeout,\n        provider: this.name,\n      });\n      throwIfRateLimited(data, this.name);\n      return data;\n    }, options?.signal);\n  }\n\n  /**\n   * Date a completed balance read and preserve any chain position the response exposes.\n   *\n   * @param {Omit<Balance, \"fetchedAt\" | \"blockNumber\" | \"blockHash\">} balance - The `balance` value.\n   * @param {Readonly<{ blockNumber?: number | null; blockHash?: string | null }>} position - The `position` value.\n   * @returns {Balance} The resulting value.\n   */\n  protected snapshotBalance(\n    balance: Omit<Balance, \"fetchedAt\" | \"blockNumber\" | \"blockHash\">,\n    position: Readonly<{ blockNumber?: number | null; blockHash?: string | null }> = {},\n  ): Balance {\n    return {\n      ...balance,\n      fetchedAt: new Date().toISOString(),\n      blockNumber: position.blockNumber ?? null,\n      blockHash: position.blockHash ?? null,\n    };\n  }\n}\n\n/** Concrete provider class accepted by the registry. */\nexport interface ProviderConstructor {\n  /** Stable registry key owned by the concrete class. */\n  readonly key: string;\n  new (config: Readonly<ProviderConfig>): Provider;\n}\n\n/** One operation that provider selection can require. */\nexport type ProviderCapability = keyof ProviderCapabilities;\n\n/** What the registry answers about a provider without loading its module. */\nexport interface ProviderMeta {\n  /** Chains the provider can serve, consulted during auto-selection. */\n  chains: readonly ChainKey[];\n  /** Operations the provider can serve. Omit to keep external registrations backward-compatible. */\n  capabilities?: readonly ProviderCapability[];\n  /** Public endpoint advertised for the provider. */\n  defaultURL?: string;\n}\n\n/**\n * One provider in the built-in list.\n *\n * The metadata is repeated here instead of read off the class so that listing providers, matching a\n * chain or reporting an endpoint never loads provider code. `load` pulls the class in when someone\n * actually asks for an instance.\n */\nexport interface ProviderEntry extends ProviderMeta {\n  key: string;\n  load: () => Promise<ProviderConstructor>;\n}\n\n/**\n * Operations exposed only by providers that support them.\n *\n * Unsupported methods stay absent at runtime. Check `capabilities` before calling.\n */\nexport interface Provider {\n  /** Fetch one transaction by its hash. */\n  getTxDetail?(hash: string, chain?: ChainKey): Promise<Transaction>;\n\n  /** List the unspent outputs an address still controls, on chains that track them. */\n  getUtxos?(address: string, chain?: ChainKey): Promise<Utxo[]>;\n\n  /** Fetch available metadata, ABI, and source for a contract address. */\n  getContractInfo?(address: string, chain?: ChainKey): Promise<ContractInfo>;\n\n  /** List token holdings for an address. */\n  getTokenBalances?(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TokenBalanceOptions>,\n  ): Promise<TokenBalance[]>;\n\n  /** List fungible-token transfers involving an address. */\n  getTokenTransfers?(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TokenTransferOptions>,\n  ): Promise<TokenTransfer[]>;\n\n  /** Fetch the provider's current gas-price suggestions. */\n  getGasData?(chain?: ChainKey): Promise<GasData>;\n\n  /** Fetch a block by number. */\n  getBlockInfo?(blockNumber: number, chain?: ChainKey): Promise<BlockInfo>;\n}\n"],"mappings":"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"}