@ckb-ccc/core
Version:
Core of CCC - CKBer's Codebase
325 lines (324 loc) • 11.6 kB
JavaScript
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);
}
}
}