UNPKG

@ckb-ccc/core

Version:

Core of CCC - CKBer's Codebase

705 lines (646 loc) 18.6 kB
import { Cell, CellDep, OutPoint, OutPointLike, ScriptLike, Transaction, TransactionLike, } from "../ckb/index.js"; import { Zero } from "../fixedPoint/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; import { Num, NumLike, numFrom, numMax, numMin } from "../num/index.js"; import { reduceAsync, sleep } from "../utils/index.js"; import { ClientCache } from "./cache/index.js"; import { ClientCacheMemory } from "./cache/memory.js"; import { CONFIRMED_BLOCK_TIME, ClientCollectableSearchKeyLike, DEFAULT_MAX_FEE_RATE, DEFAULT_MIN_FEE_RATE, } from "./clientTypes.advanced.js"; import { CellDepInfo, CellDepInfoLike, ClientBlock, ClientBlockHeader, ClientFindCellsResponse, ClientFindTransactionsGroupedResponse, ClientFindTransactionsResponse, ClientIndexerSearchKey, ClientIndexerSearchKeyLike, ClientIndexerSearchKeyTransactionLike, ClientTransactionResponse, ErrorClientMaxFeeRateExceeded, ErrorClientWaitTransactionTimeout, OutputsValidator, ScriptInfo, } from "./clientTypes.js"; import { KnownScript } from "./knownScript.js"; function hasHeaderConfirmed(header: ClientBlockHeader): boolean { return numFrom(Date.now()) - header.timestamp >= CONFIRMED_BLOCK_TIME; } /** * @public */ export abstract class Client { public cache: ClientCache; constructor(config?: { cache?: ClientCache }) { this.cache = config?.cache ?? new ClientCacheMemory(); } abstract get url(): string; abstract get addressPrefix(): string; abstract getKnownScript(script: KnownScript): Promise<ScriptInfo>; abstract getFeeRateStatistics( blockRange?: NumLike, ): Promise<{ mean: Num; median: Num }>; async getFeeRate( blockRange?: NumLike, options?: { maxFeeRate?: NumLike }, ): Promise<Num> { 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); } abstract getTip(): Promise<Num>; abstract getTipHeader(verbosity?: number | null): Promise<ClientBlockHeader>; abstract getBlockByNumberNoCache( blockNumber: NumLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise<ClientBlock | undefined>; abstract getBlockByHashNoCache( blockHash: HexLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise<ClientBlock | undefined>; abstract getHeaderByNumberNoCache( blockNumber: NumLike, verbosity?: number | null, ): Promise<ClientBlockHeader | undefined>; abstract getHeaderByHashNoCache( blockHash: HexLike, verbosity?: number | null, ): Promise<ClientBlockHeader | undefined>; async getBlockByNumber( blockNumber: NumLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise<ClientBlock | undefined> { 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: HexLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise<ClientBlock | undefined> { 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: NumLike, verbosity?: number | null, ): Promise<ClientBlockHeader | undefined> { 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: HexLike, verbosity?: number | null, ): Promise<ClientBlockHeader | undefined> { 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; } abstract estimateCycles(transaction: TransactionLike): Promise<Num>; abstract sendTransactionDry( transaction: TransactionLike, validator?: OutputsValidator, ): Promise<Num>; abstract sendTransactionNoCache( transaction: TransactionLike, validator?: OutputsValidator, ): Promise<Hex>; abstract getTransactionNoCache( txHash: HexLike, ): Promise<ClientTransactionResponse | undefined>; async getCell(outPointLike: OutPointLike): Promise<Cell | undefined> { 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: OutPointLike, ): Promise<{ cell: Cell; header?: ClientBlockHeader } | undefined> { 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 }; } abstract getCellLiveNoCache( outPointLike: OutPointLike, withData?: boolean | null, includeTxPool?: boolean | null, ): Promise<Cell | undefined>; async getCellLive( outPointLike: OutPointLike, withData?: boolean | null, includeTxPool?: boolean | null, ): Promise<Cell | undefined> { const cell = await this.getCellLiveNoCache( outPointLike, withData, includeTxPool, ); if (withData && cell) { await this.cache.recordCells(cell); } return cell; } abstract findCellsPagedNoCache( key: ClientIndexerSearchKeyLike, order?: "asc" | "desc", limit?: NumLike, after?: string, ): Promise<ClientFindCellsResponse>; async findCellsPaged( key: ClientIndexerSearchKeyLike, order?: "asc" | "desc", limit?: NumLike, after?: string, ): Promise<ClientFindCellsResponse> { const res = await this.findCellsPagedNoCache(key, order, limit, after); await this.cache.recordCells(res.cells); return res; } async *findCellsOnChain( key: ClientIndexerSearchKeyLike, order?: "asc" | "desc", limit = 10, ): AsyncGenerator<Cell> { let last: string | undefined = 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: ClientCollectableSearchKeyLike, order?: "asc" | "desc", limit = 10, ): AsyncGenerator<Cell> { 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: ScriptLike, type?: ScriptLike | null, withData = true, order?: "asc" | "desc", limit = 10, ): AsyncGenerator<Cell> { return this.findCells( { script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { script: type, }, withData, }, order, limit, ); } findCellsByType( type: ScriptLike, withData = true, order?: "asc" | "desc", limit = 10, ): AsyncGenerator<Cell> { return this.findCells( { script: type, scriptType: "type", scriptSearchMode: "exact", withData, }, order, limit, ); } async findSingletonCellByType( type: ScriptLike, withData = false, ): Promise<Cell | undefined> { for await (const cell of this.findCellsByType( type, withData, undefined, 1, )) { return cell; } } async getCellDeps( ...cellDepsInfoLike: (CellDepInfoLike | CellDepInfoLike[])[] ): Promise<CellDep[]> { 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, }); }), ); } abstract findTransactionsPaged( key: Omit<ClientIndexerSearchKeyTransactionLike, "groupByTransaction"> & { groupByTransaction: true; }, order?: "asc" | "desc", limit?: NumLike, after?: string, ): Promise<ClientFindTransactionsGroupedResponse>; abstract findTransactionsPaged( key: Omit<ClientIndexerSearchKeyTransactionLike, "groupByTransaction"> & { groupByTransaction?: false | null; }, order?: "asc" | "desc", limit?: NumLike, after?: string, ): Promise<ClientFindTransactionsResponse>; abstract findTransactionsPaged( key: ClientIndexerSearchKeyTransactionLike, order?: "asc" | "desc", limit?: NumLike, after?: string, ): Promise< ClientFindTransactionsResponse | ClientFindTransactionsGroupedResponse >; findTransactions( key: Omit<ClientIndexerSearchKeyTransactionLike, "groupByTransaction"> & { groupByTransaction: true; }, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsGroupedResponse["transactions"][0]>; findTransactions( key: Omit<ClientIndexerSearchKeyTransactionLike, "groupByTransaction"> & { groupByTransaction?: false | null; }, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsResponse["transactions"][0]>; findTransactions( key: ClientIndexerSearchKeyTransactionLike, order?: "asc" | "desc", limit?: number, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] >; async *findTransactions( key: ClientIndexerSearchKeyTransactionLike, order?: "asc" | "desc", limit = 10, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] > { let last: string | undefined = undefined; while (true) { const { transactions, lastCursor, }: | ClientFindTransactionsResponse | ClientFindTransactionsGroupedResponse = 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: ScriptLike, type: ScriptLike | undefined | null, groupByTransaction: true, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsGroupedResponse["transactions"][0]>; findTransactionsByLock( lock: ScriptLike, type?: ScriptLike | null, groupByTransaction?: false | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsResponse["transactions"][0]>; findTransactionsByLock( lock: ScriptLike, type?: ScriptLike | null, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] >; findTransactionsByLock( lock: ScriptLike, type?: ScriptLike | null, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit = 10, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] > { return this.findTransactions( { script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { script: type, }, groupByTransaction, }, order, limit, ); } findTransactionsByType( type: ScriptLike, groupByTransaction: true, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsGroupedResponse["transactions"][0]>; findTransactionsByType( type: ScriptLike, groupByTransaction?: false | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<ClientFindTransactionsResponse["transactions"][0]>; findTransactionsByType( type: ScriptLike, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] >; findTransactionsByType( type: ScriptLike, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit = 10, ): AsyncGenerator< | ClientFindTransactionsResponse["transactions"][0] | ClientFindTransactionsGroupedResponse["transactions"][0] > { return this.findTransactions( { script: type, scriptType: "type", scriptSearchMode: "exact", groupByTransaction, }, order, limit, ); } abstract getCellsCapacity(key: ClientIndexerSearchKeyLike): Promise<Num>; async getBalanceSingle(lock: ScriptLike): Promise<Num> { return this.getCellsCapacity({ script: lock, scriptType: "lock", scriptSearchMode: "exact", filter: { scriptLenRange: [0, 1], outputDataLenRange: [0, 1], }, }); } async getBalance(locks: ScriptLike[]): Promise<Num> { return reduceAsync( locks, async (acc, lock) => acc + (await this.getBalanceSingle(lock)), Zero, ); } async sendTransaction( transaction: TransactionLike, validator?: OutputsValidator, options?: { maxFeeRate?: NumLike }, ): Promise<Hex> { 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: HexLike, ): Promise<ClientTransactionResponse | undefined> { 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: HexLike, ): Promise< | { transaction: ClientTransactionResponse; header?: ClientBlockHeader } | undefined > { 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: HexLike, confirmations: number = 0, timeout: number = 60000, interval: number = 2000, ): Promise<ClientTransactionResponse | undefined> { const startTime = Date.now(); let tx: ClientTransactionResponse | undefined; 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); } } }