UNPKG

@ckb-ccc/core

Version:

Core of CCC - CKBer's Codebase

325 lines (324 loc) 11.6 kB
import { Cell, CellDep, OutPoint, Transaction, } from "../ckb/index.js"; import { Zero } from "../fixedPoint/index.js"; import { hexFrom } from "../hex/index.js"; import { numFrom, numMax, numMin } from "../num/index.js"; import { reduceAsync, sleep } from "../utils/index.js"; import { ClientCacheMemory } from "./cache/memory.js"; import { CONFIRMED_BLOCK_TIME, DEFAULT_MAX_FEE_RATE, DEFAULT_MIN_FEE_RATE, } from "./clientTypes.advanced.js"; import { CellDepInfo, ClientIndexerSearchKey, ErrorClientMaxFeeRateExceeded, ErrorClientWaitTransactionTimeout, } from "./clientTypes.js"; function hasHeaderConfirmed(header) { return numFrom(Date.now()) - header.timestamp >= CONFIRMED_BLOCK_TIME; } /** * @public */ export class Client { constructor(config) { this.cache = config?.cache ?? new ClientCacheMemory(); } async getFeeRate(blockRange, options) { const feeRate = numMax((await this.getFeeRateStatistics(blockRange)).median, DEFAULT_MIN_FEE_RATE); const maxFeeRate = numFrom(options?.maxFeeRate ?? DEFAULT_MAX_FEE_RATE); if (maxFeeRate === Zero) { return feeRate; } return numMin(feeRate, maxFeeRate); } async getBlockByNumber(blockNumber, verbosity, withCycles) { const block = await this.cache.getBlockByNumber(blockNumber); if (block) { return block; } const res = await this.getBlockByNumberNoCache(blockNumber, verbosity, withCycles); if (res && hasHeaderConfirmed(res.header)) { await this.cache.recordBlocks(res); } return res; } async getBlockByHash(blockHash, verbosity, withCycles) { const block = await this.cache.getBlockByHash(blockHash); if (block) { return block; } const res = await this.getBlockByHashNoCache(blockHash, verbosity, withCycles); if (res && hasHeaderConfirmed(res.header)) { await this.cache.recordBlocks(res); } return res; } async getHeaderByNumber(blockNumber, verbosity) { const header = await this.cache.getHeaderByNumber(blockNumber); if (header) { return header; } const res = await this.getHeaderByNumberNoCache(blockNumber, verbosity); if (res && hasHeaderConfirmed(res)) { await this.cache.recordHeaders(res); } return res; } async getHeaderByHash(blockHash, verbosity) { const header = await this.cache.getHeaderByHash(blockHash); if (header) { return header; } const res = await this.getHeaderByHashNoCache(blockHash, verbosity); if (res && hasHeaderConfirmed(res)) { await this.cache.recordHeaders(res); } return res; } async getCell(outPointLike) { const outPoint = OutPoint.from(outPointLike); const cached = await this.cache.getCell(outPoint); if (cached) { return cached; } const transaction = await this.getTransaction(outPoint.txHash); if (!transaction) { return; } const output = transaction.transaction.getOutput(outPoint.index); if (!output) { return; } const cell = Cell.from({ outPoint, ...output, }); await this.cache.recordCells(cell); return cell; } async getCellWithHeader(outPointLike) { const outPoint = OutPoint.from(outPointLike); const res = await this.getTransactionWithHeader(outPoint.txHash); if (!res) { return; } const { transaction, header } = res; const output = transaction.transaction.getOutput(outPoint.index); if (!output) { return; } const cell = Cell.from({ outPoint, ...output, }); await this.cache.recordCells(cell); return { cell, header }; } async getCellLive(outPointLike, withData, includeTxPool) { const cell = await this.getCellLiveNoCache(outPointLike, withData, includeTxPool); if (withData && cell) { await this.cache.recordCells(cell); } return cell; } async findCellsPaged(key, order, limit, after) { const res = await this.findCellsPagedNoCache(key, order, limit, after); await this.cache.recordCells(res.cells); return res; } async *findCellsOnChain(key, order, limit = 10) { let last = undefined; while (true) { const { cells, lastCursor } = await this.findCellsPaged(key, order, limit, last); for (const cell of cells) { yield cell; } if (cells.length === 0 || cells.length < limit) { return; } last = lastCursor; } } /** * Find cells by search key designed for collectable cells. * The result also includes cached cells, the order param only works for cells fetched from RPC. * * @param keyLike - The search key. * @returns A async generator for yielding cells. */ async *findCells(keyLike, order, limit = 10) { const key = ClientIndexerSearchKey.from(keyLike); const foundedOutPoints = []; for await (const cell of this.cache.findCells(key)) { foundedOutPoints.push(cell.outPoint); yield cell; } for await (const cell of this.findCellsOnChain(key, order, limit)) { if ((await this.cache.isUnusable(cell.outPoint)) || foundedOutPoints.some((founded) => founded.eq(cell.outPoint))) { continue; } yield cell; } } findCellsByLock(lock, type, withData = true, order, limit = 10) { return this.findCells({ script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { script: type, }, withData, }, order, limit); } findCellsByType(type, withData = true, order, limit = 10) { return this.findCells({ script: type, scriptType: "type", scriptSearchMode: "exact", withData, }, order, limit); } async findSingletonCellByType(type, withData = false) { for await (const cell of this.findCellsByType(type, withData, undefined, 1)) { return cell; } } async getCellDeps(...cellDepsInfoLike) { return Promise.all(cellDepsInfoLike.flat().map(async (infoLike) => { const { cellDep, type } = CellDepInfo.from(infoLike); if (type === undefined) { return cellDep; } const found = await this.findSingletonCellByType(type); if (!found) { return cellDep; } return CellDep.from({ outPoint: found.outPoint, depType: cellDep.depType, }); })); } async *findTransactions(key, order, limit = 10) { let last = undefined; while (true) { const { transactions, lastCursor, } = await this.findTransactionsPaged(key, order, limit, last); for (const tx of transactions) { yield tx; } if (transactions.length === 0 || transactions.length < limit) { return; } last = lastCursor; } } findTransactionsByLock(lock, type, groupByTransaction, order, limit = 10) { return this.findTransactions({ script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { script: type, }, groupByTransaction, }, order, limit); } findTransactionsByType(type, groupByTransaction, order, limit = 10) { return this.findTransactions({ script: type, scriptType: "type", scriptSearchMode: "exact", groupByTransaction, }, order, limit); } async getBalanceSingle(lock) { return this.getCellsCapacity({ script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { scriptLenRange: [0, 1], outputDataLenRange: [0, 1], }, }); } async getBalance(locks) { return reduceAsync(locks, async (acc, lock) => acc + (await this.getBalanceSingle(lock)), Zero); } async sendTransaction(transaction, validator, options) { const tx = Transaction.from(transaction); const maxFeeRate = numFrom(options?.maxFeeRate ?? DEFAULT_MAX_FEE_RATE); const feeRate = await tx.getFeeRate(this); if (maxFeeRate > Zero && feeRate > maxFeeRate) { throw new ErrorClientMaxFeeRateExceeded(maxFeeRate, feeRate); } const txHash = await this.sendTransactionNoCache(tx, validator); await this.cache.markTransactions(tx); return txHash; } async getTransaction(txHashLike) { const txHash = hexFrom(txHashLike); const res = await this.getTransactionNoCache(txHash); if (res) { await this.cache.recordTransactionResponses(res); return res; } return this.cache.getTransactionResponse(txHash); } /** * This method gets specified transaction with its block header (if existed). * This is mainly for caching because we need the header to test if we can safely trust the cached tx status. * @param txHashLike */ async getTransactionWithHeader(txHashLike) { const txHash = hexFrom(txHashLike); const tx = await this.cache.getTransactionResponse(txHash); if (tx?.blockHash) { const header = await this.getHeaderByHash(tx.blockHash); if (header && hasHeaderConfirmed(header)) { return { transaction: tx, header, }; } } const res = await this.getTransactionNoCache(txHash); if (!res) { return; } await this.cache.recordTransactionResponses(res); return { transaction: res, header: res.blockHash ? await this.getHeaderByHash(res.blockHash) : undefined, }; } async waitTransaction(txHash, confirmations = 0, timeout = 60000, interval = 2000) { const startTime = Date.now(); let tx; const getTx = async () => { const res = await this.getTransaction(txHash); if (!res || res.blockNumber == null || ["sent", "pending", "proposed"].includes(res.status)) { return undefined; } tx = res; return res; }; while (true) { if (!tx) { if (await getTx()) { continue; } } else if (confirmations === 0) { return tx; } else if ((await this.getTipHeader()).number - tx.blockNumber >= confirmations) { return tx; } if (Date.now() - startTime + interval >= timeout) { throw new ErrorClientWaitTransactionTimeout(timeout); } await sleep(interval); } } }