Skip to content

Commit

Permalink
docs: improve doc of LockScriptInfo (#554)
Browse files Browse the repository at this point in the history
  • Loading branch information
doitian authored Sep 1, 2023
1 parent 1cb43fe commit ed5dc56
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import {
Cell,
CellCollector,
CellProvider,
QueryOptions,
blockchain,
} from "@ckb-lumos/base";
import * as apiTypes from "@ckb-lumos/base/lib/api";
import { BI } from "@ckb-lumos/bi";
import * as codec from "@ckb-lumos/codec";
import { getConfig } from "@ckb-lumos/config-manager";
import { Options, TransactionSkeletonType } from "@ckb-lumos/helpers";
import { List, Map } from "immutable";
import * as commons from "../../src";
import { addCellDep } from "../../src/helper";

// Compare two objects by convert them to molecule buffer to ignore the representation only
// differences.
function isSameScript(a: apiTypes.Script, b: apiTypes.Script) {
return (
codec.bytes.equal(
blockchain.Script.pack(a),
blockchain.Script.pack(b),
)
);
}

const MAX_U64_PLUS_ONE = BI.from(1).shl(64);
function packInt64LE(number: BI) {
return codec.number.Uint64LE.pack(
number.isNegative() ? MAX_U64_PLUS_ONE.add(number) : number
);
}

// This example demonstrates how to use a custom script [CapacityDiff](https://github.com/doitian/ckb-sdk-examples-capacity-diff).
//
// CapacityDiff verifies the witness matches the capacity difference.
//
// - The script loads the witness for the first input in the script group using the WitnessArgs layout.
// - The total input capacity is the sum of all the input cells in the script group.
// - The total output capacity is the sum of all the output cells having the same lock script as the script group.
// - The capacity difference is a 64-bit signed integer which equals to total output capacity minus total input capacity.
// - The witness is encoded using two's complement and little endian.
const capacityDiffLockInfo: commons.LockScriptInfo = {
codeHash: "0x",
hashType: "type",
lockScriptInfo: {
CellCollector: class {
cellCollector: CellCollector;
fromScript = {
codeHash: "0x",
hashType: "type" as apiTypes.HashType,
args: "0x",
};

constructor(
fromInfo: commons.FromInfo,
cellProvider: CellProvider,
{ config, queryOptions }: Options & { queryOptions?: QueryOptions }
) {
if (!cellProvider) {
throw new Error(`Cell provider is missing!`);
}
config ??= getConfig();
const script = commons.parseFromInfo(fromInfo, { config }).fromScript;

// Please note that the cell collector is called for each specific fromInfo.
// Be cautious not to include input cells for accounts that are not locked by this script.
const template = config.SCRIPTS.CAPACITY_DIFF!;
if (
script.codeHash !== template.CODE_HASH ||
script.hashType !== template.HASH_TYPE
) {
return;
}

// Now we can apply the queryOptions to search the live cells.
queryOptions ??= {};
queryOptions = {
...queryOptions,
lock: script,
type: queryOptions.type ?? "empty",
};

this.cellCollector = cellProvider.collector(queryOptions);
}

async *collect() {
if (this.cellCollector) {
for await (const inputCell of this.cellCollector.collect()) {
yield inputCell;
}
}
}
},

// What to do when a inputCell has been found by the cell provider.
// - Add input and output cell
// - Add cell deps.
// - Fill witness to make fee calculation correct.
setupInputCell: async (
txSkeleton: TransactionSkeletonType,
inputCell: Cell,
_fromInfo: commons.FromInfo,
{ config, since, defaultWitness } = {}
) => {
// use default config when config is not provided
config ??= getConfig();
const fromScript = inputCell.cellOutput.lock;
const txMutable = txSkeleton.asMutable();

//===========================
// I. Common Skeletons
//
// There are many steps that setupInputCell must perform carefully, otherwise the whole transaction builder will fail.
//===========================
// 1.Add inputCell to txSkeleton
txMutable.update("inputs", (inputs: List<Cell>) =>
inputs.push(inputCell)
);

// 2. Add output. The function `lumos.commons.common.transfer` will scan outputs for available balance for each account.
const outputCell = {
cellOutput: {
...inputCell.cellOutput,
},
data: inputCell.data,
};
txMutable.update("outputs", (outputs: List<Cell>) =>
outputs.push(outputCell)
);

// 3. Set Since
if (since) {
txMutable.setIn(
["inputSinces", txMutable.get("inputs").size - 1],
since
);
}

// 4. Insert a witness to ensure they are aligned to the location of the corresponding input cells.
txMutable.update("witnesses", (witnesses: List<string>) =>
witnesses.push(defaultWitness ?? "0x")
);
//=> Common Skeletons End Here

//===========================
// II. CellDeps
//===========================
// Assume that script onchain infos are stored as CAPACITY_DIFF
const template = config.SCRIPTS.CAPACITY_DIFF;
if (!template) {
throw new Error(
"Provided config does not have CAPACITY_DIFF script setup!"
);
}
const scriptOutPoint = {
txHash: template.TX_HASH,
index: template.INDEX,
};
// The helper method addCellDep avoids adding duplicated cell deps.
addCellDep(txMutable, {
outPoint: scriptOutPoint,
depType: template.DEP_TYPE,
});

//===========================
// II. Witness Placeholder
//===========================
// Fill witness. These code are copied from
// https://github.com/ckb-js/lumos/blob/1cb43fe72dc95c4b3283acccb5120b7bcaeb9346/packages/common-scripts/src/secp256k1_blake160.ts#L90
//
// It takes a lot of code to set the witness for the first input cell in
// the script group to 8 bytes of zeros.
const firstIndex = txMutable
.get("inputs")
.findIndex((input: Cell) =>
isSameScript(input.cellOutput.lock, fromScript)
);

if (firstIndex !== -1) {
// Ensure witnesses are aligned to inputs
const toFillWitnessesCount =
firstIndex + 1 - txMutable.get("witnesses").size;
if (toFillWitnessesCount > 0) {
txMutable.update("witnesses", (witnesses: List<string>) =>
witnesses.concat(Array(toFillWitnessesCount).fill("0x"))
);
}
txMutable.updateIn(["witnesses", firstIndex], (witness: any) => {
const witnessArgs = {
...(witness === "0x"
? {}
: blockchain.WitnessArgs.unpack(codec.bytes.bytify(witness))),
lock: "0x0000000000000000",
};
return codec.bytes.hexify(blockchain.WitnessArgs.pack(witnessArgs));
});
}

return txMutable.asImmutable();
},

// Create entries in txSkeleton.signingEntries
prepareSigningEntries: (
txSkeleton: TransactionSkeletonType,
{ config }
) => {
// use default config when config is not provided
config ??= getConfig();
const template = config.SCRIPTS.CAPACITY_DIFF;
if (!template) {
throw new Error(
`Provided config does not have CAPACITY_DIFF script setup!`
);
}

const balances = Map<
string,
{ index: number; capacity: BI }
>().asMutable();
// Group inputs by args and tally the total capacity as negative values.
txSkeleton.get("inputs").forEach((input: Cell, index: number) => {
const {
capacity,
lock: { codeHash, hashType, args },
} = input.cellOutput;
if (
template.CODE_HASH === codeHash &&
template.HASH_TYPE === hashType
) {
if (balances.has(args)) {
balances.updateIn([args, "capacity"], (total: any) =>
total.sub(capacity)
);
} else {
balances.set(args, { index, capacity: BI.from(0).sub(capacity) });
}
}
});
// Add capacity of output cells to the tally.
txSkeleton.get("outputs").forEach((output: Cell) => {
const {
capacity,
lock: { codeHash, hashType, args },
} = output.cellOutput;
if (
template.CODE_HASH === codeHash &&
template.HASH_TYPE === hashType &&
balances.has(args)
) {
balances.updateIn([args, "capacity"], (total: any) =>
total.add(capacity)
);
}
});
// Create signing entries. Indeed, for this simple script, we could set
// the witness directly. However, for serious lock script, it often
// requires sining by the private
// key.
return txSkeleton.update(
"signingEntries",
(entries: List<{ index: number; type: string; message: string }>) =>
entries.concat(
balances
.asImmutable()
.valueSeq()
.map(({ index, capacity }: any) => ({
index,
// This is the only supported type, which indicate the signature
// follows the WitnewsArgs layout.
type: "witness_args_lock",
message: codec.bytes.hexify(packInt64LE(capacity)),
}))
)
);
},
},
};

commons.common.registerCustomLockScriptInfos([capacityDiffLockInfo]);
54 changes: 50 additions & 4 deletions packages/common-scripts/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,61 @@ const { ScriptValue } = values;
import { Set } from "immutable";
import { isAcpScript } from "./helper";
import { BI, BIish } from "@ckb-lumos/bi";
import { CellCollectorConstructor } from "./type";
import { CellCollectorConstructor, CellCollectorType } from "./type";
import omnilock from "./omnilock";

function defaultLogger(level: string, message: string) {
console.log(`[${level}] ${message}`);
}

export type { CellCollectorConstructor, CellCollectorType };

/**
* CellCollector should be a class which implement CellCollectorInterface.
* If you want to work well with `transfer`, `injectCapacity`, `payFee`, `payFeeByFeeRate`,
* please add the `output` at the end of `txSkeleton.get("outputs")`
* LockScriptInfo describes how to integrate a lock script in transaction building.
*
* Custom lock scripts must register their LockScriptInfo before using
* `transfer`, `injectCapacity`, `payFee`, `payFeeByFeeRate` via
* `registerCustomLockScriptInfos`.
*
* See an example in
* [custom_lock_script_info.ts](https://github.com/ckb-js/lumos/blob/develop/packages/common-scripts/examples/custom_lock_script_info/custom_lock_script_info.ts).
*/
export interface LockScriptInfo {
codeHash: Hash;
hashType: HashType;
/**
* @interface
*/
lockScriptInfo: {
/**
* Collects input cell candidates for the lock script.
*
* It's a constructor that initializes objects implementing function
* `collect()` to provide input cells. Attention that transaction builders
* will not match `fromInfo` and lock script. It's the responsibility of
* `CellCollector` to filter based on `fromInfo`. For example, when
* `fromInfo` does not match, the function `collect()` should not return
* any cell.
*/
CellCollector: CellCollectorConstructor;

/**
* Called when a candidate input cell is found.
*
* What this function should do:
*
* 1. Frist double-check the cell and decide whether continue the following steps or skip.
* 2. Add the cell as an input in the `txSkeleton` and an output cell with
* the same fields since functions like `transfer`, `injectCapacity`,
* `payFee`, and `payFeeByFeeRate` collects account balance in outputs.
* 3. Add `cellDeps`
* 4. Prefill witnesses to ensure the transaction size will not increase after signing.
*
* @param txSkeleton transaction skeleton built so far
* @param inputCell the new input cell candidate
* @param fromInfo which account the inputCell belongs to
* @return the updated transaction skeleton
*/
setupInputCell(
txSkeleton: TransactionSkeletonType,
inputCell: Cell,
Expand All @@ -57,10 +96,16 @@ export interface LockScriptInfo {
since?: PackedSince;
}
): Promise<TransactionSkeletonType>;

/**
* Scans the transaction and add signing entries into `txSkeleton.signingEnties`.
* @return the updated txSkeleton
*/
prepareSigningEntries(
txSkeleton: TransactionSkeletonType,
options: Options
): TransactionSkeletonType;

setupOutputCell?: (
txSkeleton: TransactionSkeletonType,
outputCell: Cell,
Expand Down Expand Up @@ -96,6 +141,7 @@ function getLockScriptInfos(): LockScriptInfosType {
return lockScriptInfos;
}

/** Registers LockScriptInfo for custom scripts. */
export function registerCustomLockScriptInfos(infos: LockScriptInfo[]): void {
lockScriptInfos._customInfos = infos;
}
Expand Down
15 changes: 13 additions & 2 deletions packages/common-scripts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import secp256k1Blake160Multisig from "./secp256k1_blake160_multisig";
import { MultisigScript, FromInfo, parseFromInfo } from "./from_info";
import dao from "./dao";
import locktimePool, { LocktimeCell } from "./locktime_pool";
import common, { LockScriptInfo } from "./common";
import common, {
LockScriptInfo,
CellCollectorConstructor,
CellCollectorType,
} from "./common";
import sudt from "./sudt";
import anyoneCanPay from "./anyone_can_pay";
import { createP2PKHMessageGroup } from "./p2pkh";
Expand Down Expand Up @@ -34,4 +38,11 @@ export default {
anyoneCanPay,
};

export type { LocktimeCell, MultisigScript, FromInfo, LockScriptInfo };
export type {
LocktimeCell,
MultisigScript,
FromInfo,
LockScriptInfo,
CellCollectorConstructor,
CellCollectorType,
};

2 comments on commit ed5dc56

@vercel
Copy link

@vercel vercel bot commented on ed5dc56 Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 New canary release: 0.0.0-canary-ed5dc56-20230901063035

npm install @ckb-lumos/[email protected]

Please sign in to comment.