{"version":3,"file":"blockstream.mjs","names":["createChain"],"sources":["../../src/providers/blockstream.ts"],"sourcesContent":["/**\n * Blockstream provider for Bitcoin.\n *\n * Public API, no key needed. The service exposes balances, transactions and blocks through the\n * Esplora wire format at blockstream.info.\n *\n * https://github.com/Blockstream/esplora/blob/master/API.md\n */\n\nimport { create as createChain } from \"@agntn/chains\";\nimport { NotFoundError, UnsupportedChainError } from \"../core/errors.js\";\nimport { assertSafePathSegment } from \"../core/path-safety.js\";\nimport { Provider } from \"../core/provider.js\";\nimport { normalizeBaseUrl } from \"../core/client.js\";\nimport { formatWei } from \"../core/types.js\";\nimport {\n  getEsploraAddressHistory,\n  getEsploraUtxos,\n  selectEsploraRecipientOutput,\n} from \"../core/esplora.js\";\nimport type { EsploraUnspentOutput } from \"../core/esplora.js\";\nimport type {\n  Balance,\n  BlockInfo,\n  ChainKey,\n  ProviderCapabilities,\n  ProviderConfig,\n  TokenTransfer,\n  Transaction,\n  TxHistoryOptions,\n  TxStatus,\n  Utxo,\n} from \"../core/types.js\";\n\nconst DEFAULT_BASE = \"https://blockstream.info\";\n\ninterface EsploraAddressSummary {\n  readonly mempool_stats?: {\n    readonly funded_txo_sum: number | string;\n    readonly spent_txo_sum: number | string;\n  };\n  readonly chain_stats: {\n    readonly funded_txo_sum: number | string;\n    readonly spent_txo_sum: number | string;\n  };\n}\n\ninterface EsploraAddressTx {\n  readonly txid: string;\n  readonly vin: ReadonlyArray<{\n    readonly prevout: {\n      readonly scriptpubkey_address?: string;\n      readonly value: number;\n    } | null;\n  }>;\n  readonly vout: ReadonlyArray<{\n    readonly scriptpubkey_address?: string;\n    readonly scriptpubkey_type: string;\n    readonly value: number;\n  }>;\n  readonly fee: number;\n  readonly status: {\n    readonly confirmed: boolean;\n    readonly block_height?: number;\n    readonly block_time?: number;\n  };\n}\n\ninterface EsploraBlock {\n  readonly id: string;\n  readonly height: number;\n  readonly timestamp: number;\n  readonly tx_count: number;\n  readonly size: number;\n  readonly weight: number;\n  readonly previousblockhash: string;\n}\n\n/* Convert satoshis to BTC without crossing the floating-point boundary. */\nfunction satToBitcoin(satoshis: number | string | bigint): string {\n  return formatWei(String(satoshis), 8);\n}\n\nfunction transactionTimestamp(status: Readonly<EsploraAddressTx[\"status\"]>): string | undefined {\n  return status.block_time ? new Date(status.block_time * 1000).toISOString() : undefined;\n}\n\nfunction addressTotals(\n  raw: Readonly<EsploraAddressTx>,\n  address: string,\n): { in: number; out: number } {\n  const totalIn = raw.vin\n    .filter((input) => input.prevout?.scriptpubkey_address === address)\n    .reduce((sum, input) => sum + (input.prevout?.value ?? 0), 0);\n  const totalOut = raw.vout\n    .filter((output) => output.scriptpubkey_address === address)\n    .reduce((sum, output) => sum + output.value, 0);\n  return { in: totalIn, out: totalOut };\n}\n\nfunction sendingAddressParties(\n  raw: Readonly<EsploraAddressTx>,\n  address: string,\n): { readonly from: string; readonly to: string } {\n  const recipient = selectEsploraRecipientOutput(raw.vout, address);\n  const sender = raw.vin.find((input) => input.prevout?.scriptpubkey_address === address);\n  return {\n    from: sender?.prevout?.scriptpubkey_address ?? address,\n    to: recipient.address ?? address,\n  };\n}\n\nfunction addressParties(\n  raw: Readonly<EsploraAddressTx>,\n  address: string,\n  isSend: boolean,\n): { readonly from: string; readonly to: string } {\n  if (isSend) return sendingAddressParties(raw, address);\n  return { from: raw.vin[0]?.prevout?.scriptpubkey_address ?? \"unknown\", to: address };\n}\n\nfunction mapAddressTx(raw: Readonly<EsploraAddressTx>, address: string): Transaction {\n  const totals = addressTotals(raw, address);\n  const netSatoshis = totals.out - totals.in;\n  const isSend = totals.in > 0;\n  const transferredSatoshis = isSend ? Math.max(0, Math.abs(netSatoshis) - raw.fee) : netSatoshis;\n  const { from, to } = addressParties(raw, address, isSend);\n\n  return {\n    hash: raw.txid,\n    blockNumber: raw.status.block_height ?? 0,\n    timestamp: transactionTimestamp(raw.status),\n    from,\n    to,\n    value: transferredSatoshis.toString(),\n    valueFormatted: satToBitcoin(transferredSatoshis),\n    fee: raw.fee.toString(),\n    status: (raw.status.confirmed ? \"success\" : \"pending\") as TxStatus,\n    isContractInteraction: false,\n    tokenTransfers: [] as TokenTransfer[],\n    raw: raw as unknown as Record<string, unknown>,\n  };\n}\n\nexport class Blockstream extends Provider {\n  static readonly key = \"blockstream\";\n\n  private readonly baseUrl: string;\n  private readonly defaultChain: ChainKey;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config);\n    this.baseUrl = normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE);\n    this.defaultChain = config.defaultChain ?? \"bitcoin\";\n  }\n\n  get capabilities(): ProviderCapabilities {\n    return {\n      balances: true,\n      txHistory: true,\n      txDetail: true,\n      utxos: true,\n      contractInfo: false,\n      tokenBalances: false,\n      tokenTransfers: false,\n      gasData: false,\n      blockInfo: true,\n    };\n  }\n\n  private api<T>(chain: ChainKey, path: string): Promise<T> {\n    if (chain !== \"bitcoin\") throw new UnsupportedChainError(chain, \"blockstream\");\n    return this.getJSON<T>(`${this.baseUrl}${path}`);\n  }\n\n  async getBalance(address: string, chain?: ChainKey): Promise<Balance> {\n    const selectedChain = chain ?? this.defaultChain;\n    assertSafePathSegment(address, \"address\");\n    const data = await this.api<EsploraAddressSummary>(\n      selectedChain,\n      `/api/address/${encodeURIComponent(address)}`,\n    );\n    const fundedSatoshis = BigInt(data.chain_stats.funded_txo_sum);\n    const spentSatoshis = BigInt(data.chain_stats.spent_txo_sum);\n    const balanceSatoshis = fundedSatoshis - spentSatoshis;\n\n    const unconfirmed = data.mempool_stats\n      ? BigInt(data.mempool_stats.funded_txo_sum) - BigInt(data.mempool_stats.spent_txo_sum)\n      : undefined;\n\n    return this.snapshotBalance({\n      ...(unconfirmed === undefined ? {} : { unconfirmed: unconfirmed.toString() }),\n      address,\n      chain: selectedChain,\n      balance: balanceSatoshis.toString(),\n      balanceFormatted: satToBitcoin(balanceSatoshis),\n      funded: fundedSatoshis.toString(),\n      spent: spentSatoshis.toString(),\n      symbol: createChain(selectedChain).symbol,\n    });\n  }\n\n  async getTxHistory(\n    address: string,\n    chain?: ChainKey,\n    options?: Readonly<TxHistoryOptions>,\n  ): Promise<Transaction[]> {\n    const selectedChain = chain ?? this.defaultChain;\n    const transactions = await getEsploraAddressHistory(address, options?.limit, async (path) =>\n      this.api<EsploraAddressTx[]>(selectedChain, path),\n    );\n\n    return transactions.map((transaction) => mapAddressTx(transaction, address));\n  }\n\n  override async getUtxos(address: string, chain?: ChainKey): Promise<Utxo[]> {\n    const selectedChain = chain ?? this.defaultChain;\n    return getEsploraUtxos(address, async (path) =>\n      this.api<EsploraUnspentOutput[]>(selectedChain, path),\n    );\n  }\n\n  override async getTxDetail(hash: string, chain?: ChainKey): Promise<Transaction> {\n    const selectedChain = chain ?? this.defaultChain;\n    assertSafePathSegment(hash, \"tx hash\");\n    const transaction = await this.api<EsploraAddressTx>(\n      selectedChain,\n      `/api/tx/${encodeURIComponent(hash)}`,\n    );\n    const output = selectEsploraRecipientOutput(transaction.vout);\n\n    return {\n      hash: transaction.txid,\n      blockNumber: transaction.status.block_height ?? 0,\n      timestamp: transactionTimestamp(transaction.status),\n      from: transaction.vin[0]?.prevout?.scriptpubkey_address ?? \"unknown\",\n      to: output.address,\n      value: output.value.toString(),\n      valueFormatted: satToBitcoin(output.value),\n      fee: transaction.fee.toString(),\n      status: (transaction.status.confirmed ? \"success\" : \"pending\") as TxStatus,\n      isContractInteraction: false,\n      tokenTransfers: [],\n      raw: transaction as unknown as Record<string, unknown>,\n    };\n  }\n\n  override async getBlockInfo(blockNumber: number, chain?: ChainKey): Promise<BlockInfo> {\n    const selectedChain = chain ?? this.defaultChain;\n    assertSafePathSegment(String(blockNumber), \"block number\");\n    const blocks = await this.api<EsploraBlock[]>(\n      selectedChain,\n      `/api/blocks/${encodeURIComponent(String(blockNumber))}`,\n    );\n    const block = blocks.find((candidate) => candidate.height === blockNumber);\n    if (!block) throw new NotFoundError(`Block ${blockNumber}`, \"blockstream\");\n\n    return {\n      number: block.height,\n      hash: block.id,\n      parentHash: block.previousblockhash,\n      timestamp: new Date(block.timestamp * 1000).toISOString(),\n      miner: \"\",\n      gasUsed: block.size.toString(),\n      gasLimit: block.weight.toString(),\n      txCount: block.tx_count,\n    };\n  }\n}\n"],"mappings":"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"}