Skip to content

Commit

Permalink
feat(common-scripts): inject from registered locks in dao (#596)
Browse files Browse the repository at this point in the history
Co-authored-by: Chen Yu <[email protected]>
  • Loading branch information
homura and Keith-CY authored Jan 25, 2024
1 parent 1e2fa29 commit ad483d9
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-years-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/common-scripts": minor
---

feat: support registered lock in dao operations
75 changes: 60 additions & 15 deletions packages/common-scripts/src/dao.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* eslint-disable import/no-named-as-default-member */

import {
parseAddress,
TransactionSkeletonType,
Options,
generateAddress,
encodeToAddress,
minimalCellCapacityCompatible,
} from "@ckb-lumos/helpers";
import {
Expand All @@ -22,7 +25,7 @@ import { getConfig, Config } from "@ckb-lumos/config-manager";
const { parseSince } = sinceUtils;
import secp256k1Blake160 from "./secp256k1_blake160";
import secp256k1Blake160Multisig from "./secp256k1_blake160_multisig";
import { FromInfo, parseFromInfo } from "./from_info";
import { FromInfo, isMultisigFromInfo, parseFromInfo } from "./from_info";
import {
addCellDep,
isSecp256k1Blake160Script,
Expand All @@ -31,6 +34,7 @@ import {
} from "./helper";
import { BI, BIish } from "@ckb-lumos/bi";
import { RPC } from "@ckb-lumos/rpc";
import common from "./common";

const DEPOSIT_DAO_DATA: HexString = "0x0000000000000000";
const DAO_LOCK_PERIOD_EPOCHS_COMPATIBLE = BI.from(180);
Expand Down Expand Up @@ -98,12 +102,20 @@ export async function* listDaoCells(
}
}

interface DepositOptions {
config?: Config;
/**
* enable using non-system script in inputs
*/
enableNonSystemScript?: boolean;
}

// TODO: reject multisig with non absolute-epoch-number locktime lock
/**
* deposit a cell to DAO
*
* @param txSkeleton
* @param fromInfo
* @param fromInfo only system script is enabled by default, to enable non-system script as inputs, please set enableNonSystemScript to true in options
* @param toAddress deposit cell lock address
* @param amount capacity in shannon
* @param options
Expand All @@ -113,7 +125,7 @@ export async function deposit(
fromInfo: FromInfo,
toAddress: Address,
amount: BIish,
{ config = undefined }: Options = {}
{ config = undefined, enableNonSystemScript = false }: DepositOptions = {}
): Promise<TransactionSkeletonType> {
config = config || getConfig();
const DAO_SCRIPT = config.SCRIPTS.DAO;
Expand Down Expand Up @@ -179,16 +191,36 @@ export async function deposit(
fromInfo,
{ config }
);
} else if (enableNonSystemScript) {
txSkeleton = await common.injectCapacity(
txSkeleton,
[fromInfo],
amount,
encodeToAddress(parseFromInfo(fromInfo).fromScript, { config }),
undefined,
{ config }
);
}
} else if (fromInfo) {
txSkeleton = await secp256k1Blake160Multisig.injectCapacity(
txSkeleton,
outputIndex,
fromInfo,
{
config,
}
);
if (isMultisigFromInfo(fromInfo)) {
txSkeleton = await secp256k1Blake160Multisig.injectCapacity(
txSkeleton,
outputIndex,
fromInfo,
{
config,
}
);
} else if (enableNonSystemScript) {
txSkeleton = await common.injectCapacity(
txSkeleton,
[fromInfo],
amount,
encodeToAddress(parseFromInfo(fromInfo).fromScript, { config }),
undefined,
{ config }
);
}
}

return txSkeleton;
Expand Down Expand Up @@ -217,6 +249,14 @@ function _checkFromInfoSince(fromInfo: FromInfo, config: Config): void {
}
}

export interface WithdrawOptions {
config?: Config;
/**
* enable using non-system script in inputs
*/
enableNonSystemScript?: boolean;
}

/**
* withdraw an deposited DAO cell
*
Expand All @@ -229,7 +269,7 @@ async function withdraw(
txSkeleton: TransactionSkeletonType,
fromInput: Cell,
fromInfo?: FromInfo,
{ config = undefined }: Options = {}
{ config = undefined, enableNonSystemScript = false }: WithdrawOptions = {}
): Promise<TransactionSkeletonType> {
config = config || getConfig();
_checkDaoScript(config);
Expand Down Expand Up @@ -268,9 +308,7 @@ async function withdraw(
txSkeleton,
fromInput,
undefined,
{
config,
}
{ config }
);
} else if (isSecp256k1Blake160MultisigScript(fromLockScript, config)) {
txSkeleton = await secp256k1Blake160Multisig.setupInputCell(
Expand All @@ -279,6 +317,13 @@ async function withdraw(
fromInfo || generateAddress(fromLockScript, { config }),
{ config }
);
} else if (enableNonSystemScript) {
txSkeleton = await common.setupInputCell(
txSkeleton,
fromInput,
fromInfo || encodeToAddress(fromLockScript, { config }),
{ config }
);
}

const targetOutputIndex: number = txSkeleton.get("outputs").size - 1;
Expand Down
12 changes: 12 additions & 0 deletions packages/common-scripts/src/from_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export interface MultisigScript {
since?: PackedSince;
}

export function isMultisigFromInfo(obj: unknown): obj is MultisigScript {
if (!obj || typeof obj !== "object") return false;

const maybeMultisig = obj as Partial<MultisigScript>;

return (
typeof maybeMultisig?.R === "number" &&
typeof maybeMultisig?.M === "number" &&
Array.isArray(maybeMultisig?.publicKeyHashes)
);
}

export interface ACP {
address: Address;
destroyable?: boolean; // default to false
Expand Down
142 changes: 142 additions & 0 deletions packages/common-scripts/tests/dao-with-custom-lock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import test, { afterEach, beforeEach } from "ava";
import { registerCustomLockScriptInfos } from "../src/common";
import { TestCellCollector } from "./helper";
import {
encodeToAddress,
TransactionSkeleton,
TransactionSkeletonType,
} from "@ckb-lumos/helpers";
import { Cell, Script } from "@ckb-lumos/base";
import { BI, parseUnit } from "@ckb-lumos/bi";
import { CellProvider } from "./cell_provider";
import { dao } from "../src";
import { Config, predefined } from "@ckb-lumos/config-manager";
import { hexify } from "@ckb-lumos/codec/lib/bytes";
import { Uint64 } from "@ckb-lumos/codec/lib/number";
import { randomBytes } from "node:crypto";

const { LINA } = predefined;

const nonSystemLockCodeHash = "0x" + "aa".repeat(32);

beforeEach(() => {
registerCustomLockScriptInfos([
{
codeHash: nonSystemLockCodeHash,
hashType: "type",
lockScriptInfo: {
CellCollector: TestCellCollector,
async setupInputCell(
txSkeleton: TransactionSkeletonType,
inputCell: Cell
): Promise<TransactionSkeletonType> {
txSkeleton = txSkeleton.update("inputs", (inputs) =>
inputs.push(inputCell)
);

txSkeleton = txSkeleton.update("outputs", (outputs) =>
outputs.push(inputCell)
);

return txSkeleton;
},
prepareSigningEntries(txSkeleton) {
return txSkeleton;
},
},
},
]);
});

// reset custom lock script infos
afterEach(() => {
registerCustomLockScriptInfos([]);
});

test("deposit from the non-system script", async (t) => {
const fromScript: Script = {
codeHash: nonSystemLockCodeHash,
hashType: "type",
args: "0x",
};

const toScript: Script = {
codeHash: "0x" + "bb".repeat(32),
hashType: "type",
args: "0x",
};
const nonSystemLockCell = {
cellOutput: {
capacity: parseUnit("5000000", "ckb").toHexString(),
lock: fromScript,
},
data: "0x",
};
let txSkeleton = TransactionSkeleton({
cellProvider: new CellProvider([nonSystemLockCell]),
});

txSkeleton = await dao.deposit(
txSkeleton,
encodeToAddress(fromScript),
encodeToAddress(toScript),
parseUnit("10000", "ckb"),
{ enableNonSystemScript: true }
);

t.deepEqual(txSkeleton.get("inputs").get(0), nonSystemLockCell);
t.is(txSkeleton.get("outputs").size, 2);
t.deepEqual(txSkeleton.get("outputs").get(0)?.cellOutput, {
capacity: parseUnit("10000", "ckb").toHexString(),
lock: toScript,
type: generateDaoTypeScript(LINA),
});
t.deepEqual(txSkeleton.get("outputs").get(1)?.cellOutput, {
capacity: BI.from(nonSystemLockCell.cellOutput.capacity)
.sub(parseUnit("10000", "ckb"))
.toHexString(),
lock: fromScript,
type: undefined,
});
});

test("withdraw with registered lock script", async (t) => {
const fromScript: Script = {
codeHash: nonSystemLockCodeHash,
hashType: "type",
args: "0x",
};

const nonSystemLockCell: Cell = {
cellOutput: {
capacity: parseUnit("5000000", "ckb").toHexString(),
lock: fromScript,
type: generateDaoTypeScript(LINA),
},
data: hexify(Uint64.pack(0)),
blockHash: hexify(randomBytes(32)),
blockNumber: "0x123456",
outPoint: { txHash: hexify(randomBytes(32)), index: "0x0" },
};
let txSkeleton = TransactionSkeleton({
cellProvider: new CellProvider([nonSystemLockCell]),
});

txSkeleton = await dao.withdraw(txSkeleton, nonSystemLockCell, undefined, {
enableNonSystemScript: true,
});

t.deepEqual(txSkeleton.inputs.get(-1), nonSystemLockCell);
t.deepEqual(txSkeleton.outputs.get(-1), {
...nonSystemLockCell,
data: hexify(Uint64.pack(0x123456)),
});
});

const generateDaoTypeScript = (config: Config): Script => {
return {
codeHash: config.SCRIPTS.DAO!.CODE_HASH,
hashType: config.SCRIPTS.DAO!.HASH_TYPE,
args: "0x",
};
};
52 changes: 51 additions & 1 deletion packages/common-scripts/tests/helper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import {
TransactionSkeletonType,
TransactionSkeleton,
Options,
} from "@ckb-lumos/helpers";
import { Cell, CellDep, blockchain } from "@ckb-lumos/base";
import {
Cell,
CellDep,
blockchain,
Script,
CellProvider,
QueryOptions,
CellCollector as BaseCellCollectorType,
} from "@ckb-lumos/base";
import { bytes } from "@ckb-lumos/codec";
import { CellCollectorType, FromInfo, parseFromInfo } from "../src";
import { Config, getConfig } from "@ckb-lumos/config-manager";

export interface txObject {
inputs: Cell[];
Expand Down Expand Up @@ -41,3 +52,42 @@ export function txSkeletonFromJson(

return skeleton;
}

export class TestCellCollector implements CellCollectorType {
readonly fromScript: Script;
private readonly config: Config;
private cellCollector: BaseCellCollectorType;

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

this.config = config;

queryOptions = {
...queryOptions,
lock: this.fromScript,
type: queryOptions.type || "empty",
};

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

async *collect(): AsyncGenerator<Cell> {
for await (const inputCell of this.cellCollector.collect()) {
yield inputCell;
}
}
}

2 comments on commit ad483d9

@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-ad483d9-20240125131247

npm install @ckb-lumos/[email protected]

@vercel
Copy link

@vercel vercel bot commented on ad483d9 Jan 25, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.