@@ -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− fromWei,
19− toWei,
20− hexify,
21− renderSignedTx,
22− signTransaction,
23−} from '../../lib/txn';
24−
25−import { withTxnConfirmations } from '../../store/txnConfirmations';
26−import { withNetwork } from '../../store/network';
27−import { withHistory } from '../../store/history';
28−import { withWallet } from '../../store/wallet';
29−import { withTxnCursor } from '../../store/txnCursor';
30−
31−const SUBMISSION_STATES = {
32− PROMPT: 'Send transaction',
33− PREPARING: 'Preparing...',
34− FUNDING: 'Finding transaction funding...',
35− SENDING: 'Sending transaction...',
36−};
37−
38−class StatelessTransaction extends React.Component {
39− constructor(props) {
40− super(props);
41−
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− };
55−
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− }
69−
70− componentDidMount() {
71− const { props } = this;
72−
73− const addr = need.addressFromWallet(props.wallet);
74−
75− props.web3.matchWith({
76− Nothing: () => {},
77− Just: w3 => {
78− const validWeb3 = w3.value;
79−
80− const getTxMetadata = [
81− validWeb3.eth.getTransactionCount(addr),
82− validWeb3.eth.net.getId(),
83− validWeb3.eth.getGasPrice(),
84− ];
85−
86− Promise.all(getTxMetadata).then(r => {
87− const txMetadata = {
88− nonce: r[0],
89− chainId: r[1],
90− gasPrice: fromWei(r[2], 'gwei'),
91− };
92−
93− this.setState({ ...txMetadata });
94− });
95− },
96− });
97− }
98−
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− }
110−
111− this.clearStx();
112− }
113−
114− setUserApproval() {
115− const { state } = this;
116− this.setState({ userApproval: !state.userApproval });
117− }
118−
119− toggleGasDetails() {
120− this.setState({
121− showGasDetails: !this.state.showGasDetails,
122− });
123− }
124−
125− setStx(stx) {
126− this.setState({
127− stx,
128− userApproval: false,
129− });
130− }
131−
132− setTxn(txn) {
133− this.setState({ txn });
134− }
135−
136− createUnsignedTxn() {
137− const txn = this.props.createUnsignedTxn();
138−
139− this.setState({ txn });
140− }
141−
142− clearTxn() {
143− this.setState({
144− userApproval: false,
145− txn: Nothing(),
146− stx: Nothing(),
147− });
148− }
149−
150− setNonce(nonce) {
151− this.setState({ nonce });
152− this.clearStx();
153− }
154−
155− setChainId(chainId) {
156− this.setState({ chainId });
157− this.clearStx();
158− }
159−
160− setGasPrice(gasPrice) {
161− this.setState({ gasPrice });
162− this.clearStx();
163− }
164−
165− rangeChange(e) {
166− this.setGasPrice(e.target.value);
167− }
168−
169− setGasLimit(gasLimit) {
170− this.setState({ gasLimit });
171− this.clearStx();
172− }
173−
174− clearStx() {
175− this.setState({
176− userApproval: false,
177− stx: Nothing(),
178− });
179− }
180−
181− async sendTxn() {
182− const { props, state } = this;
183− const web3 = props.web3.value;
184−
185− const stx = state.stx.matchWith({
186− Just: tx => tx.value,
187− Nothing: () => {
188− throw new Error(BRIDGE_ERROR.MISSING_TXN);
189− },
190− });
191−
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− }
206−
207− this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
208−
209− const rawTx = hexify(stx.serialize());
210− const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
211−
212− const address = need.addressFromWallet(props.wallet);
213− let balance = await web3.eth.getBalance(address);
214− let hasBalance = balance >= cost;
215−
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− }
222−
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− });
231−
232− // if we have the balance, proceed with submission
233− } else {
234− this.setState({ txStatus: SUBMISSION_STATES.SENDING });
235−
236− sendSignedTransaction(web3, stx, usedTank, props.setTxnConfirmations)
237− .then(txHash => {
238− props.setTxnCursor(Just(Ok(txHash)));
239−
240− let routeData = {};
241− if (props.newNetworkSeed) {
242− props.setNetworkSeed(Just(props.newNetworkSeed));
243− routeData.promptKeyfile = true;
244− }
245−
246− if (props.newRevision) {
247− props.setNetworkRevision(Just(props.newRevision));
248− }
249−
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− }
260−
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− }
275−
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− };
286−
287− return map[chainId];
288− }
289−
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− }
322−
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;
337−
338− const {
339− setNonce,
340− setChainId,
341− setGasLimit,
342− setGasPrice,
343− toggleGasDetails,
344− setUserApproval,
345− sendTxn,
346− createUnsignedTxn,
347− handleChainUpdate,
348− } = this;
349−
350− const { state } = this;
351−
352− const canSign = Just.hasInstance(txn);
353− const canApprove = Just.hasInstance(stx);
354− const canSend = Just.hasInstance(stx) && userApproval === true;
355−
356− const chainOptions = this.getChainOptions();
357−
358− const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
359−
360− const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
361−
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− );
372−
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− });
382−
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>
397−
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− />
407−
408− <div className="flex space-between text-sm mb-8">
409− <div>Cheap</div>
410− <div>Fast</div>
411− </div>
412− </React.Fragment>
413− );
414−
415− const toggleGasDetailsDialogue = (
416− <span className="text-link" onClick={toggleGasDetails}>
417− Gas Details
418− </span>
419− );
420−
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− );
431−
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− );
442−
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− );
453−
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− );
467−
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− );
478−
479− const onlineParamsDialogue = web3.matchWith({
480− Just: _ => <div />,
481− Nothing: _ => (
482− <React.Fragment>
483− {nonceDialogue}
484− {chainDialogue}
485− {customChainDialogue}
486− </React.Fragment>
487− ),
488− });
489−
490− const signTxnButton = (
491− <Button
492− disabled={!canSign}
493− className={'mt-8'}
494− prop-size={'lg wide'}
495− prop-color={signerButtonColor}
496− onClick={() =>
497− signTransaction({
498− ...this.props,
499− ...this.state,
500− setStx: this.setStx,
501− wallet: this.props.wallet.value,
502− txn: txn.value,
503− })
504− }>
505− {'Sign Transaction'}
506− </Button>
507− );
508−
509− const signedTxnDisplay = stx.matchWith({
510− Nothing: _ => '',
511− Just: tx => (
512− <React.Fragment>
513− <H3 className={'mt-8'}>{'Signed Transaction'}</H3>
514− <Code>{JSON.stringify(renderSignedTx(tx.value), null, 2)}</Code>
515− </React.Fragment>
516− ),
517− });
518−
519− const confirmButton = (
520− <CheckboxButton
521− className={'mt-8'}
522− disabled={!canApprove}
523− onToggle={setUserApproval}
524− state={userApproval}
525− label="I approve this transaction and wish to send."
526− />
527− );
528−
529− const sending = txStatus !== SUBMISSION_STATES.PROMPT;
530− const sendTxnSpinner = sending ? '' : 'hide';
531− const sendTxnButton = (
532− <Button
533− prop-size={'xl wide'}
534− className={'mt-8'}
535− disabled={!canSend || sending}
536− onClick={sendTxn}>
537− <span className="relative">
538− <span className={`btn-spinner ${sendTxnSpinner}`} />
539− {txStatus}
540− </span>
541− </Button>
542− );
543−
544− const sendDialogue = web3.matchWith({
545− Nothing: _ => '',
546− Just: _ => (
547− <React.Fragment>
548− {confirmButton}
549− {sendTxnButton}
550− </React.Fragment>
551− ),
552− });
553−
554− const txnErrorDialogue = Nothing.hasInstance(state.txError) ? (
555− ''
556− ) : (
557− <Warning className={'mt-8'}>
558− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
559− {'There was an error sending your transaction.'}
560− </H3>
561− {state.txError.value}
562− </Warning>
563− );
564−
565− return (
566− <React.Fragment>
567− {generateTxnButton}
568− {unsignedTxnDisplay}
569−
570− {gasPriceRangeDialogue}
571− {toggleGasDetailsDialogue}
572−
573− {showGasDetails && (
574− <div>
575− {gasPriceDialogue}
576− {gasLimitDialogue}
577− </div>
578− )}
579− {onlineParamsDialogue}
580−
581− {signTxnButton}
582−
583− {signedTxnDisplay}
584− {sendDialogue}
585−
586− {txnErrorDialogue}
587− </React.Fragment>
588− );
589− }
590−}
591−
592−export default compose(
593− withNetwork,
594− withTxnConfirmations,
595− withHistory,
596− withWallet,
597− withTxnCursor
598−)(StatelessTransaction);