on master help sign in
Merge branch 'mino' into feat/indigo-admin
commit41620df0e6b0330dbcb06a3ace0fb7272c4e160e authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 16 Jul 2019, 14:57 (7 years ago) parent06b6de44af7360
25 files changed+222−184
package-lock.json+13 package.json+1 src/Bridge.js+4−1 src/components/old/StatelessTransaction.js+11−9 src/lib/constants.js+2−2 src/lib/disclaimerCookie.js+9 src/lib/error.js+11−11 src/lib/invite.js+3−8 src/lib/need.js+6−6 src/lib/routeNames.js+1 src/lib/router.js+2 src/lib/tank.js+18−29 src/lib/txn.js+44−70 src/lib/useEthereumTransaction.js+3−3 src/lib/useImpliedTicket.js+14 src/lib/useMailer.js+6−10 src/store/network.js+1−1 src/store/wallet.js+1−1 src/views/Activate.js−2 src/views/Activate/ActivateCode.js+7−5 src/views/Admin/AdminTransfer.js+1−1 src/views/Activate/ActivateDisclaimer.js → src/views/Disclaimer.js+7−3 src/views/Invite/InviteEmail.js+42−16 src/views/Landing.js+14−5 src/views/SentTransaction.js+1−1
package-lock.json+13
@@ -2573,6 +2573,14 @@
25732573 "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
25742574 "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
25752575 },
2576+ "async-retry": {
2577+ "version": "1.2.3",
2578+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.2.3.tgz",
2579+ "integrity": "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==",
2580+ "requires": {
2581+ "retry": "0.12.0"
2582+ }
2583+ },
25762584 "asynckit": {
25772585 "version": "0.4.0",
25782586 "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -14927,6 +14935,11 @@
1492714935 "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
1492814936 "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
1492914937 },
14938+ "retry": {
14939+ "version": "0.12.0",
14940+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
14941+ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
14942+ },
1493014943 "rgb-regex": {
1493114944 "version": "1.0.1",
1493214945 "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
package.json+1
@@ -18,6 +18,7 @@
1818 "@ledgerhq/hw-app-eth": "^4.61.0",
1919 "@ledgerhq/hw-transport-u2f": "^4.61.0",
2020 "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
21+ "async-retry": "^1.2.3",
2122 "azimuth-js": "^0.15.0",
2223 "azimuth-solidity": "^1.2.0",
2324 "bip32": "^1.0.2",
src/Bridge.js+4−1
@@ -12,6 +12,7 @@ import { ROUTES } from 'lib/router';
1212import { NETWORK_TYPES } from 'lib/network';
1313import { walletFromMnemonic } from 'lib/wallet';
1414import { isDevelopment } from 'lib/flags';
15+import { hasDisclaimed } from 'lib/disclaimerCookie';
1516
1617import 'style/index.scss';
1718
@@ -29,7 +30,9 @@ const INITIAL_ROUTES = IS_STUBBED
2930 { key: ROUTE_NAMES.POINTS },
3031 { key: ROUTE_NAMES.POINT },
3132 ]
32− : [{ key: ROUTE_NAMES.LANDING }];
33+ : hasDisclaimed()
34+ ? [{ key: ROUTE_NAMES.LANDING }]
35+ : [{ key: ROUTE_NAMES.DISCLAIMER }];
3336
3437const INITIAL_WALLET = IS_STUBBED
3538 ? walletFromMnemonic(
src/components/old/StatelessTransaction.js+11−9
@@ -8,6 +8,7 @@ import { Button } from './Base';
88import { CheckboxButton, Input, InnerLabel, InnerLabelDropdown } from './Base';
99import { Warning } from './Base';
1010
11+import { GAS_LIMITS } from '../../lib/constants';
1112import { BRIDGE_ERROR } from '../../lib/error';
1213import { ROUTE_NAMES } from '../../lib/routeNames';
1314import { compose } from '../../lib/lib';
@@ -40,7 +41,7 @@ class StatelessTransaction extends React.Component {
4041
4142 this.state = {
4243 gasPrice: '5',
43− gasLimit: '600000',
44+ gasLimit: GAS_LIMITS.DEFAULT.toString(),
4445 showGasDetails: false,
4546 userApproval: false,
4647 chainId: '1',
@@ -184,7 +185,7 @@ class StatelessTransaction extends React.Component {
184185 const stx = state.stx.matchWith({
185186 Just: tx => tx.value,
186187 Nothing: () => {
187− throw BRIDGE_ERROR.MISSING_TXN;
188+ throw new Error(BRIDGE_ERROR.MISSING_TXN);
188189 },
189190 });
190191
@@ -232,12 +233,7 @@ class StatelessTransaction extends React.Component {
232233 } else {
233234 this.setState({ txStatus: SUBMISSION_STATES.SENDING });
234235
235− sendSignedTransaction(
236− web3,
237− state.stx,
238− usedTank,
239− props.setTxnConfirmations
240− )
236+ sendSignedTransaction(web3, stx, usedTank, props.setTxnConfirmations)
241237 .then(txHash => {
242238 props.setTxnCursor(Just(Ok(txHash)));
243239
@@ -498,7 +494,13 @@ class StatelessTransaction extends React.Component {
498494 prop-size={'lg wide'}
499495 prop-color={signerButtonColor}
500496 onClick={() =>
501− signTransaction({ ...this.props, ...this.state, setStx: this.setStx })
497+ signTransaction({
498+ ...this.props,
499+ ...this.state,
500+ setStx: this.setStx,
501+ wallet: this.props.wallet.value,
502+ txn: txn.value,
503+ })
502504 }>
503505 {'Sign Transaction'}
504506 </Button>
src/lib/constants.js+2−2
@@ -20,7 +20,7 @@ const SEED_ENTROPY_BITS = 128;
2020//TODO move into azimuth-js
2121const GAS_LIMITS = {
2222 SPAWN: 250000,
23− TRANSFER: 490000,
23+ TRANSFER: 550000,
2424 CONFIGURE_KEYS: 100000,
2525 SET_PROXY: 120000,
2626 //
@@ -32,7 +32,7 @@ const GAS_LIMITS = {
3232 //
3333 GIFT_PLANET: 400000, //NOTE low sample size
3434 //
35− DEFAULT: 500000,
35+ DEFAULT: 550000,
3636};
3737
3838// TODO: this is walletgen-ui specific, move into a wallet router later
src/lib/disclaimerCookie.jsnew file+9
@@ -0,0 +1,9 @@
1+const COOKIE_ID = 'bridge_disclaimed';
2+
3+export const hasDisclaimed = () => {
4+ return document.cookie.includes(COOKIE_ID);
5+};
6+
7+export const setDisclaimerCookie = () => {
8+ document.cookie = COOKIE_ID;
9+};
src/lib/error.js+11−11
@@ -1,15 +1,15 @@
11const BRIDGE_ERROR = {
2− MISSING_WEB3: new Error('no web3 object found'),
3− MISSING_WALLET: new Error('no wallet found'),
4− MISSING_TXN: new Error('no transaction found'),
5− MISSING_CONTRACTS: new Error('no contracts found'),
6− MISSING_KEYSTORE: new Error('no keystore text found'),
7− MISSING_MNEMONIC: new Error('no authentication mnemonic found'),
8− MISSING_URBIT_WALLET: new Error('no urbit wallet found'),
9− MISSING_POINT: new Error('no point found'),
10− INVALID_ROUTE: new Error('invalid route'),
11− INVALID_NETWORK_TYPE: new Error('invalid network type'),
12− INVALID_WALLET_TYPE: new Error('invalid wallet type'),
2+ MISSING_WEB3: 'no web3 object found',
3+ MISSING_WALLET: 'no wallet found',
4+ MISSING_TXN: 'no transaction found',
5+ MISSING_CONTRACTS: 'no contracts found',
6+ MISSING_KEYSTORE: 'no keystore text found',
7+ MISSING_MNEMONIC: 'no authentication mnemonic found',
8+ MISSING_URBIT_WALLET: 'no urbit wallet found',
9+ MISSING_POINT: 'no point found',
10+ INVALID_ROUTE: 'invalid route',
11+ INVALID_NETWORK_TYPE: 'invalid network type',
12+ INVALID_WALLET_TYPE: 'invalid wallet type',
1313};
1414
1515const renderTxnError = (web3, msg) => {
src/lib/invite.js+3−8
@@ -1,6 +1,4 @@
1−import { Just } from 'folktale/maybe';
21import Tx from 'ethereumjs-tx';
3−
42import * as azimuth from 'azimuth-js';
53import * as kg from 'urbit-key-generation/dist';
64import * as wg from './walletgen';
@@ -9,9 +7,10 @@ import * as tank from './tank';
97import JSZip from 'jszip';
108import saveAs from 'file-saver';
119
12−import { hexify, sendTransactionsAndAwaitConfirm } from './txn';
10+import { hexify } from './txn';
1311import { deriveNetworkSeedFromUrbitWallet } from './keys';
1412import { addHexPrefix } from './wallet';
13+import { sendAndAwaitAll } from './txn';
1514import { GAS_LIMITS } from './constants';
1615
1716// the initial network key revision is always 1
@@ -239,11 +238,7 @@ export async function reticketPointBetweenWallets({
239238
240239 progress(TRANSACTION_STATES.TRANSFERRING);
241240
242− await sendTransactionsAndAwaitConfirm(
243− web3,
244− txPairs.map(p => Just(p.signed)),
245− usedTank
246− );
241+ await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
247242
248243 //
249244 // move leftover eth
src/lib/need.js+6−6
@@ -30,26 +30,26 @@ export const web3 = needBuilder(() => {
3030});
3131
3232export const contracts = needBuilder(() => {
33− throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS.message);
33+ throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS);
3434});
3535
3636export const wallet = needBuilder(() => {
37− throw new Error(BRIDGE_ERROR.MISSING_WALLET.message);
37+ throw new Error(BRIDGE_ERROR.MISSING_WALLET);
3838});
3939
4040export const addressFromWallet = obj => wallet(obj).address;
4141
4242export const point = needBuilder(() => {
43− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
43+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
4444});
4545
4646export const pointCache = needBuilder(() => {
47− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
47+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
4848});
4949
5050export const fromPointCache = (cache, point) => {
5151 if (!(point in cache)) {
52− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
52+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
5353 }
5454
5555 return cache[point];
@@ -60,7 +60,7 @@ export const keystore = obj => {
6060 return ks.value.matchWith({
6161 Ok: result => result.value,
6262 Error: _ => {
63− throw BRIDGE_ERROR.MISSING_KEYSTORE;
63+ throw new Error(BRIDGE_ERROR.MISSING_KEYSTORE);
6464 },
6565 });
6666};
src/lib/routeNames.js+1
@@ -1,5 +1,6 @@
11export const ROUTE_NAMES = {
22 DEFAULT: Symbol('DEFAULT'),
3+ DISCLAIMER: Symbol('DISCLAIMER'),
34 //
45 ACTIVATE: Symbol('ACTIVATE'),
56 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
src/lib/router.js+2
@@ -1,4 +1,5 @@
11import Landing from '../views/Landing';
2+import Disclaimer from '../views/Disclaimer';
23import Login from '../views/Login.js';
34import InvitesManage from '../views/InvitesManage.js';
45import Admin from '../views/Admin';
@@ -24,6 +25,7 @@ import { ROUTE_NAMES } from './routeNames';
2425
2526export const ROUTES = {
2627 [ROUTE_NAMES.LANDING]: Landing,
28+ [ROUTE_NAMES.DISCLAIMER]: Disclaimer,
2729 [ROUTE_NAMES.ACTIVATE]: Activate,
2830 [ROUTE_NAMES.INVITE]: Invite,
2931 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
src/lib/tank.js+18−29
@@ -1,7 +1,7 @@
11// /lib/tank: functions for funding transactions
22
3−import { waitForTransactionConfirm } from './txn';
4−import { CHECK_BLOCK_EVERY_MS } from './constants';
3+import retry from 'async-retry';
4+import { RETRY_OPTIONS, waitForTransactionConfirm } from './txn';
55
66//NOTE if accessing this in a localhost configuration fails with "CORS request
77// did not succeed", you might need to visit localhost:3001 or whatever
@@ -93,40 +93,29 @@ const ensureFundsFor = async (
9393 return false;
9494};
9595
96−// resolves when address has at least minBalance
97−//
98−//TODO should maybe do a "we got it" callback also, so clients can hide msg?
99−async function waitForBalance(
100− web3,
101− address,
102− minBalance,
103− askForFunding,
104− gotFunding
105−) {
106− console.log('tank: awaiting balance', address, minBalance);
107− return new Promise((resolve, reject) => {
108− const checkForBalance = async () => {
109− const balance = await web3.eth.getBalance(address);
110−
111− if (balance >= minBalance) {
112− if (gotFunding) {
113− gotFunding();
114− }
115−
116− resolve();
117− } else {
96+// returns a promise that resolves when address has at least minBalance
97+function waitForBalance(web3, address, minBalance, askForFunding, gotFunding) {
98+ let oldBalance = null;
99+ return retry(async (bail, n) => {
100+ const balance = await web3.eth.getBalance(address);
101+ if (balance >= minBalance) {
102+ // if we ever asked for funding, retract that request now
103+ if (gotFunding && oldBalance !== null) {
104+ gotFunding();
105+ }
106+ return;
107+ } else {
108+ if (balance !== oldBalance) {
118109 // TODO: minBalance is a `number` type
119110 // but we want to display ETH, which will never accept a number
120111 // but instead wants string or BN/BigNumber.
121112 // this will be heavily refactored to correctly do BN math, but until
122113 // then we'll manually convert this number to an integer string
123114 askForFunding(address, minBalance.toFixed(), balance);
124− setTimeout(checkForBalance, CHECK_BLOCK_EVERY_MS);
125115 }
126− };
127−
128− checkForBalance();
129− });
116+ throw new Error('retrying');
117+ }
118+ }, RETRY_OPTIONS);
130119}
131120
132121export { remainingTransactions, fundTransactions, ensureFundsFor };
src/lib/txn.js+44−70
@@ -2,14 +2,21 @@ import * as ob from 'urbit-ob';
22import { Just } from 'folktale/maybe';
33import Tx from 'ethereumjs-tx';
44import { toWei, fromWei, toHex } from 'web3-utils';
5+import retry from 'async-retry';
56
6−import { BRIDGE_ERROR } from './error';
77import { NETWORK_TYPES } from './network';
88import { ledgerSignTransaction } from './ledger';
99import { trezorSignTransaction } from './trezor';
1010import { WALLET_TYPES, addHexPrefix } from './wallet';
1111import { CHECK_BLOCK_EVERY_MS } from './constants';
1212
13+const RETRY_OPTIONS = {
14+ retries: 99999,
15+ factor: 1,
16+ minTimeout: CHECK_BLOCK_EVERY_MS,
17+ randomize: false,
18+};
19+
1320const signTransaction = async config => {
1421 let {
1522 wallet,
@@ -24,6 +31,7 @@ const signTransaction = async config => {
2431 gasLimit,
2532 } = config;
2633
34+ //TODO require these in txn object
2735 nonce = toHex(nonce);
2836 chainId = toHex(chainId);
2937 gasPrice = toHex(toWei(gasPrice, 'gwei'));
@@ -74,47 +82,27 @@ const signTransaction = async config => {
7482 ? Object.assign(txParams, eip155Params)
7583 : txParams;
7684
77− const wal = wallet.matchWith({
78− Just: w => w.value,
79− Nothing: () => {
80− throw BRIDGE_ERROR.MISSING_WALLET;
81− },
82− });
83−
84− const sec = wal.privateKey;
85−
86− const utx = txn.matchWith({
87− Just: tx => Object.assign(tx.value, signingParams),
88− Nothing: () => {
89− throw BRIDGE_ERROR.MISSING_TXN;
90− },
91− });
85+ const utx = Object.assign(txn, signingParams);
9286
9387 const stx = new Tx(utx);
9488
89+ //TODO should try-catch and display error message to user,
90+ // ie ledger's "pls enable contract data"
91+ // needs to maybe happen at call-site though
9592 if (walletType === WALLET_TYPES.LEDGER) {
9693 await ledgerSignTransaction(stx, walletHdPath);
9794 } else if (walletType === WALLET_TYPES.TREZOR) {
9895 await trezorSignTransaction(stx, walletHdPath);
9996 } else {
100− stx.sign(sec);
97+ stx.sign(wallet.privateKey);
10198 }
10299
103100 setStx(Just(stx));
104101 return stx;
105102};
106103
107−// TODO(shrugs): refactor the hell out of all of these functions
108−// but especially this one
109−const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
110− const txn = stx.matchWith({
111− Just: tx => tx.value,
112− Nothing: () => {
113− throw BRIDGE_ERROR.MISSING_TXN;
114− },
115− });
116−
117− const rawTx = hexify(txn.serialize());
104+const sendSignedTransaction = (web3, stx, doubtNonceError) => {
105+ const rawTx = hexify(stx.serialize());
118106
119107 return new Promise(async (resolve, reject) => {
120108 web3.eth
@@ -122,39 +110,25 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
122110 .on('transactionHash', hash => {
123111 resolve(hash);
124112 })
125− //TODO do we also reach this if network is slow? web3 only tries a set
126− // amount of times... should we instead do waitForTransactionConfirm
127− // in on-transactionHash? we don't care (much) about additional
128− // confirms anyway.
129− .on('confirmation', (confirmationNum, txn) => {
130− if (confirmationCb) {
131− confirmationCb(txn.transactionHash, confirmationNum + 1);
132− }
133− resolve(txn.transactionHash);
134− })
135113 .on('error', err => {
136114 // if there's a nonce error, but we used the gas tank, it's likely
137115 // that it's because the tank already submitted our transaction.
138116 // we just wait for first confirmation here.
139117 console.error(err);
140− if (
141− doubtNonceError &&
142− (err.message || '').includes("the tx doesn't have the correct nonce.")
143− ) {
144− console.log('nonce error, likely from gas tank submission, ignoring');
118+ const isKnownError = (err.message || '').includes(
119+ 'known transaction: '
120+ );
121+ const isNonceError = (err.message || '').includes(
122+ "the tx doesn't have the correct nonce."
123+ );
124+ if (isKnownError || (doubtNonceError && isNonceError)) {
125+ console.log(
126+ 'tx send error likely from gas tank submission, ignoring'
127+ );
145128 const txHash = web3.utils.keccak256(rawTx);
146− //TODO can we do does-exists check first?
147− //TODO max wait time before assume fail?
148− waitForTransactionConfirm(web3, txHash).then(res => {
149− if (res) {
150− resolve(txHash);
151− confirmationCb(txHash, 1);
152− } else {
153− reject('Unexpected tx failure');
154− }
155− });
129+ resolve(txHash);
156130 } else {
157− reject(err.message);
131+ reject(err.message || 'Transaction sending failed!');
158132 }
159133 });
160134 });
@@ -162,23 +136,22 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
162136
163137// returns a Promise<bool>, where the bool indicates tx success/failure
164138const waitForTransactionConfirm = (web3, txHash) => {
165− return new Promise((resolve, reject) => {
166− const checkForConfirm = async () => {
167− console.log('checking for confirm', txHash);
168− const receipt = await web3.eth.getTransactionReceipt(txHash);
169− console.log('tried, got', receipt);
170− let confirmed = receipt !== null;
171− if (confirmed) resolve(receipt.status === true);
172− else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
173− };
174− checkForConfirm();
175− });
139+ return retry(async (bail, n) => {
140+ const receipt = await web3.eth.getTransactionReceipt(txHash);
141+ const confirmed = receipt !== null;
142+ if (confirmed) return receipt.status === true;
143+ else throw new Error('retrying');
144+ }, RETRY_OPTIONS);
176145};
177146
178−const isTransactionConfirmed = async (web3, txHash) => {
179− const receipt = await web3.eth.getTransactionReceipt(txHash);
180− console.log('got confirm state', receipt !== null, receipt.confirmations);
181− return receipt !== null;
147+// returns a Promise that resolves when all stxs have been sent & confirmed
148+const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
149+ return Promise.all(
150+ stxs.map(async tx => {
151+ const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
152+ await waitForTransactionConfirm(web3, txHash);
153+ })
154+ );
182155};
183156
184157const sendTransactionsAndAwaitConfirm = async (web3, signedTxs, usedTank) =>
@@ -217,11 +190,12 @@ const canDecodePatp = p => {
217190};
218191
219192export {
193+ RETRY_OPTIONS,
220194 signTransaction,
221195 sendSignedTransaction,
222196 waitForTransactionConfirm,
223197 sendTransactionsAndAwaitConfirm,
224− isTransactionConfirmed,
198+ sendAndAwaitAll,
225199 getTxnInfo,
226200 hexify,
227201 renderSignedTx,
src/lib/useEthereumTransaction.js+3−3
@@ -60,7 +60,7 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
6060 // txn.gasPrice = toHex(toWei(gasPrice, 'gwei'));
6161
6262 const txn = await signTransaction({
63− wallet, // wallet: _wallet,
63+ wallet: _wallet,
6464 walletType,
6565 walletHdPath,
6666 networkType,
@@ -78,13 +78,13 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
7878 setError(error);
7979 }
8080 }, [
81+ _wallet,
8182 chainId,
8283 gasLimit,
8384 gasPrice,
8485 networkType,
8586 nonce,
8687 unsignedTransaction,
87− wallet,
8888 walletHdPath,
8989 walletType,
9090 ]);
@@ -105,7 +105,7 @@ export default function useEthereumTransaction(initialGasLimit = 0) {
105105 try {
106106 const txHash = await sendSignedTransaction(
107107 _web3,
108− Just(signedTransaction),
108+ signedTransaction,
109109 /* doubtNonceError */ false
110110 );
111111
src/lib/useImpliedTicket.jsnew file+14
@@ -0,0 +1,14 @@
1+import * as ob from 'urbit-ob';
2+
3+import { prependSig } from './transformers';
4+
5+/**
6+ * pull the suggested ticket from the #hash in the url
7+ */
8+export default function useImpliedTicket() {
9+ const hash = window.location.hash.slice(1);
10+ const ticket = prependSig(hash);
11+ const isValid = ob.isValidPatq(ticket);
12+
13+ return isValid ? ticket : null;
14+}
src/lib/useMailer.js+6−10
@@ -9,14 +9,14 @@ const STUB_MAILER = process.env.REACT_APP_STUB_MAILER === 'true';
99function useHasReceivedCache() {
1010 const [cache, addToCache] = useSetState();
1111
12− const getHasRecieved = useCallback(
12+ const getHasReceived = useCallback(
1313 email => cache[email] || Nothing(), //
1414 [cache]
1515 );
1616
1717 const syncHasReceivedForEmail = useCallback(
1818 async email => {
19− if (Just.hasInstance(getHasRecieved(email))) {
19+ if (Just.hasInstance(getHasReceived(email))) {
2020 // never update the cache after we know about it
2121 return;
2222 }
@@ -29,10 +29,10 @@ function useHasReceivedCache() {
2929 const _hasReceived = await hasReceived(email);
3030 addToCache({ [email]: Just(_hasReceived) });
3131 },
32− [getHasRecieved, addToCache]
32+ [getHasReceived, addToCache]
3333 );
3434
35− return { getHasRecieved, syncHasReceivedForEmail };
35+ return { getHasReceived, syncHasReceivedForEmail };
3636}
3737
3838export default function useMailer(emails) {
@@ -43,14 +43,10 @@ export default function useMailer(emails) {
4343 const _sendMail = useCallback(async (email, ticket, rawTx) => {
4444 if (STUB_MAILER) {
4545 console.log(`${email} - ${ticket}`);
46− return;
46+ return true;
4747 }
4848
49− const mailSuccess = await sendMail(email, ticket, rawTx);
50−
51− if (!mailSuccess) {
52− throw new Error(`Internal mailing error when mailing ${email}`);
53− }
49+ return await sendMail(email, ticket, rawTx);
5450 }, []);
5551
5652 return { ...hasReceivedCache, sendMail: _sendMail };
src/store/network.js+1−1
@@ -20,7 +20,7 @@ function _useNetwork(initialNetworkType = null) {
2020
2121 const setNetworkType = networkType => {
2222 if (!includes(NETWORK_TYPES, networkType)) {
23− throw BRIDGE_ERROR.INVALID_NETWORK_TYPE;
23+ throw new Error(BRIDGE_ERROR.INVALID_NETWORK_TYPE);
2424 }
2525 _setNetworkType(networkType);
2626 };
src/store/wallet.js+1−1
@@ -44,7 +44,7 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
4444 const setWalletType = useCallback(
4545 walletType => {
4646 if (!includes(WALLET_TYPES, walletType)) {
47− throw BRIDGE_ERROR.INVALID_WALLET_TYPE;
47+ throw new Error(BRIDGE_ERROR.INVALID_WALLET_TYPE);
4848 }
4949
5050 _setWalletType(walletType);
src/views/Activate.js−2
@@ -5,7 +5,6 @@ import useRouter from 'lib/useRouter';
55import { ActivateFlowProvider } from './Activate/ActivateFlow';
66import useActivateFlowState from './Activate/useActivateFlowState';
77import ActivateCode from './Activate/ActivateCode';
8−import ActivateDisclaimer from './Activate/ActivateDisclaimer';
98import { LocalRouterProvider } from 'lib/LocalRouter';
109import ActivatePassport from './Activate/ActivatePassport';
1110
@@ -17,7 +16,6 @@ const NAMES = {
1716
1817const VIEWS = {
1918 [NAMES.CODE]: ActivateCode,
20− [NAMES.DISCLAIMER]: ActivateDisclaimer,
2119 [NAMES.PASSPORT]: ActivatePassport,
2220};
2321
src/views/Activate/ActivateCode.js+7−5
@@ -21,11 +21,13 @@ import { generateWallet } from 'lib/invite';
2121import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
2222import { useActivateFlow } from './ActivateFlow';
2323import { useLocalRouter } from 'lib/LocalRouter';
24+import useImpliedTicket from 'lib/useImpliedTicket';
2425
2526export default function ActivateCode() {
2627 const history = useHistory();
2728 const { names, push } = useLocalRouter();
2829 const { contracts } = useNetwork();
30+ const impliedTicket = useImpliedTicket();
2931 const [generalError, setGeneralError] = useState();
3032 const [deriving, setDeriving] = useState(false);
3133 const {
@@ -39,16 +41,14 @@ export default function ActivateCode() {
3941 const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
4042 name: 'ticket',
4143 label: 'Code',
44+ initialValue: impliedTicket || '',
4245 autoFocus: true,
4346 });
4447
4548 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
4649 history,
4750 ]);
48− const goToDisclaimer = useCallback(() => push(names.DISCLAIMER), [
49− names,
50− push,
51− ]);
51+ const goToPassport = useCallback(() => push(names.PASSPORT), [names, push]);
5252
5353 const pass = derivedWallet.matchWith({
5454 Nothing: () => false,
@@ -91,6 +91,8 @@ export default function ActivateCode() {
9191 'This invite code has multiple points available.\n' +
9292 "Once you've activated this point, activate the next with the same process."
9393 );
94+ } else {
95+ setGeneralError(false);
9496 }
9597 } else {
9698 setGeneralError(
@@ -137,7 +139,7 @@ export default function ActivateCode() {
137139 className="mt4"
138140 disabled={!pass || deriving}
139141 accessory={deriving ? <Blinky /> : undefined}
140− onClick={goToDisclaimer}
142+ onClick={goToPassport}
141143 solid
142144 full>
143145 {deriving && 'Deriving...'}
src/views/Admin/AdminTransfer.js+1−1
@@ -1,5 +1,5 @@
11import React, { useCallback } from 'react';
2−import { Grid, P, Text, Input } from 'indigo-react';
2+import { Grid, Text, Input } from 'indigo-react';
33import { useLocalRouter } from 'lib/LocalRouter';
44
55import ViewHeader from 'components/ViewHeader';
src/views/Activate/ActivateDisclaimer.js → src/views/Disclaimer.jsrenamed+7−3
@@ -3,7 +3,8 @@ import cn from 'classnames';
33import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
44
55import { useCheckboxInput } from 'lib/useInputs';
6−import { useLocalRouter } from 'lib/LocalRouter';
6+import { useHistory } from 'store/history';
7+import { setDisclaimerCookie } from 'lib/disclaimerCookie';
78
89import View from 'components/View';
910import { ForwardButton } from 'components/Buttons';
@@ -12,13 +13,16 @@ import WarningBox from 'components/WarningBox';
1213const TEXT_STYLE = 'f5';
1314
1415export default function ActivateDisclaimer() {
15− const { push, names } = useLocalRouter();
16+ const { popAndPush, names } = useHistory();
1617 const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
1718 name: 'checkbox',
1819 label: 'I acknowledge and understand these rights',
1920 });
2021
21− const goToPassport = useCallback(() => push(names.PASSPORT), [push, names]);
22+ const goToPassport = useCallback(() => {
23+ setDisclaimerCookie();
24+ popAndPush(names.LANDING);
25+ }, [popAndPush, names]);
2226
2327 return (
2428 <View>
src/views/Invite/InviteEmail.js+42−16
@@ -1,6 +1,5 @@
11import React, { useCallback, useEffect, useMemo, useState } from 'react';
22import cn from 'classnames';
3−import { Just } from 'folktale/maybe';
43import * as azimuth from 'azimuth-js';
54import {
65 Grid,
@@ -101,7 +100,7 @@ export default function InviteEmail() {
101100 const { syncInvites, getInvites } = usePointCache();
102101 const { pointCursor } = usePointCursor();
103102 const point = need.point(pointCursor);
104− const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
103+ const { getHasReceived, syncHasReceivedForEmail, sendMail } = useMailer();
105104
106105 const { availableInvites } = getInvites(point);
107106 const maxInvitesToSend = availableInvites.matchWith({
@@ -142,14 +141,14 @@ export default function InviteEmail() {
142141 () =>
143142 inputConfigs.map(config => {
144143 config.disabled = !canInput;
145− const hasReceivedError = getHasRecieved(config.name).matchWith({
144+ const hasReceivedError = getHasReceived(config.name).matchWith({
146145 Nothing: () => null, // loading
147146 Just: p => p.value && HAS_RECEIVED_TEXT,
148147 });
149148 config.error = hasReceivedError || errors[config.name];
150149 return config;
151150 }),
152− [inputConfigs, errors, canInput, getHasRecieved]
151+ [inputConfigs, errors, canInput, getHasReceived]
153152 );
154153
155154 // construct the state of the set of inputs we're rendering below
@@ -187,6 +186,27 @@ export default function InviteEmail() {
187186 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
188187 }
189188
189+ //TODO want to do this on-input, but that gets weird. see #188
190+ let errorCount = 0;
191+ await Promise.all(
192+ inputs.map(async input => {
193+ const email = input.data;
194+ const hasReceived = getHasReceived(email).matchWith({
195+ Nothing: () => false, // loading
196+ Just: p => p.value,
197+ });
198+ if (hasReceived) {
199+ errorCount++;
200+ addError({
201+ [input.name]: `${email} has already received an invite.`,
202+ });
203+ }
204+ })
205+ );
206+ if (errorCount > 0) {
207+ throw new Error(`Some of these already have a point!`);
208+ }
209+
190210 const nonce = await _web3.eth.getTransactionCount(_wallet.address);
191211 const chainId = await _web3.eth.net.getId();
192212 const planets = await azimuth.delegatedSending.getPlanetsToSend(
@@ -207,7 +227,6 @@ export default function InviteEmail() {
207227 );
208228 }
209229
210− let errorCount = 0;
211230 clearInvites();
212231 // NB(shrugs) - must be processed in serial because main thread, etc
213232 for (let i = 0; i < inputs.length; i++) {
@@ -227,12 +246,12 @@ export default function InviteEmail() {
227246 owner.keys.address
228247 );
229248 const signedTx = await signTransaction({
230− wallet,
249+ wallet: _wallet,
231250 walletType,
232251 walletHdPath,
233252 networkType,
234253 // TODO: ^ make a useTransactionSigner to encapsulate this logic
235− txn: Just(inviteTx),
254+ txn: inviteTx,
236255 gasPrice: GAS_PRICE_GWEI.toString(),
237256 gasLimit: GAS_LIMIT.toString(),
238257 nonce: nonce + i,
@@ -258,18 +277,19 @@ export default function InviteEmail() {
258277 );
259278 }
260279 }, [
261− point,
262280 contracts,
263281 web3,
264− addInvite,
282+ wallet,
283+ inputs,
284+ point,
265285 clearInvites,
286+ getHasReceived,
287+ addError,
266288 syncInvites,
267− inputs,
268− networkType,
269− wallet,
270289 walletType,
271290 walletHdPath,
272− addError,
291+ networkType,
292+ addInvite,
273293 ]);
274294
275295 const sendInvites = useCallback(async () => {
@@ -300,7 +320,7 @@ export default function InviteEmail() {
300320 try {
301321 const txHash = await sendSignedTransaction(
302322 _web3,
303− Just(invite.signedTx),
323+ invite.signedTx,
304324 tankWasUsed
305325 );
306326
@@ -317,12 +337,18 @@ export default function InviteEmail() {
317337 }
318338
319339 try {
320− await sendMail(invite.email, invite.ticket, invite.rawTx);
340+ const success = await sendMail(
341+ invite.email,
342+ invite.ticket,
343+ invite.rawTx
344+ );
345+ if (!success) throw new Error('Failed to send mail');
321346 } catch (error) {
322347 console.error(error);
323348 errorCount++;
349+ //TODO make sure this actually gets displayed
324350 addError({
325− [input.name]: `Mailing Failure for ${invite.email}`,
351+ [input.name]: `Mailing Failure for ${invite.email}, please send them this code manually: ${invite.ticket}`,
326352 });
327353 }
328354
src/views/Landing.js+14−5
@@ -1,14 +1,23 @@
11import React from 'react';
22import { H1, P } from 'indigo-react';
33
4−import { ROUTE_NAMES } from '../lib/routeNames';
54import { useHistory } from '../store/history';
5+import useImpliedPoint from 'lib/useImpliedPoint';
6+import useImpliedTicket from 'lib/useImpliedTicket';
67
78import View from 'components/View';
89import { ForwardButton } from 'components/Buttons';
910
1011function Landing() {
11− const history = useHistory();
12+ const { push, names } = useHistory();
13+
14+ const impliedPoint = useImpliedPoint();
15+ const impliedTicket = useImpliedTicket();
16+ if (impliedPoint) {
17+ push(names.LOGIN);
18+ } else if (impliedTicket) {
19+ push(names.ACTIVATE);
20+ }
1221
1322 return (
1423 <View>
@@ -22,21 +31,21 @@ function Landing() {
2231 <ForwardButton
2332 className="mt3"
2433 detail="If you were sent an Azimuth invite code."
25− onClick={() => history.push(ROUTE_NAMES.ACTIVATE)}>
34+ onClick={() => push(names.ACTIVATE)}>
2635 Activate a Point
2736 </ForwardButton>
2837
2938 <ForwardButton
3039 className="mt3"
3140 detail="Login to an activated point."
32− onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
41+ onClick={() => push(names.LOGIN)}>
3342 Login
3443 </ForwardButton>
3544
3645 <ForwardButton
3746 className="mt3"
3847 detail="View an Azimuth point without signing into a wallet."
39− onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
48+ onClick={() => push(names.VIEW_POINT)}>
4049 View a Point
4150 </ForwardButton>
4251 </View>
src/views/SentTransaction.js+1−1
@@ -125,7 +125,7 @@ function SentTransaction(props) {
125125
126126 const result = txnCursor.matchWith({
127127 Nothing: _ => {
128− throw BRIDGE_ERROR.MISSING_TXN;
128+ throw new Error(BRIDGE_ERROR.MISSING_TXN);
129129 },
130130 Just: res => res.value,
131131 });