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