on master help sign in
Merge pull request #234 from urbit/feat/ethereum-design-improvements
01a3773Matt Condon
package-lock.json+13
@@ -4827,6 +4827,14 @@
4827 "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
4827 "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
4828 "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
4828 "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
4829 },
4829 },
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+ },
4830 "core-js-compat": {
4838 "core-js-compat": {
4831 "version": "3.1.3",
4839 "version": "3.1.3",
4832 "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.3.tgz",
4840 "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.3.tgz",
@@ -17179,6 +17187,11 @@
17179 "repeat-string": "^1.6.1"
17187 "repeat-string": "^1.6.1"
17180 }
17188 }
17181 },
17189 },
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+ },
17182 "tough-cookie": {
17195 "tough-cookie": {
17183 "version": "2.4.3",
17196 "version": "2.4.3",
17184 "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
17197 "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
package.json+1
@@ -26,6 +26,7 @@
26 "bip39": "^2.5.0",
26 "bip39": "^2.5.0",
27 "bn.js": "^4.11.8",
27 "bn.js": "^4.11.8",
28 "classnames": "^2.2.6",
28 "classnames": "^2.2.6",
29+ "copy-to-clipboard": "^3.2.0",
29 "ethereum-blockies-base64": "^1.0.2",
30 "ethereum-blockies-base64": "^1.0.2",
30 "ethereumjs-tx": "^1.3.7",
31 "ethereumjs-tx": "^1.3.7",
31 "file-saver": "^2.0.0",
32 "file-saver": "^2.0.0",
src/components/Accordion.js+1−1
@@ -24,7 +24,7 @@ export default function Accordion({
24 <Grid.Item
24 <Grid.Item
25 full
25 full
26 className={cn('f5 pv3 rel', {
26 className={cn('f5 pv3 rel', {
27− clickable: !option.disabled,
27+ pointer: !option.disabled,
28 gray3: option.disabled,
28 gray3: option.disabled,
29 })}
29 })}
30 onClick={
30 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 = [] }) {
16 as={Breadcrumb}
16 as={Breadcrumb}
17 onClick={route.action}
17 onClick={route.action}
18 disabled={disabled}
18 disabled={disabled}
19− className={cn(textStyle, { 'clickable underline': !disabled })}>
19+ className={cn(textStyle, { 'pointer underline': !disabled })}>
20 {route.text}
20 {route.text}
21 </Flex.Item>
21 </Flex.Item>
22 {i !== lastIndex && (
22 {i !== lastIndex && (
src/components/InlineEthereumTransaction.js+74−27
@@ -8,19 +8,21 @@ import {
8 LinkButton,
8 LinkButton,
9 H5,
9 H5,
10 Text,
10 Text,
11− HelpText,
12} from 'indigo-react';
11} from 'indigo-react';
13import { fromWei } from 'web3-utils';
12import { fromWei } from 'web3-utils';
14
13
15import { useExploreTxUrl } from 'lib/explorer';
14import { useExploreTxUrl } from 'lib/explorer';
16import { hexify } from 'lib/txn';
15import { hexify } from 'lib/txn';
17
16
18−import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
19−import WarningBox from './WarningBox';
20import { composeValidator, buildCheckboxValidator } from 'form/validators';
17import { composeValidator, buildCheckboxValidator } from 'form/validators';
21import BridgeForm from 'form/BridgeForm';
18import BridgeForm from 'form/BridgeForm';
22import Condition from 'form/Condition';
19import Condition from 'form/Condition';
23
20
21+import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
22+import WarningBox from './WarningBox';
23+import CopyButton from './CopyButton';
24+import ProgressButton from './ProgressButton';
25+
24export default function InlineEthereumTransaction({
26export default function InlineEthereumTransaction({
25 // from useEthereumTransaction.bind
27 // from useEthereumTransaction.bind
26 initializing,
28 initializing,
@@ -41,6 +43,7 @@ export default function InlineEthereumTransaction({
41 chainId,
43 chainId,
42 needFunds,
44 needFunds,
43 signedTransaction,
45 signedTransaction,
46+ confirmationProgress,
44
47
45 // additional from parent
48 // additional from parent
46 label = 'Generate & Sign Transaction',
49 label = 'Generate & Sign Transaction',
@@ -52,9 +55,9 @@ export default function InlineEthereumTransaction({
52 // show configure controls pre-broadcast
55 // show configure controls pre-broadcast
53 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
56 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
54 // show the send/loading button while signed, broadcasting, or confirme
57 // 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;
56 const canBroadcast = signed && !needFunds;
60 const canBroadcast = signed && !needFunds;
57− const isLoading = broadcasted || confirmed;
58 // show signed tx only when signing (for offline usage)
61 // show signed tx only when signing (for offline usage)
59 const showSignedTx = signed;
62 const showSignedTx = signed;
60
63
@@ -78,7 +81,7 @@ export default function InlineEthereumTransaction({
78 [resetGasPrice]
81 [resetGasPrice]
79 );
82 );
80
83
81− const renderPrimaryButton = () => {
84+ const renderPrimarySection = () => {
82 if (error) {
85 if (error) {
83 return (
86 return (
84 <Grid.Item full as={RestartButton} solid onClick={() => reset()}>
87 <Grid.Item full as={RestartButton} solid onClick={() => reset()}>
@@ -87,12 +90,9 @@ export default function InlineEthereumTransaction({
87 );
90 );
88 } else if (completed) {
91 } else if (completed) {
89 return (
92 return (
90− <>
93+ <Grid.Item full className="pv4 black f5">
91− <Grid.Divider />
94+ Transaction Complete
92− <Grid.Item full as={RestartButton} onClick={onReturn}>
93− Return
94− </Grid.Item>
95+ </Grid.Item>
95− </>
96 );
96 );
97 } else if (showBroadcastButton) {
97 } else if (showBroadcastButton) {
98 return (
98 return (
@@ -102,11 +102,20 @@ export default function InlineEthereumTransaction({
102 solid
102 solid
103 success
103 success
104 disabled={!canBroadcast}
104 disabled={!canBroadcast}
105− loading={isLoading}
106 onClick={() => broadcast()}>
105 onClick={() => broadcast()}>
107 Send Transaction
106 Send Transaction
108 </Grid.Item>
107 </Grid.Item>
109 );
108 );
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+ );
110 } else {
119 } else {
111 return (
120 return (
112 <Grid.Item
121 <Grid.Item
@@ -121,12 +130,17 @@ export default function InlineEthereumTransaction({
121 }
130 }
122 };
131 };
123
132
133+ const serializedTxHex = useMemo(
134+ () => signedTransaction && hexify(signedTransaction.serialize()),
135+ [signedTransaction]
136+ );
137+
124 return (
138 return (
125 <Grid className={cn(className, 'mt1')}>
139 <Grid className={cn(className, 'mt1')}>
126 <BridgeForm validate={validate} onValues={onValues}>
140 <BridgeForm validate={validate} onValues={onValues}>
127 {({ handleSubmit }) => (
141 {({ handleSubmit }) => (
128 <>
142 <>
129− {renderPrimaryButton()}
143+ {renderPrimarySection()}
130
144
131 {error && (
145 {error && (
132 <Grid.Item full as={ErrorText} className="mt1">
146 <Grid.Item full as={ErrorText} className="mt1">
@@ -149,12 +163,14 @@ export default function InlineEthereumTransaction({
149 full
163 full
150 as={ToggleInput}
164 as={ToggleInput}
151 name="useAdvanced"
165 name="useAdvanced"
152− label="Advanced Configuration"
166+ label="Advanced"
153− inverseLabel="Cancel Advanced Configuration"
167+ inverseLabel="Back to Defaults"
168+ inverseColor="red3"
154 disabled={!showConfigureInput || initializing}
169 disabled={!showConfigureInput || initializing}
155 />
170 />
156
171
157 <Condition when="useAdvanced" is={true}>
172 <Condition when="useAdvanced" is={true}>
173+ <Grid.Divider />
158 <Grid.Item
174 <Grid.Item
159 full
175 full
160 as={Flex}
176 as={Flex}
@@ -183,13 +199,7 @@ export default function InlineEthereumTransaction({
183 <Flex.Item as={Text}>Cheap</Flex.Item>
199 <Flex.Item as={Text}>Cheap</Flex.Item>
184 <Flex.Item as={Text}>Fast</Flex.Item>
200 <Flex.Item as={Text}>Fast</Flex.Item>
185 </Grid.Item>
201 </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" />
193 </Condition>
203 </Condition>
194 </>
204 </>
195 )}
205 )}
@@ -200,14 +210,42 @@ export default function InlineEthereumTransaction({
200 full
210 full
201 as={ToggleInput}
211 as={ToggleInput}
202 name="viewSigned"
212 name="viewSigned"
203− label="View Signed Transaction"
213+ label="Signed Transaction"
204− inverseLabel="Hide Signed Transaction"
214+ inverseLabel="Hide"
205 disabled={!showSignedTx}
215 disabled={!showSignedTx}
206 />
216 />
207 <Condition when="viewSigned" is={true}>
217 <Condition when="viewSigned" is={true}>
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>
208− <Grid.Item full as="code" className="f6 mono gray4 wrap">
245+ <Grid.Item full as="code" className="mb4 f6 mono gray4 wrap">
209− {hexify(signedTransaction.serialize())}
246+ {serializedTxHex}
210 </Grid.Item>
247 </Grid.Item>
248+ <Grid.Divider />
211 </Condition>
249 </Condition>
212 </>
250 </>
213 )}
251 )}
@@ -232,6 +270,15 @@ export default function InlineEthereumTransaction({
232 <Grid.Divider />
270 <Grid.Divider />
233 </>
271 </>
234 )}
272 )}
273+
274+ {completed && (
275+ <>
276+ <Grid.Item full as={RestartButton} onClick={onReturn}>
277+ Return
278+ </Grid.Item>
279+ <Grid.Divider />
280+ </>
281+ )}
235 </>
282 </>
236 )}
283 )}
237 </BridgeForm>
284 </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';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4// progrss is [0, 1]
4// progrss is [0, 1]
5−export default function LoadingBar({ className, progress = 1 }) {
5+export default function LoadingBar({ className, progress = 1.0 }) {
6 return (
6 return (
7 <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
7 <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
8 <div
8 <div
9− className="abs bg-green3"
9+ className="abs bg-green3 animated-width"
10 style={{
10 style={{
11 top: 0,
11 top: 0,
12 bottom: 0,
12 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({
25 key={option.value}
25 key={option.value}
26 onClick={() => onTabChange(option.value)}
26 onClick={() => onTabChange(option.value)}
27 className={cn(
27 className={cn(
28− 'f5 pv3 clickable nowrap',
28+ 'f5 pv3 pointer nowrap',
29 {
29 {
30 'black b-black bb1': isActive,
30 'black b-black bb1': isActive,
31 gray3: !isActive,
31 gray3: !isActive,
src/form/Inputs.js+2−3
@@ -16,9 +16,8 @@ const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
16const PLACEHOLDER_MNEMONIC =
16const PLACEHOLDER_MNEMONIC =
17 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
17 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
18const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
18const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
19−const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
19+const PLACEHOLDER_ADDRESS = '0x';
20−const PLACEHOLDER_PRIVATE_KEY =
20+const PLACEHOLDER_PRIVATE_KEY = '0x';
21− '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
22const PLACEHOLDER_EMAIL = 'email@example.com';
21const PLACEHOLDER_EMAIL = 'email@example.com';
23
22
24const formatPat = buildFormatter([downcase, ensurePatFormat]);
23const formatPat = buildFormatter([downcase, ensurePatFormat]);
src/indigo-react/components/Button.js+6−4
@@ -15,6 +15,7 @@ export default function Button({
15 className,
15 className,
16 accessory = '→',
16 accessory = '→',
17 onClick,
17 onClick,
18+ background,
18 children,
19 children,
19 ...rest
20 ...rest
20}) {
21}) {
@@ -23,7 +24,7 @@ export default function Button({
23 as={As}
24 as={As}
24 gap={1}
25 gap={1}
25 className={cn(
26 className={cn(
26− 'pointer pv4 truncate flex-row justify-between us-none',
27+ 'rel pointer pv4 truncate flex-row justify-between us-none',
27 {
28 {
28 p4: solid,
29 p4: solid,
29 },
30 },
@@ -49,17 +50,18 @@ export default function Button({
49 }}
50 }}
50 onClick={!disabled && onClick ? onClick : undefined}
51 onClick={!disabled && onClick ? onClick : undefined}
51 {...rest}>
52 {...rest}>
53+ {background}
52− <Grid.Item full as={Flex} justify="between">
54+ <Grid.Item full as={Flex} justify="between" className="z2">
53 <span>{children}</span>
55 <span>{children}</span>
54 <div className={cn('pl4')}>{accessory}</div>
56 <div className={cn('pl4')}>{accessory}</div>
55 </Grid.Item>
57 </Grid.Item>
56 {detail && (
58 {detail && (
57− <Grid.Item full as={HelpText}>
59+ <Grid.Item full as={HelpText} className="z2">
58 {detail}
60 {detail}
59 </Grid.Item>
61 </Grid.Item>
60 )}
62 )}
61 {disabled && disabledDetail && (
63 {disabled && disabledDetail && (
62− <Grid.Item full className="f6 black mt1">
64+ <Grid.Item full className="f6 black mt1 z2">
63 {disabledDetail}
65 {disabledDetail}
64 </Grid.Item>
66 </Grid.Item>
65 )}
67 )}
src/indigo-react/components/CheckboxInput.js+1−1
@@ -39,7 +39,7 @@ export default function CheckboxInput({
39 <Flex.Item
39 <Flex.Item
40 flex
40 flex
41 as="label"
41 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"
43 htmlFor={name}>
43 htmlFor={name}>
44 <Flex
44 <Flex
45 justify="center"
45 justify="center"
src/indigo-react/components/LinkButton.js+2−2
@@ -19,9 +19,9 @@ export default function LinkButton({
19 }),
19 }),
20 }}
20 }}
21 className={cn(
21 className={cn(
22− 'pointer underline',
22+ 'us-none pointer underline',
23 {
23 {
24− black: !disabled,
24+ // NOTE: inherit styling from parent otherwise
25 gray4: disabled,
25 gray4: disabled,
26 },
26 },
27 className
27 className
src/indigo-react/components/SelectInput.js+1−1
@@ -61,7 +61,7 @@ export default function SelectInput({
61 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
61 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
62 {label}
62 {label}
63 </Flex.Item>
63 </Flex.Item>
64− <Flex.Item as={Flex} row className="rel clickable" onClick={toggleOpen}>
64+ <Flex.Item as={Flex} row className="rel pointer" onClick={toggleOpen}>
65 <Flex.Item
65 <Flex.Item
66 flex
66 flex
67 className={cn(
67 className={cn(
src/indigo-react/components/ToggleInput.js+8−2
@@ -10,6 +10,7 @@ export default function ToggleInput({
10 name,
10 name,
11 label,
11 label,
12 inverseLabel,
12 inverseLabel,
13+ inverseColor = 'black',
13 className,
14 className,
14
15
15 //
16 //
@@ -49,10 +50,15 @@ export default function ToggleInput({
49 flex
50 flex
50 as="label"
51 as="label"
51 className={cn('f6 pv2 lh-tall us-none flex-row align-center', {
52 className={cn('f6 pv2 lh-tall us-none flex-row align-center', {
52− clickable: !disabled,
53+ pointer: !disabled,
53 })}
54 })}
54 htmlFor={name}>
55 htmlFor={name}>
56+ <LinkButton
55− <LinkButton disabled={disabled} className="f5">
57+ disabled={disabled}
58+ className={cn('f5', {
59+ black: !input.checked,
60+ [inverseColor]: input.checked,
61+ })}>
56 {input.checked ? inverseLabel : label}
62 {input.checked ? inverseLabel : label}
57 </LinkButton>
63 </LinkButton>
58 </Flex.Item>
64 </Flex.Item>
src/lib/constants.js+3
@@ -69,6 +69,8 @@ const PROFILE_STATES = {
69 INPUT_ERROR: Symbol('INPUT_ERROR'),
69 INPUT_ERROR: Symbol('INPUT_ERROR'),
70};
70};
71
71
72+const PROGRESS_ANIMATION_DELAY_MS = 500; // .animated-width
73+
72export {
74export {
73 CHECK_BLOCK_EVERY_MS,
75 CHECK_BLOCK_EVERY_MS,
74 DEFAULT_GAS_PRICE_GWEI,
76 DEFAULT_GAS_PRICE_GWEI,
@@ -87,4 +89,5 @@ export {
87 GALAXY_ENTROPY_BITS,
89 GALAXY_ENTROPY_BITS,
88 SEED_ENTROPY_BITS,
90 SEED_ENTROPY_BITS,
89 ZOD,
91 ZOD,
92+ PROGRESS_ANIMATION_DELAY_MS,
90};
93};
src/lib/useSuggestedGasPrice.js → src/lib/getSuggestedGasPrice.jsrenamed+10−17
@@ -1,13 +1,13 @@
1−import { useState, useEffect } from 'react';
2import { NETWORK_TYPES } from './network';
1import { NETWORK_TYPES } from './network';
3import { DEFAULT_GAS_PRICE_GWEI } from './constants';
2import { DEFAULT_GAS_PRICE_GWEI } from './constants';
4
3
5−async function getSuggestedGasPrice(networkType) {
4+export default async function getSuggestedGasPrice(networkType) {
6 switch (networkType) {
5 switch (networkType) {
7 case NETWORK_TYPES.ROPSTEN:
6 case NETWORK_TYPES.ROPSTEN:
8 return 10;
7 return 10;
9 case NETWORK_TYPES.OFFLINE:
8 case NETWORK_TYPES.OFFLINE:
10 return DEFAULT_GAS_PRICE_GWEI;
9 return DEFAULT_GAS_PRICE_GWEI;
10+ case NETWORK_TYPES.LOCAL:
11 default:
11 default:
12 try {
12 try {
13 const response = await fetch(
13 const response = await fetch(
@@ -17,24 +17,17 @@ async function getSuggestedGasPrice(networkType) {
17 cache: 'no-cache',
17 cache: 'no-cache',
18 }
18 }
19 );
19 );
20+
20 const json = await response.json();
21 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);
22 } catch (e) {
29 } catch (e) {
23 return DEFAULT_GAS_PRICE_GWEI;
30 return DEFAULT_GAS_PRICE_GWEI;
24 }
31 }
25 }
32 }
26}
33}
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 {
10} from './keys';
10} from './keys';
11import { addHexPrefix } from './wallet';
11import { addHexPrefix } from './wallet';
12import { sendAndAwaitAll } from './txn';
12import { sendAndAwaitAll } from './txn';
13−import { getSuggestedGasPrice } from './useSuggestedGasPrice';
13+import getSuggestedGasPrice from './getSuggestedGasPrice';
14import { GAS_LIMITS } from './constants';
14import { GAS_LIMITS } from './constants';
15import { toWei } from 'web3-utils';
15import { toWei } from 'web3-utils';
16
16
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 @@
1import { useCallback, useEffect, useState } from 'react';
1import { useCallback, useEffect, useState } from 'react';
2import { Just } from 'folktale/maybe';
2import { Just } from 'folktale/maybe';
3−import { fromWei, toWei } from 'web3-utils';
3+import { toWei } from 'web3-utils';
4
4
5import { useNetwork } from 'store/network';
5import { useNetwork } from 'store/network';
6import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
7import { usePointCursor } from 'store/pointCursor';
7import { usePointCursor } from 'store/pointCursor';
8−import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
9
8
9+import {
10+ GAS_LIMITS,
11+ DEFAULT_GAS_PRICE_GWEI,
12+ PROGRESS_ANIMATION_DELAY_MS,
10−import { GAS_LIMITS } from './constants';
13+} from './constants';
11import {
14import {
12 signTransaction,
15 signTransaction,
13 sendSignedTransaction,
16 sendSignedTransaction,
14 waitForTransactionConfirm,
17 waitForTransactionConfirm,
15 hexify,
18 hexify,
16} from './txn';
19} from './txn';
17−import * as need from './need';
20+import * as need from 'lib/need';
18−import { ensureFundsFor } from './tank';
21+import { ensureFundsFor } from 'lib/tank';
19−import useDeepEqualReference from './useDeepEqualReference';
22+import useDeepEqualReference from 'lib/useDeepEqualReference';
23+import useGasPrice from 'lib/useGasPrice';
24+import timeout from 'lib/timeout';
20
25
21const STATE = {
26const STATE = {
22 NONE: 'NONE',
27 NONE: 'NONE',
@@ -37,7 +42,7 @@ export default function useEthereumTransaction(
37 transactionBuilder,
42 transactionBuilder,
38 refetch,
43 refetch,
39 initialGasLimit = GAS_LIMITS.DEFAULT,
44 initialGasLimit = GAS_LIMITS.DEFAULT,
40− initialGasPrice = 20
45+ initialGasPrice = DEFAULT_GAS_PRICE_GWEI
41) {
46) {
42 const { wallet, walletType, walletHdPath } = useWallet();
47 const { wallet, walletType, walletHdPath } = useWallet();
43 const { web3, networkType } = useNetwork();
48 const { web3, networkType } = useNetwork();
@@ -59,14 +64,13 @@ export default function useEthereumTransaction(
59 const [state, setState] = useState(STATE.NONE);
64 const [state, setState] = useState(STATE.NONE);
60 const [chainId, setChainId] = useState();
65 const [chainId, setChainId] = useState();
61 const [nonce, setNonce] = useState();
66 const [nonce, setNonce] = useState();
62− const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
67+ const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
63− const [suggestedGasPrice, setSuggestedGasPrice] = useState(gasPrice); // gwei
64− const { gasPrice: estimatedGasPrice } = useSuggestedGasPrice();
65 const [gasLimit] = useState(initialGasLimit);
68 const [gasLimit] = useState(initialGasLimit);
66 const [unsignedTransaction, setUnsignedTransaction] = useState();
69 const [unsignedTransaction, setUnsignedTransaction] = useState();
67 const [signedTransaction, setSignedTransaction] = useState();
70 const [signedTransaction, setSignedTransaction] = useState();
68 const [txHash, setTxHash] = useState();
71 const [txHash, setTxHash] = useState();
69 const [needFunds, setNeedFunds] = useState();
72 const [needFunds, setNeedFunds] = useState();
73+ const [confirmationProgress, setConfirmationProgress] = useState(0.0);
70
74
71 const initializing = nonce === undefined || chainId === undefined;
75 const initializing = nonce === undefined || chainId === undefined;
72 const constructed = !!unsignedTransaction;
76 const constructed = !!unsignedTransaction;
@@ -128,6 +132,7 @@ export default function useEthereumTransaction(
128
132
129 const broadcast = useCallback(async () => {
133 const broadcast = useCallback(async () => {
130 try {
134 try {
135+ setConfirmationProgress(0.0);
131 setError(undefined);
136 setError(undefined);
132
137
133 const rawTx = hexify(signedTransaction.serialize());
138 const rawTx = hexify(signedTransaction.serialize());
@@ -158,8 +163,14 @@ export default function useEthereumTransaction(
158 setState(STATE.BROADCASTED);
163 setState(STATE.BROADCASTED);
159 setTxHash(txHash);
164 setTxHash(txHash);
160
165
166+ await timeout(PROGRESS_ANIMATION_DELAY_MS);
167+
168+ setConfirmationProgress(0.2);
169+
161 await waitForTransactionConfirm(_web3, txHash);
170 await waitForTransactionConfirm(_web3, txHash);
162
171
172+ setConfirmationProgress(0.9);
173+
163 setState(STATE.CONFIRMED);
174 setState(STATE.CONFIRMED);
164 } catch (error) {
175 } catch (error) {
165 setError(error);
176 setError(error);
@@ -179,25 +190,12 @@ export default function useEthereumTransaction(
179 setSignedTransaction(undefined);
190 setSignedTransaction(undefined);
180 setNonce(undefined);
191 setNonce(undefined);
181 setChainId(undefined);
192 setChainId(undefined);
182− setGasPrice(suggestedGasPrice);
193+ resetGasPrice();
183 setState(STATE.NONE);
194 setState(STATE.NONE);
184 setError(undefined);
195 setError(undefined);
185 setNeedFunds(undefined);
196 setNeedFunds(undefined);
186− }, [suggestedGasPrice, setError]);
197+ setConfirmationProgress(0.0);
187−
198+ }, [resetGasPrice, setError]);
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]);
201
199
202 useEffect(() => {
200 useEffect(() => {
203 let mounted = true;
201 let mounted = true;
@@ -226,15 +224,7 @@ export default function useEthereumTransaction(
226 })();
224 })();
227
225
228 return () => (mounted = false);
226 return () => (mounted = false);
229− }, [
227+ }, [_wallet, _web3, setError, nonce, chainId, networkType, initialGasPrice]);
230− _wallet,
231− _web3,
232− setError,
233− nonce,
234− chainId,
235− networkType,
236− estimatedGasPrice,
237− ]);
238
228
239 useEffect(() => {
229 useEffect(() => {
240 let mounted = true;
230 let mounted = true;
@@ -260,6 +250,14 @@ export default function useEthereumTransaction(
260 return;
250 return;
261 }
251 }
262
252
253+ setConfirmationProgress(1.0);
254+
255+ await timeout(PROGRESS_ANIMATION_DELAY_MS);
256+
257+ if (!mounted) {
258+ return;
259+ }
260+
263 setState(STATE.COMPLETED);
261 setState(STATE.COMPLETED);
264 })();
262 })();
265 }
263 }
@@ -283,7 +281,7 @@ export default function useEthereumTransaction(
283 reset,
281 reset,
284 error,
282 error,
285 inputsLocked,
283 inputsLocked,
286− //
284+ confirmationProgress,
287 txHash,
285 txHash,
288 signedTransaction,
286 signedTransaction,
289 gasPrice,
287 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'] {
66 background-color: #e6e6e6;
66 background-color: #e6e6e6;
67 }
67 }
68}
68}
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 @@
21 background-color: #333333;
21 background-color: #333333;
22}
22}
23
23
24−.clickable {
25− cursor: pointer;
26−}
27−
28.minh-100 {
24.minh-100 {
29 min-height: 100vh;
25 min-height: 100vh;
30}
26}
src/views/Invite/InviteEmail.js+5−5
@@ -35,14 +35,13 @@ import {
35} from 'lib/txn';
35} from 'lib/txn';
36import * as tank from 'lib/tank';
36import * as tank from 'lib/tank';
37import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
37import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
38−import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
39import * as need from 'lib/need';
38import * as need from 'lib/need';
40import * as wg from 'lib/walletgen';
39import * as wg from 'lib/walletgen';
41import useSetState from 'lib/useSetState';
40import useSetState from 'lib/useSetState';
42import pluralize from 'lib/pluralize';
41import pluralize from 'lib/pluralize';
43import useMailer from 'lib/useMailer';
42import useMailer from 'lib/useMailer';
44
43
45−import LoadableButton from 'components/LoadableButton';
44+import ProgressButton from 'components/ProgressButton';
46import Highlighted from 'components/Highlighted';
45import Highlighted from 'components/Highlighted';
47import BridgeForm from 'form/BridgeForm';
46import BridgeForm from 'form/BridgeForm';
48import { Field } from 'react-final-form';
47import { Field } from 'react-final-form';
@@ -57,6 +56,7 @@ import { FORM_ERROR } from 'final-form';
57import SubmitButton from 'form/SubmitButton';
56import SubmitButton from 'form/SubmitButton';
58import FormError from 'form/FormError';
57import FormError from 'form/FormError';
59import { WARNING, onlyHasWarning } from 'form/helpers';
58import { WARNING, onlyHasWarning } from 'form/helpers';
59+import useGasPrice from 'lib/useGasPrice';
60
60
61const INITIAL_VALUES = { emails: [''] };
61const INITIAL_VALUES = { emails: [''] };
62
62
@@ -106,7 +106,7 @@ export default function InviteEmail() {
106 const { pointCursor } = usePointCursor();
106 const { pointCursor } = usePointCursor();
107 const point = need.point(pointCursor);
107 const point = need.point(pointCursor);
108 const { getHasReceived, sendMail } = useMailer();
108 const { getHasReceived, sendMail } = useMailer();
109− const { gasPrice } = useSuggestedGasPrice(networkType);
109+ const { gasPrice } = useGasPrice();
110
110
111 const cachedEmails = useRef({});
111 const cachedEmails = useRef({});
112
112
@@ -245,7 +245,7 @@ export default function InviteEmail() {
245 nonce: nonce + i,
245 nonce: nonce + i,
246 // TODO: ^ make a useTransactionSigner to encapsulate this logic
246 // TODO: ^ make a useTransactionSigner to encapsulate this logic
247 txn: inviteTx,
247 txn: inviteTx,
248− gasPrice: gasPrice.toString(),
248+ gasPrice: gasPrice.toString(), // expects string gwei
249 gasLimit: GAS_LIMIT.toString(),
249 gasLimit: GAS_LIMIT.toString(),
250 });
250 });
251
251
@@ -518,7 +518,7 @@ export default function InviteEmail() {
518 ) : (
518 ) : (
519 <Grid.Item
519 <Grid.Item
520 full
520 full
521− as={LoadableButton}
521+ as={ProgressButton}
522 className="mt4"
522 className="mt4"
523 disabled={!canSend}
523 disabled={!canSend}
524 accessory={`${visualProgress}/${fields.length}`}
524 accessory={`${visualProgress}/${fields.length}`}
src/views/Points.js+1−1
@@ -46,7 +46,7 @@ const PointList = function({ points, className, actions, ...rest }) {
46 <Flex col>
46 <Flex col>
47 <Passport.Mini
47 <Passport.Mini
48 point={point}
48 point={point}
49− className="clickable"
49+ className="pointer"
50 onClick={() => {
50 onClick={() => {
51 setPointCursor(Just(point));
51 setPointCursor(Just(point));
52 push(names.POINT);
52 push(names.POINT);