Skip to content

Commit

Permalink
Refactor ckb-tthw-js
Browse files Browse the repository at this point in the history
  • Loading branch information
Flouse committed May 8, 2023
1 parent ccd25f3 commit 9382ee2
Show file tree
Hide file tree
Showing 9 changed files with 4,589 additions and 504 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ https://github.com/Flouse/ckb-tthw/blob/ada7d3729b0f2d360e86bd8a6ed6da40397f98bb

## TODO
- [ ] implement interactive tutorial experiences

In this tutorial, a browser-based runtime called [WebContainers](https://webcontainers.io/) is leveraged to create a minimal development environment only in the browser to acheive interactive tutorial experiences. Let's use the latest web capabilities to deliver a nice browser-based development experience for a new generation of interactive courses.

similar to
Expand Down
8 changes: 4 additions & 4 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ But if you don't, there's no need to worry, just follow this tutorial step by st

Although some of the complexity is wrapped up, intuitively writing "Hello Common Knowledge Base!" into a cell on CKB testnet is really just `three steps`:

https://github.com/Flouse/ckb-tthw/blob/ada7d3729b0f2d360e86bd8a6ed6da40397f98bb/js/index.ts#L86-L94
https://github.com/Flouse/ckb-tthw/blob/ada7d3729b0f2d360e86bd8a6ed6da40397f98bb/js/index.ts#L86-L97

### Talk is cheap. Run the code.

Expand All @@ -38,9 +38,9 @@ Let's dive into two functions that take up most of the code space. The [code and
This function creates a new transaction that adds a cell with the proposed on-chain message.

1. Create a transaction skeleton that serves as a blueprint for the final transaction.
Define the output cell, which includes the capacity and lock script, and add it to the transaction skeleton, which is a mutable data structure used to construct a CKB transaction incrementally.
2. Modify the transaction skeleton to include the necessary capacity to cover the output cell by injecting enough input cells.
3. Pay the transaction fee by `payFeeByFeeRate` function, again, provided by Lumos.
2. Define the output cell, which includes the capacity and lock script, and add it to the transaction skeleton, which is a mutable data structure used to construct a CKB transaction incrementally.
3. Modify the transaction skeleton to include the necessary capacity to cover the output cell by injecting enough input cells.
4. Pay the transaction fee by `payFeeByFeeRate` function, again, provided by Lumos.

### Function `signAndSendTx`
This function is self-explanatory:
Expand Down
100 changes: 94 additions & 6 deletions js/helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { hd, config, helpers, HexString, BI, Indexer, Address } from '@ckb-lumos/lumos';
import {
hd, config, helpers as lumosHelpers, HexString, BI, Indexer, Address, Script, Cell, Transaction, WitnessArgs
} from '@ckb-lumos/lumos';
import { blockchain } from '@ckb-lumos/base'
import { bytes } from "@ckb-lumos/codec";
import { Account } from './type';

export const CKB_TESTNET_EXPLORER = "https://pudge.explorer.nervos.org";
Expand All @@ -8,31 +12,32 @@ export const ckbIndexer = new Indexer(CKB_TESTNET_RPC);
// This tutorial uses CKB testnet.
// CKB Testnet Explorer: https://pudge.explorer.nervos.org
config.initializeConfig(config.predefined.AGGRON4);
export const TESTNET_SCRIPTS = config.predefined.AGGRON4.SCRIPTS;

// get the address of CKB testnet from the private key
export const getAddressByPrivateKey = (privateKey: HexString): Address => {
const args = hd.key.privateKeyToBlake160(privateKey);
const template = config.predefined.AGGRON4.SCRIPTS["SECP256K1_BLAKE160"]!;
const template = TESTNET_SCRIPTS["SECP256K1_BLAKE160"]!;
const lockScript = {
codeHash: template.CODE_HASH,
hashType: template.HASH_TYPE,
args: args,
};

return helpers.encodeToAddress(lockScript);
return lumosHelpers.encodeToAddress(lockScript);
}

// generate an Account from the private key
export const generateAccountFromPrivateKey = (privateKey: string): Account => {
const pubKey = hd.key.privateToPublic(privateKey);
const args = hd.key.publicKeyToBlake160(pubKey);
const template = config.predefined.AGGRON4.SCRIPTS["SECP256K1_BLAKE160"]!;
const template = TESTNET_SCRIPTS["SECP256K1_BLAKE160"]!;
const lockScript = {
codeHash: template.CODE_HASH,
hashType: template.HASH_TYPE,
args: args,
};
const address = helpers.encodeToAddress(lockScript);
const address = lumosHelpers.encodeToAddress(lockScript);

return {
lockScript,
Expand All @@ -51,7 +56,7 @@ export const generateAccountFromPrivateKey = (privateKey: string): Account => {
*/
export async function getCapacities(address: string): Promise<BI> {
const collector = ckbIndexer.collector({
lock: helpers.parseAddress(address),
lock: lumosHelpers.parseAddress(address),
});

let capacities = BI.from(0);
Expand All @@ -62,6 +67,89 @@ export async function getCapacities(address: string): Promise<BI> {
return capacities;
}

export async function capacityOf(lock: Script): Promise<BI> {
const collector = ckbIndexer.collector({ lock });

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

return balance;
}

/**
* collect input cells with empty output data
* @param lock The lock script protects the input cells
* @param requiredCapacity The required capacity sum of the input cells
*/
export async function collectInputCells(
lock: Script,
requiredCapacity: bigint
): Promise<Cell[]> {
const collector = ckbIndexer.collector({
lock,
// filter cells by output data len range, [inclusive, exclusive)
// data length range: [0, 1), which means the data length is 0
outputDataLenRange: ["0x0", "0x1"]
});

let _needCapacity = requiredCapacity;
let collected: Cell[] = [];
for await (const inputCell of collector.collect()) {
collected.push(inputCell);
_needCapacity -= BigInt(inputCell.cellOutput.capacity);
if (_needCapacity <= 0) break;
}

return collected;
}

/**
* the first witness of the fromAddress script has a WitnessArgs
* constructed with 65-byte zero filled values
*/
export function addWitness(
txSkeleton: lumosHelpers.TransactionSkeletonType,
// TODO: fromScript: Script
): lumosHelpers.TransactionSkeletonType {
const firstLockInputIndex = 0;

/* 65-byte zeros in hex */
const SECP_SIGNATURE_PLACEHOLDER =
"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
const newWitnessArgs: WitnessArgs = { lock: SECP_SIGNATURE_PLACEHOLDER };
const witness = bytes.hexify(blockchain.WitnessArgs.pack(newWitnessArgs));

return txSkeleton.update("witnesses", (witnesses) =>
witnesses.set(firstLockInputIndex, witness)
);
}

/**
* Calculate transaction fee
*
* @param txSkeleton {@link lumosHelpers.TransactionSkeletonType}
* @param feeRate how many shannons per KB charge
* @returns fee, unit: shannons
*
* See https://github.com/nervosnetwork/ckb/wiki/Transaction-%C2%BB-Transaction-Fee
*/
export function calculateTxFee(
txSkeleton: lumosHelpers.TransactionSkeletonType,
feeRate: bigint
): bigint {
const tx: Transaction = lumosHelpers.createTransactionFromSkeleton(txSkeleton);
const serializedTx = blockchain.Transaction.pack(tx);
// 4 is serialized offset bytesize
const txSize = BigInt(serializedTx.byteLength + 4);

const ratio = 1000n;
const base = txSize * feeRate;
const fee = base / ratio;
return fee * ratio < base ? fee + 1n: fee;
}

/**
* Get faucet from https://github.com/Flouse/nervos-functions#faucet
*/
Expand Down
93 changes: 66 additions & 27 deletions js/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Hash, Cell, RPC, commons, helpers as lumosHelpers, HexString, hd } from "@ckb-lumos/lumos";
import { generateAccountFromPrivateKey, ckbIndexer, CKB_TESTNET_EXPLORER } from "./helper";
import { minimalScriptCapacity } from "@ckb-lumos/helpers"
import {
CKB_TESTNET_EXPLORER, TESTNET_SCRIPTS,
generateAccountFromPrivateKey, ckbIndexer, collectInputCells, calculateTxFee, addWitness
} from "./helper";
import { CHARLIE } from "./test-keys";
import { Account } from "./type";
import { Account, CapacityUnit } from "./type";

// get a test key used for demo purposes
const testPrivKey = CHARLIE.PRIVATE_KEY;
Expand All @@ -10,21 +14,52 @@ const testPrivKey = CHARLIE.PRIVATE_KEY;
const testAccount: Account = generateAccountFromPrivateKey(testPrivKey);
console.assert(testAccount.address === CHARLIE.ADDRESS);

/** create a new transaction that adds a cell with the message "Hello Common Knowledge Base!" */
/**
* create a new transaction that adds a cell with a given message
* @param onChainMemo The message to be sent writted in the target cell
* See https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md
*/
const constructHelloWorldTx = async (
onChainMemo: string
): Promise<lumosHelpers.TransactionSkeletonType> => {
const onChainMemoHex: string = "0x" + Buffer.from(onChainMemo).toString("hex");
console.log(`onChainMemoHex: ${onChainMemoHex}`);

const { injectCapacity, payFeeByFeeRate } = commons.common;
let txSkeleton = lumosHelpers.TransactionSkeleton({ cellProvider: ckbIndexer });
// CapacityUnit.Byte = 100000000, because 1 CKB = 100000000 shannon
const dataOccupiedCapacity = BigInt(CapacityUnit.Byte * onChainMemo.length);

// FAQ: How do you set the value of capacity in a Cell?
// See: https://docs.nervos.org/docs/essays/faq/#how-do-you-set-the-value-of-capacity-in-a-cell
const targetCellCapacity = BigInt(8 + 32 + 20 + 1 + onChainMemo.length) * 100000000n;
const minimalCellCapacity = minimalScriptCapacity(testAccount.lockScript) + 800000000n; // 8 CKB for Capacity field itself
const targetCellCapacity = minimalCellCapacity + dataOccupiedCapacity;

// collect the sender's live input cells with enough CKB capacity
const inputCells: Cell[] = await collectInputCells(
testAccount.lockScript,
// requiredCapacity = targetCellCapacity + minimalCellCapacity
targetCellCapacity + minimalCellCapacity
);
const collectedCapacity = inputCells.reduce((acc: bigint, cell: Cell) => {
return acc + BigInt(cell.cellOutput.capacity);
}, 0n);

let txSkeleton = lumosHelpers.TransactionSkeleton({ cellProvider: ckbIndexer });
// push the live input cells into the transaction's inputs array
txSkeleton = txSkeleton.update("inputs", (inputs) => inputs.push(...inputCells));

// the transaction needs cellDeps to indicate the lockScript's code (SECP256K1_BLAKE160)
txSkeleton = txSkeleton.update("cellDeps", (cellDeps) =>
cellDeps.push({
outPoint: {
txHash: TESTNET_SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
index: TESTNET_SCRIPTS.SECP256K1_BLAKE160.INDEX,
},
depType: TESTNET_SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
})
);

const targetOutput: Cell = {
// push the output cells into the transaction's outputs array
const targetCell: Cell = {
cellOutput: {
capacity: "0x" + targetCellCapacity.toString(16),
// In this demo, we only want to write a message on chain, so we define the
Expand All @@ -33,29 +68,33 @@ const constructHelloWorldTx = async (
},
data: onChainMemoHex,
};
// push the target output cell into the transaction's outputs array
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(targetOutput));

// FIXME: The data of the input cells should be empty => don't inject memo cells
txSkeleton = await injectCapacity(
txSkeleton,
[testAccount.address],
targetCellCapacity,
undefined,
undefined,
{
enableDeductCapacity: false
}
);
txSkeleton = await payFeeByFeeRate(txSkeleton, [testAccount.address], 1000, undefined, {
enableDeductCapacity: false
const changeCell: Cell = {
cellOutput: {
capacity: "0x" + (collectedCapacity - targetCellCapacity).toString(16),
lock: testAccount.lockScript,
},
data: "0x",
};
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(...[targetCell, changeCell]));

// add witness placeholder for the skeleton, it helps in transaction fee estimation
txSkeleton = addWitness(txSkeleton);

const fee: bigint = calculateTxFee(txSkeleton, 1002n /** fee rate */);
// fee = sum(all input cells' capacity) - sum(all output cells' capacity),
// therefore the changeCell's capacity is reduced to cover the transaction fee
txSkeleton = txSkeleton.update("outputs", (outputs) => {
if (outputs.size < 2) throw new Error("outputs.size < 2");
const changeCellCapacity = collectedCapacity - targetCellCapacity - fee;
changeCell.cellOutput.capacity = "0x" + changeCellCapacity.toString(16)
return outputs.set(-1, changeCell);
});

console.debug(`txSkeleton: ${JSON.stringify(txSkeleton, undefined, 2)}`);
return txSkeleton;
}

/** Sign the prepared transaction skeleton, then send it to CKB. */
/** sign the prepared transaction skeleton, then send it to a CKB node. */
const signAndSendTx = async (
txSkeleton: lumosHelpers.TransactionSkeletonType,
privateKey: HexString,
Expand All @@ -66,8 +105,8 @@ const signAndSendTx = async (
const message = txSkeleton.get('signingEntries').get(0)?.message;

// sign the transaction with the private key
const Sig = hd.key.signRecoverable(message!, privateKey);
const signedTx = lumosHelpers.sealTransaction(txSkeleton, [Sig]);
const sig = hd.key.signRecoverable(message!, privateKey);
const signedTx = lumosHelpers.sealTransaction(txSkeleton, [sig]);

// create a new RPC instance pointing to CKB testnet
const rpc = new RPC("https://testnet.ckb.dev/rpc");
Expand Down
Loading

0 comments on commit 9382ee2

Please sign in to comment.