Skip to content

Commit

Permalink
feat(common-scripts): integrete omnilock p2pkh (ckb-js#405)
Browse files Browse the repository at this point in the history
* chore(lumos): add pr template

* feat: support omnilock

* test: add test cases for omnilock

* Revert "chore(lumos): add pr template"

This reverts commit 2e78b59.

* chore: fix CI

* chore: changes to the comments

* chore: use sealTransaction to put signature to witnesses

* fix: can't resolve stream in browser
  • Loading branch information
zhangyouxin authored Sep 16, 2022
1 parent 73af0bc commit 09bd023
Show file tree
Hide file tree
Showing 14 changed files with 706 additions and 139 deletions.
4 changes: 2 additions & 2 deletions examples/omni-lock-secp256k1-blake160/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export function App() {
const pubkeyHash = key.privateKeyToBlake160(pk);

const omniLock: Script = {
codeHash: CONFIG.SCRIPTS.OMNI_LOCK.CODE_HASH,
hashType: CONFIG.SCRIPTS.OMNI_LOCK.HASH_TYPE,
codeHash: CONFIG.SCRIPTS.OMNILOCK.CODE_HASH,
hashType: CONFIG.SCRIPTS.OMNILOCK.HASH_TYPE,
// omni flag pubkey hash omni lock flags
// chain identity eth addr function flag()
// 00: Nervos 👇 00: owner
Expand Down
161 changes: 30 additions & 131 deletions examples/omni-lock-secp256k1-blake160/lib.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import { bytes } from '@ckb-lumos/codec';
import { blockchain } from '@ckb-lumos/base'
import { BI, Cell, config, helpers, Indexer, RPC, utils, commons, hd, Hash } from "@ckb-lumos/lumos";
import { SerializeRcLockWitnessLock } from "./generated/omni";

export const CONFIG = config.createConfig({
PREFIX: "ckt",
SCRIPTS: {
...config.predefined.AGGRON4.SCRIPTS,
// for more about Omni lock, please check https://github.com/XuJiandong/docs-bank/blob/master/omni_lock.md
OMNI_LOCK: {
CODE_HASH: "0x79f90bb5e892d80dd213439eeab551120eb417678824f282b4ffb5f21bad2e1e",
HASH_TYPE: "type",
TX_HASH: "0x9154df4f7336402114d04495175b37390ce86a4906d2d4001cf02c3e6d97f39c",
INDEX: "0x0",
DEP_TYPE: "code",
},
},
});
import { Transaction } from "@ckb-lumos/base";
import { bytes } from "@ckb-lumos/codec";
import { blockchain } from "@ckb-lumos/base";
import { BI, config, helpers, Indexer, RPC, commons, hd, Hash } from "@ckb-lumos/lumos";

export const CONFIG = config.predefined.AGGRON4;
config.initializeConfig(CONFIG);

const CKB_RPC_URL = "https://testnet.ckb.dev/rpc";
const CKB_INDEXER_URL = "https://testnet.ckb.dev/indexer";
const rpc = new RPC(CKB_RPC_URL);
Expand All @@ -28,141 +13,55 @@ const indexer = new Indexer(CKB_INDEXER_URL, CKB_RPC_URL);
export function asyncSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export interface TransferOptions {
from: string;
to: string;
amount: string;
}

const SECP_SIGNATURE_PLACEHOLDER = (
"0x" +
"00".repeat(
SerializeRcLockWitnessLock({
signature: new Uint8Array(65),
}).byteLength
)
);

export async function buildTransfer(options: TransferOptions) {
let tx = helpers.TransactionSkeleton({});
let txSkeleton = helpers.TransactionSkeleton({ cellProvider: indexer });
const fromScript = helpers.parseAddress(options.from);
const fromAddress = helpers.encodeToAddress(fromScript, { config: CONFIG });
const toScript = helpers.parseAddress(options.to);

// additional 1 ckb for tx fee
// the tx fee could calculated by tx size
// this is just a simple example
const neededCapacity = BI.from(options.amount).add(100000000);
let collectedSum = BI.from(0);
const collectedCells: Cell[] = [];
const collector = indexer.collector({ lock: fromScript, type: "empty" });
for await (const cell of collector.collect()) {
collectedSum = collectedSum.add(cell.cellOutput.capacity);
collectedCells.push(cell);
if (BI.from(collectedSum).gte(neededCapacity)) break;
}

if (collectedSum.lt(neededCapacity)) {
throw new Error(`Not enough CKB, expected: ${neededCapacity}, actual: ${collectedSum} `);
}

const transferOutput: Cell = {
cellOutput: {
capacity: BI.from(options.amount).toHexString(),
lock: toScript,
},
data: "0x",
};

const changeOutput: Cell = {
cellOutput: {
capacity: collectedSum.sub(neededCapacity).toHexString(),
lock: fromScript,
},
data: "0x",
};

tx = tx.update("inputs", (inputs) => inputs.push(...collectedCells));
tx = tx.update("outputs", (outputs) => outputs.push(transferOutput, changeOutput));
tx = tx.update("cellDeps", (cellDeps) =>
cellDeps.push(
// omni lock dep
{
outPoint: {
txHash: CONFIG.SCRIPTS.OMNI_LOCK.TX_HASH,
index: CONFIG.SCRIPTS.OMNI_LOCK.INDEX,
},
depType: CONFIG.SCRIPTS.OMNI_LOCK.DEP_TYPE,
},
// SECP256K1 lock is depended by omni lock
{
outPoint: {
txHash: CONFIG.SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
index: CONFIG.SCRIPTS.SECP256K1_BLAKE160.INDEX,
},
depType: CONFIG.SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
}
)
const toAddress = helpers.encodeToAddress(toScript, { config: CONFIG });
txSkeleton = await commons.common.transfer(
txSkeleton,
[fromAddress],
toAddress,
BigInt(70 * 10 ** 8),
undefined,
undefined,
{ config: CONFIG }
);

const newWitnessArgs = { lock: SECP_SIGNATURE_PLACEHOLDER };
const witness = bytes.hexify(blockchain.WitnessArgs.pack(newWitnessArgs))

// fill txSkeleton's witness with 0
for (let i = 0; i < tx.inputs.toArray().length; i++) {
tx = tx.update("witnesses", (witnesses) => witnesses.push(witness));
}

return tx;
}

export function toMessages(tx: helpers.TransactionSkeletonType) {
const hasher = new utils.CKBHasher();

// locks you want to sign
const signLock = tx.inputs.get(0)?.cellOutput.lock!;

const messageGroup = commons.createP2PKHMessageGroup(tx, [signLock], {
hasher: {
update: (message) => hasher.update(message.buffer),
digest: () => new Uint8Array(bytes.bytify(hasher.digestHex())),
},
});

return messageGroup[0];
txSkeleton = await commons.common.payFee(txSkeleton, [fromAddress], 1000, undefined, { config: CONFIG });
return txSkeleton;
}


export async function signByPrivateKey(txSkeleton: helpers.TransactionSkeletonType, privateKey: string) {
const messages = toMessages(txSkeleton)

const signature = hd.key.signRecoverable(messages.message, privateKey);

const signedWitness = bytes.hexify(blockchain.WitnessArgs.pack({
lock: bytes.hexify(SerializeRcLockWitnessLock({
signature: bytes.bytify(signature),
})),
}))

txSkeleton = txSkeleton.update("witnesses", (witnesses) => witnesses.set(0, signedWitness));

return txSkeleton;
txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
const message = txSkeleton.get("signingEntries").get(0)!.message;
const signature = hd.key.signRecoverable(message, privateKey);
const packedSignature = bytes.hexify(
commons.omnilock.OmnilockWitnessLock.pack({
signature: signature,
})
);
const signedTx = helpers.sealTransaction(txSkeleton, [packedSignature]);
return signedTx;
}

export async function sendTransaction(tx: helpers.TransactionSkeletonType): Promise<Hash> {
const signedTx = helpers.createTransactionFromSkeleton(tx);
return rpc.sendTransaction(signedTx, 'passthrough');
export async function sendTransaction(tx: Transaction): Promise<Hash> {
return rpc.sendTransaction(tx, "passthrough");
}

export async function capacityOf(address: string): Promise<BI> {
const collector = indexer.collector({
lock: helpers.parseAddress(address),
});

let balance = BI.from(0);
for await (const cell of collector.collect()) {
balance = balance.add(cell.cellOutput.capacity);
}

return balance;
}
3 changes: 2 additions & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"parcel": "^2.2.1"
},
"alias": {
"process": false
"process": false,
"stream": false
}
}
83 changes: 83 additions & 0 deletions packages/common-scripts/examples/omnilock/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { bytes } from "@ckb-lumos/codec";
import {
TransactionSkeleton,
encodeToAddress,
sealTransaction,
} from "@ckb-lumos/helpers";
import { key } from "@ckb-lumos/hd";
import { initializeConfig, predefined } from "@ckb-lumos/config-manager/lib";
import { common, omnilock } from "@ckb-lumos/common-scripts";
import { Script } from "@ckb-lumos/base";
import { Indexer } from "@ckb-lumos/ckb-indexer";
import { RPC } from "@ckb-lumos/rpc";

const CKB_RPC_URL = "https://testnet.ckb.dev/rpc";
const CKB_INDEXER_URL = "https://testnet.ckb.dev/indexer";
const rpc = new RPC(CKB_RPC_URL);
const indexer = new Indexer(CKB_INDEXER_URL, CKB_RPC_URL);

async function main() {
let txSkeleton = TransactionSkeleton({ cellProvider: indexer });
const ckbConfig = predefined.AGGRON4;
initializeConfig(ckbConfig);
const ALICE_PRIVKEY =
"0x1234567812345678123456781234567812345678123456781234567812345678";
const aliceArgs = key.privateKeyToBlake160(ALICE_PRIVKEY);
console.log("alice args is:", aliceArgs);

const aliceOmnilock: Script = omnilock.createOmnilockScript({
auth: {
flag: "SECP256K1_BLAKE160",
content: aliceArgs,
},
});
const aliceSecplock: Script = {
codeHash: ckbConfig.SCRIPTS.SECP256K1_BLAKE160.CODE_HASH,
hashType: ckbConfig.SCRIPTS.SECP256K1_BLAKE160.HASH_TYPE,
args: aliceArgs,
};

console.log("aliceOmnilock is:", aliceOmnilock);
const aliceOmnilockAddress = encodeToAddress(aliceOmnilock, {
config: ckbConfig,
});
const aliceSecplockAddress = encodeToAddress(aliceSecplock, {
config: ckbConfig,
});

console.log("aliceOmnilockAddress is:", aliceOmnilockAddress);

txSkeleton = await common.transfer(
txSkeleton,
[aliceOmnilockAddress],
aliceSecplockAddress,
BigInt(70 * 10 ** 8),
undefined,
undefined,
{ config: ckbConfig }
);

txSkeleton = await common.payFee(
txSkeleton,
[aliceOmnilockAddress],
1000,
undefined,
{ config: ckbConfig }
);

txSkeleton = common.prepareSigningEntries(txSkeleton, { config: ckbConfig });

const message = txSkeleton.get("signingEntries").get(0)!.message;

const sig = key.signRecoverable(message, ALICE_PRIVKEY);
const omnilockSig = bytes.hexify(
omnilock.OmnilockWitnessLock.pack({ signature: sig })
);

const tx = sealTransaction(txSkeleton, [omnilockSig]);
const hash = await rpc.sendTransaction(tx, "passthrough");
console.log("tx is:", tx);
console.log("The transaction hash is", hash);
return hash;
}
main();
1 change: 1 addition & 0 deletions packages/common-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
]
},
"devDependencies": {
"@ckb-lumos/debugger": "0.19.0-alpha.3",
"@types/keccak": "^3.0.1",
"keccak": "^3.0.1"
},
Expand Down
11 changes: 11 additions & 0 deletions packages/common-scripts/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Set } from "immutable";
import { isAcpScript } from "./helper";
import { BI, BIish } from "@ckb-lumos/bi";
import { CellCollectorConstructor } from "./type";
import omnilock from "./omnilock";

function defaultLogger(level: string, message: string) {
console.log(`[${level}] ${message}`);
Expand Down Expand Up @@ -107,6 +108,7 @@ function generateLockScriptInfos({ config = undefined }: Options = {}): void {
const secpTemplate = config?.SCRIPTS.SECP256K1_BLAKE160;
const multisigTemplate = config?.SCRIPTS.SECP256K1_BLAKE160_MULTISIG;
const acpTemplate = config?.SCRIPTS.ANYONE_CAN_PAY;
const omnilockTemplate = config?.SCRIPTS.OMNILOCK;

const predefinedInfos: LockScriptInfo[] = [];

Expand Down Expand Up @@ -145,6 +147,15 @@ function generateLockScriptInfos({ config = undefined }: Options = {}): void {
} else {
defaultLogger("warn", "ANYONE_CAN_PAY script info not found in config!");
}
if (omnilockTemplate) {
predefinedInfos.push({
codeHash: omnilockTemplate.CODE_HASH,
hashType: omnilockTemplate.HASH_TYPE,
lockScriptInfo: omnilock,
});
} else {
defaultLogger("warn", "OMNILOCK script info not found in config!");
}

return predefinedInfos;
};
Expand Down
Loading

0 comments on commit 09bd023

Please sign in to comment.