@offckb/cli 0.1.0-rc4 → 0.1.0-rc6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,35 @@
1
1
  ## OffCKB
2
2
 
3
- CKB local development environment for your first try
3
+ [![npm](https://img.shields.io/npm/v/@offckb/cli.svg?maxAge=1000)](https://www.npmjs.com/package/@offckb/cli)
4
+ [![CI](https://github.com/retricsu/offckb/actions/workflows/node.js.yml/badge.svg)](https://github.com/retricsu/offckb/actions/workflows/node.js.yml)
5
+ [![npm](https://img.shields.io/npm/dt/@offckb/cli.svg?maxAge=1000)](https://www.npmjs.com/package/@offckb/cli)
6
+ [![npm](https://img.shields.io/npm/l/@offckb/cli.svg?maxAge=1000)](https://github.com/jeffijoe/@offckb/cli/blob/master/LICENSE.md)
7
+ [![node](https://img.shields.io/node/v/@offckb/cli.svg?maxAge=1000)](https://www.npmjs.com/package/@offckb/cli)
8
+
9
+ **CKB local development network for your first try.**
10
+
11
+ - One-line command to start a devnet
12
+ - no docker required
13
+ - pre-funded test accounts
14
+ - packed with the most useful scripts like Omnilock and Spore-contract
15
+ - multiple minimal Dapp templates to learn and get your hands dirty
16
+
17
+ Start building on Nervos blockchain, right now, right away!
18
+
19
+ ## Table of Contents
20
+
21
+ - [OffCKB](#offckb)
22
+ - [Table of Contents](#table-of-contents)
23
+ - [Install](#install)
24
+ - [Usage](#usage)
25
+ - [Get started](#get-started)
26
+ - [Step 1: Create A Project](#step-1-create-a-project)
27
+ - [Step 2: Start the Devnet](#step-2-start-the-devnet)
28
+ - [Step 3: Access Pre-funded Accounts](#step-3-access-pre-funded-accounts)
29
+ - [Built-in scripts](#built-in-scripts)
30
+ - [Accounts](#accounts)
31
+ - [About Lumos](#about-lumos)
32
+ - [Contributing](#contributing)
4
33
 
5
34
  ## Install
6
35
 
@@ -20,7 +49,9 @@ offckb accounts # list 20 accounts info with prefund CKB tokens
20
49
  offckb list-hashes # list built-in scripts hashes, equals `ckb list-hashes`
21
50
  ```
22
51
 
23
- ### Get started
52
+ ## Get started
53
+
54
+ ### Step 1: Create A Project
24
55
 
25
56
  ```sh
26
57
  offckb init my-awesome-ckb-dapp
@@ -44,21 +75,53 @@ Server running at http://localhost:1234
44
75
  ✨ Built in 10ms
45
76
  ```
46
77
 
47
- open another terminal and start the devnet:
78
+ ### Step 2: Start the Devnet
79
+
80
+ Open another terminal and run:
48
81
 
49
82
  ```sh
50
83
  offckb node
84
+
85
+ # result
86
+ # ...
87
+ CKB-Miner: 2024-03-04 14:35:12.563 +00:00 client INFO ckb_miner::miner Found! #3181 0x3749481a320824fe21077eaa8ec9d024a7b62d031720c27c1ef1681e8ab349e8
88
+
89
+ CKB-Miner: 2024-03-04 14:35:17.567 +00:00 client INFO ckb_miner::miner Found! #3184 0xa612a9ea35f292a6473e23e88856283aea8b1bc6a607147bef5c06c94e964f2f
90
+ #...
51
91
  ```
52
92
 
53
- open another terminal and check the accounts to use:
93
+ You can leave this terminal open to keep the devnet running, feel free to `ctrl+c` to exit the terminal and stop the local blockchain.
94
+
95
+ ### Step 3: Access Pre-funded Accounts
96
+
97
+ Open another terminal and check the accounts to use:
54
98
 
55
99
  ```sh
56
100
  offckb accounts
101
+
102
+ # result
103
+ Print account list, each account is funded with 42_000_000_00000000 capacity in the genesis block.
104
+ [
105
+ {
106
+ privkey: '0x6109170b275a09ad54877b82f7d9930f88cab5717d484fb4741ae9d1dd078cd6',
107
+ pubkey: '0x02025fa7b61b2365aa459807b84df065f1949d58c0ae590ff22dd2595157bffefa',
108
+ lockScript: {
109
+ codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
110
+ hashType: 'type',
111
+ args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
112
+ },
113
+ address: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqvwg2cen8extgq8s5puft8vf40px3f599cytcyd8',
114
+ args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
115
+ },
116
+ {
117
+ privkey: '0x9f315d5a9618a39fdc487c7a67a8581d40b045bd7a42d83648ca80ef3b2cb4a1',
118
+ pubkey: '0x026efa0579f09cc7c1129b78544f70098c90b2ab155c10746316f945829c034a2d',
119
+ lockScript: {
57
120
  ```
58
121
 
59
122
  Copy some private keys and visit http://localhost:1234 to play your first CKB Dapp!
60
123
 
61
- ### Built-in scripts
124
+ ## Built-in scripts
62
125
 
63
126
  - [x] xUDT https://github.com/nervosnetwork/rfcs/pull/428
64
127
  - commit id: 410b16c
@@ -67,21 +130,23 @@ Copy some private keys and visit http://localhost:1234 to play your first CKB Da
67
130
  - [x] AnyoneCanPay https://github.com/cryptape/anyone-can-pay
68
131
  - commit id: b845b3b
69
132
  - [x] AlwaysSuccess https://github.com/nervosnetwork/ckb-production-scripts/blob/master/c/always_success.c
70
- - commit id: 410b16c
71
- - [ ] Spore https://github.com/sporeprotocol/spore-contract
133
+ - commit id: 410b16c
134
+ - [x] Spore https://github.com/sporeprotocol/spore-contract
135
+ - version: 0.2.2-beta.1
72
136
 
73
- ### Accounts
137
+ ## Accounts
74
138
 
75
139
  `offckb` comes with 20 accounts, each account is funded with 42_000_000_00000000 capacity in the genesis block.
76
140
 
77
141
  all the private keys are recorded in the `account/keys` file.
78
142
  detail informations about each account are recorded in the `account/account.json` file.
79
143
 
144
+ :warning: **DO NOT SEND REAL ASSETS INTO ALL THESE ACCOUNTS, YOU CAN LOOSE YOUR MONEY** :warning:
80
145
 
81
146
  ## About Lumos
82
147
 
83
148
  `offckb` uses [Lumos](https://github.com/ckb-js/lumos) as the CKB Dapp framework to build the template projects.
84
149
 
85
- ## Development
150
+ ## Contributing
86
151
 
87
152
  check [development doc](/docs/develop.md)
@@ -49,7 +49,21 @@ create_type_id = true
49
49
  [[genesis.system_cells]]
50
50
  file = { file = "always_success" }
51
51
  create_type_id = true
52
-
52
+ [[genesis.system_cells]]
53
+ file = { file = "spore-scripts/spore" }
54
+ create_type_id = false
55
+ [[genesis.system_cells]]
56
+ file = { file = "spore-scripts/cluster" }
57
+ create_type_id = false
58
+ [[genesis.system_cells]]
59
+ file = { file = "spore-scripts/cluster_agent" }
60
+ create_type_id = false
61
+ [[genesis.system_cells]]
62
+ file = { file = "spore-scripts/cluster_proxy" }
63
+ create_type_id = false
64
+ [[genesis.system_cells]]
65
+ file = { file = "spore-scripts/spore_extension_lua" }
66
+ create_type_id = false
53
67
 
54
68
  [genesis.system_cells_lock]
55
69
  code_hash = "0x0000000000000000000000000000000000000000000000000000000000000000"
@@ -21,21 +21,20 @@ function selectTemplate() {
21
21
  message: 'Select a dapp template',
22
22
  choices: [
23
23
  {
24
- name: 'Transfer CKB',
24
+ name: 'View and Transfer Balance',
25
25
  value: 'transfer',
26
- description: 'a simple dapp to check CKB balance and transfer CKB from address to address',
26
+ description: 'a simple dapp to check CKB balance and transfer CKB',
27
27
  },
28
- new prompts_1.Separator(),
29
28
  {
30
- name: 'Issue Token With XUDT scripts(coming)',
29
+ name: 'Issue Token via XUDT scripts',
31
30
  value: 'xudt',
32
- description: 'a simple dapp to issue your own token with XUDT scripts(coming)',
33
- disabled: true,
31
+ description: 'a simple dapp to issue your own token via XUDT scripts',
34
32
  },
33
+ new prompts_1.Separator(),
35
34
  {
36
- name: 'Issue Token With Max Supply via Omnilock And XUDT scripts(coming)',
35
+ name: 'Issue Token With Max Supply Limit via Omnilock And XUDT scripts(coming)',
37
36
  value: 'xudt',
38
- description: 'a simple dapp to issue your own token with XUDT scripts',
37
+ description: 'a simple dapp to issue your own token with max supply limit via XUDT scripts',
39
38
  disabled: true,
40
39
  },
41
40
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@offckb/cli",
3
- "version": "0.1.0-rc4",
4
- "description": "ckb development environment for professionals",
3
+ "version": "0.1.0-rc6",
4
+ "description": "ckb development network for your first try",
5
5
  "author": "Retric Su <retric@cryptape.com>",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -4,7 +4,7 @@
4
4
  "SECP256K1_BLAKE160": {
5
5
  "CODE_HASH": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
6
6
  "HASH_TYPE": "type",
7
- "TX_HASH": "0x37f2b7799b199491f4732c572c086afdace0bf92992faf0b90bae44cdd119f9e",
7
+ "TX_HASH": "0x75be96e1871693f030db27ddae47890a28ab180e88e36ebb3575d9f1377d3da7",
8
8
  "INDEX": "0x0",
9
9
  "DEP_TYPE": "depGroup",
10
10
  "SHORT_ID": 1
@@ -12,14 +12,14 @@
12
12
  "SECP256K1_BLAKE160_MULTISIG": {
13
13
  "CODE_HASH": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8",
14
14
  "HASH_TYPE": "type",
15
- "TX_HASH": "0x37f2b7799b199491f4732c572c086afdace0bf92992faf0b90bae44cdd119f9e",
15
+ "TX_HASH": "0x75be96e1871693f030db27ddae47890a28ab180e88e36ebb3575d9f1377d3da7",
16
16
  "INDEX": "0x1",
17
17
  "DEP_TYPE": "depGroup"
18
18
  },
19
19
  "DAO": {
20
20
  "CODE_HASH": "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e",
21
21
  "HASH_TYPE": "type",
22
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
22
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
23
23
  "INDEX": "0x2",
24
24
  "DEP_TYPE": "code",
25
25
  "SHORT_ID": 2
@@ -27,35 +27,35 @@
27
27
  "SUDT": {
28
28
  "CODE_HASH": "0x6283a479a3cf5d4276cd93594de9f1827ab9b55c7b05b3d28e4c2e0a696cfefd",
29
29
  "HASH_TYPE": "type",
30
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
30
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
31
31
  "INDEX": "0x5",
32
32
  "DEP_TYPE": "code"
33
33
  },
34
34
  "XUDT": {
35
35
  "CODE_HASH": "0x1a1e4fef34f5982906f745b048fe7b1089647e82346074e0f32c2ece26cf6b1e",
36
36
  "HASH_TYPE": "type",
37
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
37
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
38
38
  "INDEX": "0x6",
39
39
  "DEP_TYPE": "code"
40
40
  },
41
41
  "OMNILOCK": {
42
42
  "CODE_HASH": "0x9c6933d977360f115a3e9cd5a2e0e475853681b80d775d93ad0f8969da343e56",
43
43
  "HASH_TYPE": "type",
44
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
44
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
45
45
  "INDEX": "0x7",
46
46
  "DEP_TYPE": "code"
47
47
  },
48
48
  "ANYONE_CAN_PAY": {
49
49
  "CODE_HASH": "0xe09352af0066f3162287763ce4ddba9af6bfaeab198dc7ab37f8c71c9e68bb5b",
50
50
  "HASH_TYPE": "type",
51
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
51
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
52
52
  "INDEX": "0x8",
53
53
  "DEP_TYPE": "code"
54
54
  },
55
55
  "ALWAYS_SUCCESS": {
56
56
  "CODE_HASH": "0xbb4469004225b39e983929db71fe2253cba1d49a76223e9e1d212cdca1f79f28",
57
57
  "HASH_TYPE": "type",
58
- "TX_HASH": "0x920bb4250dc6216c08ee0713f86b3fcb01bf444027b08b7d92db0ed1c0fb9214",
58
+ "TX_HASH": "0x1dbed8dcfe0f18359c65c5e9546fd15cd69de73ea0a502345be30180649c9467",
59
59
  "INDEX": "0x9",
60
60
  "DEP_TYPE": "code"
61
61
  }
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Transfer CKB</title>
7
+ <title>View and Transfer Balance</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -55,7 +55,7 @@ export function App() {
55
55
 
56
56
  return (
57
57
  <div>
58
- <h1>Transfer CKB between addresses</h1>
58
+ <h1>View and Transfer Balance</h1>
59
59
  <label htmlFor="private-key">Private Key: </label>&nbsp;
60
60
  <input id="private-key" type="text" onChange={onInputPrivKey} />
61
61
  <ul>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>happy coding with offckb</title>
7
+ <title>Issue Custom Token</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -1,26 +1,25 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
- import { Script } from '@ckb-lumos/lumos';
4
- import { capacityOf, generateAccountFromPrivateKey, transfer } from './lib';
3
+ import { Cell, Script, Transaction, utils } from '@ckb-lumos/lumos';
4
+ import { issueToken, queryIssuedTokenCells, transferTokenToAddress } from './lib';
5
+ import { capacityOf, generateAccountFromPrivateKey, readTokenAmount } from './util';
5
6
 
6
7
  const app = document.getElementById('root');
7
8
  ReactDOM.render(<App />, app);
8
9
 
9
- export function App() {
10
+ function IssuedToken() {
10
11
  const [privKey, setPrivKey] = useState('');
11
- const [fromAddr, setFromAddr] = useState('');
12
- const [fromLock, setFromLock] = useState<Script>();
12
+ const [lockScript, setLockScript] = useState<Script>();
13
13
  const [balance, setBalance] = useState('0');
14
-
15
- const [toAddr, setToAddr] = useState('');
16
14
  const [amount, setAmount] = useState('');
15
+ const [issuedTokenCell, setIssuedTokenCell] = useState<Cell>();
16
+ const [txHash, setTxHash] = useState<string>();
17
17
 
18
18
  useEffect(() => {
19
19
  const updateFromInfo = async () => {
20
20
  const { lockScript, address } = generateAccountFromPrivateKey(privKey);
21
21
  const capacity = await capacityOf(address);
22
- setFromAddr(address);
23
- setFromLock(lockScript);
22
+ setLockScript(lockScript);
24
23
  setBalance(capacity.toString());
25
24
  };
26
25
 
@@ -29,27 +28,185 @@ export function App() {
29
28
  }
30
29
  }, [privKey]);
31
30
 
31
+ const onInputPrivKey = (e: React.ChangeEvent<HTMLInputElement>) => {
32
+ const priv = e.target.value;
33
+ const privateKeyRegex = /^0x[0-9a-fA-F]{64}$/;
34
+
35
+ const isValid = privateKeyRegex.test(priv);
36
+ if (isValid) {
37
+ setPrivKey(priv);
38
+ } else {
39
+ alert(
40
+ `Invalid private key: must start with 0x and 32 bytes length. Ensure you're using a valid private key from the offckb accounts list.`,
41
+ );
42
+ }
43
+ };
44
+ const enabledIssue = +amount > 0 && +balance > 6100000000;
45
+
32
46
  return (
33
- <div>
47
+ <>
48
+ <h2>Issue Custom Token</h2>
49
+ <p></p>
34
50
  <label htmlFor="private-key">Private Key: </label>&nbsp;
35
- <input id="private-key" type="text" onChange={(e) => setPrivKey(e.target.value)} />
51
+ <input id="private-key" type="text" onChange={onInputPrivKey} />
36
52
  <ul>
37
- <li>CKB Address: {fromAddr}</li>
53
+ <li>Balance(Total Capacity): {(+balance).toLocaleString()}</li>
38
54
  <li>
39
- Current lock script:
40
- <pre>{JSON.stringify(fromLock, null, 2)}</pre>
55
+ Lock Script:
56
+ <pre>{JSON.stringify(lockScript, null, 2)}</pre>
41
57
  </li>
42
-
43
- <li>Total capacity: {balance}</li>
58
+ <li>Lock Script Hash: {lockScript && utils.computeScriptHash(lockScript)}</li>
44
59
  </ul>
45
- <label htmlFor="to-address">Transfer to Address: </label>&nbsp;
46
- <input id="to-address" type="text" onChange={(e) => setToAddr(e.target.value)} />
47
60
  <br />
48
- <label htmlFor="amount">Amount</label>
61
+ <label htmlFor="amount">Token Amount: </label>
49
62
  &nbsp;
50
- <input id="amount" type="text" onChange={(e) => setAmount(e.target.value)} />
63
+ <input id="amount" type="number" onChange={(e) => setAmount(e.target.value)} />
51
64
  <br />
52
- <button onClick={() => transfer({ amount, from: fromAddr, to: toAddr, privKey }).catch(alert)}>Transfer</button>
65
+ <button
66
+ disabled={!enabledIssue}
67
+ onClick={() =>
68
+ issueToken(privKey, amount)
69
+ .then((result) => {
70
+ setIssuedTokenCell(result.targetOutput);
71
+ setTxHash(result.hash);
72
+ })
73
+ .catch(alert)
74
+ }
75
+ >
76
+ Issue Token
77
+ </button>
78
+ <div>
79
+ {txHash && issuedTokenCell && (
80
+ <>
81
+ <h4>Result</h4>
82
+ <li>Transaction hash: {txHash}</li>
83
+ <li>
84
+ Token xUDT args: {issuedTokenCell.cellOutput.type.args}{' '}
85
+ <strong>
86
+ {
87
+ '(Noticed that the xUDT args works like the unique id for your issued token, Think of it like an ERC20 contract address)'
88
+ }
89
+ </strong>
90
+ </li>
91
+ <li>
92
+ Token cell: <pre>{JSON.stringify(issuedTokenCell, null, 2)}</pre>
93
+ </li>
94
+ </>
95
+ )}
96
+ </div>
97
+ </>
98
+ );
99
+ }
100
+
101
+ function ViewIssuedToken() {
102
+ const [xudtArgs, setXudtArgs] = useState<string>();
103
+ const [cells, setCells] = useState<Cell[]>([]);
104
+ return (
105
+ <>
106
+ <h2>View Custom Token</h2>
107
+ <div>
108
+ <label htmlFor="xudt-args">xDUT args: </label>&nbsp;
109
+ <input id="xudt-args" type="text" onChange={(e) => setXudtArgs(e.target.value)} />
110
+ </div>
111
+
112
+ <button disabled={!xudtArgs} onClick={() => queryIssuedTokenCells(xudtArgs).then(setCells).catch(alert)}>
113
+ Query Issued Token
114
+ </button>
115
+ {cells.length > 0 && <h3>Result: all the cells which hosted this issued token</h3>}
116
+ {cells.map((cell, index) => (
117
+ <div key={index}>
118
+ <p>Cell #{index}</p>
119
+ <li>Token amount: {readTokenAmount(cell.data).toNumber()}</li>
120
+ <li>Token xUDT args: {cell.cellOutput.type.args}</li>
121
+ <li>Token holder's lock script args: {cell.cellOutput.lock.args}</li>
122
+ <hr />
123
+ </div>
124
+ ))}
125
+ </>
126
+ );
127
+ }
128
+
129
+ function TransferIssuedToken() {
130
+ const [udtArgs, setUdtArgs] = useState<string>('');
131
+ const [senderPrivkey, setSenderPrivkey] = useState<string>('');
132
+ const [transferAmount, setTransferAmount] = useState('');
133
+ const [receiverAddress, setReceiverAddress] = useState('');
134
+ const [tx, setTx] = useState<Transaction>();
135
+
136
+ const onInputSenderPrivKey = (e: React.ChangeEvent<HTMLInputElement>) => {
137
+ const priv = e.target.value;
138
+ const privateKeyRegex = /^0x[0-9a-fA-F]{64}$/;
139
+ const isValid = privateKeyRegex.test(priv);
140
+ if (isValid) {
141
+ setSenderPrivkey(priv);
142
+ } else {
143
+ alert(
144
+ `Invalid private key: must start with 0x and 32 bytes length. Ensure you're using a valid private key from the offckb accounts list.`,
145
+ );
146
+ }
147
+ };
148
+
149
+ const enabledCheck =
150
+ senderPrivkey.length > 0 && udtArgs.length > 0 && transferAmount.length > 0 && receiverAddress.length > 0;
151
+
152
+ return (
153
+ <>
154
+ <h2>Transfer Custom Token</h2>
155
+ <label htmlFor="sender-private-key">Private Key: </label>&nbsp;
156
+ <input id="sender-private-key" type="text" onChange={onInputSenderPrivKey} />
157
+ <br />
158
+ <label htmlFor="udt">xUDT args: </label>
159
+ &nbsp;
160
+ <input id="udt" type="text" onChange={(e) => setUdtArgs(e.target.value)} />
161
+ <br />
162
+ <label htmlFor="transferAmount">Transfer Token Amount: </label>
163
+ &nbsp;
164
+ <input id="transferAmount" type="number" onChange={(e) => setTransferAmount(e.target.value)} />
165
+ <br />
166
+ <label htmlFor="receiverAddress">Receiver Address: </label>
167
+ &nbsp;
168
+ <input id="receiverAddress" type="text" onChange={(e) => setReceiverAddress(e.target.value)} />
169
+ <br />
170
+ <button
171
+ disabled={!enabledCheck}
172
+ onClick={() =>
173
+ transferTokenToAddress(udtArgs, senderPrivkey, transferAmount, receiverAddress)
174
+ .then((res) => setTx(res.tx))
175
+ .catch(alert)
176
+ }
177
+ >
178
+ Transfer Custom Token
179
+ </button>
180
+ <div>
181
+ {tx && (
182
+ <>
183
+ <h4>Result</h4>
184
+ <li>
185
+ Transaction: <pre>{JSON.stringify(tx, null, 2)}</pre>
186
+ </li>
187
+ </>
188
+ )}
189
+ </div>
190
+ </>
191
+ );
192
+ }
193
+
194
+ export function App() {
195
+ return (
196
+ <div>
197
+ <h1>
198
+ xUDT Scripts Dapp Example &nbsp;
199
+ <small>
200
+ <a href="https://github.com/XuJiandong/rfcs/blob/xudt/rfcs/0052-extensible-udt/0052-extensible-udt.md#xudt-witness">
201
+ {'(see xUDT specs)'}
202
+ </a>
203
+ </small>
204
+ </h1>
205
+ <IssuedToken />
206
+ <hr />
207
+ <ViewIssuedToken />
208
+ <hr />
209
+ <TransferIssuedToken />
53
210
  </div>
54
211
  );
55
212
  }
@@ -1,73 +1,58 @@
1
- import { bytes } from '@ckb-lumos/codec';
2
- import { helpers, Address, Script, hd, config, Cell, commons, WitnessArgs, BI } from '@ckb-lumos/lumos';
3
- import { values, blockchain } from '@ckb-lumos/base';
1
+ import { helpers, hd, config, Cell, commons, BI, utils } from '@ckb-lumos/lumos';
2
+ import { blockchain, HexString } from '@ckb-lumos/base';
4
3
  import { indexer, lumosConfig, rpc } from './ckb';
5
- const { ScriptValue } = values;
4
+ import { bytes, number } from '@ckb-lumos/codec';
5
+ import { xudtWitnessType } from './scheme';
6
+ import { addCellDep, generateAccountFromPrivateKey } from './util';
6
7
 
7
8
  config.initializeConfig(lumosConfig);
8
9
 
9
- type Account = {
10
- lockScript: Script;
11
- address: Address;
12
- pubKey: string;
13
- };
14
- export const generateAccountFromPrivateKey = (privKey: string): Account => {
15
- const pubKey = hd.key.privateToPublic(privKey);
16
- const args = hd.key.publicKeyToBlake160(pubKey);
17
- const template = lumosConfig.SCRIPTS['SECP256K1_BLAKE160']!;
18
- const lockScript = {
19
- codeHash: template.CODE_HASH,
20
- hashType: template.HASH_TYPE,
21
- args: args,
22
- };
23
- const address = helpers.encodeToAddress(lockScript, { config: lumosConfig });
24
- return {
25
- lockScript,
26
- address,
27
- pubKey,
10
+ export async function issueToken(privKey: string, amount: string) {
11
+ const { lockScript } = generateAccountFromPrivateKey(privKey);
12
+ const xudtDeps = lumosConfig.SCRIPTS.XUDT;
13
+ const lockDeps = lumosConfig.SCRIPTS.SECP256K1_BLAKE160;
14
+
15
+ const xudtArgs = utils.computeScriptHash(lockScript) + '00000000';
16
+ const typeScript = {
17
+ codeHash: xudtDeps.CODE_HASH,
18
+ hashType: xudtDeps.HASH_TYPE,
19
+ args: xudtArgs,
28
20
  };
29
- };
30
21
 
31
- export async function capacityOf(address: string): Promise<BI> {
32
- const collector = indexer.collector({
33
- lock: helpers.parseAddress(address, { config: lumosConfig }),
22
+ let txSkeleton = helpers.TransactionSkeleton();
23
+ txSkeleton = addCellDep(txSkeleton, {
24
+ outPoint: {
25
+ txHash: lockDeps.TX_HASH,
26
+ index: lockDeps.INDEX,
27
+ },
28
+ depType: lockDeps.DEP_TYPE,
34
29
  });
35
-
36
- let balance = BI.from(0);
37
- for await (const cell of collector.collect()) {
38
- balance = balance.add(cell.cellOutput.capacity);
39
- }
40
-
41
- return balance;
42
- }
43
-
44
- interface Options {
45
- from: string;
46
- to: string;
47
- amount: string;
48
- privKey: string;
49
- }
50
-
51
- export async function transfer(options: Options): Promise<string> {
52
- let txSkeleton = helpers.TransactionSkeleton({});
53
- const fromScript = helpers.parseAddress(options.from, {
54
- config: lumosConfig,
30
+ txSkeleton = addCellDep(txSkeleton, {
31
+ outPoint: {
32
+ txHash: xudtDeps.TX_HASH,
33
+ index: xudtDeps.INDEX,
34
+ },
35
+ depType: xudtDeps.DEP_TYPE,
55
36
  });
56
- const toScript = helpers.parseAddress(options.to, { config: lumosConfig });
57
37
 
58
- if (BI.from(options.amount).lt(BI.from('6100000000'))) {
59
- throw new Error(
60
- `every cell's capacity must be at least 61 CKB, see https://medium.com/nervosnetwork/understanding-the-nervos-dao-and-cell-model-d68f38272c24`,
61
- );
62
- }
38
+ const targetOutput: Cell = {
39
+ cellOutput: {
40
+ capacity: '0x0',
41
+ lock: lockScript,
42
+ type: typeScript,
43
+ },
44
+ data: bytes.hexify(number.Uint128LE.pack(amount)),
45
+ };
63
46
 
64
47
  // additional 0.001 ckb for tx fee
65
48
  // the tx fee could calculated by tx size
66
49
  // this is just a simple example
67
- const neededCapacity = BI.from(options.amount).add(100000);
50
+ const capacity = helpers.minimalCellCapacity(targetOutput);
51
+ targetOutput.cellOutput.capacity = '0x' + capacity.toString(16);
52
+ const neededCapacity = BI.from(capacity.toString(10)).add(100000);
68
53
  let collectedSum = BI.from(0);
69
54
  const collected: Cell[] = [];
70
- const collector = indexer.collector({ lock: fromScript, type: 'empty' });
55
+ const collector = indexer.collector({ lock: lockScript, type: 'empty' });
71
56
  for await (const cell of collector.collect()) {
72
57
  collectedSum = collectedSum.add(cell.cellOutput.capacity);
73
58
  collected.push(cell);
@@ -78,76 +63,190 @@ export async function transfer(options: Options): Promise<string> {
78
63
  throw new Error(`Not enough CKB, ${collectedSum} < ${neededCapacity}`);
79
64
  }
80
65
 
81
- const transferOutput: Cell = {
66
+ const changeOutput: Cell = {
82
67
  cellOutput: {
83
- capacity: BI.from(options.amount).toHexString(),
84
- lock: toScript,
68
+ capacity: collectedSum.sub(neededCapacity).toHexString(),
69
+ lock: lockScript,
85
70
  },
86
71
  data: '0x',
87
72
  };
88
73
 
74
+ txSkeleton = txSkeleton.update('inputs', (inputs) => inputs.push(...collected));
75
+ txSkeleton = txSkeleton.update('outputs', (outputs) => outputs.push(targetOutput, changeOutput));
76
+ /* 65-byte zeros in hex */
77
+ const lockWitness =
78
+ '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
79
+ const outputTypeWitness = xudtWitnessType.pack({ extension_data: [] });
80
+ const witnessArgs = blockchain.WitnessArgs.pack({ lock: lockWitness, outputType: outputTypeWitness });
81
+ const witness = bytes.hexify(witnessArgs);
82
+ txSkeleton = txSkeleton.update('witnesses', (witnesses) => witnesses.set(0, witness));
83
+
84
+ // signing
85
+ txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
86
+ const message = txSkeleton.get('signingEntries').get(0)?.message;
87
+ const Sig = hd.key.signRecoverable(message!, privKey);
88
+ const tx = helpers.sealTransaction(txSkeleton, [Sig]);
89
+ console.log(tx);
90
+
91
+ const hash = await rpc.sendTransaction(tx, 'passthrough');
92
+ console.log('The transaction hash is', hash);
93
+ return { hash, targetOutput };
94
+ }
95
+
96
+ export async function queryIssuedTokenCells(xudtArgs: HexString) {
97
+ const xudtDeps = lumosConfig.SCRIPTS.XUDT;
98
+ const typeScript = {
99
+ codeHash: xudtDeps.CODE_HASH,
100
+ hashType: xudtDeps.HASH_TYPE,
101
+ args: xudtArgs,
102
+ };
103
+
104
+ const collected: Cell[] = [];
105
+ const collector = indexer.collector({ type: typeScript });
106
+ for await (const cell of collector.collect()) {
107
+ collected.push(cell);
108
+ }
109
+ return collected;
110
+ }
111
+
112
+ export async function transferTokenToAddress(
113
+ udtIssuerArgs: string,
114
+ senderPrivKey: string,
115
+ amount: string,
116
+ receiverAddress: string,
117
+ ) {
118
+ const { lockScript: senderLockScript } = generateAccountFromPrivateKey(senderPrivKey);
119
+
120
+ const receiverLockScript = helpers.parseAddress(receiverAddress);
121
+
122
+ const xudtDeps = lumosConfig.SCRIPTS.XUDT;
123
+ const lockDeps = lumosConfig.SCRIPTS.SECP256K1_BLAKE160;
124
+
125
+ const xudtArgs = udtIssuerArgs;
126
+ const typeScript = {
127
+ codeHash: xudtDeps.CODE_HASH,
128
+ hashType: xudtDeps.HASH_TYPE,
129
+ args: xudtArgs,
130
+ };
131
+
132
+ let txSkeleton = helpers.TransactionSkeleton();
133
+ txSkeleton = addCellDep(txSkeleton, {
134
+ outPoint: {
135
+ txHash: lockDeps.TX_HASH,
136
+ index: lockDeps.INDEX,
137
+ },
138
+ depType: lockDeps.DEP_TYPE,
139
+ });
140
+ txSkeleton = addCellDep(txSkeleton, {
141
+ outPoint: {
142
+ txHash: xudtDeps.TX_HASH,
143
+ index: xudtDeps.INDEX,
144
+ },
145
+ depType: xudtDeps.DEP_TYPE,
146
+ });
147
+
148
+ const targetOutput: Cell = {
149
+ cellOutput: {
150
+ capacity: '0x0',
151
+ lock: receiverLockScript,
152
+ type: typeScript,
153
+ },
154
+ data: bytes.hexify(number.Uint128LE.pack(amount)),
155
+ };
156
+
157
+ const capacity = helpers.minimalCellCapacity(targetOutput);
158
+ targetOutput.cellOutput.capacity = '0x' + capacity.toString(16);
159
+ // additional 0.001 ckb for tx fee
160
+ // the tx fee could calculated by tx size
161
+ // this is just a simple example
162
+ const neededCapacity = BI.from(capacity.toString(10)).add(100000);
163
+ let collectedSum = BI.from(0);
164
+ let collectedAmount = BI.from(0);
165
+ const collected: Cell[] = [];
166
+ const collector = indexer.collector({ lock: senderLockScript, type: typeScript });
167
+ for await (const cell of collector.collect()) {
168
+ collectedSum = collectedSum.add(cell.cellOutput.capacity);
169
+ collectedAmount = collectedAmount.add(number.Uint128LE.unpack(cell.data));
170
+ collected.push(cell);
171
+ if (collectedAmount >= BI.from(amount)) break;
172
+ }
173
+
174
+ let changeOutputTokenAmount = BI.from(0);
175
+ if (collectedAmount.gt(BI.from(amount))) {
176
+ changeOutputTokenAmount = collectedAmount.sub(BI.from(amount));
177
+ }
178
+
89
179
  const changeOutput: Cell = {
90
180
  cellOutput: {
91
- capacity: collectedSum.sub(neededCapacity).toHexString(),
92
- lock: fromScript,
181
+ capacity: '0x0',
182
+ lock: senderLockScript,
183
+ type: typeScript,
93
184
  },
94
- data: '0x',
185
+ data: bytes.hexify(number.Uint128LE.pack(changeOutputTokenAmount.toString(10))),
95
186
  };
96
187
 
97
- txSkeleton = txSkeleton.update('inputs', (inputs) => inputs.push(...collected));
98
- txSkeleton = txSkeleton.update('outputs', (outputs) => outputs.push(transferOutput, changeOutput));
99
- txSkeleton = txSkeleton.update('cellDeps', (cellDeps) =>
100
- cellDeps.push({
101
- outPoint: {
102
- txHash: lumosConfig.SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
103
- index: lumosConfig.SCRIPTS.SECP256K1_BLAKE160.INDEX,
104
- },
105
- depType: lumosConfig.SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
106
- }),
107
- );
108
-
109
- const firstIndex = txSkeleton
110
- .get('inputs')
111
- .findIndex((input) =>
112
- new ScriptValue(input.cellOutput.lock, { validate: false }).equals(
113
- new ScriptValue(fromScript, { validate: false }),
114
- ),
115
- );
116
- if (firstIndex !== -1) {
117
- while (firstIndex >= txSkeleton.get('witnesses').size) {
118
- txSkeleton = txSkeleton.update('witnesses', (witnesses) => witnesses.push('0x'));
188
+ const changeOutputNeededCapacity = BI.from(helpers.minimalCellCapacity(changeOutput));
189
+
190
+ const extraNeededCapacity = collectedSum.lt(neededCapacity)
191
+ ? neededCapacity.sub(collectedSum).add(changeOutputNeededCapacity)
192
+ : collectedSum.sub(neededCapacity).add(changeOutputNeededCapacity);
193
+
194
+ if (extraNeededCapacity.gt(0)) {
195
+ let extraCollectedSum = BI.from(0);
196
+ const extraCollectedCells: Cell[] = [];
197
+ const collector = indexer.collector({ lock: senderLockScript, type: 'empty' });
198
+ for await (const cell of collector.collect()) {
199
+ extraCollectedSum = extraCollectedSum.add(cell.cellOutput.capacity);
200
+ extraCollectedCells.push(cell);
201
+ if (extraCollectedSum >= extraNeededCapacity) break;
119
202
  }
120
- let witness: string = txSkeleton.get('witnesses').get(firstIndex)!;
121
- const newWitnessArgs: WitnessArgs = {
122
- /* 65-byte zeros in hex */
123
- lock: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
124
- };
125
- if (witness !== '0x') {
126
- const witnessArgs = blockchain.WitnessArgs.unpack(bytes.bytify(witness));
127
- const lock = witnessArgs.lock;
128
- if (!!lock && !!newWitnessArgs.lock && !bytes.equal(lock, newWitnessArgs.lock)) {
129
- throw new Error('Lock field in first witness is set aside for signature!');
130
- }
131
- const inputType = witnessArgs.inputType;
132
- if (inputType) {
133
- newWitnessArgs.inputType = inputType;
134
- }
135
- const outputType = witnessArgs.outputType;
136
- if (outputType) {
137
- newWitnessArgs.outputType = outputType;
138
- }
203
+
204
+ if (extraCollectedSum.lt(extraNeededCapacity)) {
205
+ throw new Error(`Not enough CKB for change, ${extraCollectedSum} < ${extraNeededCapacity}`);
206
+ }
207
+
208
+ txSkeleton = txSkeleton.update('inputs', (inputs) => inputs.push(...extraCollectedCells));
209
+
210
+ const change2Capacity = extraCollectedSum.sub(changeOutputNeededCapacity);
211
+ if (change2Capacity.gt(61000000000)) {
212
+ changeOutput.cellOutput.capacity = changeOutputNeededCapacity.toHexString();
213
+ const changeOutput2: Cell = {
214
+ cellOutput: {
215
+ capacity: change2Capacity.toHexString(),
216
+ lock: senderLockScript,
217
+ },
218
+ data: '0x',
219
+ };
220
+ txSkeleton = txSkeleton.update('outputs', (outputs) => outputs.push(changeOutput2));
221
+ } else {
222
+ changeOutput.cellOutput.capacity = extraCollectedSum.toHexString();
139
223
  }
140
- witness = bytes.hexify(blockchain.WitnessArgs.pack(newWitnessArgs));
141
- txSkeleton = txSkeleton.update('witnesses', (witnesses) => witnesses.set(firstIndex, witness));
142
224
  }
143
225
 
226
+ txSkeleton = txSkeleton.update('inputs', (inputs) => inputs.push(...collected));
227
+ txSkeleton = txSkeleton.update('outputs', (outputs) => outputs.push(targetOutput, changeOutput));
228
+ /* 65-byte zeros in hex */
229
+ const lockWitness =
230
+ '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
231
+
232
+ const inputTypeWitness = xudtWitnessType.pack({ extension_data: [] });
233
+ const outputTypeWitness = xudtWitnessType.pack({ extension_data: [] });
234
+ const witnessArgs = blockchain.WitnessArgs.pack({
235
+ lock: lockWitness,
236
+ inputType: inputTypeWitness,
237
+ outputType: outputTypeWitness,
238
+ });
239
+ const witness = bytes.hexify(witnessArgs);
240
+ txSkeleton = txSkeleton.update('witnesses', (witnesses) => witnesses.set(0, witness));
241
+
242
+ // signing
144
243
  txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
145
244
  const message = txSkeleton.get('signingEntries').get(0)?.message;
146
- const Sig = hd.key.signRecoverable(message!, options.privKey);
245
+ const Sig = hd.key.signRecoverable(message!, senderPrivKey);
147
246
  const tx = helpers.sealTransaction(txSkeleton, [Sig]);
148
- const hash = await rpc.sendTransaction(tx, 'passthrough');
149
- console.log('The transaction hash is', hash);
150
- alert(`The transaction hash is ${hash}`);
247
+ console.log('tx: ', tx);
151
248
 
152
- return hash;
249
+ const txHash = await rpc.sendTransaction(tx, 'passthrough');
250
+ console.log('The transaction hash is', txHash);
251
+ return { txHash, tx };
153
252
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@offckb/lumos-scaffold",
2
+ "name": "@offckb/xudt",
3
3
  "version": "0.1.0",
4
4
  "description": "",
5
5
  "main": "index.js",
@@ -0,0 +1,27 @@
1
+ import { molecule, number } from '@ckb-lumos/codec';
2
+ import { BytesOpt, Byte32, Bytes } from '@ckb-lumos/codec/lib/blockchain';
3
+ const { vector, option, table } = molecule;
4
+
5
+ const { Uint8 } = number;
6
+
7
+ const Script = table(
8
+ {
9
+ codeHash: Byte32,
10
+ hashType: Uint8,
11
+ args: Bytes,
12
+ },
13
+ ['codeHash', 'hashType', 'args'],
14
+ );
15
+ const ScriptOpt = option(Script);
16
+ const ScriptVecOpt = option(vector(Script));
17
+
18
+ // specs: https://github.com/XuJiandong/rfcs/blob/xudt/rfcs/0052-extensible-udt/0052-extensible-udt.md#xudt-witness
19
+ export const xudtWitnessType = table(
20
+ {
21
+ owner_script: ScriptOpt,
22
+ owner_signature: BytesOpt,
23
+ raw_extension_data: ScriptVecOpt,
24
+ extension_data: vector(Bytes),
25
+ },
26
+ ['owner_script', 'owner_signature', 'raw_extension_data', 'extension_data'],
27
+ );
@@ -0,0 +1,66 @@
1
+ import { helpers, Address, Script, hd, BI, CellDep } from '@ckb-lumos/lumos';
2
+ import { values } from '@ckb-lumos/base';
3
+ import { indexer, lumosConfig } from './ckb';
4
+ import { TransactionSkeletonType } from '@ckb-lumos/helpers';
5
+ import { number } from '@ckb-lumos/codec';
6
+
7
+ type Account = {
8
+ lockScript: Script;
9
+ address: Address;
10
+ pubKey: string;
11
+ };
12
+ export const generateAccountFromPrivateKey = (privKey: string): Account => {
13
+ const pubKey = hd.key.privateToPublic(privKey);
14
+ const args = hd.key.publicKeyToBlake160(pubKey);
15
+ const template = lumosConfig.SCRIPTS['SECP256K1_BLAKE160']!;
16
+ const lockScript = {
17
+ codeHash: template.CODE_HASH,
18
+ hashType: template.HASH_TYPE,
19
+ args: args,
20
+ };
21
+ const address = helpers.encodeToAddress(lockScript, { config: lumosConfig });
22
+ return {
23
+ lockScript,
24
+ address,
25
+ pubKey,
26
+ };
27
+ };
28
+
29
+ export async function capacityOf(address: string): Promise<BI> {
30
+ const collector = indexer.collector({
31
+ lock: helpers.parseAddress(address, { config: lumosConfig }),
32
+ });
33
+
34
+ let balance = BI.from(0);
35
+ for await (const cell of collector.collect()) {
36
+ balance = balance.add(cell.cellOutput.capacity);
37
+ }
38
+
39
+ return balance;
40
+ }
41
+
42
+ export function addCellDep(txSkeleton: TransactionSkeletonType, newCellDep: CellDep): TransactionSkeletonType {
43
+ const cellDep = txSkeleton.get('cellDeps').find((cellDep) => {
44
+ return (
45
+ cellDep.depType === newCellDep.depType &&
46
+ new values.OutPointValue(cellDep.outPoint, { validate: false }).equals(
47
+ new values.OutPointValue(newCellDep.outPoint, { validate: false }),
48
+ )
49
+ );
50
+ });
51
+
52
+ if (!cellDep) {
53
+ txSkeleton = txSkeleton.update('cellDeps', (cellDeps) => {
54
+ return cellDeps.push({
55
+ outPoint: newCellDep.outPoint,
56
+ depType: newCellDep.depType,
57
+ });
58
+ });
59
+ }
60
+
61
+ return txSkeleton;
62
+ }
63
+
64
+ export function readTokenAmount(amount: string) {
65
+ return number.Uint128LE.unpack(amount);
66
+ }