on master help sign in
Redesign L1 transactions
715f8b0Will Galebach
src/components/InlineEthereumTransaction.js+27−160
@@ -1,34 +1,21 @@
1import React, { useMemo, useCallback, useState } from 'react';
1import React, { useMemo, useCallback, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import {
4− Grid,
5− ToggleInput,
6− ErrorText,
7− Flex,
8− LinkButton,
9− H5,
10− Text,
11−} from 'indigo-react';
3+import { Grid, ErrorText, Flex, LinkButton, H5, Text } from 'indigo-react';
12import { toBN } from 'web3-utils';
4import { toBN } from 'web3-utils';
13
5
14−import { ReactComponent as InfoIcon } from 'assets/info.svg';
15−
16import { useExploreTxUrls } from 'lib/explorer';
6import { useExploreTxUrls } from 'lib/explorer';
17−import { hexify } from 'lib/txn';
18import { safeFromWei, safeToWei } from 'lib/lib';
7import { safeFromWei, safeToWei } from 'lib/lib';
19
8
20import { composeValidator, buildCheckboxValidator } from 'form/validators';
9import { composeValidator, buildCheckboxValidator } from 'form/validators';
21import BridgeForm from 'form/BridgeForm';
10import BridgeForm from 'form/BridgeForm';
22−import Condition from 'form/Condition';
23
11
24import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
12import { GenerateButton, ForwardButton, RestartButton } from './Buttons';
25−import CopyButton from './copiable/CopyButton';
26import ProgressButton from './ProgressButton';
13import ProgressButton from './ProgressButton';
27−import { convertToInt } from 'lib/convertToInt';
28import NeedFundsNotice from './NeedFundsNotice';
14import NeedFundsNotice from './NeedFundsNotice';
29import NoticeBox from './NoticeBox';
15import NoticeBox from './NoticeBox';
30
16
31import './InlineEthereumTransaction.scss';
17import './InlineEthereumTransaction.scss';
18+import FeeDropdown from './L2/Dropdowns/FeeDropdown';
32
19
33export default function InlineEthereumTransaction({
20export default function InlineEthereumTransaction({
34 // from useEthereumTransaction.bind
21 // from useEthereumTransaction.bind
@@ -47,10 +34,7 @@ export default function InlineEthereumTransaction({
47 setGasPrice,
34 setGasPrice,
48 resetGasPrice,
35 resetGasPrice,
49 txHashes,
36 txHashes,
50− nonce,
51− chainId,
52 needFunds,
37 needFunds,
53− signedTransactions,
54 confirmationProgress,
38 confirmationProgress,
55 gasLimit,
39 gasLimit,
56 unsignedTransactions,
40 unsignedTransactions,
@@ -76,12 +60,6 @@ export default function InlineEthereumTransaction({
76 // show signed tx only when signing (for offline usage)
60 // show signed tx only when signing (for offline usage)
77 const showSignedTx = signed;
61 const showSignedTx = signed;
78
62
79− const [showGasDetails, _setShowGasDetails] = useState(false);
80− const toggleGasDetails = useCallback(
81− () => _setShowGasDetails(!showGasDetails),
82− [_setShowGasDetails, showGasDetails]
83− );
84−
85 const validate = useMemo(
63 const validate = useMemo(
86 () =>
64 () =>
87 composeValidator({
65 composeValidator({
@@ -182,13 +160,6 @@ export default function InlineEthereumTransaction({
182 }
160 }
183 };
161 };
184
162
185− const serializedTxsHex = useMemo(
186− () =>
187− signedTransactions &&
188− signedTransactions.map(stx => hexify(stx.serialize())),
189− [signedTransactions]
190− );
191−
192 const numTxs = useMemo(() => (unsignedTransactions || []).length || 1, [
163 const numTxs = useMemo(() => (unsignedTransactions || []).length || 1, [
193 unsignedTransactions,
164 unsignedTransactions,
194 ]);
165 ]);
@@ -206,18 +177,36 @@ export default function InlineEthereumTransaction({
206 [gasLimit, gasPrice, numTxs]
177 [gasLimit, gasPrice, numTxs]
207 );
178 );
208
179
209− const gasInfo = useMemo(() => {
210− const extra = numTxs === 1 ? '' : `* ${numTxs} txs`;
211− return showGasDetails
212− ? `${gasPrice} Gwei * ${gasLimit} ${extra} = ${maxCost} ETH`
213− : `${maxCost} ETH`;
214− }, [showGasDetails, maxCost, gasPrice, gasLimit, numTxs]);
215−
216 return (
180 return (
217 <Grid className={cn(className, 'mt1')}>
181 <Grid className={cn(className, 'mt1')}>
218 <BridgeForm validate={validate} onValues={onValues}>
182 <BridgeForm validate={validate} onValues={onValues}>
219 {() => (
183 {() => (
220 <>
184 <>
185+ {showConfigureInput && (
186+ <>
187+ <Grid.Item
188+ className="mt2 text eth-tx-button"
189+ full
190+ as={Flex}
191+ row
192+ justify="between">
193+ <Flex.Item as={H5}>Gas Fee</Flex.Item>
194+ <FeeDropdown setGasPrice={setGasPrice} />
195+ </Grid.Item>
196+
197+ <Grid.Item
198+ className="mt2 text eth-tx-button"
199+ full
200+ as={Flex}
201+ row
202+ justify="between">
203+ <Flex.Item as={H5}>Max Transaction Cost</Flex.Item>
204+ <Flex.Item as={H5}>{maxCost} ETH</Flex.Item>
205+ </Grid.Item>
206+ <Grid.Divider className="mt4" />
207+ </>
208+ )}
209+
221 {renderPrimarySection()}
210 {renderPrimarySection()}
222
211
223 {error && (
212 {error && (
@@ -235,66 +224,6 @@ export default function InlineEthereumTransaction({
235 />
224 />
236 )}
225 )}
237
226
238− {showConfigureInput && (
239− <>
240− <Grid.Item
241− className="eth-tx-button"
242− full
243− as={ToggleInput}
244− name="useAdvanced"
245− label="Advanced"
246− inverseLabel="Back to Defaults"
247− inverseColor="red3"
248− disabled={!showConfigureInput || initializing}
249− />
250−
251− <Condition when="useAdvanced" is={true}>
252− <Grid.Item className="mb2 eth-tx-button" full></Grid.Item>
253− <Grid.Divider />
254− <Grid.Item
255− className="mt2 eth-tx-button"
256− full
257− as={Flex}
258− row
259− justify="between">
260− <Flex.Item as={H5}>Transaction Fee</Flex.Item>
261−
262− <Flex.Item as={H5}>
263− {gasInfo}{' '}
264− <InfoIcon
265− className="pointer"
266− onClick={toggleGasDetails}
267− />
268− </Flex.Item>
269− </Grid.Item>
270−
271− {/* TODO(shrugs): move to indigo/RangeInput */}
272− <Grid.Item
273− className="eth-tx-button"
274− full
275− as="input"
276− type="range"
277− min="1"
278− max="400"
279− value={gasPrice}
280− onChange={e =>
281− setGasPrice(convertToInt(e.target.value, 10))
282− }
283− />
284− <Grid.Item
285− className="f6 mt1 eth-tx-button"
286− full
287− as={Flex}
288− row
289− justify="between">
290− <Flex.Item as={Text}>Cheap</Flex.Item>
291− <Flex.Item as={Text}>Fast</Flex.Item>
292− </Grid.Item>
293− <Grid.Divider className="mt4" />
294− </Condition>
295− </>
296− )}
297−
298 {showSignedTx && fakeSigned && (
227 {showSignedTx && fakeSigned && (
299 <>
228 <>
300 <Grid.Item className="mt2 eth-tx-button" full as={NoticeBox}>
229 <Grid.Item className="mt2 eth-tx-button" full as={NoticeBox}>
@@ -302,27 +231,6 @@ export default function InlineEthereumTransaction({
302 </Grid.Item>
231 </Grid.Item>
303 </>
232 </>
304 )}
233 )}
305−
306− {showSignedTx && !fakeSigned && (
307− <>
308− <Grid.Item
309− className="eth-tx-button"
310− full
311− as={ToggleInput}
312− name="viewSigned"
313− label="Signed Transaction"
314− inverseLabel="Hide"
315− disabled={!showSignedTx}
316− />
317− <Condition when="viewSigned" is={true}>
318− <SignedTransactionList
319− serializedTxsHex={serializedTxsHex}
320− maxCost={maxCost}
321− nonce={nonce}
322− />
323− </Condition>
324− </>
325− )}
326 </>
234 </>
327 )}
235 )}
328 </BridgeForm>
236 </BridgeForm>
@@ -330,47 +238,6 @@ export default function InlineEthereumTransaction({
330 );
238 );
331}
239}
332
240
333−function SignedTransactionList({ serializedTxsHex, nonce, maxCost }) {
334− return serializedTxsHex.map((serializedTxHex, i) => (
335− <React.Fragment key={i}>
336− <Grid.Divider />
337− <Grid.Item
338− className="pv4 black f5 eth-tx-button"
339− full
340− as={Flex}
341− justify="between">
342− <Flex.Item>Nonce</Flex.Item>
343− <Flex.Item>{nonce + i}</Flex.Item>
344− </Grid.Item>
345− <Grid.Divider />
346− <Grid.Item
347− className="pv4 black f5 eth-tx-button"
348− full
349− as={Flex}
350− justify="between">
351− <Flex.Item>Transaction Cost</Flex.Item>
352− <Flex.Item>{maxCost} ETH</Flex.Item>
353− </Grid.Item>
354− <Grid.Divider />
355− <Grid.Item
356− className="mt3 mb2 eth-tx-button"
357− full
358− as={Flex}
359− justify="between">
360− <Flex.Item as={H5}>Signed Transaction Hex</Flex.Item>
361− <Flex.Item as={CopyButton} text={serializedTxHex} />
362− </Grid.Item>
363− <Grid.Item
364− className="mb4 f6 mono gray4 wrap eth-tx-button"
365− full
366− as="code">
367− {serializedTxHex}
368− </Grid.Item>
369− <Grid.Divider />
370− </React.Fragment>
371− ));
372−}
373−
374function TransactionReceipt({ txHashes, finalCost, onClose }) {
241function TransactionReceipt({ txHashes, finalCost, onClose }) {
375 const txUrls = useExploreTxUrls(txHashes);
242 const txUrls = useExploreTxUrls(txHashes);
376 return (
243 return (
src/components/InlineEthereumTransaction.scss+11−1
@@ -1,5 +1,15 @@
1@import 'style/shared/variables.scss';
1@import 'style/shared/variables.scss';
2
2
3−.eth-tx-button {
3+span.eth-tx-button {
4 border-radius: $medium-border-radius;
4 border-radius: $medium-border-radius;
5+ font-size: 14px;
6+ padding-top: 8px;
7+ padding-bottom: 8px;
8+ line-height: 16px;
9+ vertical-align: middle;
10+}
11+
12+.eth-tx-button.text > h5 {
13+ font-size: 14px;
14+ font-weight: 500;
5}
15}
src/components/L2/Dropdowns/FeeDropdown.scssnew file+32
@@ -0,0 +1,32 @@
1+@import 'style/shared/variables.scss';
2+
3+.fee-dropdown {
4+ font-size: 14px;
5+ position: relative;
6+
7+ .selector {
8+ min-width: 150px;
9+ }
10+
11+ .content-border {
12+ z-index: 3;
13+
14+ .dropdown-content {
15+ .prices {
16+ .price {
17+ display: flex;
18+ flex-direction: row;
19+ justify-content: space-between;
20+ align-items: center;
21+ cursor: pointer;
22+
23+ padding: 8px 12px 8px 16px;
24+
25+ &:hover {
26+ opacity: 0.8;
27+ }
28+ }
29+ }
30+ }
31+ }
32+}
src/components/L2/Dropdowns/FeeDropdown.tsxnew file+62
@@ -0,0 +1,62 @@
1+import React, { useCallback, useEffect, useState } from 'react';
2+import { Box, Row } from '@tlon/indigo-react';
3+import { DEFAULT_GAS_PRICE_GWEI } from 'lib/constants';
4+import useGasPrice from 'lib/useGasPrice';
5+
6+import Dropdown from './Dropdown';
7+import './FeeDropdown.scss';
8+
9+const PRICE_LABELS = ['Fast', 'Average', 'Slow'];
10+
11+export interface GasPrice {
12+ price: number;
13+ wait: number;
14+}
15+
16+const formatPriceWait = ({ price, wait }: GasPrice) =>
17+ `${price} gwei (${Math.round(wait * 100) / 100} min)`;
18+
19+const FeeDropdown = ({ setGasPrice }: { setGasPrice: (g: number) => void }) => {
20+ const { suggestedGasPrices } = useGasPrice(DEFAULT_GAS_PRICE_GWEI);
21+
22+ const [open, setOpen] = useState<boolean>(false);
23+ const [selected, setSelected] = useState<GasPrice>(
24+ suggestedGasPrices.average
25+ );
26+
27+ useEffect(() => {
28+ setSelected(suggestedGasPrices.average);
29+ }, [suggestedGasPrices]);
30+
31+ const selectPrice = useCallback(
32+ (value: GasPrice) => () => {
33+ setSelected(value);
34+ setGasPrice(value.price);
35+ setOpen(false);
36+ },
37+ [setGasPrice, setSelected, setOpen]
38+ );
39+
40+ return (
41+ <Dropdown
42+ className="fee-dropdown"
43+ open={open}
44+ value={formatPriceWait(selected)}
45+ toggleOpen={() => setOpen(!open)}>
46+ <Box className="prices">
47+ {Object.values(suggestedGasPrices).map(
48+ (value: GasPrice, ind: number) => (
49+ <Row
50+ className="price"
51+ onClick={selectPrice(value)}
52+ key={value.wait}>
53+ {PRICE_LABELS[ind]}: {formatPriceWait(value)}
54+ </Row>
55+ )
56+ )}
57+ </Box>
58+ </Dropdown>
59+ );
60+};
61+
62+export default FeeDropdown;
src/lib/getSuggestedGasPrice.js+37−17
@@ -1,12 +1,32 @@
1import { NETWORK_TYPES } from './network';
1import { NETWORK_TYPES } from './network';
2import { DEFAULT_GAS_PRICE_GWEI, MAX_GAS_PRICE_GWEI } from './constants';
2import { DEFAULT_GAS_PRICE_GWEI, MAX_GAS_PRICE_GWEI } from './constants';
3
3
4+// ethgasstation returns values in floating point, one order of magitude
5+// more than gwei. see: https://docs.ethgasstation.info
6+// we don't want to charge users more than the gas tank funds
7+const minGas = gas => Math.min(Math.ceil(gas / 10), MAX_GAS_PRICE_GWEI);
8+
9+export const defaultGasValues = value => ({
10+ fast: {
11+ price: value,
12+ wait: 1,
13+ },
14+ average: {
15+ price: value,
16+ wait: 1,
17+ },
18+ low: {
19+ price: value,
20+ wait: 1,
21+ },
22+});
23+
4export default async function getSuggestedGasPrice(networkType) {
24export default async function getSuggestedGasPrice(networkType) {
5 switch (networkType) {
25 switch (networkType) {
6 case NETWORK_TYPES.ROPSTEN:
26 case NETWORK_TYPES.ROPSTEN:
7− return 10;
27+ return defaultGasValues(10);
8 case NETWORK_TYPES.OFFLINE:
28 case NETWORK_TYPES.OFFLINE:
9− return DEFAULT_GAS_PRICE_GWEI;
29+ return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
10 case NETWORK_TYPES.LOCAL:
30 case NETWORK_TYPES.LOCAL:
11 default:
31 default:
12 try {
32 try {
@@ -23,22 +43,22 @@ export default async function getSuggestedGasPrice(networkType) {
23
43
24 const json = await response.json();
44 const json = await response.json();
25
45
26− if (json && json.status !== '1') {
46+ return {
27− console.warn('suggested gas price warning', json.message);
47+ fast: {
28− }
48+ price: minGas(json.fast),
29−
49+ wait: json.fastWait,
30− const suggestedGasPrice = parseInt(
50+ },
31− json && json.result && json.result.ProposeGasPrice
51+ average: {
32− );
52+ price: minGas(json.average),
33− if (isNaN(suggestedGasPrice)) {
53+ wait: json.avgWait,
34− throw new Error('strange gas price response', json);
54+ },
35− }
55+ low: {
36−
56+ price: minGas(json.safeLow),
37− // safeguard against obscene suggestions
57+ wait: json.safeLowWait,
38− return Math.min(suggestedGasPrice, MAX_GAS_PRICE_GWEI);
58+ },
59+ };
39 } catch (e) {
60 } catch (e) {
40− console.warn('error fetching gas price', e);
61+ return defaultGasValues(DEFAULT_GAS_PRICE_GWEI);
41− return DEFAULT_GAS_PRICE_GWEI;
42 }
62 }
43 }
63 }
44}
64}
src/lib/reticket.js+3−1
@@ -149,7 +149,9 @@ export async function reticketPointBetweenWallets({
149
149
150 progress(TRANSACTION_PROGRESS.SIGNING);
150 progress(TRANSACTION_PROGRESS.SIGNING);
151
151
152+ const gasPriceGwei = (
152− const gasPriceGwei = (await getSuggestedGasPrice(networkType)).toFixed();
153+ await getSuggestedGasPrice(networkType)
154+ ).average.price.toFixed();
153 const chainId = await web3.eth.net.getId();
155 const chainId = await web3.eth.net.getId();
154 const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
156 const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
155 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
157 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
src/lib/useGasPrice.js+14−9
@@ -2,7 +2,9 @@ import { useCallback, useEffect, useState } from 'react';
2
2
3import { useNetwork } from 'store/network';
3import { useNetwork } from 'store/network';
4
4
5+import getSuggestedGasPrices, {
6+ defaultGasValues,
5−import getSuggestedGasPrice from 'lib/getSuggestedGasPrice';
7+} from 'lib/getSuggestedGasPrice';
6import { DEFAULT_GAS_PRICE_GWEI } from './constants';
8import { DEFAULT_GAS_PRICE_GWEI } from './constants';
7
9
8/**
10/**
@@ -14,33 +16,36 @@ import { DEFAULT_GAS_PRICE_GWEI } from './constants';
14 */
16 */
15export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
17export default function useGasPrice(initialGasPrice = DEFAULT_GAS_PRICE_GWEI) {
16 const { networkType } = useNetwork();
18 const { networkType } = useNetwork();
17− const [suggestedGasPrice, setSuggestedGasPrice] = useState(initialGasPrice); // gwei
19+ const [suggestedGasPrices, setSuggestedGasPrices] = useState(
20+ defaultGasValues(initialGasPrice)
21+ ); // fast, average, low. In gwei
18 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
22 const [gasPrice, setGasPrice] = useState(initialGasPrice); // gwei
19
23
20 useEffect(() => {
24 useEffect(() => {
21 let mounted = true;
25 let mounted = true;
22
26
23 (async () => {
27 (async () => {
24− const suggestedGasPrice = await getSuggestedGasPrice(networkType);
28+ const suggestedGasPrices = await getSuggestedGasPrices(networkType);
25
29
26 if (!mounted) {
30 if (!mounted) {
27 return;
31 return;
28 }
32 }
29
33
30 // TODO: BN
34 // TODO: BN
31− setSuggestedGasPrice(suggestedGasPrice);
35+ setSuggestedGasPrices(suggestedGasPrices);
32− setGasPrice(suggestedGasPrice);
36+ setGasPrice(suggestedGasPrices.average.price);
33 })();
37 })();
34
38
35 return () => (mounted = true);
39 return () => (mounted = true);
36 }, [networkType]);
40 }, [networkType]);
37
41
38− const resetGasPrice = useCallback(() => setGasPrice(suggestedGasPrice), [
42+ const resetGasPrice = useCallback(
39− suggestedGasPrice,
43+ () => setGasPrice(suggestedGasPrices.average.price),
44+ [suggestedGasPrices]
40− ]);
45+ );
41
46
42 return {
47 return {
43− suggestedGasPrice,
48+ suggestedGasPrices,
44 gasPrice,
49 gasPrice,
45 setGasPrice,
50 setGasPrice,
46 resetGasPrice,
51 resetGasPrice,
src/lib/utils/roller.ts+4−3
@@ -1,6 +1,6 @@
1−import { signTransactionHash } from 'lib/authToken';
2−import { Invite } from 'lib/types/Invite';
3import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2+import { Invite } from 'lib/types/Invite';
3+import { signTransactionHash } from 'lib/authToken';
4import { randomHex } from 'web3-utils';
4import { randomHex } from 'web3-utils';
5
5
6import RollerRPCAPI, {
6import RollerRPCAPI, {
@@ -113,13 +113,14 @@ export const transferPointRequest = async (
113 api: RollerRPCAPI,
113 api: RollerRPCAPI,
114 _wallet: any,
114 _wallet: any,
115 _point: Ship,
115 _point: Ship,
116+ planet: number,
116 proxy: string,
117 proxy: string,
117 nonce: number,
118 nonce: number,
118 address: string,
119 address: string,
119 reset?: boolean
120 reset?: boolean
120) => {
121) => {
121 const from = {
122 const from = {
122− ship: _point, // ship to transfer
123+ ship: planet, //planet to transfer
123 proxy,
124 proxy,
124 };
125 };
125
126
src/views/Point/MigrateL2.tsx+3−4
@@ -201,10 +201,9 @@ export default function MigrateL2() {
201 {star && (
201 {star && (
202 <Box>
202 <Box>
203 Transferring this point will allow you to conduct all
203 Transferring this point will allow you to conduct all
204− transactions on Layer 2 and is irreversible. Setting the spawn
204+ transactions on Layer 2. Setting the spawn proxy of a star to
205− proxy of a star to the Layer 2 contract is reversible, will
206− allow you to create planet invites for free, but will require
205+ Layer 2 will allow you to create planet invites for free, but
207− you to do all other transactions on Layer 1.
206+ will require you to do all other transactions on Layer 1.
208 </Box>
207 </Box>
209 )}
208 )}
210 </Box>
209 </Box>