on 764521d help sign in
bridge/src/components/old/StatelessTransaction.js
599 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 fromWei,
19 toWei,
20 hexify,
21 renderSignedTx,
22 signTransaction,
23} from '../../lib/txn';
25import { withTxnConfirmations } from '../../store/txnConfirmations';
26import { withNetwork } from '../../store/network';
27import { withHistory } from '../../store/history';
28import { withWallet } from '../../store/wallet';
29import { withTxnCursor } from '../../store/txnCursor';
31const SUBMISSION_STATES = {
32 PROMPT: 'Send transaction',
33 PREPARING: 'Preparing...',
34 FUNDING: 'Finding transaction funding...',
35 SENDING: 'Sending transaction...',
36};
38class StatelessTransaction extends React.Component {
39 constructor(props) {
40 super(props);
42 this.state = {
43 gasPrice: '5',
44 gasLimit: GAS_LIMITS.DEFAULT.toString(),
45 showGasDetails: false,
46 userApproval: false,
47 chainId: '1',
48 customChain: false,
49 nonce: '0',
50 stx: Nothing(),
51 txn: Nothing(),
52 txStatus: SUBMISSION_STATES.PROMPT,
53 txError: Nothing(),
54 };
56 this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
57 this.setUserApproval = this.setUserApproval.bind(this);
58 this.setTxn = this.setTxn.bind(this);
59 this.setStx = this.setStx.bind(this);
60 this.sendTxn = this.sendTxn.bind(this);
61 this.setNonce = this.setNonce.bind(this);
62 this.setChainId = this.setChainId.bind(this);
63 this.setGasPrice = this.setGasPrice.bind(this);
64 this.setGasLimit = this.setGasLimit.bind(this);
65 this.rangeChange = this.rangeChange.bind(this);
66 this.toggleGasDetails = this.toggleGasDetails.bind(this);
67 this.handleChainUpdate = this.handleChainUpdate.bind(this);
68 }
70 componentDidMount() {
71 const { props } = this;
73 const addr = need.addressFromWallet(props.wallet);
75 props.web3.matchWith({
76 Nothing: () => {},
77 Just: w3 => {
78 const validWeb3 = w3.value;
80 const getTxMetadata = [
81 validWeb3.eth.getTransactionCount(addr),
82 validWeb3.eth.net.getId(),
83 validWeb3.eth.getGasPrice(),
84 ];
86 Promise.all(getTxMetadata).then(r => {
87 const txMetadata = {
88 nonce: r[0],
89 chainId: r[1],
90 gasPrice: fromWei(r[2], 'gwei'),
91 };
93 this.setState({ ...txMetadata });
94 });
95 },
96 });
97 }
99 handleChainUpdate(chainId) {
100 if (chainId === 'custom') {
101 this.setState({
102 customChain: true,
103 });
104 } else {
105 this.setState({
106 customChain: false,
107 chainId,
108 });
109 }
111 this.clearStx();
112 }
114 setUserApproval() {
115 const { state } = this;
116 this.setState({ userApproval: !state.userApproval });
117 }
119 toggleGasDetails() {
120 this.setState({
121 showGasDetails: !this.state.showGasDetails,
122 });
123 }
125 setStx(stx) {
126 this.setState({
127 stx,
128 userApproval: false,
129 });
130 }
132 setTxn(txn) {
133 this.setState({ txn });
134 }
136 createUnsignedTxn() {
137 const txn = this.props.createUnsignedTxn();
139 this.setState({ txn });
140 }
142 clearTxn() {
143 this.setState({
144 userApproval: false,
145 txn: Nothing(),
146 stx: Nothing(),
147 });
148 }
150 setNonce(nonce) {
151 this.setState({ nonce });
152 this.clearStx();
153 }
155 setChainId(chainId) {
156 this.setState({ chainId });
157 this.clearStx();
158 }
160 setGasPrice(gasPrice) {
161 this.setState({ gasPrice });
162 this.clearStx();
163 }
165 rangeChange(e) {
166 this.setGasPrice(e.target.value);
167 }
169 setGasLimit(gasLimit) {
170 this.setState({ gasLimit });
171 this.clearStx();
172 }
174 clearStx() {
175 this.setState({
176 userApproval: false,
177 stx: Nothing(),
178 });
179 }
181 async sendTxn() {
182 const { props, state } = this;
183 const web3 = props.web3.value;
185 const stx = state.stx.matchWith({
186 Just: tx => tx.value,
187 Nothing: () => {
188 throw new Error(BRIDGE_ERROR.MISSING_TXN);
189 },
190 });
192 if (props.beforeSend) {
193 try {
194 this.setState({ txStatus: SUBMISSION_STATES.PREPARING });
195 const res = await props.beforeSend(stx);
196 if (res === false) throw new Error('beforeSend disallowed sending');
197 } catch (e) {
198 console.log('beforeSend error', e);
199 this.setState({
200 txStatus: SUBMISSION_STATES.PROMPT,
201 txError: Just('Something went wrong!'),
202 });
203 return;
204 }
205 }
207 this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
209 const rawTx = hexify(stx.serialize());
210 const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
212 const address = need.addressFromWallet(props.wallet);
213 let balance = await web3.eth.getBalance(address);
214 let hasBalance = balance >= cost;
216 let usedTank = false;
217 // if we need to, try and fund the transaction
218 if (!hasBalance) {
219 usedTank = await this.ensureFundsFor(web3, address, cost, [rawTx]);
220 hasBalance = usedTank;
221 }
223 // if we still don't have sufficient balance, fail and tell the user
224 if (!hasBalance) {
225 this.setState({
226 txStatus: SUBMISSION_STATES.PROMPT,
227 txError: Just(`Insufficient funds.
228 Address ${address} needs at least ${fromWei(cost.toString())} ETH,
229 currently has ${fromWei(balance.toString())} ETH.`),
230 });
232 // if we have the balance, proceed with submission
233 } else {
234 this.setState({ txStatus: SUBMISSION_STATES.SENDING });
236 sendSignedTransaction(web3, stx, usedTank, props.setTxnConfirmations)
237 .then(txHash => {
238 props.setTxnCursor(Just(Ok(txHash)));
240 let routeData = {};
241 if (props.newNetworkSeed) {
242 props.setNetworkSeed(Just(props.newNetworkSeed));
243 routeData.promptKeyfile = true;
244 }
246 if (props.newRevision) {
247 props.setNetworkRevision(Just(props.newRevision));
248 }
250 props.history.popAndPush(ROUTE_NAMES.SENT_TRANSACTION, routeData);
251 })
252 .catch(err => {
253 this.setState({
254 txStatus: SUBMISSION_STATES.PROMPT,
255 txError: Just(err),
256 });
257 });
258 }
259 }
261 // uses the gas tank to ensure funds, but if we have to ask the user
262 // for funding, just resolve instead of waiting
263 // returns true if the gas tank was used, false otherwise
264 ensureFundsFor(web3, address, cost, signedTxs) {
265 return new Promise((resolve, reject) => {
266 tank
267 .ensureFundsFor(web3, null, address, cost, signedTxs, () => {
268 resolve(false);
269 })
270 .then(res => {
271 resolve(res);
272 });
273 });
274 }
276 getChainTitle(chainId) {
277 let map = {
278 '1': 'Mainnet - 1',
279 '2': 'Morden - 2',
280 '3': 'Ropsten - 3',
281 '4': 'Goerli - 4',
282 '42': 'Kovan - 42',
283 '1337': 'Geth private chains - 1337',
284 custom: 'Custom',
285 };
287 return map[chainId];
288 }
290 getChainOptions() {
291 return [
292 {
293 title: this.getChainTitle('1'),
294 value: '1',
295 },
296 {
297 title: this.getChainTitle('2'),
298 value: '2',
299 },
300 {
301 title: this.getChainTitle('3'),
302 value: '3',
303 },
304 {
305 title: this.getChainTitle('4'),
306 value: '4',
307 },
308 {
309 title: this.getChainTitle('42'),
310 value: '42',
311 },
312 {
313 title: this.getChainTitle('1337'),
314 value: '1337',
315 },
316 {
317 title: this.getChainTitle('custom'),
318 value: 'custom',
319 },
320 ];
321 }
323 render() {
324 const { web3, canGenerate } = this.props;
325 const {
326 gasPrice,
327 gasLimit,
328 nonce,
329 chainId,
330 txn,
331 stx,
332 userApproval,
333 showGasDetails,
334 customChain,
335 txStatus,
336 } = this.state;
338 const {
339 setNonce,
340 setChainId,
341 setGasLimit,
342 setGasPrice,
343 toggleGasDetails,
344 setUserApproval,
345 sendTxn,
346 createUnsignedTxn,
347 handleChainUpdate,
348 } = this;
350 const { state } = this;
352 const canSign = Just.hasInstance(txn);
353 const canApprove = Just.hasInstance(stx);
354 const canSend = Just.hasInstance(stx) && userApproval === true;
356 const chainOptions = this.getChainOptions();
358 const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
360 const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
362 const generateTxnButton = (
363 <Button
364 className={'mt-8'}
365 disabled={!canGenerate}
366 prop-color={generateButtonColor}
367 prop-size={'lg wide'}
368 onClick={createUnsignedTxn}>
369 {'Generate Transaction'}
370 </Button>
371 );
373 const unsignedTxnDisplay = txn.matchWith({
374 Nothing: _ => '',
375 Just: tx => (
376 <React.Fragment>
377 <H3 className={'mt-8'}>{'Unsigned Transaction'}</H3>
378 <Code>{JSON.stringify(tx.value, null, 2)}</Code>
379 </React.Fragment>
380 ),
381 });
383 const gasPriceRangeDialogue = (
384 <React.Fragment>
385 <div className="mt-12 flex space-between align-baseline">
386 <div>
387 <span>Gas Price:</span>
388 <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
389 </div>
390 <div className="text-sm">
391 <span>Max transaction fee: </span>
392 <span className="text-700">
393 {(gasPrice * gasLimit) / 1000000000} eth
394 </span>
395 </div>
396 </div>
398 <input
399 className="mt-4"
400 type="range"
401 min="2"
402 max="20"
403 list="gweiVals"
404 value={gasPrice}
405 onChange={this.rangeChange}
406 />
408 <div className="flex space-between text-sm mb-8">
409 <div>Cheap</div>
410 <div>Fast</div>
411 </div>
412 </React.Fragment>
413 );
415 const toggleGasDetailsDialogue = (
416 <span className="text-link" onClick={toggleGasDetails}>
417 Gas Details
418 </span>
419 );
421 const gasPriceDialogue = (
422 <Input
423 className={'mono mt-4'}
424 prop-size={'md'}
425 prop-format={'innerLabel'}
426 value={gasPrice}
427 onChange={setGasPrice}>
428 <InnerLabel>{'Gas Price (gwei)'}</InnerLabel>
429 </Input>
430 );
432 const gasLimitDialogue = (
433 <Input
434 className={'mono mt-4'}
435 prop-size={'md'}
436 prop-format={'innerLabel'}
437 value={gasLimit}
438 onChange={setGasLimit}>
439 <InnerLabel>{'Gas Limit'}</InnerLabel>
440 </Input>
441 );
443 const nonceDialogue = (
444 <Input
445 className={'mono mt-4 mb-4'}
446 prop-size={'md'}
447 prop-format={'innerLabel'}
448 value={nonce}
449 onChange={setNonce}>
450 <InnerLabel>{'Nonce'}</InnerLabel>
451 </Input>
452 );
454 const chainDialogueTitle = customChain
455 ? 'Custom'
456 : this.getChainTitle(chainId);
457 const chainDialogue = (
458 <InnerLabelDropdown
459 className={'mt-6'}
460 fullWidth={true}
461 title={'Chain ID'}
462 options={chainOptions}
463 handleUpdate={handleChainUpdate}
464 currentSelectionTitle={chainDialogueTitle}
465 />
466 );
468 const customChainDialogue = !customChain ? null : (
469 <Input
470 className={'mono mt-4 mb-8'}
471 prop-size={'md'}
472 prop-format={'innerLabel'}
473 value={chainId}
474 onChange={setChainId}>
475 <InnerLabel>{'Chain ID'}</InnerLabel>
476 </Input>
477 );
479 const onlineParamsDialogue = web3.matchWith({
480 Just: _ => <div />,
481 Nothing: _ => (
482 <React.Fragment>
483 {nonceDialogue}
484 {chainDialogue}
485 {customChainDialogue}
486 </React.Fragment>
487 ),
488 });
490 const signTxnButton = (
491 <Button
492 disabled={!canSign}
493 className={'mt-8'}
494 prop-size={'lg wide'}
495 prop-color={signerButtonColor}
496 onClick={async () =>
497 this.setStx(
498 await signTransaction({
499 ...this.props,
500 ...this.state,
501 wallet: this.props.wallet.value,
502 txn: txn.value,
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 withTxnConfirmations,
596 withHistory,
597 withWallet,
598 withTxnCursor
599)(StatelessTransaction);