@offckb/cli 0.1.0-rc5 → 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 +13 -2
- package/package.json +1 -1
- package/templates/xudt/index.tsx +153 -47
- package/templates/xudt/lib.ts +145 -72
- package/templates/xudt/util.ts +66 -0
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ Start building on Nervos blockchain, right now, right away!
|
|
|
23
23
|
- [Install](#install)
|
|
24
24
|
- [Usage](#usage)
|
|
25
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)
|
|
26
29
|
- [Built-in scripts](#built-in-scripts)
|
|
27
30
|
- [Accounts](#accounts)
|
|
28
31
|
- [About Lumos](#about-lumos)
|
|
@@ -48,6 +51,8 @@ offckb list-hashes # list built-in scripts hashes, equals `ckb list-hashes`
|
|
|
48
51
|
|
|
49
52
|
## Get started
|
|
50
53
|
|
|
54
|
+
### Step 1: Create A Project
|
|
55
|
+
|
|
51
56
|
```sh
|
|
52
57
|
offckb init my-awesome-ckb-dapp
|
|
53
58
|
|
|
@@ -70,7 +75,9 @@ Server running at http://localhost:1234
|
|
|
70
75
|
✨ Built in 10ms
|
|
71
76
|
```
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
### Step 2: Start the Devnet
|
|
79
|
+
|
|
80
|
+
Open another terminal and run:
|
|
74
81
|
|
|
75
82
|
```sh
|
|
76
83
|
offckb node
|
|
@@ -83,7 +90,11 @@ CKB-Miner: 2024-03-04 14:35:17.567 +00:00 client INFO ckb_miner::miner Found! #
|
|
|
83
90
|
#...
|
|
84
91
|
```
|
|
85
92
|
|
|
86
|
-
open
|
|
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:
|
|
87
98
|
|
|
88
99
|
```sh
|
|
89
100
|
offckb accounts
|
package/package.json
CHANGED
package/templates/xudt/index.tsx
CHANGED
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
|
-
import { Cell, Script } from '@ckb-lumos/lumos';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
computeLockScriptHashFromPrivateKey,
|
|
7
|
-
generateAccountFromPrivateKey,
|
|
8
|
-
issueToken,
|
|
9
|
-
queryIssuedTokenCells,
|
|
10
|
-
readTokenAmount,
|
|
11
|
-
} 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';
|
|
12
6
|
|
|
13
7
|
const app = document.getElementById('root');
|
|
14
8
|
ReactDOM.render(<App />, app);
|
|
15
9
|
|
|
16
|
-
|
|
10
|
+
function IssuedToken() {
|
|
17
11
|
const [privKey, setPrivKey] = useState('');
|
|
18
|
-
const [
|
|
19
|
-
const [fromLock, setFromLock] = useState<Script>();
|
|
12
|
+
const [lockScript, setLockScript] = useState<Script>();
|
|
20
13
|
const [balance, setBalance] = useState('0');
|
|
21
|
-
|
|
22
14
|
const [amount, setAmount] = useState('');
|
|
23
|
-
const [
|
|
15
|
+
const [issuedTokenCell, setIssuedTokenCell] = useState<Cell>();
|
|
16
|
+
const [txHash, setTxHash] = useState<string>();
|
|
24
17
|
|
|
25
18
|
useEffect(() => {
|
|
26
19
|
const updateFromInfo = async () => {
|
|
27
20
|
const { lockScript, address } = generateAccountFromPrivateKey(privKey);
|
|
28
21
|
const capacity = await capacityOf(address);
|
|
29
|
-
|
|
30
|
-
setFromLock(lockScript);
|
|
22
|
+
setLockScript(lockScript);
|
|
31
23
|
setBalance(capacity.toString());
|
|
32
24
|
};
|
|
33
25
|
|
|
@@ -37,7 +29,6 @@ export function App() {
|
|
|
37
29
|
}, [privKey]);
|
|
38
30
|
|
|
39
31
|
const onInputPrivKey = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
|
-
// Regular expression to match a valid private key with "0x" prefix
|
|
41
32
|
const priv = e.target.value;
|
|
42
33
|
const privateKeyRegex = /^0x[0-9a-fA-F]{64}$/;
|
|
43
34
|
|
|
@@ -50,57 +41,172 @@ export function App() {
|
|
|
50
41
|
);
|
|
51
42
|
}
|
|
52
43
|
};
|
|
53
|
-
|
|
54
44
|
const enabledIssue = +amount > 0 && +balance > 6100000000;
|
|
55
|
-
|
|
45
|
+
|
|
56
46
|
return (
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
Issue Custom Token{' '}
|
|
60
|
-
<small>
|
|
61
|
-
<a href="https://github.com/XuJiandong/rfcs/blob/xudt/rfcs/0052-extensible-udt/0052-extensible-udt.md#xudt-witness">
|
|
62
|
-
{'(xUDT specs)'}
|
|
63
|
-
</a>
|
|
64
|
-
</small>
|
|
65
|
-
</h1>
|
|
47
|
+
<>
|
|
48
|
+
<h2>Issue Custom Token</h2>
|
|
66
49
|
<p></p>
|
|
67
50
|
<label htmlFor="private-key">Private Key: </label>
|
|
68
51
|
<input id="private-key" type="text" onChange={onInputPrivKey} />
|
|
69
52
|
<ul>
|
|
70
|
-
<li>
|
|
53
|
+
<li>Balance(Total Capacity): {(+balance).toLocaleString()}</li>
|
|
71
54
|
<li>
|
|
72
|
-
|
|
73
|
-
<pre>{JSON.stringify(
|
|
55
|
+
Lock Script:
|
|
56
|
+
<pre>{JSON.stringify(lockScript, null, 2)}</pre>
|
|
74
57
|
</li>
|
|
75
|
-
|
|
76
|
-
<li>Total capacity: {(+balance).toLocaleString()}</li>
|
|
58
|
+
<li>Lock Script Hash: {lockScript && utils.computeScriptHash(lockScript)}</li>
|
|
77
59
|
</ul>
|
|
78
60
|
<br />
|
|
79
|
-
<label htmlFor="amount">Token Amount</label>
|
|
61
|
+
<label htmlFor="amount">Token Amount: </label>
|
|
80
62
|
|
|
81
|
-
<input id="amount" type="
|
|
63
|
+
<input id="amount" type="number" onChange={(e) => setAmount(e.target.value)} />
|
|
82
64
|
<br />
|
|
83
|
-
<button
|
|
84
|
-
|
|
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
|
|
85
77
|
</button>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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>
|
|
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
|
|
92
114
|
</button>
|
|
93
115
|
{cells.length > 0 && <h3>Result: all the cells which hosted this issued token</h3>}
|
|
94
116
|
{cells.map((cell, index) => (
|
|
95
117
|
<div key={index}>
|
|
96
118
|
<p>Cell #{index}</p>
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
<p>token holder's lockScript args: {cell.cellOutput.lock.args}</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>
|
|
101
122
|
<hr />
|
|
102
123
|
</div>
|
|
103
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>
|
|
156
|
+
<input id="sender-private-key" type="text" onChange={onInputSenderPrivKey} />
|
|
157
|
+
<br />
|
|
158
|
+
<label htmlFor="udt">xUDT args: </label>
|
|
159
|
+
|
|
160
|
+
<input id="udt" type="text" onChange={(e) => setUdtArgs(e.target.value)} />
|
|
161
|
+
<br />
|
|
162
|
+
<label htmlFor="transferAmount">Transfer Token Amount: </label>
|
|
163
|
+
|
|
164
|
+
<input id="transferAmount" type="number" onChange={(e) => setTransferAmount(e.target.value)} />
|
|
165
|
+
<br />
|
|
166
|
+
<label htmlFor="receiverAddress">Receiver Address: </label>
|
|
167
|
+
|
|
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
|
|
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 />
|
|
104
210
|
</div>
|
|
105
211
|
);
|
|
106
212
|
}
|
package/templates/xudt/lib.ts
CHANGED
|
@@ -1,74 +1,12 @@
|
|
|
1
|
-
import { helpers,
|
|
2
|
-
import {
|
|
1
|
+
import { helpers, hd, config, Cell, commons, BI, utils } from '@ckb-lumos/lumos';
|
|
2
|
+
import { blockchain, HexString } from '@ckb-lumos/base';
|
|
3
3
|
import { indexer, lumosConfig, rpc } from './ckb';
|
|
4
|
-
import { TransactionSkeletonType } from '@ckb-lumos/helpers';
|
|
5
4
|
import { bytes, number } from '@ckb-lumos/codec';
|
|
6
5
|
import { xudtWitnessType } from './scheme';
|
|
6
|
+
import { addCellDep, generateAccountFromPrivateKey } from './util';
|
|
7
7
|
|
|
8
8
|
config.initializeConfig(lumosConfig);
|
|
9
9
|
|
|
10
|
-
type Account = {
|
|
11
|
-
lockScript: Script;
|
|
12
|
-
address: Address;
|
|
13
|
-
pubKey: string;
|
|
14
|
-
};
|
|
15
|
-
export const generateAccountFromPrivateKey = (privKey: string): Account => {
|
|
16
|
-
const pubKey = hd.key.privateToPublic(privKey);
|
|
17
|
-
const args = hd.key.publicKeyToBlake160(pubKey);
|
|
18
|
-
const template = lumosConfig.SCRIPTS['SECP256K1_BLAKE160']!;
|
|
19
|
-
const lockScript = {
|
|
20
|
-
codeHash: template.CODE_HASH,
|
|
21
|
-
hashType: template.HASH_TYPE,
|
|
22
|
-
args: args,
|
|
23
|
-
};
|
|
24
|
-
const address = helpers.encodeToAddress(lockScript, { config: lumosConfig });
|
|
25
|
-
return {
|
|
26
|
-
lockScript,
|
|
27
|
-
address,
|
|
28
|
-
pubKey,
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const computeLockScriptHashFromPrivateKey = (privkey: string) => {
|
|
33
|
-
const { lockScript } = generateAccountFromPrivateKey(privkey);
|
|
34
|
-
return utils.computeScriptHash(lockScript);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export async function capacityOf(address: string): Promise<BI> {
|
|
38
|
-
const collector = indexer.collector({
|
|
39
|
-
lock: helpers.parseAddress(address, { config: lumosConfig }),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let balance = BI.from(0);
|
|
43
|
-
for await (const cell of collector.collect()) {
|
|
44
|
-
balance = balance.add(cell.cellOutput.capacity);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return balance;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function addCellDep(txSkeleton: TransactionSkeletonType, newCellDep: CellDep): TransactionSkeletonType {
|
|
51
|
-
const cellDep = txSkeleton.get('cellDeps').find((cellDep) => {
|
|
52
|
-
return (
|
|
53
|
-
cellDep.depType === newCellDep.depType &&
|
|
54
|
-
new values.OutPointValue(cellDep.outPoint, { validate: false }).equals(
|
|
55
|
-
new values.OutPointValue(newCellDep.outPoint, { validate: false }),
|
|
56
|
-
)
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (!cellDep) {
|
|
61
|
-
txSkeleton = txSkeleton.update('cellDeps', (cellDeps) => {
|
|
62
|
-
return cellDeps.push({
|
|
63
|
-
outPoint: newCellDep.outPoint,
|
|
64
|
-
depType: newCellDep.depType,
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return txSkeleton;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
10
|
export async function issueToken(privKey: string, amount: string) {
|
|
73
11
|
const { lockScript } = generateAccountFromPrivateKey(privKey);
|
|
74
12
|
const xudtDeps = lumosConfig.SCRIPTS.XUDT;
|
|
@@ -152,14 +90,11 @@ export async function issueToken(privKey: string, amount: string) {
|
|
|
152
90
|
|
|
153
91
|
const hash = await rpc.sendTransaction(tx, 'passthrough');
|
|
154
92
|
console.log('The transaction hash is', hash);
|
|
155
|
-
|
|
93
|
+
return { hash, targetOutput };
|
|
156
94
|
}
|
|
157
95
|
|
|
158
|
-
export async function queryIssuedTokenCells(
|
|
159
|
-
const { lockScript } = generateAccountFromPrivateKey(privKey);
|
|
96
|
+
export async function queryIssuedTokenCells(xudtArgs: HexString) {
|
|
160
97
|
const xudtDeps = lumosConfig.SCRIPTS.XUDT;
|
|
161
|
-
|
|
162
|
-
const xudtArgs = utils.computeScriptHash(lockScript) + '00000000';
|
|
163
98
|
const typeScript = {
|
|
164
99
|
codeHash: xudtDeps.CODE_HASH,
|
|
165
100
|
hashType: xudtDeps.HASH_TYPE,
|
|
@@ -174,6 +109,144 @@ export async function queryIssuedTokenCells(privKey: string) {
|
|
|
174
109
|
return collected;
|
|
175
110
|
}
|
|
176
111
|
|
|
177
|
-
export function
|
|
178
|
-
|
|
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
|
+
|
|
179
|
+
const changeOutput: Cell = {
|
|
180
|
+
cellOutput: {
|
|
181
|
+
capacity: '0x0',
|
|
182
|
+
lock: senderLockScript,
|
|
183
|
+
type: typeScript,
|
|
184
|
+
},
|
|
185
|
+
data: bytes.hexify(number.Uint128LE.pack(changeOutputTokenAmount.toString(10))),
|
|
186
|
+
};
|
|
187
|
+
|
|
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;
|
|
202
|
+
}
|
|
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();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
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
|
|
243
|
+
txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
|
|
244
|
+
const message = txSkeleton.get('signingEntries').get(0)?.message;
|
|
245
|
+
const Sig = hd.key.signRecoverable(message!, senderPrivKey);
|
|
246
|
+
const tx = helpers.sealTransaction(txSkeleton, [Sig]);
|
|
247
|
+
console.log('tx: ', tx);
|
|
248
|
+
|
|
249
|
+
const txHash = await rpc.sendTransaction(tx, 'passthrough');
|
|
250
|
+
console.log('The transaction hash is', txHash);
|
|
251
|
+
return { txHash, tx };
|
|
179
252
|
}
|
|
@@ -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
|
+
}
|