on e94ff4f help sign in
bridge/src/components/old/StatelessTransaction.js
598 lines 15 kB history raw
1import { Nothing, Just } from 'folktale/maybe';
2import { Ok } from 'folktale/result';
3import React from 'react';
4import * as need from '../../lib/need';
6import { Code, H3 } from './Base';
7import { Button } from './Base';
8import { CheckboxButton, Input, InnerLabel, InnerLabelDropdown } from './Base';
9import { Warning } from './Base';
11import { GAS_LIMITS } from '../../lib/constants';
12import { BRIDGE_ERROR } from '../../lib/error';
13import { ROUTE_NAMES } from '../../lib/routeNames';
14import { compose } from '../../lib/lib';
15import * as tank from '../../lib/tank';
16import {
17 sendSignedTransaction,
18 hexify,
19 renderSignedTx,
20 signTransaction,
21} from '../../lib/txn';
23import { withNetwork } from '../../store/network';
24import { withHistory } from '../../store/history';
25import { withWallet } from '../../store/wallet';
26import { withTxnCursor } from '../../store/txnCursor';
27import { fromWei, toWei } from 'web3-utils';
29const SUBMISSION_STATES = {
30 PROMPT: 'Send transaction',
31 PREPARING: 'Preparing...',
32 FUNDING: 'Finding transaction funding...',
33 SENDING: 'Sending transaction...',
34};
36class StatelessTransaction extends React.Component {
37 constructor(props) {
38 super(props);
40 this.state = {
41 gasPrice: '5',
42 gasLimit: GAS_LIMITS.DEFAULT.toString(),
43 showGasDetails: false,
44 userApproval: false,
45 chainId: '1',
46 customChain: false,
47 nonce: '0',
48 stx: Nothing(),
49 txn: Nothing(),
50 txStatus: SUBMISSION_STATES.PROMPT,
51 txError: Nothing(),
52 };
54 this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
55 this.setUserApproval = this.setUserApproval.bind(this);
56 this.setTxn = this.setTxn.bind(this);
57 this.setStx = this.setStx.bind(this);
58 this.sendTxn = this.sendTxn.bind(this);
59 this.setNonce = this.setNonce.bind(this);
60 this.setChainId = this.setChainId.bind(this);
61 this.setGasPrice = this.setGasPrice.bind(this);
62 this.setGasLimit = this.setGasLimit.bind(this);
63 this.rangeChange = this.rangeChange.bind(this);
64 this.toggleGasDetails = this.toggleGasDetails.bind(this);
65 this.handleChainUpdate = this.handleChainUpdate.bind(this);
66 }
68 componentDidMount() {
69 const { props } = this;
71 const addr = need.addressFromWallet(props.wallet);
73 props.web3.matchWith({
74 Nothing: () => {},
75 Just: w3 => {
76 const validWeb3 = w3.value;
78 const getTxMetadata = [
79 validWeb3.eth.getTransactionCount(addr),
80 validWeb3.eth.net.getId(),
81 validWeb3.eth.getGasPrice(),
82 ];
84 Promise.all(getTxMetadata).then(r => {
85 const txMetadata = {
86 nonce: r[0],
87 chainId: r[1],
88 gasPrice: fromWei(r[2], 'gwei'),
89 };
91 this.setState({ ...txMetadata });
92 });
93 },
94 });
95 }
97 handleChainUpdate(chainId) {
98 if (chainId === 'custom') {
99 this.setState({
100 customChain: true,
101 });
102 } else {
103 this.setState({
104 customChain: false,
105 chainId,
106 });
107 }
109 this.clearStx();
110 }
112 setUserApproval() {
113 const { state } = this;
114 this.setState({ userApproval: !state.userApproval });
115 }
117 toggleGasDetails() {
118 this.setState({
119 showGasDetails: !this.state.showGasDetails,
120 });
121 }
123 setStx(stx) {
124 this.setState({
125 stx,
126 userApproval: false,
127 });
128 }
130 setTxn(txn) {
131 this.setState({ txn });
132 }
134 createUnsignedTxn() {
135 const txn = this.props.createUnsignedTxn();
137 this.setState({ txn });
138 }
140 clearTxn() {
141 this.setState({
142 userApproval: false,
143 txn: Nothing(),
144 stx: Nothing(),
145 });
146 }
148 setNonce(nonce) {
149 this.setState({ nonce });
150 this.clearStx();
151 }
153 setChainId(chainId) {
154 this.setState({ chainId });
155 this.clearStx();
156 }
158 setGasPrice(gasPrice) {
159 this.setState({ gasPrice });
160 this.clearStx();
161 }
163 rangeChange(e) {
164 this.setGasPrice(e.target.value);
165 }
167 setGasLimit(gasLimit) {
168 this.setState({ gasLimit });
169 this.clearStx();
170 }
172 clearStx() {
173 this.setState({
174 userApproval: false,
175 stx: Nothing(),
176 });
177 }
179 async sendTxn() {
180 const { props, state } = this;
181 const web3 = props.web3.value;
183 const stx = state.stx.matchWith({
184 Just: tx => tx.value,
185 Nothing: () => {
186 throw new Error(BRIDGE_ERROR.MISSING_TXN);
187 },
188 });
190 if (props.beforeSend) {
191 try {
192 this.setState({ txStatus: SUBMISSION_STATES.PREPARING });
193 const res = await props.beforeSend(stx);
194 if (res === false) throw new Error('beforeSend disallowed sending');
195 } catch (e) {
196 console.log('beforeSend error', e);
197 this.setState({
198 txStatus: SUBMISSION_STATES.PROMPT,
199 txError: Just('Something went wrong!'),
200 });
201 return;
202 }
203 }
205 this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
207 const rawTx = hexify(stx.serialize());
208 const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
210 const address = need.addressFromWallet(props.wallet);
211 let balance = await web3.eth.getBalance(address);
212 let hasBalance = balance >= cost;
214 let usedTank = false;
215 // if we need to, try and fund the transaction
216 if (!hasBalance) {
217 usedTank = await this.ensureFundsFor(web3, address, cost, [rawTx]);
218 hasBalance = usedTank;
219 }
221 // if we still don't have sufficient balance, fail and tell the user
222 if (!hasBalance) {
223 this.setState({
224 txStatus: SUBMISSION_STATES.PROMPT,
225 txError: Just(`Insufficient funds.
226 Address ${address} needs at least ${fromWei(cost.toString())} ETH,
227 currently has ${fromWei(balance.toString())} ETH.`),
228 });
230 // if we have the balance, proceed with submission
231 } else {
232 this.setState({ txStatus: SUBMISSION_STATES.SENDING });
234 sendSignedTransaction(web3, stx, usedTank)
235 .then(txHash => {
236 props.setTxnCursor(Just(Ok(txHash)));
238 let routeData = {};
239 if (props.newNetworkSeed) {
240 props.setNetworkSeed(Just(props.newNetworkSeed));
241 routeData.promptKeyfile = true;
242 }
244 if (props.newRevision) {
245 props.setNetworkRevision(Just(props.newRevision));
246 }
248 props.history.popAndPush(ROUTE_NAMES.SENT_TRANSACTION, routeData);
249 })
250 .catch(err => {
251 this.setState({
252 txStatus: SUBMISSION_STATES.PROMPT,
253 txError: Just(err),
254 });
255 });
256 }
257 }
259 // uses the gas tank to ensure funds, but if we have to ask the user
260 // for funding, just resolve instead of waiting
261 // returns true if the gas tank was used, false otherwise
262 ensureFundsFor(web3, address, cost, signedTxs) {
263 return new Promise((resolve, reject) => {
264 tank
265 .ensureFundsFor(web3, null, address, cost, signedTxs, () => {
266 resolve(false);
267 })
268 .then(res => {
269 resolve(res);
270 });
271 });
272 }
274 getChainTitle(chainId) {
275 let map = {
276 '1': 'Mainnet - 1',
277 '2': 'Morden - 2',
278 '3': 'Ropsten - 3',
279 '4': 'Goerli - 4',
280 '42': 'Kovan - 42',
281 '1337': 'Geth private chains - 1337',
282 custom: 'Custom',
283 };
285 return map[chainId];
286 }
288 getChainOptions() {
289 return [
290 {
291 title: this.getChainTitle('1'),
292 value: '1',
293 },
294 {
295 title: this.getChainTitle('2'),
296 value: '2',
297 },
298 {
299 title: this.getChainTitle('3'),
300 value: '3',
301 },
302 {
303 title: this.getChainTitle('4'),
304 value: '4',
305 },
306 {
307 title: this.getChainTitle('42'),
308 value: '42',
309 },
310 {
311 title: this.getChainTitle('1337'),
312 value: '1337',
313 },
314 {
315 title: this.getChainTitle('custom'),
316 value: 'custom',
317 },
318 ];
319 }
321 render() {
322 const { web3, canGenerate } = this.props;
323 const {
324 gasPrice,
325 gasLimit,
326 nonce,
327 chainId,
328 txn,
329 stx,
330 userApproval,
331 showGasDetails,
332 customChain,
333 txStatus,
334 } = this.state;
336 const {
337 setNonce,
338 setChainId,
339 setGasLimit,
340 setGasPrice,
341 toggleGasDetails,
342 setUserApproval,
343 sendTxn,
344 createUnsignedTxn,
345 handleChainUpdate,
346 } = this;
348 const { state } = this;
350 const canSign = Just.hasInstance(txn);
351 const canApprove = Just.hasInstance(stx);
352 const canSend = Just.hasInstance(stx) && userApproval === true;
354 const chainOptions = this.getChainOptions();
356 const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
358 const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
360 const generateTxnButton = (
361 <Button
362 className={'mt-8'}
363 disabled={!canGenerate}
364 prop-color={generateButtonColor}
365 prop-size={'lg wide'}
366 onClick={createUnsignedTxn}>
367 {'Generate Transaction'}
368 </Button>
369 );
371 const unsignedTxnDisplay = txn.matchWith({
372 Nothing: _ => '',
373 Just: tx => (
374 <React.Fragment>
375 <H3 className={'mt-8'}>{'Unsigned Transaction'}</H3>
376 <Code>{JSON.stringify(tx.value, null, 2)}</Code>
377 </React.Fragment>
378 ),
379 });
381 const gasPriceRangeDialogue = (
382 <React.Fragment>
383 <div className="mt-12 flex space-between align-baseline">
384 <div>
385 <span>Gas Price:</span>
386 <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
387 </div>
388 <div className="text-sm">
389 <span>Max transaction fee: </span>
390 <span className="text-700">
391 {(gasPrice * gasLimit) / 1000000000} eth
392 </span>
393 </div>
394 </div>
396 <input
397 className="mt-4"
398 type="range"
399 min="2"
400 max="20"
401 list="gweiVals"
402 value={gasPrice}
403 onChange={this.rangeChange}
404 />
406 <div className="flex space-between text-sm mb-8">
407 <div>Cheap</div>
408 <div>Fast</div>
409 </div>
410 </React.Fragment>
411 );
413 const toggleGasDetailsDialogue = (
414 <span className="text-link" onClick={toggleGasDetails}>
415 Gas Details
416 </span>
417 );
419 const gasPriceDialogue = (
420 <Input
421 className={'mono mt-4'}
422 prop-size={'md'}
423 prop-format={'innerLabel'}
424 value={gasPrice}
425 onChange={setGasPrice}>
426 <InnerLabel>{'Gas Price (gwei)'}</InnerLabel>
427 </Input>
428 );
430 const gasLimitDialogue = (
431 <Input
432 className={'mono mt-4'}
433 prop-size={'md'}
434 prop-format={'innerLabel'}
435 value={gasLimit}
436 onChange={setGasLimit}>
437 <InnerLabel>{'Gas Limit'}</InnerLabel>
438 </Input>
439 );
441 const nonceDialogue = (
442 <Input
443 className={'mono mt-4 mb-4'}
444 prop-size={'md'}
445 prop-format={'innerLabel'}
446 value={nonce}
447 onChange={setNonce}>
448 <InnerLabel>{'Nonce'}</InnerLabel>
449 </Input>
450 );
452 const chainDialogueTitle = customChain
453 ? 'Custom'
454 : this.getChainTitle(chainId);
455 const chainDialogue = (
456 <InnerLabelDropdown
457 className={'mt-6'}
458 fullWidth={true}
459 title={'Chain ID'}
460 options={chainOptions}
461 handleUpdate={handleChainUpdate}
462 currentSelectionTitle={chainDialogueTitle}
463 />
464 );
466 const customChainDialogue = !customChain ? null : (
467 <Input
468 className={'mono mt-4 mb-8'}
469 prop-size={'md'}
470 prop-format={'innerLabel'}
471 value={chainId}
472 onChange={setChainId}>
473 <InnerLabel>{'Chain ID'}</InnerLabel>
474 </Input>
475 );
477 const onlineParamsDialogue = web3.matchWith({
478 Just: _ => <div />,
479 Nothing: _ => (
480 <React.Fragment>
481 {nonceDialogue}
482 {chainDialogue}
483 {customChainDialogue}
484 </React.Fragment>
485 ),
486 });
488 const signTxnButton = (
489 <Button
490 disabled={!canSign}
491 className={'mt-8'}
492 prop-size={'lg wide'}
493 prop-color={signerButtonColor}
494 onClick={async () =>
495 this.setStx(
496 Just(
497 await signTransaction({
498 ...this.props,
499 ...this.state,
500 wallet: this.props.wallet.value,
501 txn: txn.value,
502 })
503 )
504 )
505 }>
506 {'Sign Transaction'}
507 </Button>
508 );
510 const signedTxnDisplay = stx.matchWith({
511 Nothing: _ => '',
512 Just: tx => (
513 <React.Fragment>
514 <H3 className={'mt-8'}>{'Signed Transaction'}</H3>
515 <Code>{JSON.stringify(renderSignedTx(tx.value), null, 2)}</Code>
516 </React.Fragment>
517 ),
518 });
520 const confirmButton = (
521 <CheckboxButton
522 className={'mt-8'}
523 disabled={!canApprove}
524 onToggle={setUserApproval}
525 state={userApproval}
526 label="I approve this transaction and wish to send."
527 />
528 );
530 const sending = txStatus !== SUBMISSION_STATES.PROMPT;
531 const sendTxnSpinner = sending ? '' : 'hide';
532 const sendTxnButton = (
533 <Button
534 prop-size={'xl wide'}
535 className={'mt-8'}
536 disabled={!canSend || sending}
537 onClick={sendTxn}>
538 <span className="relative">
539 <span className={`btn-spinner ${sendTxnSpinner}`} />
540 {txStatus}
541 </span>
542 </Button>
543 );
545 const sendDialogue = web3.matchWith({
546 Nothing: _ => '',
547 Just: _ => (
548 <React.Fragment>
549 {confirmButton}
550 {sendTxnButton}
551 </React.Fragment>
552 ),
553 });
555 const txnErrorDialogue = Nothing.hasInstance(state.txError) ? (
556 ''
557 ) : (
558 <Warning className={'mt-8'}>
559 <H3 style={{ marginTop: 0, paddingTop: 0 }}>
560 {'There was an error sending your transaction.'}
561 </H3>
562 {state.txError.value}
563 </Warning>
564 );
566 return (
567 <React.Fragment>
568 {generateTxnButton}
569 {unsignedTxnDisplay}
571 {gasPriceRangeDialogue}
572 {toggleGasDetailsDialogue}
574 {showGasDetails && (
575 <div>
576 {gasPriceDialogue}
577 {gasLimitDialogue}
578 </div>
579 )}
580 {onlineParamsDialogue}
582 {signTxnButton}
584 {signedTxnDisplay}
585 {sendDialogue}
587 {txnErrorDialogue}
588 </React.Fragment>
589 );
590 }
593export default compose(
594 withNetwork,
595 withHistory,
596 withWallet,
597 withTxnCursor
598)(StatelessTransaction);