on 4ea8104 help sign in
bridge/src/components/InlineEthereumTransaction.js
410 lines 12 kB history raw
1import React, { useMemo, useCallback, useState } from 'react';
2import cn from 'classnames';
3import {
4 Grid,
5 ToggleInput,
6 ErrorText,
7 Flex,
8 LinkButton,
9 H5,
10 Text,
11} from 'indigo-react';
12import { toBN } from 'web3-utils';
14import { ReactComponent as InfoIcon } from 'assets/info.svg';
16import { useExploreTxUrls } from 'lib/explorer';
17import { hexify } from 'lib/txn';
18import { safeFromWei, safeToWei } from 'lib/lib';
20import { composeValidator, buildCheckboxValidator } from 'form/validators';
21import BridgeForm from 'form/BridgeForm';
22import Condition from 'form/Condition';
24import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
25import CopyButton from './copiable/CopyButton';
26import ProgressButton from './ProgressButton';
27import { convertToInt } from 'lib/convertToInt';
28import NeedFundsNotice from './NeedFundsNotice';
29import NoticeBox from './NoticeBox';
31import './InlineEthereumTransaction.scss';
33export default function InlineEthereumTransaction({
34 // from useEthereumTransaction.bind
35 initializing,
36 canSign,
37 generateAndSign,
38 signed,
39 fakeSigned,
40 broadcast,
41 broadcasted,
42 confirmed,
43 completed,
44 reset,
45 error,
46 gasPrice,
47 setGasPrice,
48 resetGasPrice,
49 txHashes,
50 nonce,
51 chainId,
52 needFunds,
53 signedTransactions,
54 confirmationProgress,
55 gasLimit,
56 unsignedTransactions,
57 finalCost,
59 // additional from parent
60 label = 'Generate & Sign Transaction',
61 className,
62 onReturn,
63}) {
64 // show receipt after successful broadcast
65 const [showReceipt, setShowReceipt] = useState(false);
66 const toggleShowReceipt = useCallback(() => setShowReceipt(!showReceipt), [
67 setShowReceipt,
68 showReceipt,
69 ]);
70 // show configure controls pre-broadcast
71 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
72 // show the send/loading button while signed, broadcasting, or confirme
73 const showBroadcastButton = signed;
74 const showLoadingButton = broadcasted || confirmed;
75 const canBroadcast = signed && !needFunds;
76 // show signed tx only when signing (for offline usage)
77 const showSignedTx = signed;
79 const [showGasDetails, _setShowGasDetails] = useState(false);
80 const toggleGasDetails = useCallback(
81 () => _setShowGasDetails(!showGasDetails),
82 [_setShowGasDetails, showGasDetails]
83 );
85 const validate = useMemo(
86 () =>
87 composeValidator({
88 useAdvanced: buildCheckboxValidator(),
89 viewSigned: buildCheckboxValidator(),
90 }),
91 []
92 );
94 const onValues = useCallback(
95 ({ valid, values, form }) => {
96 if (!values.useAdvanced) {
97 resetGasPrice();
98 }
99 },
100 [resetGasPrice]
101 );
103 const renderPrimarySection = () => {
104 if (error) {
105 return (
106 <Grid.Item
107 className="eth-tx-button"
108 full
109 as={RestartButton}
110 solid
111 onClick={() => reset()}>
112 Reset Transaction
113 </Grid.Item>
114 );
115 } else if (completed) {
116 return (
117 <>
118 <Grid.Divider />
119 <Grid.Item
120 className="eth-tx-button"
121 full
122 as={RestartButton}
123 onClick={onReturn}>
124 Return
125 </Grid.Item>
126 <Grid.Divider />
127 {showReceipt && (
128 <TransactionReceipt
129 txHashes={txHashes}
130 finalCost={finalCost}
131 onClose={toggleShowReceipt}
132 />
133 )}
135 {!showReceipt && (
136 <Grid.Item
137 className="underline mv2 pointer"
138 full
139 onClick={toggleShowReceipt}>
140 View Receipt
141 </Grid.Item>
142 )}
143 </>
144 );
145 } else if (showBroadcastButton) {
146 return (
147 <Grid.Item
148 className="eth-tx-button"
149 full
150 as={ForwardButton}
151 solid
152 success
153 disabled={!canBroadcast}
154 onClick={() => broadcast()}>
155 Send Transaction
156 </Grid.Item>
157 );
158 } else if (showLoadingButton) {
159 return (
160 <Grid.Item
161 className="eth-tx-button"
162 full
163 as={ProgressButton}
164 success
165 disabled
166 progress={confirmationProgress}>
167 Sending Transaction
168 </Grid.Item>
169 );
170 } else {
171 return (
172 <Grid.Item
173 className="eth-tx-button"
174 full
175 as={GenerateButton}
176 onClick={generateAndSign}
177 disabled={!canSign}
178 loading={!canSign && initializing}>
179 {label}
180 </Grid.Item>
181 );
182 }
183 };
185 const serializedTxsHex = useMemo(
186 () =>
187 signedTransactions &&
188 signedTransactions.map(stx => hexify(stx.serialize())),
189 [signedTransactions]
190 );
192 const numTxs = useMemo(() => (unsignedTransactions || []).length || 1, [
193 unsignedTransactions,
194 ]);
195 const maxCost = useMemo(
196 () =>
197 safeFromWei(
198 safeToWei(
199 toBN(gasLimit)
200 .mul(toBN(gasPrice))
201 .mul(toBN(numTxs)),
202 'gwei'
203 ),
204 'ether'
205 ),
206 [gasLimit, gasPrice, numTxs]
207 );
209 const gasInfo = useMemo(() => {
210 const extra = numTxs === 1 ? '' : `* ${numTxs} txs`;
211 return showGasDetails
212 ? `${gasPrice} Gwei * ${gasLimit} ${extra} = ${maxCost} ETH`
213 : `${maxCost} ETH`;
214 }, [showGasDetails, maxCost, gasPrice, gasLimit, numTxs]);
216 return (
217 <Grid className={cn(className, 'mt1')}>
218 <BridgeForm validate={validate} onValues={onValues}>
219 {() => (
220 <>
221 {renderPrimarySection()}
223 {error && (
224 <Grid.Item
225 className="eth-tx-button" full as={ErrorText} className="mv1">
226 {error.message}
227 </Grid.Item>
228 )}
230 {needFunds && (
231 <Grid.Item
232 className="eth-tx-button"
233 full
234 as={NeedFundsNotice}
235 className="mt3"
236 {...needFunds}
237 />
238 )}
240 {showConfigureInput && (
241 <>
242 <Grid.Item
243 className="eth-tx-button"
244 full
245 as={ToggleInput}
246 name="useAdvanced"
247 label="Advanced"
248 inverseLabel="Back to Defaults"
249 inverseColor="red3"
250 disabled={!showConfigureInput || initializing}
251 />
253 <Condition when="useAdvanced" is={true}>
254 <Grid.Item
255 className="eth-tx-button" full className="mb2"></Grid.Item>
256 <Grid.Divider />
257 <Grid.Item
258 className="eth-tx-button"
259 full
260 as={Flex}
261 row
262 justify="between"
263 className="mt2">
264 <Flex.Item as={H5}>Transaction Fee</Flex.Item>
266 <Flex.Item as={H5}>
267 {gasInfo}{' '}
268 <InfoIcon
269 className="pointer"
270 onClick={toggleGasDetails}
271 />
272 </Flex.Item>
273 </Grid.Item>
275 {/* TODO(shrugs): move to indigo/RangeInput */}
276 <Grid.Item
277 className="eth-tx-button"
278 full
279 as="input"
280 type="range"
281 min="1"
282 max="400"
283 value={gasPrice}
284 onChange={e =>
285 setGasPrice(convertToInt(e.target.value, 10))
286 }
287 />
288 <Grid.Item
289 className="eth-tx-button"
290 full
291 as={Flex}
292 row
293 justify="between"
294 className="f6 mt1">
295 <Flex.Item as={Text}>Cheap</Flex.Item>
296 <Flex.Item as={Text}>Fast</Flex.Item>
297 </Grid.Item>
298 <Grid.Divider className="mt4" />
299 </Condition>
300 </>
301 )}
303 {showSignedTx && fakeSigned && (
304 <>
305 <Grid.Item
306 className="eth-tx-button" full as={NoticeBox} className="mt2">
307 Your wallet will sign the transaction upon sending it.
308 </Grid.Item>
309 </>
310 )}
312 {showSignedTx && !fakeSigned && (
313 <>
314 <Grid.Item
315 className="eth-tx-button"
316 full
317 as={ToggleInput}
318 name="viewSigned"
319 label="Signed Transaction"
320 inverseLabel="Hide"
321 disabled={!showSignedTx}
322 />
323 <Condition when="viewSigned" is={true}>
324 <SignedTransactionList
325 serializedTxsHex={serializedTxsHex}
326 maxCost={maxCost}
327 nonce={nonce}
328 />
329 </Condition>
330 </>
331 )}
332 </>
333 )}
334 </BridgeForm>
335 </Grid>
336 );
339function SignedTransactionList({ serializedTxsHex, nonce, maxCost }) {
340 return serializedTxsHex.map((serializedTxHex, i) => (
341 <React.Fragment key={i}>
342 <Grid.Divider />
343 <Grid.Item
344 className="eth-tx-button" full as={Flex} justify="between" className="pv4 black f5">
345 <Flex.Item>Nonce</Flex.Item>
346 <Flex.Item>{nonce + i}</Flex.Item>
347 </Grid.Item>
348 <Grid.Divider />
349 <Grid.Item
350 className="eth-tx-button" full as={Flex} justify="between" className="pv4 black f5">
351 <Flex.Item>Transaction Cost</Flex.Item>
352 <Flex.Item>{maxCost} ETH</Flex.Item>
353 </Grid.Item>
354 <Grid.Divider />
355 <Grid.Item
356 className="eth-tx-button" full as={Flex} justify="between" className="mt3 mb2">
357 <Flex.Item as={H5}>Signed Transaction Hex</Flex.Item>
358 <Flex.Item as={CopyButton} text={serializedTxHex} />
359 </Grid.Item>
360 <Grid.Item
361 className="eth-tx-button" full as="code" className="mb4 f6 mono gray4 wrap">
362 {serializedTxHex}
363 </Grid.Item>
364 <Grid.Divider />
365 </React.Fragment>
366 ));
369function TransactionReceipt({ txHashes, finalCost, onClose }) {
370 const txUrls = useExploreTxUrls(txHashes);
371 return (
372 <>
373 <Grid.Item
374 className="eth-tx-button" full as={Flex} justify="between" className="pv2 black f5">
375 <Flex.Item>Receipt</Flex.Item>
376 <Flex.Item onClick={onClose} className="underline pointer">
377 Close
378 </Flex.Item>
379 </Grid.Item>
380 <Grid.Item
381 className="eth-tx-button" full as={Flex} justify="between" className="pv2 black f5">
382 <Flex.Item>Transaction Cost</Flex.Item>
383 <Flex.Item className="mono">{finalCost} ETH</Flex.Item>
384 </Grid.Item>
385 <Grid.Divider />
386 <Grid.Item
387 className="eth-tx-button" full as={Flex} col className="pv4">
388 <Flex.Item as={H5}>Receipt</Flex.Item>
390 {txHashes &&
391 txHashes.map((txHash, i) => (
392 <Flex.Item as={Flex} className="mv3">
393 <>
394 <Flex.Item
395 key={i}
396 flex
397 as="code"
398 className="f6 mono gray4 wrap ">
399 {txHash}
400 </Flex.Item>
401 <Flex.Item className="ml8" as={LinkButton} href={txUrls[i]}>
402 Etherscan↗
403 </Flex.Item>
404 </>
405 </Flex.Item>
406 ))}
407 </Grid.Item>
408 </>
409 );