@ickb/core
Version:
iCKB Core utils built on top of CCC
271 lines (251 loc) • 8.49 kB
text/typescript
import { ccc } from "@ckb-ccc/core";
import {
defaultFindCellsLimit,
unique,
type Epoch,
type ScriptDeps,
type SmartTransaction,
type UdtHandler,
} from "@ickb/utils";
import { DaoManager } from "@ickb/dao";
import {
type IckbDepositCell,
ickbDepositCellFrom,
type ReceiptCell,
receiptCellFrom,
} from "./cells.js";
import { ReceiptData } from "./entities.js";
/**
* Manages logic related to deposits and receipts in the blockchain.
* Implements the ScriptDeps interface.
*/
export class LogicManager implements ScriptDeps {
/**
* Creates an instance of LogicManager.
*
* @param script - The script associated with the manager.
* @param cellDeps - The cell dependencies for the manager.
* @param daoManager - The DAO manager for handling deposits and receipts.
* @param udtHandler - The handler for User Defined Tokens (UDTs).
*/
constructor(
public readonly script: ccc.Script,
public readonly cellDeps: ccc.CellDep[],
public readonly daoManager: DaoManager,
public readonly udtHandler: UdtHandler,
) {}
/**
* Checks if the specified cell is an iCKB receipt.
*
* @param cell - The cell to check.
* @returns True if the cell is a receipt, otherwise false.
*/
isReceipt(cell: ccc.Cell): boolean {
return Boolean(cell.cellOutput.type?.eq(this.script));
}
/**
* Checks if the specified cell is an iCKB deposit.
*
* @param cell - The cell to check.
* @returns True if the cell is a deposit, otherwise false.
*/
isDeposit(cell: ccc.Cell): boolean {
return (
this.daoManager.isDeposit(cell) && cell.cellOutput.lock.eq(this.script)
);
}
/**
* Processes a deposit transaction.
*
* @param tx - The transaction to add the deposit to.
* @param depositQuantity - The quantity of deposits.
* @param depositAmount - The amount of each deposit.
* @param lock - The lock script for the output receipt cell.
*/
deposit(
tx: SmartTransaction,
depositQuantity: number,
depositAmount: ccc.FixedPoint,
lock: ccc.Script,
): void {
if (depositQuantity <= 0) {
return;
}
if (depositAmount < ccc.fixedPointFrom(1082)) {
throw Error("iCKB deposit minimum is 1082 CKB");
}
if (depositAmount > ccc.fixedPointFrom(1000082)) {
throw Error("iCKB deposit minimum is 1082 CKB");
}
tx.addCellDeps(this.cellDeps);
tx.addUdtHandlers(this.udtHandler);
const capacities = Array.from(
{ length: Number(depositQuantity) },
() => depositAmount,
);
this.daoManager.deposit(tx, capacities, this.script);
// Add the Receipt to the outputs
tx.addOutput(
{
lock: lock,
type: this.script,
},
ReceiptData.encode({ depositQuantity, depositAmount }),
);
// Check that there are at most 64 output cells, see:
// https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md#gotchas
if (tx.outputs.length > 64) {
throw Error("More than 64 output cells in a NervosDAO transaction");
}
}
/**
* Completes a deposit transaction by transforming the receipts into iCKB UDTs.
*
* @param tx - The transaction to add the receipts to.
* @param receipts - The receipts to add to the transaction.
*/
completeDeposit(tx: SmartTransaction, receipts: ReceiptCell[]): void {
if (receipts.length === 0) {
return;
}
tx.addCellDeps(this.cellDeps);
tx.addUdtHandlers(this.udtHandler);
tx.addHeaders(receipts.map((r) => r.header));
for (const { cell } of receipts) {
tx.addInput(cell);
}
}
/**
* Async generator that finds and yields receipt cells matching the given lock scripts.
*
* Receipt cells are identified by `this.script` (the receipt type script)
* and must also pass `this.isReceipt(cell)`.
*
* @param client
* A CKB client instance providing:
* - `findCells(query, order, limit)` for cached searches
* - `findCellsOnChain(query, order, limit)` for direct on-chain searches
*
* @param locks
* An array of lock scripts. Only cells whose `cellOutput.lock` exactly matches
* one of these scripts will be considered.
*
* @param options
* Optional parameters to control query behavior:
* - `onChain?: boolean`
* If `true`, uses `findCellsOnChain`. Otherwise, uses `findCells`. Default: `false`.
* - `limit?: number`
* Maximum number of cells to fetch per lock script. Defaults to `defaultFindCellsLimit` (400).
*
* @yields
* {@link ReceiptCell} objects for each valid receipt cell found.
*
* @remarks
* - Deduplicates `locks` via `unique(locks)`.
* - Applies an RPC filter with:
* - `script: this.script` (receipt type script)
* - Skips any cell that:
* 1. Fails `this.isReceipt(cell)`
* 2. Has a non-matching lock script
* - Converts each raw cell via `receiptCellFrom({ client, cell })` before yielding.
*/
async *findReceipts(
client: ccc.Client,
locks: ccc.Script[],
options?: {
/**
* If true, fetch cells directly from the chain RPC. Otherwise, use cached results.
* @default false
*/
onChain?: boolean;
/**
* Batch size per lock script. Defaults to {@link defaultFindCellsLimit}.
*/
limit?: number;
},
): AsyncGenerator<ReceiptCell> {
const limit = options?.limit ?? defaultFindCellsLimit;
for (const lock of unique(locks)) {
const findCellsArgs = [
{
script: lock,
scriptType: "lock",
filter: {
script: this.script,
},
scriptSearchMode: "exact",
withData: true,
},
"asc",
limit,
] as const;
for await (const cell of options?.onChain
? client.findCellsOnChain(...findCellsArgs)
: client.findCells(...findCellsArgs)) {
if (!this.isReceipt(cell) || !cell.cellOutput.lock.eq(lock)) {
continue;
}
yield receiptCellFrom({ client, cell });
}
}
}
/**
* Async generator that finds and yields iCKB deposit cells.
*
* Wraps DAO deposit detection for the iCKB token by delegating
* to `this.daoManager.findDeposits` and converting results.
*
* @param client
* A CKB RPC client instance implementing:
* - `getTipHeader()` to fetch the latest block header
* - `findCells` / `findCellsOnChain` for cell queries
*
* @param options
* Optional parameters to control the search:
* - `tip?: ClientBlockHeader`
* Block header to use as reference for epoch/lock calculations.
* If omitted, `client.getTipHeader()` is called to obtain the latest header.
* - `onChain?: boolean`
* When `true`, forces direct on-chain queries via `findCellsOnChain`.
* Otherwise, uses cached results via `findCells`. Default: `false`.
* - `minLockUp?: Epoch`
* Minimum lock-up period in epochs. Defaults to manager’s configured minimum (~10 min).
* - `maxLockUp?: Epoch`
* Maximum lock-up period in epochs. Defaults to manager’s configured maximum (~3 days).
* - `limit?: number`
* Maximum cells per batch when querying. Defaults to `defaultFindCellsLimit` (400).
*
* @returns
* An async generator yielding `IckbDepositCell` objects, each representing
* an iCKB deposit derived from a DAO deposit cell.
*
* @remarks
* - Enforces that `options.tip` is a `ClientBlockHeader` instance by calling
* `ClientBlockHeader.from(...)` if a plain object is provided.
* - Delegates to `this.daoManager.findDeposits(client, [this.script], options)` to locate
* raw DAO deposit cells locked under `this.script`.
* - Converts each raw `DaoCell` into an `IckbDepositCell` via `ickbDepositCellFrom`.
*/
async *findDeposits(
client: ccc.Client,
options?: {
tip?: ccc.ClientBlockHeader;
onChain?: boolean;
minLockUp?: Epoch;
maxLockUp?: Epoch;
limit?: number;
},
): AsyncGenerator<IckbDepositCell> {
const tip = options?.tip
? ccc.ClientBlockHeader.from(options.tip)
: await client.getTipHeader();
options = { ...options, tip };
for await (const deposit of this.daoManager.findDeposits(
client,
[this.script],
options,
)) {
yield ickbDepositCellFrom(deposit);
}
}
}