Skip to content

Commit 9382ee2

Browse files
committed
Refactor ckb-tthw-js
1 parent ccd25f3 commit 9382ee2

9 files changed

Lines changed: 4589 additions & 504 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ https://github.com/Flouse/ckb-tthw/blob/ada7d3729b0f2d360e86bd8a6ed6da40397f98bb
1616

1717
## TODO
1818
- [ ] implement interactive tutorial experiences
19+
1920
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.
2021

2122
similar to

js/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ But if you don't, there's no need to worry, just follow this tutorial step by st
1111

1212
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`:
1313

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

1616
### Talk is cheap. Run the code.
1717

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

4040
1. Create a transaction skeleton that serves as a blueprint for the final transaction.
41-
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.
42-
2. Modify the transaction skeleton to include the necessary capacity to cover the output cell by injecting enough input cells.
43-
3. Pay the transaction fee by `payFeeByFeeRate` function, again, provided by Lumos.
41+
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.
42+
3. Modify the transaction skeleton to include the necessary capacity to cover the output cell by injecting enough input cells.
43+
4. Pay the transaction fee by `payFeeByFeeRate` function, again, provided by Lumos.
4444

4545
### Function `signAndSendTx`
4646
This function is self-explanatory:

js/helper.ts

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { hd, config, helpers, HexString, BI, Indexer, Address } from '@ckb-lumos/lumos';
1+
import {
2+
hd, config, helpers as lumosHelpers, HexString, BI, Indexer, Address, Script, Cell, Transaction, WitnessArgs
3+
} from '@ckb-lumos/lumos';
4+
import { blockchain } from '@ckb-lumos/base'
5+
import { bytes } from "@ckb-lumos/codec";
26
import { Account } from './type';
37

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

1217
// get the address of CKB testnet from the private key
1318
export const getAddressByPrivateKey = (privateKey: HexString): Address => {
1419
const args = hd.key.privateKeyToBlake160(privateKey);
15-
const template = config.predefined.AGGRON4.SCRIPTS["SECP256K1_BLAKE160"]!;
20+
const template = TESTNET_SCRIPTS["SECP256K1_BLAKE160"]!;
1621
const lockScript = {
1722
codeHash: template.CODE_HASH,
1823
hashType: template.HASH_TYPE,
1924
args: args,
2025
};
2126

22-
return helpers.encodeToAddress(lockScript);
27+
return lumosHelpers.encodeToAddress(lockScript);
2328
}
2429

2530
// generate an Account from the private key
2631
export const generateAccountFromPrivateKey = (privateKey: string): Account => {
2732
const pubKey = hd.key.privateToPublic(privateKey);
2833
const args = hd.key.publicKeyToBlake160(pubKey);
29-
const template = config.predefined.AGGRON4.SCRIPTS["SECP256K1_BLAKE160"]!;
34+
const template = TESTNET_SCRIPTS["SECP256K1_BLAKE160"]!;
3035
const lockScript = {
3136
codeHash: template.CODE_HASH,
3237
hashType: template.HASH_TYPE,
3338
args: args,
3439
};
35-
const address = helpers.encodeToAddress(lockScript);
40+
const address = lumosHelpers.encodeToAddress(lockScript);
3641

3742
return {
3843
lockScript,
@@ -51,7 +56,7 @@ export const generateAccountFromPrivateKey = (privateKey: string): Account => {
5156
*/
5257
export async function getCapacities(address: string): Promise<BI> {
5358
const collector = ckbIndexer.collector({
54-
lock: helpers.parseAddress(address),
59+
lock: lumosHelpers.parseAddress(address),
5560
});
5661

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

70+
export async function capacityOf(lock: Script): Promise<BI> {
71+
const collector = ckbIndexer.collector({ lock });
72+
73+
let balance: BI = BI.from(0);
74+
for await (const cell of collector.collect()) {
75+
balance = balance.add(cell.cellOutput.capacity);
76+
}
77+
78+
return balance;
79+
}
80+
81+
/**
82+
* collect input cells with empty output data
83+
* @param lock The lock script protects the input cells
84+
* @param requiredCapacity The required capacity sum of the input cells
85+
*/
86+
export async function collectInputCells(
87+
lock: Script,
88+
requiredCapacity: bigint
89+
): Promise<Cell[]> {
90+
const collector = ckbIndexer.collector({
91+
lock,
92+
// filter cells by output data len range, [inclusive, exclusive)
93+
// data length range: [0, 1), which means the data length is 0
94+
outputDataLenRange: ["0x0", "0x1"]
95+
});
96+
97+
let _needCapacity = requiredCapacity;
98+
let collected: Cell[] = [];
99+
for await (const inputCell of collector.collect()) {
100+
collected.push(inputCell);
101+
_needCapacity -= BigInt(inputCell.cellOutput.capacity);
102+
if (_needCapacity <= 0) break;
103+
}
104+
105+
return collected;
106+
}
107+
108+
/**
109+
* the first witness of the fromAddress script has a WitnessArgs
110+
* constructed with 65-byte zero filled values
111+
*/
112+
export function addWitness(
113+
txSkeleton: lumosHelpers.TransactionSkeletonType,
114+
// TODO: fromScript: Script
115+
): lumosHelpers.TransactionSkeletonType {
116+
const firstLockInputIndex = 0;
117+
118+
/* 65-byte zeros in hex */
119+
const SECP_SIGNATURE_PLACEHOLDER =
120+
"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
121+
const newWitnessArgs: WitnessArgs = { lock: SECP_SIGNATURE_PLACEHOLDER };
122+
const witness = bytes.hexify(blockchain.WitnessArgs.pack(newWitnessArgs));
123+
124+
return txSkeleton.update("witnesses", (witnesses) =>
125+
witnesses.set(firstLockInputIndex, witness)
126+
);
127+
}
128+
129+
/**
130+
* Calculate transaction fee
131+
*
132+
* @param txSkeleton {@link lumosHelpers.TransactionSkeletonType}
133+
* @param feeRate how many shannons per KB charge
134+
* @returns fee, unit: shannons
135+
*
136+
* See https://github.com/nervosnetwork/ckb/wiki/Transaction-%C2%BB-Transaction-Fee
137+
*/
138+
export function calculateTxFee(
139+
txSkeleton: lumosHelpers.TransactionSkeletonType,
140+
feeRate: bigint
141+
): bigint {
142+
const tx: Transaction = lumosHelpers.createTransactionFromSkeleton(txSkeleton);
143+
const serializedTx = blockchain.Transaction.pack(tx);
144+
// 4 is serialized offset bytesize
145+
const txSize = BigInt(serializedTx.byteLength + 4);
146+
147+
const ratio = 1000n;
148+
const base = txSize * feeRate;
149+
const fee = base / ratio;
150+
return fee * ratio < base ? fee + 1n: fee;
151+
}
152+
65153
/**
66154
* Get faucet from https://github.com/Flouse/nervos-functions#faucet
67155
*/

js/index.ts

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Hash, Cell, RPC, commons, helpers as lumosHelpers, HexString, hd } from "@ckb-lumos/lumos";
2-
import { generateAccountFromPrivateKey, ckbIndexer, CKB_TESTNET_EXPLORER } from "./helper";
2+
import { minimalScriptCapacity } from "@ckb-lumos/helpers"
3+
import {
4+
CKB_TESTNET_EXPLORER, TESTNET_SCRIPTS,
5+
generateAccountFromPrivateKey, ckbIndexer, collectInputCells, calculateTxFee, addWitness
6+
} from "./helper";
37
import { CHARLIE } from "./test-keys";
4-
import { Account } from "./type";
8+
import { Account, CapacityUnit } from "./type";
59

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

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

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

2331
// FAQ: How do you set the value of capacity in a Cell?
2432
// See: https://docs.nervos.org/docs/essays/faq/#how-do-you-set-the-value-of-capacity-in-a-cell
25-
const targetCellCapacity = BigInt(8 + 32 + 20 + 1 + onChainMemo.length) * 100000000n;
33+
const minimalCellCapacity = minimalScriptCapacity(testAccount.lockScript) + 800000000n; // 8 CKB for Capacity field itself
34+
const targetCellCapacity = minimalCellCapacity + dataOccupiedCapacity;
35+
36+
// collect the sender's live input cells with enough CKB capacity
37+
const inputCells: Cell[] = await collectInputCells(
38+
testAccount.lockScript,
39+
// requiredCapacity = targetCellCapacity + minimalCellCapacity
40+
targetCellCapacity + minimalCellCapacity
41+
);
42+
const collectedCapacity = inputCells.reduce((acc: bigint, cell: Cell) => {
43+
return acc + BigInt(cell.cellOutput.capacity);
44+
}, 0n);
45+
46+
let txSkeleton = lumosHelpers.TransactionSkeleton({ cellProvider: ckbIndexer });
47+
// push the live input cells into the transaction's inputs array
48+
txSkeleton = txSkeleton.update("inputs", (inputs) => inputs.push(...inputCells));
49+
50+
// the transaction needs cellDeps to indicate the lockScript's code (SECP256K1_BLAKE160)
51+
txSkeleton = txSkeleton.update("cellDeps", (cellDeps) =>
52+
cellDeps.push({
53+
outPoint: {
54+
txHash: TESTNET_SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
55+
index: TESTNET_SCRIPTS.SECP256K1_BLAKE160.INDEX,
56+
},
57+
depType: TESTNET_SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
58+
})
59+
);
2660

27-
const targetOutput: Cell = {
61+
// push the output cells into the transaction's outputs array
62+
const targetCell: Cell = {
2863
cellOutput: {
2964
capacity: "0x" + targetCellCapacity.toString(16),
3065
// In this demo, we only want to write a message on chain, so we define the
@@ -33,29 +68,33 @@ const constructHelloWorldTx = async (
3368
},
3469
data: onChainMemoHex,
3570
};
36-
// push the target output cell into the transaction's outputs array
37-
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(targetOutput));
38-
39-
// FIXME: The data of the input cells should be empty => don't inject memo cells
40-
txSkeleton = await injectCapacity(
41-
txSkeleton,
42-
[testAccount.address],
43-
targetCellCapacity,
44-
undefined,
45-
undefined,
46-
{
47-
enableDeductCapacity: false
48-
}
49-
);
50-
txSkeleton = await payFeeByFeeRate(txSkeleton, [testAccount.address], 1000, undefined, {
51-
enableDeductCapacity: false
71+
const changeCell: Cell = {
72+
cellOutput: {
73+
capacity: "0x" + (collectedCapacity - targetCellCapacity).toString(16),
74+
lock: testAccount.lockScript,
75+
},
76+
data: "0x",
77+
};
78+
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(...[targetCell, changeCell]));
79+
80+
// add witness placeholder for the skeleton, it helps in transaction fee estimation
81+
txSkeleton = addWitness(txSkeleton);
82+
83+
const fee: bigint = calculateTxFee(txSkeleton, 1002n /** fee rate */);
84+
// fee = sum(all input cells' capacity) - sum(all output cells' capacity),
85+
// therefore the changeCell's capacity is reduced to cover the transaction fee
86+
txSkeleton = txSkeleton.update("outputs", (outputs) => {
87+
if (outputs.size < 2) throw new Error("outputs.size < 2");
88+
const changeCellCapacity = collectedCapacity - targetCellCapacity - fee;
89+
changeCell.cellOutput.capacity = "0x" + changeCellCapacity.toString(16)
90+
return outputs.set(-1, changeCell);
5291
});
5392

5493
console.debug(`txSkeleton: ${JSON.stringify(txSkeleton, undefined, 2)}`);
5594
return txSkeleton;
5695
}
57-
58-
/** Sign the prepared transaction skeleton, then send it to CKB. */
96+
97+
/** sign the prepared transaction skeleton, then send it to a CKB node. */
5998
const signAndSendTx = async (
6099
txSkeleton: lumosHelpers.TransactionSkeletonType,
61100
privateKey: HexString,
@@ -66,8 +105,8 @@ const signAndSendTx = async (
66105
const message = txSkeleton.get('signingEntries').get(0)?.message;
67106

68107
// sign the transaction with the private key
69-
const Sig = hd.key.signRecoverable(message!, privateKey);
70-
const signedTx = lumosHelpers.sealTransaction(txSkeleton, [Sig]);
108+
const sig = hd.key.signRecoverable(message!, privateKey);
109+
const signedTx = lumosHelpers.sealTransaction(txSkeleton, [sig]);
71110

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

0 commit comments

Comments
 (0)