{"version":3,"file":"koios.mjs","names":["createChain"],"sources":["../../src/providers/koios.ts"],"sourcesContent":["/**\n * Koios provider - the Cardano REST API the community runs.\n *\n * The public instance answers without a key. Balances, transaction history and detail, and the\n * native assets an address holds.\n *\n * https://api.koios.rest\n */\n\nimport type {\n  Balance,\n  ChainKey,\n  ProviderCapabilities,\n  ProviderConfig,\n  TokenBalance,\n  TokenBalanceOptions,\n  Transaction,\n  TxHistoryOptions,\n} from \"../core/types.js\";\nimport { Provider } from \"../core/provider.js\";\nimport { buildQuery, normalizeBaseUrl } from \"../core/client.js\";\nimport { NotFoundError, UnsupportedChainError } from \"../core/errors.js\";\nimport { create as createChain } from \"@agntn/chains\";\nimport { clampMaxResults, formatWei, toTimestamp } from \"../core/types.js\";\n\nconst DEFAULT_BASE = \"https://api.koios.rest/api/v1\";\n\n/** Every ADA amount Koios reports is denominated in lovelace. */\nconst ADA_DECIMALS = 6;\n\n/** Hashes per `tx_info` call. A body over 5120 bytes is refused and one hash costs 67 of them. */\nconst TX_INFO_BATCH = 70;\n\n/** Assets per `address_assets` page. */\nconst ASSET_PAGE_LIMIT = 1000;\n\n/** Pages the holdings walk visits, so one address cannot fan out unbounded requests. */\nconst ASSET_MAX_PAGES = 20;\n\n/** Characters that hide, forge a line, or reorder one. A minter picks an asset name freely. */\nconst UNPRINTABLE = /[\\p{C}\\p{Zl}\\p{Zp}]/u;\n\ninterface KoiosAddressInfo {\n  readonly address: string;\n  readonly balance: string | number;\n}\n\ninterface KoiosAddressTx {\n  readonly tx_hash: string;\n  readonly epoch_no: number;\n  readonly block_height: number;\n  readonly block_time: number;\n}\n\ninterface KoiosAsset {\n  readonly policy_id: string;\n  readonly asset_name: string | null;\n  readonly fingerprint: string;\n  readonly decimals: number | null;\n  readonly quantity: string | number;\n}\n\n/** One side of a transaction. Koios gives inputs and outputs the same shape. */\ninterface KoiosTxIo {\n  readonly value: string | number;\n  readonly payment_addr?: { readonly bech32: string };\n}\n\ninterface KoiosTxInfo {\n  readonly tx_hash: string;\n  readonly block_height: number;\n  readonly tx_timestamp: number;\n  readonly total_output: string | number;\n  readonly fee: string | number;\n  readonly inputs: readonly KoiosTxIo[];\n  readonly outputs: readonly KoiosTxIo[];\n  readonly collateral_inputs: readonly KoiosTxIo[];\n}\n\n/* ADA's supply in lovelace runs past `Number.MAX_SAFE_INTEGER`, so sums are taken in BigInt. */\nfunction lovelace(value: string | number): bigint {\n  return BigInt(String(value));\n}\n\n/* Sum the sides of a transaction that belong to one address. */\nfunction sumFor(sides: readonly KoiosTxIo[], address: string): bigint {\n  return sides.reduce(\n    (total, side) => (side.payment_addr?.bech32 === address ? total + lovelace(side.value) : total),\n    0n,\n  );\n}\n\nlet utf8: TextDecoder | undefined;\n\nfunction decoder(): TextDecoder {\n  utf8 ??= new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: true });\n  return utf8;\n}\n\n/* Read an asset name as text. A minter picks those bytes, so anything invisible stays unread. */\nfunction decodeAssetName(hex: string | null): string | undefined {\n  if (!hex || !/^(?:[0-9a-fA-F]{2})+$/.test(hex)) return undefined;\n  const bytes = Uint8Array.from(hex.match(/../g) ?? [], (byte) => Number.parseInt(byte, 16));\n  try {\n    const text = decoder().decode(bytes);\n    return UNPRINTABLE.test(text) ? undefined : text;\n  } catch {\n    return undefined;\n  }\n}\n\n/* Read one holding. The CIP-14 fingerprint names an asset whose own name is bytes, not text. */\nfunction mapTokenBalance(asset: Readonly<KoiosAsset>): TokenBalance {\n  const decimals = asset.decimals ?? 0;\n  const balance = String(asset.quantity);\n\n  return {\n    contract: `${asset.policy_id}${asset.asset_name ?? \"\"}`,\n    symbol: decodeAssetName(asset.asset_name) ?? asset.fingerprint,\n    decimals,\n    balance,\n    balanceFormatted: formatWei(balance, decimals),\n  };\n}\n\n/* The transaction as a whole: who paid in first, who was paid first, and how much left it. */\nfunction wholeTx(raw: Readonly<KoiosTxInfo>): { from: string; to: string | null; value: bigint } {\n  return {\n    from: raw.inputs[0]?.payment_addr?.bech32 ?? \"unknown\",\n    to: raw.outputs[0]?.payment_addr?.bech32 ?? null,\n    value: lovelace(raw.total_output),\n  };\n}\n\n/*\n * The same transaction seen from one address: what it sent or received, and its counterparty.\n *\n * Change returns to the sender, so what left is the shortfall minus the fee; an address that funded\n * the transaction and still came out ahead reports the gain.\n */\nfunction addressParties(\n  raw: Readonly<KoiosTxInfo>,\n  address: string,\n  isSend: boolean,\n): { readonly from: string; readonly to: string | null } {\n  if (!isSend) return { from: raw.inputs[0]?.payment_addr?.bech32 ?? \"unknown\", to: address };\n  const recipient = raw.outputs.find((output) => output.payment_addr?.bech32 !== address);\n  return { from: address, to: recipient?.payment_addr?.bech32 ?? address };\n}\n\nfunction addressView(\n  raw: Readonly<KoiosTxInfo>,\n  address: string,\n): { from: string; to: string | null; value: bigint } {\n  const paidIn = sumFor(raw.inputs, address);\n  const received = sumFor(raw.outputs, address);\n  const isSend = paidIn > 0n;\n  const net = received - paidIn;\n  const moved = net < 0n ? -net - lovelace(raw.fee) : net;\n\n  return { ...addressParties(raw, address, isSend), value: moved > 0n ? moved : 0n };\n}\n\n/*\n * Read one transaction, from the point of view of `address` when the caller has one.\n *\n * Many inputs spend into many outputs, so one from/to pair is a summary. Collateral is what marks a\n * contract call, and the phase-2 validity flag sits behind the heavier `_scripts` payload, so even\n * a script that failed and lost its collateral reads as `success`.\n */\nfunction mapTransaction(raw: Readonly<KoiosTxInfo>, address?: string): Transaction {\n  const { from, to, value } = address === undefined ? wholeTx(raw) : addressView(raw, address);\n\n  return {\n    hash: raw.tx_hash,\n    blockNumber: raw.block_height,\n    timestamp: toTimestamp(raw.tx_timestamp),\n    from,\n    to,\n    value: value.toString(),\n    valueFormatted: formatWei(value, ADA_DECIMALS),\n    fee: String(raw.fee),\n    status: \"success\",\n    isContractInteraction: raw.collateral_inputs.length > 0,\n    tokenTransfers: [],\n    raw: raw as unknown as Record<string, unknown>,\n  };\n}\n\nfunction historyQuery(\n  options: Readonly<TxHistoryOptions> | undefined,\n  limit: number,\n  page: number,\n): Readonly<Record<string, string | number | undefined>> {\n  const endBlock = options?.endBlock;\n  return {\n    limit,\n    offset: (page - 1) * limit || undefined,\n    order: options?.sort === \"asc\" ? \"block_height.asc\" : \"block_height.desc\",\n    block_height: endBlock === undefined ? undefined : `lte.${endBlock}`,\n  };\n}\n\nfunction mapHistoryRows(\n  rows: readonly KoiosAddressTx[],\n  details: readonly KoiosTxInfo[],\n  address: string,\n): Transaction[] {\n  const byHash = new Map(details.map((detail) => [detail.tx_hash, detail]));\n  return rows.flatMap((row) => {\n    const detail = byHash.get(row.tx_hash);\n    return detail ? [mapTransaction(detail, address)] : [];\n  });\n}\n\nexport class Koios extends Provider {\n  static readonly key = \"koios\";\n\n  private readonly baseUrl: string;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config);\n    this.baseUrl = normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE);\n  }\n\n  get capabilities(): ProviderCapabilities {\n    return {\n      balances: true,\n      txHistory: true,\n      txDetail: true,\n      utxos: false,\n      contractInfo: false,\n      tokenBalances: true,\n      tokenTransfers: false,\n      gasData: false,\n      blockInfo: false,\n    };\n  }\n\n  /* Post to a Koios endpoint. Addresses and hashes ride in the body, so none reach the path. */\n  private post<T>(\n    path: string,\n    body: unknown,\n    params: Readonly<Record<string, string | number | undefined>> = {},\n  ): Promise<T> {\n    return this.postJSON<T>(`${this.baseUrl}${path}${buildQuery(params)}`, body);\n  }\n\n  private assertChain(chain?: ChainKey): ChainKey {\n    const c = chain ?? \"cardano\";\n    if (c !== \"cardano\") throw new UnsupportedChainError(c, this.name);\n    return c;\n  }\n\n  /* Read full transactions in batches the body limit allows. */\n  private async txInfo(hashes: readonly string[]): Promise<KoiosTxInfo[]> {\n    const details: KoiosTxInfo[] = [];\n    for (let start = 0; start < hashes.length; start += TX_INFO_BATCH) {\n      const page = await this.post<KoiosTxInfo[]>(\"/tx_info\", {\n        _tx_hashes: hashes.slice(start, start + TX_INFO_BATCH),\n        _inputs: true,\n      });\n      details.push(...page);\n    }\n    return details;\n  }\n\n  /**\n   * Read the ADA balance. Without `select` the endpoint also ships the whole UTxO set, 222 kB of\n   * it, and an address the ledger never saw answers with an empty array just like a malformed one.\n   *\n   * @param {string} address - The `address` value.\n   * @param {ChainKey} chain - The `chain` value.\n   * @returns {Promise<Balance>} The resulting value.\n   */\n  async getBalance(address: string, chain?: ChainKey): Promise<Balance> {\n    const c = this.assertChain(chain);\n\n    const [info] = await this.post<KoiosAddressInfo[]>(\n      \"/address_info\",\n      { _addresses: [address] },\n      { select: \"address,balance\" },\n    );\n    if (!info) throw new NotFoundError(address, this.name);\n\n    const balance = String(info.balance);\n    return this.snapshotBalance({\n      address,\n      chain: c,\n      balance,\n      balanceFormatted: formatWei(balance, ADA_DECIMALS),\n      symbol: createChain(c).symbol,\n    });\n  }\n\n  /**\n   * List transactions. `tx_info` answers in its own order, so the hash list sets the order.\n   *\n   * @param {string} address - The `address` value.\n   * @param {ChainKey} chain - The `chain` value.\n   * @param {Readonly<TxHistoryOptions>} options - The `options` value.\n   * @returns {Promise<Transaction[]>} The resulting value.\n   */\n  async getTxHistory(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TxHistoryOptions>,\n  ): Promise<Transaction[]> {\n    this.assertChain(chain);\n\n    const limit = clampMaxResults(options?.limit);\n    const page = Math.max(1, Math.round(options?.page ?? 1));\n    const rows = await this.post<KoiosAddressTx[]>(\n      \"/address_txs\",\n      { _addresses: [address], _after_block_height: options?.startBlock },\n      historyQuery(options, limit, page),\n    );\n    if (rows.length === 0) return [];\n\n    const details = await this.txInfo(rows.map((row) => row.tx_hash));\n    return mapHistoryRows(rows, details, address);\n  }\n\n  override async getTxDetail(hash: string, chain?: ChainKey): Promise<Transaction> {\n    this.assertChain(chain);\n\n    const [detail] = await this.txInfo([hash]);\n    if (!detail) throw new NotFoundError(hash, this.name);\n    return mapTransaction(detail);\n  }\n\n  /**\n   * List the native assets an address holds. A single NFT project can fill several pages.\n   *\n   * @param {string} address - The `address` value.\n   * @param {ChainKey} chain - The `chain` value.\n   * @param {Readonly<TokenBalanceOptions>} options - The `options` value.\n   * @returns {Promise<TokenBalance[]>} The resulting value.\n   */\n  override async getTokenBalances(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TokenBalanceOptions>,\n  ): Promise<TokenBalance[]> {\n    this.assertChain(chain);\n\n    const tokens: TokenBalance[] = [];\n    for (let page = 0; page < ASSET_MAX_PAGES; page++) {\n      const assets = await this.post<KoiosAsset[]>(\n        \"/address_assets\",\n        { _addresses: [address] },\n        { limit: ASSET_PAGE_LIMIT, offset: page * ASSET_PAGE_LIMIT || undefined },\n      );\n      tokens.push(...assets.map(mapTokenBalance));\n      if (assets.length < ASSET_PAGE_LIMIT) break;\n    }\n\n    return options?.nonZeroOnly ? tokens.filter((token) => token.balance !== \"0\") : tokens;\n  }\n}\n"],"mappings":"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"}