{"version":3,"file":"esplora.mjs","names":[],"sources":["../../src/core/esplora.ts"],"sourcesContent":["import { assertSafePathSegment } from \"./path-safety.js\";\nimport { clampMaxResults, formatWei, toTimestamp } from \"./types.js\";\nimport type { Utxo } from \"./types.js\";\n\nconst CHAIN_PAGE_SIZE = 25;\n\n/** Every Esplora backend served here counts in satoshi-sized units. */\nconst ESPLORA_DECIMALS = 8;\n\ninterface EsploraAddressTransaction {\n  txid: string;\n  status: { confirmed: boolean };\n}\n\ninterface EsploraOutput {\n  readonly scriptpubkey_address?: string;\n  readonly scriptpubkey_type: string;\n  readonly value: number | string;\n}\n\nexport interface EsploraUnspentOutput {\n  readonly txid: string;\n  readonly vout: number;\n  readonly value: number | string;\n  readonly status: {\n    readonly confirmed: boolean;\n    readonly block_height?: number;\n    readonly block_hash?: string;\n    readonly block_time?: number;\n  };\n}\n\nfunction mapEsploraUtxo(raw: Readonly<EsploraUnspentOutput>): Utxo {\n  const value = String(raw.value);\n  return {\n    txid: raw.txid,\n    vout: raw.vout,\n    value,\n    valueFormatted: formatWei(value, ESPLORA_DECIMALS),\n    confirmed: raw.status.confirmed,\n    blockNumber: raw.status.block_height ?? null,\n    blockHash: raw.status.block_hash ?? null,\n    timestamp: raw.status.block_time ? toTimestamp(raw.status.block_time) : undefined,\n  };\n}\n\n/**\n * Fetch the unspent outputs of an Esplora address, in the order the backend lists them.\n *\n * @param {string} address - The `address` value.\n * @param {(path: string) => Promise<readonly EsploraUnspentOutput[]>} fetchUtxos - Read one\n *   provider path.\n * @returns {Promise<Utxo[]>} The resulting value.\n */\nexport async function getEsploraUtxos(\n  address: string,\n  fetchUtxos: (path: string) => Promise<readonly EsploraUnspentOutput[]>,\n): Promise<Utxo[]> {\n  assertSafePathSegment(address, \"address\");\n  const outputs = await fetchUtxos(`/api/address/${encodeURIComponent(address)}/utxo`);\n  return outputs.map(mapEsploraUtxo);\n}\n\nfunction confirmedHistoryPath(encodedAddress: string, encodedCursor: string): string {\n  return `/api/address/${encodedAddress}/txs/chain/${encodedCursor}`;\n}\n\n/**\n * Choose the first non-OP_RETURN output, optionally excluding an address from recipient selection.\n *\n * @param {readonly EsploraOutput[]} outputs - Transaction outputs in provider order.\n * @param {string} [excludedAddress] - Address whose own outputs should not win when another\n *   non-OP_RETURN output exists.\n * @returns {Readonly<{ address: string | null; value: number | string }>} The selected output pair.\n */\nexport function selectEsploraRecipientOutput(\n  outputs: readonly EsploraOutput[],\n  excludedAddress?: string,\n): Readonly<{ address: string | null; value: number | string }> {\n  const firstTransfer = outputs.find((candidate) => candidate.scriptpubkey_type !== \"op_return\");\n  const output =\n    (excludedAddress === undefined\n      ? firstTransfer\n      : outputs.find(\n          (candidate) =>\n            candidate.scriptpubkey_type !== \"op_return\" &&\n            candidate.scriptpubkey_address !== excludedAddress,\n        )) ??\n    firstTransfer ??\n    outputs[0];\n\n  return {\n    address: output?.scriptpubkey_address ?? null,\n    value: output?.value ?? 0,\n  };\n}\n\n/**\n * Fetch an Esplora address feed across its initial page and confirmed-chain cursor pages.\n *\n * @param {string} address - The `address` value.\n * @param {number | undefined} requestedLimit - The `requestedLimit` value.\n * @param {(path: string) => Promise<T[]>} fetchPage - The `fetchPage` value.\n * @param {(encodedAddress: string, encodedCursor: string) => string} nextPagePath - Build the\n *   provider-specific confirmed-history cursor path.\n * @returns {Promise<T[]>} The resulting value.\n */\nexport async function getEsploraAddressHistory<T extends EsploraAddressTransaction>(\n  address: string,\n  requestedLimit: number | undefined,\n  fetchPage: (path: string) => Promise<T[]>,\n  nextPagePath: (encodedAddress: string, encodedCursor: string) => string = confirmedHistoryPath,\n): Promise<T[]> {\n  const limit = clampMaxResults(requestedLimit);\n  assertSafePathSegment(address, \"address\");\n  const encodedAddress = encodeURIComponent(address);\n  const firstPage = await fetchPage(`/api/address/${encodedAddress}/txs`);\n  const transactions = firstPage.slice(0, limit);\n  let cursor = firstPage.findLast((transaction) => transaction.status.confirmed)?.txid;\n\n  while (transactions.length < limit && cursor !== undefined) {\n    const page = await fetchPage(nextPagePath(encodedAddress, encodeURIComponent(cursor)));\n    const nextCursor = page.at(-1)?.txid;\n    if (nextCursor === undefined || nextCursor === cursor) break;\n\n    transactions.push(...page.slice(0, limit - transactions.length));\n    if (page.length < CHAIN_PAGE_SIZE) break;\n    cursor = nextCursor;\n  }\n\n  return transactions;\n}\n"],"mappings":";;AAIA,MAAM,kBAAkB;AAGxB,MAAM,mBAAmB;AAyBzB,SAAS,eAAe,KAA2C;CACjE,MAAM,QAAQ,OAAO,IAAI,KAAK;CAC9B,OAAO;EACL,MAAM,IAAI;EACV,MAAM,IAAI;EACV;EACA,gBAAgB,UAAU,OAAO,gBAAgB;EACjD,WAAW,IAAI,OAAO;EACtB,aAAa,IAAI,OAAO,gBAAgB;EACxC,WAAW,IAAI,OAAO,cAAc;EACpC,WAAW,IAAI,OAAO,aAAa,YAAY,IAAI,OAAO,UAAU,IAAI,KAAA;CAC1E;AACF;;;;;;;;AAcE,SAAA,6BAA+B,SAAS,iBAAA;CAExC,MAAA,gBADsB,QAAW,MAAA,cAAgB,UAAmB,sBACjD,WAAA;CACrB,MAAA,UAAA,oBAAA,KAAA,IAAA,gBAAA,QAAA,MAAA,cAAA,UAAA,sBAAA,eAAA,UAAA,yBAAA,eAAA,MAAA,iBAAA,QAAA;CAEA,OAAA;EACE,SAAO,QAAA,wBAA+B;EACxC,OAAA,QAAA,SAAA;;;;;;;;CAUA,MAAA,eAAgB,UAAA,MACd,GAAA,KAAA;CAGA,IAAA,SAAM,UAAgB,UAAQ,gBAAM,YAAwB,OAAA,SAAA,CAAA,EAAA;CAC5D,OAAM,aACH,SAAA,SAAoB,WACjB,KAAA,GAAA;EASN,MAAO,OAAA,MAAA,UAAA,aAAA,gBAAA,mBAAA,MAAA,CAAA,CAAA;EACL,MAAA,aAAiB,KAAA,GAAA,EAAA,CAAA,EAAA;EACjB,IAAA,eAAe,KAAS,KAAA,eAAA,QAAA;EAC1B,aAAA,KAAA,GAAA,KAAA,MAAA,GAAA,QAAA,aAAA,MAAA,CAAA;EACF,IAAA,KAAA,SAAA,iBAAA"}