Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: improve doc of LockScriptInfo #554

Merged
merged 2 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
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.hexify(blockchain.Script.pack(a)) ===
codec.bytes.hexify(blockchain.Script.pack(b))
doitian marked this conversation as resolved.
Show resolved Hide resolved
);
}

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,
};
Loading