-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.ts
137 lines (115 loc) · 5.67 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { Hash, Cell, RPC, commons, helpers as lumosHelpers, HexString, hd } from "@ckb-lumos/lumos";
import { minimalScriptCapacity } from "@ckb-lumos/helpers"
import {
CKB_TESTNET_EXPLORER, TESTNET_SCRIPTS, encodeStringToHex,
generateAccountFromPrivateKey, ckbIndexer, collectInputCells, calculateTxFee, addWitness
} from "./helper";
import { CHARLIE } from "./test-keys";
import { Account, CapacityUnit } from "./type";
// get a test key used for demo purposes
const testPrivKey = CHARLIE.PRIVATE_KEY;
// get the account info from the test private key
const testAccount: Account = generateAccountFromPrivateKey(testPrivKey);
console.assert(testAccount.address === CHARLIE.ADDRESS);
/**
* 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: HexString = encodeStringToHex(onChainMemo);
console.log(`onChainMemoHex: ${onChainMemoHex}`);
// 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 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,
})
);
// 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
// target lock script to be the test account itself.
lock: testAccount.lockScript, // toScript
},
data: onChainMemoHex,
};
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 a CKB node. */
const signAndSendTx = async (
txSkeleton: lumosHelpers.TransactionSkeletonType,
privateKey: HexString,
): Promise<Hash> => {
const { prepareSigningEntries } = commons.common;
txSkeleton = prepareSigningEntries(txSkeleton);
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]);
// create a new RPC instance pointing to CKB testnet
const rpc = new RPC("https://testnet.ckb.dev/rpc");
// send the transaction to CKB node
const txHash = await rpc.sendTransaction(signedTx);
return txHash;
}
(async () => {
// Let's use Charlie's account as a test account which is only for demo purposes,
// please DO NOT use it in production environments!
console.log(`Charlie's account: ${JSON.stringify(testAccount, undefined, 2)}`);
console.log(`Explorer: ${CKB_TESTNET_EXPLORER}/address/${testAccount.address}\n\n`);
// Step 1: this is the message that will be written on chain
const onChainMemo: string = "Hello Common Knowledge Base!";
// Step 2: construct the transaction
let txSkeleton = await constructHelloWorldTx(onChainMemo);
// Step 3: sign and send the transaction
const txHash = await signAndSendTx(txSkeleton, testPrivKey);
console.log(`Transaction ${txHash} sent.\n`);
// Done, let's see the transaction in CKB Testnet Explorer
console.log(`See ${CKB_TESTNET_EXPLORER}/transaction/${txHash}`);
})();