on master help sign in
Merge pull request #234 from urbit/feat/ethereum-design-improvements

Ethereum design improvements

commit01a377331c855cc494510961ca4b3ff1613d8365 authorMatt Condon <mattgcondon@gmail.com> dateWed 21 Aug 2019, 17:15 (7 years ago) parent64dc3135088ea6
27 files changed+328−116
package-lock.json+13 package.json+1 src/components/Accordion.js+1−1 src/components/CopiableAddress.js+23 src/components/CopyButton.js+29 src/components/Crumbs.js+1−1 src/components/InlineEthereumTransaction.js+74−27 src/components/LoadableButton.js−7 src/components/LoadingBar.js+2−2 src/components/ProgressButton.js+32 src/components/Tabs.js+1−1 src/form/Inputs.js+2−3 src/indigo-react/components/Button.js+6−4 src/indigo-react/components/CheckboxInput.js+1−1 src/indigo-react/components/LinkButton.js+2−2 src/indigo-react/components/SelectInput.js+1−1 src/indigo-react/components/ToggleInput.js+8−2 src/lib/constants.js+3 src/lib/useSuggestedGasPrice.js → src/lib/getSuggestedGasPrice.js+10−17 src/lib/reticket.js+1−1 src/lib/useCopiable.js+23 src/lib/useEthereumTransaction.js+34−36 src/lib/useGasPrice.js+48 src/style/bridge.scss+6 src/style/indigo-proposals.scss−4 src/views/Invite/InviteEmail.js+5−5 src/views/Points.js+1−1
package-lock.json+13
@@ -4827,6 +4827,14 @@
48274827 "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
48284828 "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
48294829 },
4830+ "copy-to-clipboard": {
4831+ "version": "3.2.0",
4832+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
4833+ "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
4834+ "requires": {
4835+ "toggle-selection": "^1.0.6"
4836+ }
4837+ },
48304838 "core-js-compat": {
48314839 "version": "3.1.3",
48324840 "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.3.tgz",
@@ -17179,6 +17187,11 @@
1717917187 "repeat-string": "^1.6.1"
1718017188 }
1718117189 },
17190+ "toggle-selection": {
17191+ "version": "1.0.6",
17192+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
17193+ "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
17194+ },
1718217195 "tough-cookie": {
1718317196 "version": "2.4.3",
1718417197 "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
package.json+1
@@ -26,6 +26,7 @@
2626 "bip39": "^2.5.0",
2727 "bn.js": "^4.11.8",
2828 "classnames": "^2.2.6",
29+ "copy-to-clipboard": "^3.2.0",
2930 "ethereum-blockies-base64": "^1.0.2",
3031 "ethereumjs-tx": "^1.3.7",
3132 "file-saver": "^2.0.0",
src/components/Accordion.js+1−1
@@ -24,7 +24,7 @@ export default function Accordion({
2424 <Grid.Item
2525 full
2626 className={cn('f5 pv3 rel', {
27− clickable: !option.disabled,
27+ pointer: !option.disabled,
2828 gray3: option.disabled,
2929 })}
3030 onClick={
src/components/CopiableAddress.jsnew file+23
@@ -0,0 +1,23 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import useCopiable from 'lib/useCopiable';
5+
6+export default function CopiableAddress({
7+ as: As = 'span',
8+ children,
9+ className,
10+}) {
11+ const [doCopy, didCopy] = useCopiable(children);
12+
13+ return (
14+ <As
15+ onClick={doCopy}
16+ className={cn(className, 'mono', {
17+ pointer: !didCopy,
18+ green3: didCopy,
19+ })}>
20+ {children}
21+ </As>
22+ );
23+}
src/components/CopyButton.jsnew file+29
@@ -0,0 +1,29 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { LinkButton } from 'indigo-react';
4+
5+import useCopiable from 'lib/useCopiable';
6+
7+const renderDefaultCopyText = didCopy => (didCopy ? 'Copied!' : 'Copy');
8+
9+export default function CopyButton({
10+ as: As = LinkButton,
11+ text,
12+ children = renderDefaultCopyText,
13+ className,
14+ ...rest
15+}) {
16+ const [doCopy, didCopy] = useCopiable(text);
17+
18+ return (
19+ <As
20+ onClick={doCopy}
21+ className={cn(className, {
22+ black: !didCopy,
23+ green3: didCopy,
24+ })}
25+ {...rest}>
26+ {children(didCopy)}
27+ </As>
28+ );
29+}
src/components/Crumbs.js+1−1
@@ -16,7 +16,7 @@ export default function Crumbs({ className, routes = [] }) {
1616 as={Breadcrumb}
1717 onClick={route.action}
1818 disabled={disabled}
19− className={cn(textStyle, { 'clickable underline': !disabled })}>
19+ className={cn(textStyle, { 'pointer underline': !disabled })}>
2020 {route.text}
2121 </Flex.Item>
2222 {i !== lastIndex && (
src/components/InlineEthereumTransaction.js+74−27
@@ -8,19 +8,21 @@ import {
88 LinkButton,
99 H5,
1010 Text,
11− HelpText,
1211} from 'indigo-react';
1312import { fromWei } from 'web3-utils';
1413
1514import { useExploreTxUrl } from 'lib/explorer';
1615import { hexify } from 'lib/txn';
1716
18−import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
19−import WarningBox from './WarningBox';
2017import { composeValidator, buildCheckboxValidator } from 'form/validators';
2118import BridgeForm from 'form/BridgeForm';
2219import Condition from 'form/Condition';
2320
21+import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
22+import WarningBox from './WarningBox';
23+import CopyButton from './CopyButton';
24+import ProgressButton from './ProgressButton';
25+
2426export default function InlineEthereumTransaction({
2527 // from useEthereumTransaction.bind
2628 initializing,
@@ -41,6 +43,7 @@ export default function InlineEthereumTransaction({
4143 chainId,
4244 needFunds,
4345 signedTransaction,
46+ confirmationProgress,
4447
4548 // additional from parent
4649 label = 'Generate & Sign Transaction',
@@ -52,9 +55,9 @@ export default function InlineEthereumTransaction({
5255 // show configure controls pre-broadcast
5356 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
5457 // show the send/loading button while signed, broadcasting, or confirme
55− const showBroadcastButton = signed || broadcasted || confirmed;
58+ const showBroadcastButton = signed;
59+ const showLoadingButton = broadcasted || confirmed;
5660 const canBroadcast = signed && !needFunds;
57− const isLoading = broadcasted || confirmed;
5861 // show signed tx only when signing (for offline usage)
5962 const showSignedTx = signed;
6063
@@ -78,7 +81,7 @@ export default function InlineEthereumTransaction({
7881 [resetGasPrice]
7982 );
8083
81− const renderPrimaryButton = () => {
84+ const renderPrimarySection = () => {
8285 if (error) {
8386 return (
8487 <Grid.Item full as={RestartButton} solid onClick={() => reset()}>
@@ -87,12 +90,9 @@ export default function InlineEthereumTransaction({
8790 );
8891 } else if (completed) {
8992 return (
90− <>
91− <Grid.Divider />
92− <Grid.Item full as={RestartButton} onClick={onReturn}>
93− Return
94− </Grid.Item>
95− </>
93+ <Grid.Item full className="pv4 black f5">
94+ Transaction Complete
95+ </Grid.Item>
9696 );
9797 } else if (showBroadcastButton) {
9898 return (
@@ -102,11 +102,20 @@ export default function InlineEthereumTransaction({
102102 solid
103103 success
104104 disabled={!canBroadcast}
105− loading={isLoading}
106105 onClick={() => broadcast()}>
107106 Send Transaction
108107 </Grid.Item>
109108 );
109+ } else if (showLoadingButton) {
110+ return (
111+ <Grid.Item
112+ full
113+ as={ProgressButton}
114+ disabled={true}
115+ progress={confirmationProgress}>
116+ Sending Transaction
117+ </Grid.Item>
118+ );
110119 } else {
111120 return (
112121 <Grid.Item
@@ -121,12 +130,17 @@ export default function InlineEthereumTransaction({
121130 }
122131 };
123132
133+ const serializedTxHex = useMemo(
134+ () => signedTransaction && hexify(signedTransaction.serialize()),
135+ [signedTransaction]
136+ );
137+
124138 return (
125139 <Grid className={cn(className, 'mt1')}>
126140 <BridgeForm validate={validate} onValues={onValues}>
127141 {({ handleSubmit }) => (
128142 <>
129− {renderPrimaryButton()}
143+ {renderPrimarySection()}
130144
131145 {error && (
132146 <Grid.Item full as={ErrorText} className="mt1">
@@ -149,12 +163,14 @@ export default function InlineEthereumTransaction({
149163 full
150164 as={ToggleInput}
151165 name="useAdvanced"
152− label="Advanced Configuration"
153− inverseLabel="Cancel Advanced Configuration"
166+ label="Advanced"
167+ inverseLabel="Back to Defaults"
168+ inverseColor="red3"
154169 disabled={!showConfigureInput || initializing}
155170 />
156171
157172 <Condition when="useAdvanced" is={true}>
173+ <Grid.Divider />
158174 <Grid.Item
159175 full
160176 as={Flex}
@@ -183,13 +199,7 @@ export default function InlineEthereumTransaction({
183199 <Flex.Item as={Text}>Cheap</Flex.Item>
184200 <Flex.Item as={Text}>Fast</Flex.Item>
185201 </Grid.Item>
186− <Grid.Item full as={HelpText} className="mt3">
187− Nonce: {nonce}
188− </Grid.Item>
189− <Grid.Item full as={HelpText} className="mt1">
190− Chain ID: {chainId}
191− </Grid.Item>
192− <Grid.Divider className="mt3" />
202+ <Grid.Divider className="mt4" />
193203 </Condition>
194204 </>
195205 )}
@@ -200,14 +210,42 @@ export default function InlineEthereumTransaction({
200210 full
201211 as={ToggleInput}
202212 name="viewSigned"
203− label="View Signed Transaction"
204− inverseLabel="Hide Signed Transaction"
213+ label="Signed Transaction"
214+ inverseLabel="Hide"
205215 disabled={!showSignedTx}
206216 />
207217 <Condition when="viewSigned" is={true}>
208− <Grid.Item full as="code" className="f6 mono gray4 wrap">
209− {hexify(signedTransaction.serialize())}
218+ <Grid.Divider />
219+ <Grid.Item
220+ full
221+ as={Flex}
222+ justify="between"
223+ className="pv4 black f5">
224+ <Flex.Item>Nonce</Flex.Item>
225+ <Flex.Item>{nonce}</Flex.Item>
226+ </Grid.Item>
227+ <Grid.Divider />
228+ <Grid.Item
229+ full
230+ as={Flex}
231+ justify="between"
232+ className="pv4 black f5">
233+ <Flex.Item>Gas Price</Flex.Item>
234+ <Flex.Item>{gasPrice.toFixed()} Gwei</Flex.Item>
235+ </Grid.Item>
236+ <Grid.Divider />
237+ <Grid.Item
238+ full
239+ as={Flex}
240+ justify="between"
241+ className="mt3 mb2">
242+ <Flex.Item as={H5}>Signed Transaction Hex</Flex.Item>
243+ <Flex.Item as={CopyButton} text={serializedTxHex} />
244+ </Grid.Item>
245+ <Grid.Item full as="code" className="mb4 f6 mono gray4 wrap">
246+ {serializedTxHex}
210247 </Grid.Item>
248+ <Grid.Divider />
211249 </Condition>
212250 </>
213251 )}
@@ -232,6 +270,15 @@ export default function InlineEthereumTransaction({
232270 <Grid.Divider />
233271 </>
234272 )}
273+
274+ {completed && (
275+ <>
276+ <Grid.Item full as={RestartButton} onClick={onReturn}>
277+ Return
278+ </Grid.Item>
279+ <Grid.Divider />
280+ </>
281+ )}
235282 </>
236283 )}
237284 </BridgeForm>
src/components/LoadableButton.jsdeleted−7
@@ -1,7 +0,0 @@
1−import React from 'react';
2−import { Button } from 'indigo-react';
3−
4−// TODO: implement loading bar behind text
5−export default function LoadableButton({ ...rest }) {
6− return <Button {...rest}></Button>;
7−}
src/components/LoadingBar.js+2−2
@@ -2,11 +2,11 @@ import React from 'react';
22import cn from 'classnames';
33
44// progrss is [0, 1]
5−export default function LoadingBar({ className, progress = 1 }) {
5+export default function LoadingBar({ className, progress = 1.0 }) {
66 return (
77 <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
88 <div
9− className="abs bg-green3"
9+ className="abs bg-green3 animated-width"
1010 style={{
1111 top: 0,
1212 bottom: 0,
src/components/ProgressButton.jsnew file+32
@@ -0,0 +1,32 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Button } from 'indigo-react';
4+import { blinkIf } from './Blinky';
5+
6+export default function ProgressButton({
7+ as: As = Button,
8+ progress = 0.0,
9+ color = 'green4',
10+ success = true,
11+ ...rest
12+}) {
13+ return (
14+ <As
15+ {...rest}
16+ solid
17+ success={success}
18+ accessory={blinkIf(progress < 1.0, '->')}
19+ background={
20+ <span
21+ className={cn('abs z1 animated-width', `bg-${color}`)}
22+ style={{
23+ left: 0,
24+ top: 0,
25+ right: 0,
26+ bottom: 0,
27+ width: `${progress * 100.0}%`,
28+ }}></span>
29+ }
30+ />
31+ );
32+}
src/components/Tabs.js+1−1
@@ -25,7 +25,7 @@ export default function Tabs({
2525 key={option.value}
2626 onClick={() => onTabChange(option.value)}
2727 className={cn(
28− 'f5 pv3 clickable nowrap',
28+ 'f5 pv3 pointer nowrap',
2929 {
3030 'black b-black bb1': isActive,
3131 gray3: !isActive,
src/form/Inputs.js+2−3
@@ -16,9 +16,8 @@ const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
1616const PLACEHOLDER_MNEMONIC =
1717 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
1818const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
19−const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
20−const PLACEHOLDER_PRIVATE_KEY =
21− '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
19+const PLACEHOLDER_ADDRESS = '0x';
20+const PLACEHOLDER_PRIVATE_KEY = '0x';
2221const PLACEHOLDER_EMAIL = 'email@example.com';
2322
2423const formatPat = buildFormatter([downcase, ensurePatFormat]);
src/indigo-react/components/Button.js+6−4
@@ -15,6 +15,7 @@ export default function Button({
1515 className,
1616 accessory = '→',
1717 onClick,
18+ background,
1819 children,
1920 ...rest
2021}) {
@@ -23,7 +24,7 @@ export default function Button({
2324 as={As}
2425 gap={1}
2526 className={cn(
26− 'pointer pv4 truncate flex-row justify-between us-none',
27+ 'rel pointer pv4 truncate flex-row justify-between us-none',
2728 {
2829 p4: solid,
2930 },
@@ -49,17 +50,18 @@ export default function Button({
4950 }}
5051 onClick={!disabled && onClick ? onClick : undefined}
5152 {...rest}>
52− <Grid.Item full as={Flex} justify="between">
53+ {background}
54+ <Grid.Item full as={Flex} justify="between" className="z2">
5355 <span>{children}</span>
5456 <div className={cn('pl4')}>{accessory}</div>
5557 </Grid.Item>
5658 {detail && (
57− <Grid.Item full as={HelpText}>
59+ <Grid.Item full as={HelpText} className="z2">
5860 {detail}
5961 </Grid.Item>
6062 )}
6163 {disabled && disabledDetail && (
62− <Grid.Item full className="f6 black mt1">
64+ <Grid.Item full className="f6 black mt1 z2">
6365 {disabledDetail}
6466 </Grid.Item>
6567 )}
src/indigo-react/components/CheckboxInput.js+1−1
@@ -39,7 +39,7 @@ export default function CheckboxInput({
3939 <Flex.Item
4040 flex
4141 as="label"
42− className="f6 mr3 lh-tall us-none clickable flex-row align-center"
42+ className="f6 mr3 lh-tall us-none pointer flex-row align-center"
4343 htmlFor={name}>
4444 <Flex
4545 justify="center"
src/indigo-react/components/LinkButton.js+2−2
@@ -19,9 +19,9 @@ export default function LinkButton({
1919 }),
2020 }}
2121 className={cn(
22− 'pointer underline',
22+ 'us-none pointer underline',
2323 {
24− black: !disabled,
24+ // NOTE: inherit styling from parent otherwise
2525 gray4: disabled,
2626 },
2727 className
src/indigo-react/components/SelectInput.js+1−1
@@ -61,7 +61,7 @@ export default function SelectInput({
6161 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
6262 {label}
6363 </Flex.Item>
64− <Flex.Item as={Flex} row className="rel clickable" onClick={toggleOpen}>
64+ <Flex.Item as={Flex} row className="rel pointer" onClick={toggleOpen}>
6565 <Flex.Item
6666 flex
6767 className={cn(
src/indigo-react/components/ToggleInput.js+8−2
@@ -10,6 +10,7 @@ export default function ToggleInput({
1010 name,
1111 label,
1212 inverseLabel,
13+ inverseColor = 'black',
1314 className,
1415
1516 //
@@ -49,10 +50,15 @@ export default function ToggleInput({
4950 flex
5051 as="label"
5152 className={cn('f6 pv2 lh-tall us-none flex-row align-center', {
52− clickable: !disabled,
53+ pointer: !disabled,
5354 })}
5455 htmlFor={name}>
55− <LinkButton disabled={disabled} className="f5">
56+ <LinkButton
57+ disabled={disabled}
58+ className={cn('f5', {
59+ black: !input.checked,
60+ [inverseColor]: input.checked,
61+ })}>
5662 {input.checked ? inverseLabel : label}
5763 </LinkButton>
5864 </Flex.Item>
src/lib/constants.js+3
@@ -69,6 +69,8 @@ const PROFILE_STATES = {
6969 INPUT_ERROR: Symbol('INPUT_ERROR'),
7070};
7171
72+const PROGRESS_ANIMATION_DELAY_MS = 500; // .animated-width
73+
7274export {
7375 CHECK_BLOCK_EVERY_MS,
7476 DEFAULT_GAS_PRICE_GWEI,
@@ -87,4 +89,5 @@ export {
8789 GALAXY_ENTROPY_BITS,
8890 SEED_ENTROPY_BITS,
8991 ZOD,
92+ PROGRESS_ANIMATION_DELAY_MS,
9093};
src/lib/useSuggestedGasPrice.js → src/lib/getSuggestedGasPrice.jsrenamed+10−17
@@ -1,13 +1,13 @@
1−import { useState, useEffect } from 'react';
21import { NETWORK_TYPES } from './network';
32import { DEFAULT_GAS_PRICE_GWEI } from './constants';
43
5−async function getSuggestedGasPrice(networkType) {
4+export default async function getSuggestedGasPrice(networkType) {
65 switch (networkType) {
76 case NETWORK_TYPES.ROPSTEN:
87 return 10;
98 case NETWORK_TYPES.OFFLINE:
109 return DEFAULT_GAS_PRICE_GWEI;
10+ case NETWORK_TYPES.LOCAL:
1111 default:
1212 try {
1313 const response = await fetch(
@@ -17,24 +17,17 @@ async function getSuggestedGasPrice(networkType) {
1717 cache: 'no-cache',
1818 }
1919 );
20+
2021 const json = await response.json();
21− return Math.min(json.fast / 10, 20); // to gwei
22+
23+ // ethgasstation returns values in floating point, one order of magitude
24+ // more than gwei. see: https://docs.ethgasstation.info
25+ const suggestedGasPrice = json.fast / 10; // to gwei
26+
27+ // we don't want to charge users more than our default 20 gwei
28+ return Math.min(suggestedGasPrice, DEFAULT_GAS_PRICE_GWEI);
2229 } catch (e) {
2330 return DEFAULT_GAS_PRICE_GWEI;
2431 }
2532 }
2633}
27−
28−function useSuggestedGasPrice(networkType) {
29− const [gasPrice, _setGasPrice] = useState(DEFAULT_GAS_PRICE_GWEI);
30−
31− useEffect(() => {
32− (async () => {
33− _setGasPrice(await getSuggestedGasPrice(networkType));
34− })();
35− }, [networkType]);
36−
37− return { gasPrice };
38−}
39−
40−export { getSuggestedGasPrice, useSuggestedGasPrice };
src/lib/reticket.js+1−1
@@ -10,7 +10,7 @@ import {
1010} from './keys';
1111import { addHexPrefix } from './wallet';
1212import { sendAndAwaitAll } from './txn';
13−import { getSuggestedGasPrice } from './useSuggestedGasPrice';
13+import getSuggestedGasPrice from './getSuggestedGasPrice';
1414import { GAS_LIMITS } from './constants';
1515import { toWei } from 'web3-utils';
1616
src/lib/useCopiable.jsnew file+23
@@ -0,0 +1,23 @@
1+import { useState, useEffect, useRef, useCallback } from 'react';
2+import copy from 'copy-to-clipboard';
3+
4+const COPY_DELAY = 2 * 1000; // ms
5+
6+export default function useCopiable(text) {
7+ const [didCopy, setDidCopy] = useState(false);
8+
9+ // what we really want here is a useDebouncedCallback from use-debounced
10+ // but that's a relatively large dep for a single simple usage so ¯\_(ツ)_/¯
11+ const listener = useRef(); // <NodeJS.Listener>
12+ useEffect(() => () => clearTimeout(listener.current), []);
13+
14+ const doCopy = useCallback(() => {
15+ copy(text);
16+ setDidCopy(true);
17+
18+ clearTimeout(listener.current);
19+ listener.current = setTimeout(() => setDidCopy(false), COPY_DELAY);
20+ }, [text]);
21+
22+ return [doCopy, didCopy];
23+}
src/lib/useEthereumTransaction.js+34−36
@@ -1,22 +1,27 @@
11import { useCallback, useEffect, useState } from 'react';
22import { Just } from 'folktale/maybe';
3−import { fromWei, toWei } from 'web3-utils';
3+import { toWei } from 'web3-utils';
44
55import { useNetwork } from 'store/network';
66import { useWallet } from 'store/wallet';
77import { usePointCursor } from 'store/pointCursor';
8−import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
98
10−import { GAS_LIMITS } from './constants';
9+import {
10+ GAS_LIMITS,
11+ DEFAULT_GAS_PRICE_GWEI,
12+ PROGRESS_ANIMATION_DELAY_MS,
13+} from './constants';
1114import {
1215 signTransaction,
1316 sendSignedTransaction,
1417 waitForTransactionConfirm,
1518 hexify,
1619} from './txn';
17−import * as need from './need';
18−import { ensureFundsFor } from './tank';
19−import useDeepEqualReference from './useDeepEqualReference';
20+import * as need from 'lib/need';
21+import { ensureFundsFor } from 'lib/tank';
22+import useDeepEqualReference from 'lib/useDeepEqualReference';
23+import useGasPrice from 'lib/useGasPrice';
24+import timeout from 'lib/timeout';
2025
2126const STATE = {
2227 NONE: 'NONE',
@@ -37,7 +42,7 @@ export default function useEthereumTransaction(
3742 transactionBuilder,
3843 refetch,
3944 initialGasLimit = GAS_LIMITS.DEFAULT,
40− initialGasPrice = 20
45+ initialGasPrice = DEFAULT_GAS_PRICE_GWEI
4146) {
4247 const { wallet, walletType, walletHdPath } = useWallet();
4348 const { web3, networkType } = useNetwork();
@@ -59,14 +64,13 @@ export default function useEthereumTransaction(
5964 const [state, setState] = useState(STATE.NONE);
6065 const [chainId, setChainId] = useState();
6166 const [nonce, setNonce] = useState();
62− const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
63− const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
64− const { gasPrice: estimatedGasPrice } = useSuggestedGasPrice();
67+ const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
6568 const [gasLimit] = useState(initialGasLimit);
6669 const [unsignedTransaction, setUnsignedTransaction] = useState();
6770 const [signedTransaction, setSignedTransaction] = useState();
6871 const [txHash, setTxHash] = useState();
6972 const [needFunds, setNeedFunds] = useState();
73+ const [confirmationProgress, setConfirmationProgress] = useState(0.0);
7074
7175 const initializing = nonce === undefined || chainId === undefined;
7276 const constructed = !!unsignedTransaction;
@@ -128,6 +132,7 @@ export default function useEthereumTransaction(
128132
129133 const broadcast = useCallback(async () => {
130134 try {
135+ setConfirmationProgress(0.0);
131136 setError(undefined);
132137
133138 const rawTx = hexify(signedTransaction.serialize());
@@ -158,8 +163,14 @@ export default function useEthereumTransaction(
158163 setState(STATE.BROADCASTED);
159164 setTxHash(txHash);
160165
166+ await timeout(PROGRESS_ANIMATION_DELAY_MS);
167+
168+ setConfirmationProgress(0.2);
169+
161170 await waitForTransactionConfirm(_web3, txHash);
162171
172+ setConfirmationProgress(0.9);
173+
163174 setState(STATE.CONFIRMED);
164175 } catch (error) {
165176 setError(error);
@@ -179,25 +190,12 @@ export default function useEthereumTransaction(
179190 setSignedTransaction(undefined);
180191 setNonce(undefined);
181192 setChainId(undefined);
182− setGasPrice(suggestedGasPrice);
193+ resetGasPrice();
183194 setState(STATE.NONE);
184195 setError(undefined);
185196 setNeedFunds(undefined);
186− }, [suggestedGasPrice, setError]);
187−
188− const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
189− setGasPrice,
190− suggestedGasPrice,
191− ]);
192−
193− useEffect(() => {
194− const gasPrice = parseInt(
195− fromWei(estimatedGasPrice.toString(), 'gwei'),
196− 10
197− );
198− setSuggestedGasPrice(gasPrice);
199− setGasPrice(gasPrice);
200− }, [estimatedGasPrice]);
197+ setConfirmationProgress(0.0);
198+ }, [resetGasPrice, setError]);
201199
202200 useEffect(() => {
203201 let mounted = true;
@@ -226,15 +224,7 @@ export default function useEthereumTransaction(
226224 })();
227225
228226 return () => (mounted = false);
229− }, [
230− _wallet,
231− _web3,
232− setError,
233− nonce,
234− chainId,
235− networkType,
236− estimatedGasPrice,
237− ]);
227+ }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
238228
239229 useEffect(() => {
240230 let mounted = true;
@@ -260,6 +250,14 @@ export default function useEthereumTransaction(
260250 return;
261251 }
262252
253+ setConfirmationProgress(1.0);
254+
255+ await timeout(PROGRESS_ANIMATION_DELAY_MS);
256+
257+ if (!mounted) {
258+ return;
259+ }
260+
263261 setState(STATE.COMPLETED);
264262 })();
265263 }
@@ -283,7 +281,7 @@ export default function useEthereumTransaction(
283281 reset,
284282 error,
285283 inputsLocked,
286− //
284+ confirmationProgress,
287285 txHash,
288286 signedTransaction,
289287 gasPrice,
src/lib/useGasPrice.jsnew file+48
@@ -0,0 +1,48 @@
1+import { useCallback, useEffect, useState } from 'react';
2+
3+import { useNetwork } from 'store/network';
4+
5+import getSuggestedGasPrice from 'lib/getSuggestedGasPrice';
6+import { DEFAULT_GAS_PRICE_GWEI } from './constants';
7+
8+/**
9+ * handles all of the state of components that manage gas prices
10+ * + simple get/set
11+ * + lazy fetching optimal gas from oracle
12+ * + resetting to oracle (or sync) default
13+ * @param {Number} initialGasPrice the initial gas price available instantly
14+ */
15+export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
16+ const { networkType } = useNetwork();
17+ const [suggestedGasPrice, setSuggestedGasPrice] = useState(initialGasPrice); // gwei
18+ const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
19+
20+ useEffect(() => {
21+ let mounted = true;
22+
23+ (async () => {
24+ const suggestedGasPrice = await getSuggestedGasPrice(networkType);
25+
26+ if (!mounted) {
27+ return;
28+ }
29+
30+ // TODO: BN
31+ setSuggestedGasPrice(suggestedGasPrice);
32+ setGasPrice(suggestedGasPrice);
33+ })();
34+
35+ return () => (mounted = true);
36+ }, [networkType]);
37+
38+ const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
39+ suggestedGasPrice,
40+ ]);
41+
42+ return {
43+ suggestedGasPrice,
44+ gasPrice,
45+ setGasPrice,
46+ resetGasPrice,
47+ };
48+}
src/style/bridge.scss+6
@@ -66,3 +66,9 @@ input[type='range'] {
6666 background-color: #e6e6e6;
6767 }
6868}
69+
70+// animated width
71+
72+.animated-width {
73+ transition: width 500ms ease-out;
74+}
src/style/indigo-proposals.scss−4
@@ -21,10 +21,6 @@
2121 background-color: #333333;
2222}
2323
24−.clickable {
25− cursor: pointer;
26−}
27−
2824.minh-100 {
2925 min-height: 100vh;
3026}
src/views/Invite/InviteEmail.js+5−5
@@ -35,14 +35,13 @@ import {
3535} from 'lib/txn';
3636import * as tank from 'lib/tank';
3737import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
38−import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
3938import * as need from 'lib/need';
4039import * as wg from 'lib/walletgen';
4140import useSetState from 'lib/useSetState';
4241import pluralize from 'lib/pluralize';
4342import useMailer from 'lib/useMailer';
4443
45−import LoadableButton from 'components/LoadableButton';
44+import ProgressButton from 'components/ProgressButton';
4645import Highlighted from 'components/Highlighted';
4746import BridgeForm from 'form/BridgeForm';
4847import { Field } from 'react-final-form';
@@ -57,6 +56,7 @@ import { FORM_ERROR } from 'final-form';
5756import SubmitButton from 'form/SubmitButton';
5857import FormError from 'form/FormError';
5958import { WARNING, onlyHasWarning } from 'form/helpers';
59+import useGasPrice from 'lib/useGasPrice';
6060
6161const INITIAL_VALUES = { emails: [''] };
6262
@@ -106,7 +106,7 @@ export default function InviteEmail() {
106106 const { pointCursor } = usePointCursor();
107107 const point = need.point(pointCursor);
108108 const { getHasReceived, sendMail } = useMailer();
109− const { gasPrice } = useSuggestedGasPrice(networkType);
109+ const { gasPrice } = useGasPrice();
110110
111111 const cachedEmails = useRef({});
112112
@@ -245,7 +245,7 @@ export default function InviteEmail() {
245245 nonce: nonce + i,
246246 // TODO: ^ make a useTransactionSigner to encapsulate this logic
247247 txn: inviteTx,
248− gasPrice: gasPrice.toString(),
248+ gasPrice: gasPrice.toString(), // expects string gwei
249249 gasLimit: GAS_LIMIT.toString(),
250250 });
251251
@@ -518,7 +518,7 @@ export default function InviteEmail() {
518518 ) : (
519519 <Grid.Item
520520 full
521− as={LoadableButton}
521+ as={ProgressButton}
522522 className="mt4"
523523 disabled={!canSend}
524524 accessory={`${visualProgress}/${fields.length}`}
src/views/Points.js+1−1
@@ -46,7 +46,7 @@ const PointList = function({ points, className, actions, ...rest }) {
4646 <Flex col>
4747 <Passport.Mini
4848 point={point}
49− className="clickable"
49+ className="pointer"
5050 onClick={() => {
5151 setPointCursor(Just(point));
5252 push(names.POINT);