on master help sign in
Merge branch 'mino' into fix/tank-messaging
commitab7e847d638deade68f94ff04ac1ca7891a48b91 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 27 Aug 2019, 10:27 (7 years ago) parent89062383d8d2c7
13 files changed+139−60
src/components/InlineEthereumTransaction.js+1−1 src/form/Inputs.js+18−2 src/form/ValuesHandler.js+3−2 src/form/formatters.js+18−7 src/form/validators.js+14−1 src/lib/reticket.js+11−10 src/lib/tank.js+20−20 src/lib/useEthereumTransaction.js+3−2 src/lib/validators.js+8−2 src/store/lib/useRekeyDateStore.js+17−6 src/views/Admin/AdminNetworkingKeys.js+1−1 src/views/Invite/InviteEmail.js+21−2 src/views/Login/PrivateKey.js+4−4
src/components/InlineEthereumTransaction.js+1−1
@@ -140,7 +140,7 @@ export default function InlineEthereumTransaction({
140140 return (
141141 <Grid className={cn(className, 'mt1')}>
142142 <BridgeForm validate={validate} onValues={onValues}>
143− {({ handleSubmit }) => (
143+ {() => (
144144 <>
145145 {renderPrimarySection()}
146146
src/form/Inputs.js+18−2
@@ -7,6 +7,8 @@ import {
77 buildFormatter,
88 downcase,
99 ensurePatFormat,
10+ stripHexPrefix,
11+ ensureHexPrefix,
1012} from 'form/formatters';
1113import { DEFAULT_HD_PATH } from 'lib/wallet';
1214import InputSigil from 'components/InputSigil';
@@ -17,7 +19,9 @@ const PLACEHOLDER_MNEMONIC =
1719 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
1820const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
1921const PLACEHOLDER_ADDRESS = '0x';
20−const PLACEHOLDER_PRIVATE_KEY = '0x';
22+const PLACEHOLDER_HEX = '0x';
23+const PLACEHOLDER_PRIVATE_KEY =
24+ 'a44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
2125const PLACEHOLDER_EMAIL = 'email@example.com';
2226
2327const formatPat = buildFormatter([downcase, ensurePatFormat]);
@@ -136,11 +140,23 @@ export function HexInput({ ...rest }) {
136140 return (
137141 <Input
138142 type="text"
139− placeholder={PLACEHOLDER_PRIVATE_KEY}
143+ placeholder={PLACEHOLDER_HEX}
140144 autoCapitalize="none"
141145 autoComplete="off"
142146 autoCorrect="off"
143147 mono
148+ config={{ parse: ensureHexPrefix }}
149+ {...rest}
150+ />
151+ );
152+}
153+
154+export function PrivateKeyInput({ ...rest }) {
155+ return (
156+ <HexInput
157+ type="password"
158+ placeholder={PLACEHOLDER_PRIVATE_KEY}
159+ config={{ parse: stripHexPrefix }}
144160 {...rest}
145161 />
146162 );
src/form/ValuesHandler.js+3−2
@@ -6,12 +6,13 @@ import { useForm } from 'react-final-form';
66 */
77export default function ValuesHandler({ valid, validating, values, onValues }) {
88 const form = useForm();
9+ const validationPaused = form.isValidationPaused();
910
1011 useEffect(() => {
11− if (!validating && !form.isValidationPaused()) {
12+ if (!validating && !validationPaused) {
1213 onValues && onValues({ valid, values, form });
1314 }
14− }, [form, onValues, valid, validating, values]);
15+ }, [form, onValues, valid, validating, validationPaused, values]);
1516
1617 return null;
1718}
src/form/formatters.js+18−7
@@ -1,5 +1,6 @@
11import { compose, strSplice } from 'lib/lib';
22
3+const HEX_PREFIX = '0x';
34const TICKET_MAX_BYTE_LEN = 32; // tickets can be as large as 32 bytes
45const PAT_BLOCK_CHAR_LENGTH = 6; // pat{p,q} blocks are 6 characters long
56
@@ -38,6 +39,14 @@ export const ensurePatFormat = s => {
3839 return s;
3940};
4041
42+export const ensureHexPrefix = s => {
43+ if (!s || s.length < HEX_PREFIX.length) {
44+ return s || '';
45+ }
46+
47+ return s.indexOf(HEX_PREFIX) !== 0 ? `${HEX_PREFIX}${s}` : s;
48+};
49+
4150export const convertToNumber = s => {
4251 try {
4352 return parseInt(s, 10);
@@ -54,10 +63,12 @@ export const downcase = s => {
5463 return s.toLowerCase();
5564};
5665
57−// const hideAllButLast = s => {
58−// const ll = s[s.length -1];
59−// const bs = fill(s.length - 1, '•')
60−// return `${bs}${ll}`
61−// };
62−//
63−// const hideAll = s => fill(s.length, '•');
66+export const stripHexPrefix = s => {
67+ if (!s) {
68+ return s;
69+ }
70+
71+ return s.toLowerCase().indexOf(HEX_PREFIX) === 0
72+ ? s.slice(HEX_PREFIX.length)
73+ : s;
74+};
src/form/validators.js+14−1
@@ -13,9 +13,14 @@ import {
1313 validateGreaterThan,
1414 validateEmail,
1515 validateNotNullAddress,
16+ validateHexPrefix,
17+ validateLength,
1618} from 'lib/validators';
1719import isPromise from 'lib/isPromise';
1820
21+const PRIVATE_KEY_CHAR_LENGTH = 64; // 64 hex characters
22+const ADDRESS_CHAR_LENGTH = 40; // 40 hex characters
23+
1924// iterate over validators, exiting early if there's an error
2025const buildValidator = (validators = [], validate) => value => {
2126 for (const validator of validators) {
@@ -75,15 +80,23 @@ export const buildSelectValidator = options =>
7580export const buildHexValidator = length =>
7681 buildValidator([
7782 validateNotEmpty,
83+ validateHexPrefix,
7884 validateHexString,
7985 validateHexLength(length),
8086 ]);
87+export const buildPrivateKeyValidator = () =>
88+ buildValidator([
89+ validateNotEmpty,
90+ validateHexString,
91+ validateLength(PRIVATE_KEY_CHAR_LENGTH),
92+ ]);
8193export const buildUploadValidator = () => buildValidator([validateNotEmpty]);
8294export const buildAddressValidator = () =>
8395 buildValidator([
8496 validateNotEmpty,
97+ validateHexPrefix,
8598 validateHexString,
86− validateHexLength(40),
99+ validateHexLength(ADDRESS_CHAR_LENGTH),
87100 validateNotNullAddress,
88101 validateEthereumAddress,
89102 ]);
src/lib/reticket.js+11−10
@@ -12,7 +12,7 @@ import { addHexPrefix } from './wallet';
1212import { sendAndAwaitAll } from './txn';
1313import getSuggestedGasPrice from './getSuggestedGasPrice';
1414import { GAS_LIMITS } from './constants';
15−import { toWei } from 'web3-utils';
15+import { toWei, toBN } from 'web3-utils';
1616
1717// the initial network key revision is always 1
1818const INITIAL_NETWORK_KEY_REVISION = 1;
@@ -143,14 +143,15 @@ export async function reticketPointBetweenWallets({
143143 progress(TRANSACTION_PROGRESS.SIGNING);
144144
145145 const suggestedGasPrice = await getSuggestedGasPrice(networkType);
146− const gasPrice = parseInt(toWei(suggestedGasPrice.toFixed(), 'gwei'), 10);
147− let totalCost = 0;
146+ const gasPrice = toWei(suggestedGasPrice.toFixed(), 'gwei');
147+ const gasPriceBN = toBN(gasPrice);
148+ let totalCost = toBN(0);
148149 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
149150 txs = txs.map(tx => {
150151 tx.from = fromWallet.address;
151152 tx.nonce = inviteNonce++;
152− tx.gasPrice = gasPrice;
153− totalCost = totalCost + tx.gasPrice * tx.gas;
153+ tx.gasPrice = gasPriceBN;
154+ totalCost = totalCost.add(gasPriceBN.mul(toBN(tx.gas)));
154155 return tx;
155156 });
156157
@@ -194,15 +195,15 @@ export async function reticketPointBetweenWallets({
194195 progress(TRANSACTION_PROGRESS.CLEANING);
195196
196197 // if non-trivial eth left in invite wallet, transfer to new ownership
197− let balance = await web3.eth.getBalance(fromWallet.address);
198+ let balance = toBN(await web3.eth.getBalance(fromWallet.address));
198199 const gasLimit = GAS_LIMITS.SEND_ETH;
199− const sendEthCost = gasPrice * gasLimit;
200− if (transferEth && balance > sendEthCost) {
201− const value = balance - sendEthCost;
200+ const sendEthCost = gasPriceBN.mul(toBN(gasLimit));
201+ if (transferEth && balance.gt(sendEthCost)) {
202+ const value = balance.sub(sendEthCost);
202203 const tx = {
203204 to: toWallet.ownership.keys.address,
204205 value: value,
205− gasPrice: gasPrice,
206+ gasPrice: gasPriceBN,
206207 gas: gasLimit,
207208 nonce: inviteNonce++,
208209 };
src/lib/tank.js+20−20
@@ -1,6 +1,7 @@
11// /lib/tank: functions for funding transactions
22
33import retry from 'async-retry';
4+import { toBN } from 'web3-utils';
45import { RETRY_OPTIONS, waitForTransactionConfirm } from './txn';
56
67//NOTE if accessing this in a localhost configuration fails with "CORS request
@@ -53,24 +54,29 @@ const ensureFundsFor = async (
5354 askForFunding,
5455 gotFunding
5556) => {
56− let balance = await web3.eth.getBalance(address);
57+ const balance = toBN(await web3.eth.getBalance(address));
58+ cost = toBN(cost);
5759
58− if (cost <= balance) {
59− console.log('tank: already have sufficient funds', cost, address, balance);
60+ if (balance.gte(cost)) {
61+ console.log(
62+ `tank: already have sufficient funds: ${address} proposed ` +
63+ `a transacton(s) that will cost ${cost.toString()}wei but it ` +
64+ `already has ${balance.toString}wei`
65+ );
6066 return false;
6167 }
6268
6369 try {
70+ // TODO: if we can't always (easily) provide a point, and the
71+ // fundTransactions call is gonna fail anyway, should we maybe
72+ // just not bother doing this check in the first place?
6473 if (point !== null) {
6574 const fundsRemaining = await remainingTransactions(point);
6675 if (fundsRemaining < signedTxs.length) {
6776 throw new Error('tank: request invalid');
6877 }
6978 } else {
70− console.log('tank: no point provided. skipping remaining-funds check');
71− //TODO if we can't always (easily) provide a point, and the
72− // fundTransactions call is gonna fail anyway, should we maybe
73− // just not bother doing this check in the first place?
79+ console.log('tank: no point provided. skipping remaining-funds check...');
7480 }
7581
7682 const res = await fundTransactions(signedTxs);
@@ -79,16 +85,15 @@ const ensureFundsFor = async (
7985 }
8086
8187 await waitForTransactionConfirm(web3, res.txHash);
88+
8289 const newBalance = await web3.eth.getBalance(address);
8390 console.log(
84− 'tank: funds have confirmed',
85− balance >= cost,
86− balance,
87− newBalance
91+ `tank: funds have confirmed: ${address} now has ` +
92+ ` ${newBalance.toString()}wei, up from ${balance.toString()}wei`
8893 );
8994 return true;
9095 } catch (e) {
91− console.log('tank: funding failed, waiting on user funds.', e);
96+ console.log('tank: funding failed, waiting on user funds...', e);
9297 await waitForBalance(web3, address, cost, askForFunding, gotFunding);
9398 }
9499
@@ -98,17 +103,12 @@ const ensureFundsFor = async (
98103// returns a promise that resolves when address has at least minBalance
99104function waitForBalance(web3, address, minBalance, askForFunding, gotFunding) {
100105 return retry(async (bail, n) => {
101− const balance = await web3.eth.getBalance(address);
102− if (balance >= minBalance) {
106+ const balance = toBN(await web3.eth.getBalance(address));
107+ if (balance.gte(minBalance)) {
103108 gotFunding && gotFunding();
104109 return;
105110 } else {
106− // TODO: minBalance is a `number` type
107− // but we want to display ETH, which will never accept a number
108− // but instead wants string or BN/BigNumber.
109− // this will be heavily refactored to correctly do BN math, but until
110− // then we'll manually convert this number to an integer string
111− askForFunding(address, minBalance.toFixed(), balance);
111+ askForFunding(address, minBalance.toString(), balance);
112112 throw new Error('retrying');
113113 }
114114 }, RETRY_OPTIONS);
src/lib/useEthereumTransaction.js+3−2
@@ -1,6 +1,6 @@
11import { useCallback, useEffect, useState } from 'react';
22import { Just } from 'folktale/maybe';
3−import { toWei } from 'web3-utils';
3+import { toWei, toBN } from 'web3-utils';
44
55import { useNetwork } from 'store/network';
66import { useWallet } from 'store/wallet';
@@ -136,7 +136,8 @@ export default function useEthereumTransaction(
136136 setError(undefined);
137137
138138 const rawTx = hexify(signedTransaction.serialize());
139− const cost = toWei(gasPrice.toFixed(0), 'gwei') * gasLimit;
139+ const costGwei = toBN(gasLimit).mul(toBN(gasPrice));
140+ const cost = toWei(costGwei.toString(), 'gwei');
140141
141142 let usedTank = false;
142143 // if this ethereum transaction is being executed by a specific point
src/lib/validators.js+8−2
@@ -14,8 +14,11 @@ import { MIN_GALAXY, MAX_GALAXY } from './constants';
1414// via: https://emailregex.com/
1515const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
1616
17−// is this only hex values with a 0x prefix?
18−const isHexString = /0x[0-9A-Fa-f]/;
17+// validate only strings that use hex characters
18+const isHexString = /[0-9A-Fa-f]/;
19+
20+// validate only strings that are prefixed with 0x
21+const isHexPrefixed = /^0x/;
1922
2023// Validates a bip39 mnemonic
2124export const validateMnemonic = v =>
@@ -64,6 +67,9 @@ export const validateOneOf = (options = []) => v =>
6467export const validateHexString = v =>
6568 !isHexString.test(v) && 'This is not a valid hex string.';
6669
70+export const validateHexPrefix = v =>
71+ !isHexPrefixed.test(v) && 'Must include 0x prefix.';
72+
6773export const validateEthereumAddress = v =>
6874 !isValidAddress(v) && 'This is not a valid Ethereum address.';
6975
src/store/lib/useRekeyDateStore.js+17−6
@@ -44,18 +44,29 @@ export default function useRekeyDateStore() {
4444 },
4545 });
4646
47− if (logs.length === 0) {
48− //TODO: better encoding for "no rekeyDate" state?
49− addToRekeyDateCache({
50− [point]: Just(Result.Error('No rekey date available.')),
51− });
52− } else {
47+ // NB(shrugs) using try-catch here to simplify our error handling
48+ try {
49+ if (logs.length === 0) {
50+ throw new Error('No logs available.');
51+ }
52+
5353 // last log is most recent
5454 const blockNumber = logs[logs.length - 1].blockNumber;
55+ if (blockNumber === null) {
56+ // the "latest" parameter includes pending blocks,
57+ // which won't have a blockNumber just yet
58+ throw new Error('Latest rekey is still pending');
59+ }
60+
5561 const block = await _web3.eth.getBlock(blockNumber);
5662 addToRekeyDateCache({
5763 [point]: Just(Result.Ok(new Date(block.timestamp * 1000))),
5864 });
65+ } catch {
66+ //TODO: better encoding for "no rekeyDate" state?
67+ addToRekeyDateCache({
68+ [point]: Just(Result.Error('No rekey date available.')),
69+ });
5970 }
6071 },
6172 [web3, contracts, addToRekeyDateCache]
src/views/Admin/AdminNetworkingKeys.js+1−1
@@ -332,7 +332,7 @@ export default function AdminNetworkingKeys() {
332332 validate={validate}
333333 onValues={onValues}
334334 initialValues={initialValues}>
335− {({ handleSubmit }) => (
335+ {() => (
336336 <>
337337 <Grid.Item
338338 full
src/views/Invite/InviteEmail.js+21−2
@@ -18,7 +18,7 @@ import {
1818 AccessoryIcon,
1919} from 'indigo-react';
2020import { uniq } from 'lodash';
21−import { fromWei, toWei } from 'web3-utils';
21+import { fromWei, toWei, toBN } from 'web3-utils';
2222import { FieldArray } from 'react-final-form-arrays';
2323
2424import { usePointCursor } from 'store/pointCursor';
@@ -135,6 +135,7 @@ export default function InviteEmail() {
135135 const canInput = status === STATUS.INPUT;
136136 const isGenerating = status === STATUS.GENERATING;
137137 const canSend = status === STATUS.CAN_SEND;
138+ const isFunding = status === STATUS.FUNDING;
138139 const isSending = status === STATUS.SENDING;
139140 const isFailed = status === STATUS.FAILURE;
140141 const isDone = status === STATUS.SUCCESS;
@@ -306,11 +307,14 @@ export default function InviteEmail() {
306307 }
307308
308309 setStatus(STATUS.FUNDING);
310+ const totalCost = toBN(GAS_LIMIT)
311+ .mul(toBN(gasPrice))
312+ .mul(toBN(emails.length));
309313 const tankWasUsed = await tank.ensureFundsFor(
310314 _web3,
311315 point,
312316 _wallet.address,
313− toWei((gasPrice * GAS_LIMIT * emails.length).toFixed(), 'gwei'),
317+ toWei(totalCost.toString(), 'gwei'),
314318 names.map(name => invites[name].rawTx),
315319 (address, minBalance, balance) =>
316320 setNeedFunds({ address, minBalance, balance }),
@@ -473,6 +477,21 @@ export default function InviteEmail() {
473477 );
474478 }
475479
480+ if (isFunding) {
481+ return (
482+ <Grid.Item
483+ full
484+ as={ForwardButton}
485+ className="mt4"
486+ solid
487+ success
488+ disabled
489+ accessory={`${visualProgress}/${fields.length}`}>
490+ {buttonText(status, fields.length)}
491+ </Grid.Item>
492+ );
493+ }
494+
476495 if (isSending) {
477496 return (
478497 <Grid.Item
src/views/Login/PrivateKey.js+4−4
@@ -9,8 +9,8 @@ import useLoginView from 'lib/useLoginView';
99
1010import FormError from 'form/FormError';
1111import BridgeForm from 'form/BridgeForm';
12−import { HexInput } from 'form/Inputs';
13−import { buildHexValidator, composeValidator } from 'form/validators';
12+import { PrivateKeyInput } from 'form/Inputs';
13+import { composeValidator, buildPrivateKeyValidator } from 'form/validators';
1414
1515import SubmitButton from 'form/SubmitButton';
1616
@@ -22,7 +22,7 @@ export default function PrivateKey({ className, goHome }) {
2222 const validate = useMemo(
2323 () =>
2424 composeValidator({
25− privateKey: buildHexValidator(64),
25+ privateKey: buildPrivateKeyValidator(64),
2626 }),
2727 []
2828 );
@@ -47,7 +47,7 @@ export default function PrivateKey({ className, goHome }) {
4747 <>
4848 <Grid.Item
4949 full
50− as={HexInput}
50+ as={PrivateKeyInput}
5151 name="privateKey"
5252 label="Private key"
5353 />