@@ -1,598 +0,0 @@
1−import { Nothing, Just } from 'folktale/maybe';
2−import { Ok } from 'folktale/result';
3−import React from 'react';
4−import * as need from '../../lib/need';
5−
6−import { Code, H3 } from './Base';
7−import { Button } from './Base';
8−import { CheckboxButton, Input, InnerLabel, InnerLabelDropdown } from './Base';
9−import { Warning } from './Base';
10−
11−import { GAS_LIMITS } from '../../lib/constants';
12−import { BRIDGE_ERROR } from '../../lib/error';
13−import { ROUTE_NAMES } from '../../lib/routeNames';
14−import { compose } from '../../lib/lib';
15−import * as tank from '../../lib/tank';
16−import {
17− sendSignedTransaction,
18− hexify,
19− renderSignedTx,
20− signTransaction,
21−} from '../../lib/txn';
22−
23−import { withNetwork } from '../../store/network';
24−import { withHistory } from '../../store/history';
25−import { withWallet } from '../../store/wallet';
26−import { withTxnCursor } from '../../store/txnCursor';
27−import { fromWei, toWei } from 'web3-utils';
28−
29−const SUBMISSION_STATES = {
30− PROMPT: 'Send transaction',
31− PREPARING: 'Preparing...',
32− FUNDING: 'Finding transaction funding...',
33− SENDING: 'Sending transaction...',
34−};
35−
36−class StatelessTransaction extends React.Component {
37− constructor(props) {
38− super(props);
39−
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− };
53−
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− }
67−
68− componentDidMount() {
69− const { props } = this;
70−
71− const addr = need.addressFromWallet(props.wallet);
72−
73− props.web3.matchWith({
74− Nothing: () => {},
75− Just: w3 => {
76− const validWeb3 = w3.value;
77−
78− const getTxMetadata = [
79− validWeb3.eth.getTransactionCount(addr),
80− validWeb3.eth.net.getId(),
81− validWeb3.eth.getGasPrice(),
82− ];
83−
84− Promise.all(getTxMetadata).then(r => {
85− const txMetadata = {
86− nonce: r[0],
87− chainId: r[1],
88− gasPrice: fromWei(r[2], 'gwei'),
89− };
90−
91− this.setState({ ...txMetadata });
92− });
93− },
94− });
95− }
96−
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− }
108−
109− this.clearStx();
110− }
111−
112− setUserApproval() {
113− const { state } = this;
114− this.setState({ userApproval: !state.userApproval });
115− }
116−
117− toggleGasDetails() {
118− this.setState({
119− showGasDetails: !this.state.showGasDetails,
120− });
121− }
122−
123− setStx(stx) {
124− this.setState({
125− stx,
126− userApproval: false,
127− });
128− }
129−
130− setTxn(txn) {
131− this.setState({ txn });
132− }
133−
134− createUnsignedTxn() {
135− const txn = this.props.createUnsignedTxn();
136−
137− this.setState({ txn });
138− }
139−
140− clearTxn() {
141− this.setState({
142− userApproval: false,
143− txn: Nothing(),
144− stx: Nothing(),
145− });
146− }
147−
148− setNonce(nonce) {
149− this.setState({ nonce });
150− this.clearStx();
151− }
152−
153− setChainId(chainId) {
154− this.setState({ chainId });
155− this.clearStx();
156− }
157−
158− setGasPrice(gasPrice) {
159− this.setState({ gasPrice });
160− this.clearStx();
161− }
162−
163− rangeChange(e) {
164− this.setGasPrice(e.target.value);
165− }
166−
167− setGasLimit(gasLimit) {
168− this.setState({ gasLimit });
169− this.clearStx();
170− }
171−
172− clearStx() {
173− this.setState({
174− userApproval: false,
175− stx: Nothing(),
176− });
177− }
178−
179− async sendTxn() {
180− const { props, state } = this;
181− const web3 = props.web3.value;
182−
183− const stx = state.stx.matchWith({
184− Just: tx => tx.value,
185− Nothing: () => {
186− throw new Error(BRIDGE_ERROR.MISSING_TXN);
187− },
188− });
189−
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− }
204−
205− this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
206−
207− const rawTx = hexify(stx.serialize());
208− const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
209−
210− const address = need.addressFromWallet(props.wallet);
211− let balance = await web3.eth.getBalance(address);
212− let hasBalance = balance >= cost;
213−
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− }
220−
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− });
229−
230− // if we have the balance, proceed with submission
231− } else {
232− this.setState({ txStatus: SUBMISSION_STATES.SENDING });
233−
234− sendSignedTransaction(web3, stx, usedTank)
235− .then(txHash => {
236− props.setTxnCursor(Just(Ok(txHash)));
237−
238− let routeData = {};
239− if (props.newNetworkSeed) {
240− props.setNetworkSeed(Just(props.newNetworkSeed));
241− routeData.promptKeyfile = true;
242− }
243−
244− if (props.newRevision) {
245− props.setNetworkRevision(Just(props.newRevision));
246− }
247−
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− }
258−
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− }
273−
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− };
284−
285− return map[chainId];
286− }
287−
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− }
320−
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;
335−
336− const {
337− setNonce,
338− setChainId,
339− setGasLimit,
340− setGasPrice,
341− toggleGasDetails,
342− setUserApproval,
343− sendTxn,
344− createUnsignedTxn,
345− handleChainUpdate,
346− } = this;
347−
348− const { state } = this;
349−
350− const canSign = Just.hasInstance(txn);
351− const canApprove = Just.hasInstance(stx);
352− const canSend = Just.hasInstance(stx) && userApproval === true;
353−
354− const chainOptions = this.getChainOptions();
355−
356− const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
357−
358− const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
359−
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− );
370−
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− });
380−
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>
395−
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− />
405−
406− <div className="flex space-between text-sm mb-8">
407− <div>Cheap</div>
408− <div>Fast</div>
409− </div>
410− </React.Fragment>
411− );
412−
413− const toggleGasDetailsDialogue = (
414− <span className="text-link" onClick={toggleGasDetails}>
415− Gas Details
416− </span>
417− );
418−
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− );
429−
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− );
440−
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− );
451−
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− );
465−
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− );
476−
477− const onlineParamsDialogue = web3.matchWith({
478− Just: _ => <div />,
479− Nothing: _ => (
480− <React.Fragment>
481− {nonceDialogue}
482− {chainDialogue}
483− {customChainDialogue}
484− </React.Fragment>
485− ),
486− });
487−
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− );
509−
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− });
519−
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− );
529−
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− );
544−
545− const sendDialogue = web3.matchWith({
546− Nothing: _ => '',
547− Just: _ => (
548− <React.Fragment>
549− {confirmButton}
550− {sendTxnButton}
551− </React.Fragment>
552− ),
553− });
554−
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− );
565−
566− return (
567− <React.Fragment>
568− {generateTxnButton}
569− {unsignedTxnDisplay}
570−
571− {gasPriceRangeDialogue}
572− {toggleGasDetailsDialogue}
573−
574− {showGasDetails && (
575− <div>
576− {gasPriceDialogue}
577− {gasLimitDialogue}
578− </div>
579− )}
580− {onlineParamsDialogue}
581−
582− {signTxnButton}
583−
584− {signedTxnDisplay}
585− {sendDialogue}
586−
587− {txnErrorDialogue}
588− </React.Fragment>
589− );
590− }
591−}
592−
593−export default compose(
594− withNetwork,
595− withHistory,
596− withWallet,
597− withTxnCursor
598−)(StatelessTransaction);