1
1
import "bulma/css/bulma.css" ;
2
- import React , { ChangeEvent , useEffect , useMemo , useState } from "react" ;
2
+ import React , { useEffect , useMemo , FC , ReactNode } from "react" ;
3
+ import { useList , useSetState , useAsync } from "react-use" ;
3
4
import ReactDOM from "react-dom" ;
4
5
import { nanoid } from "nanoid" ;
5
- import { BI , Script , helpers } from "@ckb-lumos/lumos" ;
6
- import { capacityOf , createTxSkeleton , generateAccountFromPrivateKey , transfer , Options } from "./lib" ;
7
- import { BIish } from "@ckb-lumos/bi" ;
8
-
9
- type TxTarget = {
10
- amount : BIish ;
6
+ import { BI } from "@ckb-lumos/lumos" ;
7
+ import {
8
+ fetchAddressBalance ,
9
+ createUnsignedTxSkeleton ,
10
+ generateAccountFromPrivateKey ,
11
+ transfer ,
12
+ Account ,
13
+ calculateTransactionFee ,
14
+ MIN_CELL_CAPACITY ,
15
+ } from "./lib" ;
16
+
17
+ type TransferTarget = {
18
+ amount : BI ;
11
19
address : string ;
12
20
key : string ;
13
21
} ;
14
22
15
- const createTxTo = ( ) : TxTarget => ( { key : nanoid ( ) , amount : 0 , address : "" } ) ;
23
+ const createTransferTarget = ( ) : TransferTarget => ( { key : nanoid ( ) , amount : MIN_CELL_CAPACITY , address : "" } ) ;
16
24
17
25
export function App ( ) {
18
- const [ privKey , setPrivKey ] = useState ( "" ) ;
19
- const [ fromAddr , setFromAddr ] = useState ( "" ) ;
20
- const [ fromLock , setFromLock ] = useState < Script > ( ) ;
21
- const [ balance , setBalance ] = useState ( "0" ) ;
22
- const [ txHash , setTxHash ] = useState ( "" ) ;
23
- const [ errorMessage , setErrorMessage ] = useState ( "" ) ;
24
-
25
- const [ txTo , setTxTo ] = useState < TxTarget [ ] > ( [ createTxTo ( ) ] ) ;
26
- const [ txSkeleton , setTxSkeleton ] = useState < ReturnType < typeof helpers . TransactionSkeleton > | undefined > ( ) ;
27
- const setTargetByIndex = ( index : number , field : "amount" | "address" ) => ( e : ChangeEvent < HTMLInputElement > ) => {
28
- setErrorMessage ( "" ) ;
29
- const newTo = [ ...txTo ] ;
30
- if ( field === "amount" ) {
31
- newTo [ index ] . amount = e . target . value ;
32
- } else {
33
- newTo [ index ] [ "address" ] = e . target . value ;
26
+ const [ state , setState ] = useSetState ( {
27
+ privKey : "" ,
28
+ accountInfo : null as Account | null ,
29
+ balance : BI . from ( 0 ) ,
30
+ txHash : "" ,
31
+ } ) ;
32
+ const [ transferTargets , transferTargetsActions ] = useList ( [ createTransferTarget ( ) ] ) ;
33
+
34
+ // Step 1: get the unsigned transaction skeleton
35
+ // `useAsync` method can keep the transaction is newest from state
36
+ const { value : unsignedTxSkeleton } = useAsync ( async ( ) => {
37
+ if ( ! state . accountInfo ) {
38
+ return null ;
39
+ }
40
+ const skeleton = await createUnsignedTxSkeleton ( { targets : transferTargets , privKey : state . privKey } ) ;
41
+ return skeleton ;
42
+ } , [ state . accountInfo , state . privKey , transferTargets ] ) ;
43
+
44
+ // Step 2: sign the transaction and send it to CKB test network
45
+ // this method will be called when you click "Transfer" button
46
+ const doTransfer = ( ) => {
47
+ if ( ! state . accountInfo ) {
48
+ return ;
34
49
}
35
- setTxTo ( newTo ) ;
36
- } ;
37
-
38
- const insertTxTarget = ( ) => {
39
- setTxTo ( ( origin ) => [ ...origin , createTxTo ( ) ] ) ;
40
- } ;
41
50
42
- const removeTxTarget = ( index : number ) => ( ) => {
43
- setTxTo ( ( origin ) => origin . filter ( ( _ , i ) => i !== index ) ) ;
51
+ transfer ( unsignedTxSkeleton , state . privKey ) . then ( ( txHash ) => {
52
+ setState ( { txHash } ) ;
53
+ } ) ;
44
54
} ;
45
55
46
- const txOptions = useMemo < Options > (
47
- ( ) => ( {
48
- from : fromAddr ,
49
- to : txTo . map ( ( tx ) => ( { address : tx . address , amount : BI . from ( tx . amount ) } ) ) ,
50
- privKey,
51
- } ) ,
52
- [ fromAddr , txTo , privKey ]
56
+ // recalculate when transaction changes
57
+ const transactionFee = useMemo (
58
+ ( ) => ( unsignedTxSkeleton ? calculateTransactionFee ( unsignedTxSkeleton ) : BI . from ( 0 ) ) ,
59
+ [ unsignedTxSkeleton ]
53
60
) ;
54
61
62
+ // fetch and update account info and balance when private key changes
55
63
useEffect ( ( ) => {
56
- const updateFromInfo = async ( ) => {
57
- const { lockScript, address } = generateAccountFromPrivateKey ( privKey ) ;
58
- const capacity = await capacityOf ( address ) ;
59
- setFromAddr ( address ) ;
60
- setFromLock ( lockScript ) ;
61
- setBalance ( capacity . toString ( ) ) ;
62
- } ;
63
-
64
- setErrorMessage ( "" ) ;
65
- if ( privKey ) {
66
- updateFromInfo ( ) . catch ( ( e : Error ) => {
67
- setErrorMessage ( e . toString ( ) ) ;
64
+ if ( state . privKey ) {
65
+ const accountInfo = generateAccountFromPrivateKey ( state . privKey ) ;
66
+ setState ( {
67
+ accountInfo,
68
68
} ) ;
69
- }
70
- } , [ privKey ] ) ;
71
-
72
- useEffect ( ( ) => {
73
- ( async ( ) => {
74
- if ( ! txOptions . privKey || ! txOptions . from ) {
75
- return ;
76
- }
77
- try {
78
- const skeleton = await createTxSkeleton ( { ...txOptions , to : txOptions . to . filter ( ( it ) => it . address ) } ) ;
79
- setTxSkeleton ( skeleton ) ;
80
- } catch ( e ) {
81
- setErrorMessage ( e . toString ( ) ) ;
82
- }
83
- } ) ( ) ;
84
- } , [ txOptions , privKey ] ) ;
85
-
86
- const txFee = useMemo ( ( ) => {
87
- if ( ! txSkeleton ) return BI . from ( 0 ) ;
88
- const outputs = txSkeleton . outputs . reduce ( ( prev , cur ) => prev . add ( cur . cell_output . capacity ) , BI . from ( 0 ) ) ;
89
- const inputs = txSkeleton . inputs . reduce ( ( prev , cur ) => prev . add ( cur . cell_output . capacity ) , BI . from ( 0 ) ) ;
90
- return inputs . sub ( outputs ) ;
91
- } , [ txSkeleton ] ) ;
92
-
93
- const doTransfer = async ( ) => {
94
- try {
95
- const txHash = await transfer ( {
96
- from : fromAddr ,
97
- to : txTo . map ( ( tx ) => ( { address : tx . address , amount : BI . from ( tx . amount ) } ) ) ,
98
- privKey,
69
+ fetchAddressBalance ( accountInfo . address ) . then ( ( balance ) => {
70
+ setState ( { balance } ) ;
99
71
} ) ;
100
- setTxHash ( txHash ) ;
101
- } catch ( e ) {
102
- setErrorMessage ( e . toString ( ) ) ;
103
72
}
104
- } ;
73
+ } , [ state . privKey ] ) ;
105
74
106
- const txExplorer = useMemo ( ( ) => `https://pudge.explorer.nervos.org/transaction/${ txHash } ` , [ txHash ] ) ;
107
75
return (
108
76
< div className = "m-5" >
109
- < div className = "field" >
110
- < label htmlFor = "privateKey" className = "label" >
111
- Private Key
112
- </ label >
113
- < input
114
- type = "text"
115
- onChange = { ( e ) => setPrivKey ( e . target . value ) }
116
- className = "input is-primary"
117
- placeholder = "Your CKB Testnet Private Key"
118
- />
119
- </ div >
120
- < div className = "box" >
121
- < div >
122
- < strong > CKB Address: </ strong > { fromAddr }
123
- </ div >
124
- < div className = "mt-2" >
125
- < strong > Current Lockscript: </ strong > { JSON . stringify ( fromLock ) }
126
- </ div >
127
- < div className = "mt-2" >
128
- < strong > Balance: </ strong > { balance } < div className = "tag is-info is-light" > Shannon</ div >
129
- </ div >
130
- </ div >
77
+ < Field
78
+ value = { state . privKey }
79
+ onChange = { ( e ) => {
80
+ setState ( { privKey : e . target . value } ) ;
81
+ } }
82
+ label = "Private Key"
83
+ />
84
+ < ul >
85
+ < li > CKB Address: { state . accountInfo ?. address } </ li >
86
+ < li > CKB Balance: { state . balance . div ( 1e8 ) . toString ( ) } </ li >
87
+ </ ul >
131
88
< table className = "table table is-fullwidth" >
132
89
< thead >
133
90
< tr >
@@ -137,27 +94,29 @@ export function App() {
137
94
</ tr >
138
95
</ thead >
139
96
< tbody >
140
- { txTo . map ( ( txTarget , index ) => (
97
+ { transferTargets . map ( ( txTarget , index ) => (
141
98
< tr key = { txTarget . key } >
142
99
< td >
143
100
< input
144
101
type = "text"
145
102
value = { txTarget . address }
146
- onChange = { setTargetByIndex ( index , " address" ) }
103
+ onChange = { ( e ) => transferTargetsActions . updateAt ( index , { ... txTarget , address : e . target . value } ) }
147
104
className = "input"
148
105
/>
149
106
</ td >
150
107
< td >
151
108
< input
152
109
type = "text"
153
- value = { txTarget . amount as string }
154
- onChange = { setTargetByIndex ( index , "amount" ) }
110
+ value = { txTarget . amount . div ( 1e8 ) . toString ( ) }
111
+ onChange = { ( e ) =>
112
+ transferTargetsActions . updateAt ( index , { ...txTarget , amount : BI . from ( e . target . value ) . mul ( 1e8 ) } )
113
+ }
155
114
className = "input"
156
115
/>
157
116
</ td >
158
117
< td >
159
- { txTo . length > 1 && (
160
- < button onClick = { removeTxTarget ( index ) } className = "button is-danger" >
118
+ { transferTargets . length > 1 && (
119
+ < button onClick = { ( ) => transferTargetsActions . removeAt ( index ) } className = "button is-danger" >
161
120
Remove
162
121
</ button >
163
122
) }
@@ -168,11 +127,16 @@ export function App() {
168
127
< tfoot >
169
128
< tr >
170
129
< th >
171
- < div className = "button" onClick = { insertTxTarget } >
130
+ < div
131
+ className = "button"
132
+ onClick = { ( ) => {
133
+ transferTargetsActions . push ( createTransferTarget ( ) ) ;
134
+ } }
135
+ >
172
136
Add New Transfer Target
173
137
</ div >
174
138
</ th >
175
- < th > Transaction fee { txFee . toBigInt ( ) . toString ( ) } </ th >
139
+ < th > Transaction fee { ( transactionFee . toNumber ( ) / 1e8 ) . toString ( ) } </ th >
176
140
< th >
177
141
< button className = "button is-primary" onClick = { doTransfer } >
178
142
Transfer!
@@ -181,26 +145,43 @@ export function App() {
181
145
</ tr >
182
146
</ tfoot >
183
147
</ table >
184
-
185
- { txHash && (
186
- < div className = "notification is-primary" >
187
- < button className = "delete" onClick = { ( ) => setTxHash ( "" ) } />
188
- Transaction created, View it on{ " " }
189
- < a target = "_blank" href = { txExplorer } >
190
- 👉CKB Explorer
191
- </ a >
192
- </ div >
193
- ) }
194
- { errorMessage && (
195
- < div className = "notification is-danger" >
196
- < button className = "delete" onClick = { ( ) => setErrorMessage ( "" ) } />
197
- { errorMessage }
198
- </ div >
148
+ { state . txHash && (
149
+ < Notification onClear = { ( ) => setState ( { txHash : "" } ) } >
150
+ Transaction has sent, View it on{ " " }
151
+ < a href = { `https://pudge.explorer.nervos.org/transaction/${ state . txHash } ` } > CKB Explorer</ a >
152
+ </ Notification >
199
153
) }
200
154
</ div >
201
155
) ;
202
156
}
203
157
158
+ const Field : FC < { label : string ; value : string ; onChange : React . ChangeEventHandler < HTMLInputElement > } > = ( {
159
+ label,
160
+ value,
161
+ onChange,
162
+ } ) => (
163
+ < div className = "field" >
164
+ < label htmlFor = { label } className = "label" >
165
+ { label }
166
+ </ label >
167
+ < input
168
+ name = { label }
169
+ type = "text"
170
+ onChange = { onChange }
171
+ value = { value }
172
+ className = "input is-primary"
173
+ placeholder = "Your CKB Testnet Private Key"
174
+ />
175
+ </ div >
176
+ ) ;
177
+
178
+ const Notification : FC < { children : ReactNode ; onClear : ( ) => unknown } > = ( { children, onClear } ) => (
179
+ < div className = "notification is-success" >
180
+ < button className = "delete" onClick = { onClear } > </ button >
181
+ { children }
182
+ </ div >
183
+ ) ;
184
+
204
185
// prevent can not find DOM element on Codesandbox
205
186
const el = document . getElementById ( "root" ) || document . createElement ( "div" ) ;
206
187
el . id = "root" ;
0 commit comments