@@ -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);