@ickb/core
Version:
iCKB Core utils built on top of CCC
214 lines (192 loc) • 7.34 kB
text/typescript
import { ccc } from "@ckb-ccc/core";
import { ReceiptData } from "./entities.js";
import type { DaoManager } from "@ickb/dao";
import {
UdtManager,
type ExchangeRatio,
type SmartTransaction,
type UdtHandler,
} from "@ickb/utils";
/**
* IckbUdtManager is a class that implements the UdtHandler interface.
* It is responsible for handling UDT (User Defined Token) operations related to iCKB.
*/
export class IckbUdtManager extends UdtManager implements UdtHandler {
/**
* Creates an instance of IckbUdtManager.
* @param script - The script associated with the UDT.
* @param cellDeps - An array of cell dependencies.
* @param logicScript - The iCKB Logic script.
* @param daoManager - The DAO manager instance.
*/
constructor(
script: ccc.Script,
cellDeps: ccc.CellDep[],
public readonly logicScript: ccc.Script,
public readonly daoManager: DaoManager,
) {
super(script, cellDeps, "iCKB", "iCKB", 8);
}
/**
* Calculates and returns the iCKB UDT script by combining the existing UDT script
* with the iCKB logic script’s hash.
*
* This method takes the raw UDT script (user-defined token) and the iCKB logic script,
* then recalculates the UDT script’s `args` field by concatenating:
* 1. The iCKB logic script hash
* 2. A fixed 4-byte little-endian length postfix (`"00000080"`)
*
* This is used to ensure that the UDT cell carries the correct iCKB lock logic
* within its arguments.
*
* @param udt - The original UDT (user-defined token) script whose codeHash and hashType
* will be reused for the new script.
* @param ickbLogic - The iCKB logic script whose `hash()` will be prepended
* to the UDT args.
* @returns A new `ccc.Script` instance with:
* - `codeHash` and `hashType` copied from the `udt` parameter
* - `args` set to the concatenation of `ickbLogic.hash()` and `"00000080"`
*/
static calculateScript(udt: ccc.Script, ickbLogic: ccc.Script): ccc.Script {
const { codeHash, hashType } = udt;
return new ccc.Script(
codeHash,
hashType,
[ickbLogic.hash(), "00000080"].join("") as ccc.Hex,
);
}
/**
* Asynchronously retrieves the iCKB UDT input balance (token amount and capacity)
* for a given transaction.
*
* @param client - The CKB client to query cell data.
* @param tx - The smart transaction whose inputs are to be balanced.
* @returns A promise resolving to a tuple:
* - [0]: Total iCKB UDT amount in inputs (as `ccc.FixedPoint`).
* - [1]: Total capacity in UDT-related inputs (as `ccc.FixedPoint`).
*/
override async getInputsUdtBalance(
client: ccc.Client,
tx: SmartTransaction,
): Promise<[ccc.FixedPoint, ccc.FixedPoint]> {
return ccc.reduceAsync(
tx.inputs,
async (acc, input) => {
// Get all cell info
await input.completeExtraInfos(client);
const { previousOutput: outPoint, cellOutput, outputData } = input;
// Input is not well defined
if (!cellOutput || !outputData) {
throw new Error("Unable to complete input");
}
const { type, lock } = cellOutput;
if (!type) {
return acc;
}
const cell = new ccc.Cell(outPoint, cellOutput, outputData);
const [udtValue, capacity] = acc;
// An iCKB UDT Cell
if (this.isUdt(cell)) {
return [
udtValue + ccc.udtBalanceFrom(outputData),
capacity + cellOutput.capacity,
];
}
// An iCKB Receipt
if (this.logicScript.eq(type)) {
// Get header of Receipt cell and check its inclusion in HeaderDeps
const header = await tx.getHeader(client, {
type: "txHash",
value: outPoint.txHash,
});
const { depositQuantity, depositAmount } =
ReceiptData.decode(outputData);
return [
udtValue + ickbValue(depositAmount, header) * depositQuantity,
capacity + cellOutput.capacity,
];
}
// An iCKB Deposit for which the withdrawal is being requested
if (this.logicScript.eq(lock) && this.daoManager.isDeposit(cell)) {
// Get header of Deposit cell and check its inclusion in HeaderDeps
const header = await tx.getHeader(client, {
type: "txHash",
value: outPoint.txHash,
});
return [
udtValue - ickbValue(cell.capacityFree, header),
capacity + cellOutput.capacity,
];
}
return acc;
},
[0n, 0n],
);
}
}
/**
* Calculates the iCKB value based on the unoccupied CKB capacity and the block header.
*
* @param ckbUnoccupiedCapacity - The unoccupied capacity in CKB.
* @param header - The block header used for conversion.
* @returns The calculated iCKB amount.
*/
export function ickbValue(
ckbUnoccupiedCapacity: ccc.FixedPoint,
header: ccc.ClientBlockHeader,
): ccc.FixedPoint {
let ickbAmount = convert(true, ckbUnoccupiedCapacity, header, false);
if (ICKB_DEPOSIT_CAP < ickbAmount) {
// Apply a 10% discount for the amount exceeding the soft iCKB cap per deposit.
ickbAmount -= (ickbAmount - ICKB_DEPOSIT_CAP) / 10n;
}
return ickbAmount;
}
/** The maximum deposit cap for iCKB, set to 100,000 iCKB. */
export const ICKB_DEPOSIT_CAP = ccc.fixedPointFrom(100000); // 100,000 iCKB
/**
* Converts between CKB and iCKB based on the provided ratio.
*
* @param isCkb2Udt - A boolean indicating the direction of conversion (CKB to iCKB or vice versa).
* @param amount - The amount to convert.
* @param rate - The ratio information for conversion, which can be either:
* - An object containing `ckbScale` and `udtScale`.
* - A `ccc.ClientBlockHeader` for header information.
* @param accountDepositCapacity - A boolean indicating whether to account for deposit capacity
* when using ccc.ClientBlockHeader (default: true).
* @returns The converted amount in the target unit as a `ccc.FixedPoint`.
*/
export function convert(
isCkb2Udt: boolean,
amount: ccc.FixedPoint,
rate: ExchangeRatio | ccc.ClientBlockHeader,
accountDepositCapacity = true,
): ccc.FixedPoint {
if ("dao" in rate) {
rate = ickbExchangeRatio(rate, accountDepositCapacity);
}
return isCkb2Udt
? (amount * rate.ckbScale) / rate.udtScale
: (amount * rate.udtScale) / rate.ckbScale;
}
/**
* Calculates the iCKB exchange ratio based on the block header and deposit capacity.
*
* @param header - The block header used for calculating the exchange ratio.
* @param accountDepositCapacity - A boolean indicating whether to account for the deposit capacity in the calculation.
* @returns An object containing the CKB and UDT scales.
*/
export function ickbExchangeRatio(
header: ccc.ClientBlockHeader,
accountDepositCapacity = true,
): ExchangeRatio {
const AR_m = header.dao.ar;
return {
ckbScale: AR_0,
udtScale: accountDepositCapacity ? AR_m + depositCapacityDelta : AR_m,
};
}
// Constants used in calculations
const AR_0: ccc.Num = 10000000000000000n; // Base scale for CKB
const depositUsedCapacity = ccc.fixedPointFrom(82); // 82n CKB
const depositCapacityDelta = (depositUsedCapacity * AR_0) / ICKB_DEPOSIT_CAP; // Delta for deposit capacity