11import "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" ;
34import ReactDOM from "react-dom" ;
45import { 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 ;
1119 address : string ;
1220 key : string ;
1321} ;
1422
15- const createTxTo = ( ) : TxTarget => ( { key : nanoid ( ) , amount : 0 , address : "" } ) ;
23+ const createTransferTarget = ( ) : TransferTarget => ( { key : nanoid ( ) , amount : MIN_CELL_CAPACITY , address : "" } ) ;
1624
1725export 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 ;
3449 }
35- setTxTo ( newTo ) ;
36- } ;
37-
38- const insertTxTarget = ( ) => {
39- setTxTo ( ( origin ) => [ ...origin , createTxTo ( ) ] ) ;
40- } ;
4150
42- const removeTxTarget = ( index : number ) => ( ) => {
43- setTxTo ( ( origin ) => origin . filter ( ( _ , i ) => i !== index ) ) ;
51+ transfer ( unsignedTxSkeleton , state . privKey ) . then ( ( txHash ) => {
52+ setState ( { txHash } ) ;
53+ } ) ;
4454 } ;
4555
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 ]
5360 ) ;
5461
62+ // fetch and update account info and balance when private key changes
5563 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,
6868 } ) ;
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 } ) ;
9971 } ) ;
100- setTxHash ( txHash ) ;
101- } catch ( e ) {
102- setErrorMessage ( e . toString ( ) ) ;
10372 }
104- } ;
73+ } , [ state . privKey ] ) ;
10574
106- const txExplorer = useMemo ( ( ) => `https://pudge.explorer.nervos.org/transaction/${ txHash } ` , [ txHash ] ) ;
10775 return (
10876 < 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 >
13188 < table className = "table table is-fullwidth" >
13289 < thead >
13390 < tr >
@@ -137,27 +94,29 @@ export function App() {
13794 </ tr >
13895 </ thead >
13996 < tbody >
140- { txTo . map ( ( txTarget , index ) => (
97+ { transferTargets . map ( ( txTarget , index ) => (
14198 < tr key = { txTarget . key } >
14299 < td >
143100 < input
144101 type = "text"
145102 value = { txTarget . address }
146- onChange = { setTargetByIndex ( index , " address" ) }
103+ onChange = { ( e ) => transferTargetsActions . updateAt ( index , { ... txTarget , address : e . target . value } ) }
147104 className = "input"
148105 />
149106 </ td >
150107 < td >
151108 < input
152109 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+ }
155114 className = "input"
156115 />
157116 </ td >
158117 < 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" >
161120 Remove
162121 </ button >
163122 ) }
@@ -168,11 +127,16 @@ export function App() {
168127 < tfoot >
169128 < tr >
170129 < th >
171- < div className = "button" onClick = { insertTxTarget } >
130+ < div
131+ className = "button"
132+ onClick = { ( ) => {
133+ transferTargetsActions . push ( createTransferTarget ( ) ) ;
134+ } }
135+ >
172136 Add New Transfer Target
173137 </ div >
174138 </ th >
175- < th > Transaction fee { txFee . toBigInt ( ) . toString ( ) } </ th >
139+ < th > Transaction fee { ( transactionFee . toNumber ( ) / 1e8 ) . toString ( ) } </ th >
176140 < th >
177141 < button className = "button is-primary" onClick = { doTransfer } >
178142 Transfer!
@@ -181,26 +145,43 @@ export function App() {
181145 </ tr >
182146 </ tfoot >
183147 </ 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 >
199153 ) }
200154 </ div >
201155 ) ;
202156}
203157
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+
204185// prevent can not find DOM element on Codesandbox
205186const el = document . getElementById ( "root" ) || document . createElement ( "div" ) ;
206187el . id = "root" ;
0 commit comments