on master help sign in
Merge pull request #193 from urbit/mino-txn-refactor

/lib/txn improvements

commit4af73604c9f36150afa00122f1348cca126604d6 authorFang <Fang-@users.noreply.github.com> dateTue 16 Jul 2019, 23:38 (7 years ago) parent81a795b81c364b
14 files changed+109−143
package-lock.json+13 package.json+1 src/components/old/StatelessTransaction.js+9−8 src/lib/constants.js+2−2 src/lib/error.js+11−11 src/lib/invite.js+2−12 src/lib/keys.js+2−2 src/lib/need.js+6−6 src/lib/tank.js+24−35 src/lib/txn.js+33−60 src/store/network.js+1−1 src/store/wallet.js+1−1 src/views/Invite/InviteEmail.js+3−4 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/components/old/StatelessTransaction.js+9−8
@@ -185,7 +185,7 @@ class StatelessTransaction extends React.Component {
185185 const stx = state.stx.matchWith({
186186 Just: tx => tx.value,
187187 Nothing: () => {
188− throw BRIDGE_ERROR.MISSING_TXN;
188+ throw new Error(BRIDGE_ERROR.MISSING_TXN);
189189 },
190190 });
191191
@@ -233,12 +233,7 @@ class StatelessTransaction extends React.Component {
233233 } else {
234234 this.setState({ txStatus: SUBMISSION_STATES.SENDING });
235235
236− sendSignedTransaction(
237− web3,
238− state.stx,
239− usedTank,
240− props.setTxnConfirmations
241− )
236+ sendSignedTransaction(web3, stx, usedTank, props.setTxnConfirmations)
242237 .then(txHash => {
243238 props.setTxnCursor(Just(Ok(txHash)));
244239
@@ -499,7 +494,13 @@ class StatelessTransaction extends React.Component {
499494 prop-size={'lg wide'}
500495 prop-color={signerButtonColor}
501496 onClick={() =>
502− 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+ })
503504 }>
504505 {'Sign Transaction'}
505506 </Button>
src/lib/constants.js+2−2
@@ -16,7 +16,7 @@ const SEED_ENTROPY_BITS = 128;
1616//TODO move into azimuth-js
1717const GAS_LIMITS = {
1818 SPAWN: 250000,
19− TRANSFER: 490000,
19+ TRANSFER: 550000,
2020 CONFIGURE_KEYS: 100000,
2121 SET_PROXY: 120000,
2222 //
@@ -28,7 +28,7 @@ const GAS_LIMITS = {
2828 //
2929 GIFT_PLANET: 400000, //NOTE low sample size
3030 //
31− DEFAULT: 500000,
31+ DEFAULT: 550000,
3232};
3333
3434// TODO: this is walletgen-ui specific, move into a wallet router later
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+2−12
@@ -9,7 +9,7 @@ import * as tank from './tank';
99import JSZip from 'jszip';
1010import saveAs from 'file-saver';
1111
12−import { sendSignedTransaction, hexify } from './txn';
12+import { sendAndAwaitAll, hexify } from './txn';
1313import { attemptNetworkSeedDerivation } from './keys';
1414import { addHexPrefix, WALLET_TYPES } from './wallet';
1515import { GAS_LIMITS } from './constants';
@@ -258,7 +258,7 @@ async function claimPointFromInvite({
258258
259259 progress(TRANSACTION_STATES.CLAIMING);
260260
261− await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
261+ await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
262262
263263 //
264264 // move leftover eth
@@ -291,16 +291,6 @@ async function claimPointFromInvite({
291291 progress(TRANSACTION_STATES.DONE);
292292}
293293
294−async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
295− await Promise.all(
296− signedTxs.map(tx => {
297− return new Promise((resolve, reject) => {
298− sendSignedTransaction(web3, tx, usedTank, resolve);
299− });
300− })
301− );
302−}
303−
304294export {
305295 generateWallet,
306296 downloadWallet,
src/lib/keys.js+2−2
@@ -89,7 +89,7 @@ const attemptNetworkSeedDerivation = async (next, args) => {
8989 const uwal = urbitWallet.matchWith({
9090 Just: uw => uw.value,
9191 Nothing: _ => {
92− throw BRIDGE_ERROR.MISSING_URBIT_WALLET;
92+ throw new Error(BRIDGE_ERROR.MISSING_URBIT_WALLET);
9393 },
9494 });
9595
@@ -100,7 +100,7 @@ const attemptNetworkSeedDerivation = async (next, args) => {
100100 const mnemonic = authMnemonic.matchWith({
101101 Just: mnem => mnem.value,
102102 Nothing: _ => {
103− throw BRIDGE_ERROR.MISSING_MNEMONIC;
103+ throw new Error(BRIDGE_ERROR.MISSING_MNEMONIC);
104104 },
105105 });
106106
src/lib/need.js+6−6
@@ -19,26 +19,26 @@ export const web3 = needBuilder(() => {
1919});
2020
2121export const contracts = needBuilder(() => {
22− throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS.message);
22+ throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS);
2323});
2424
2525export const wallet = needBuilder(() => {
26− throw new Error(BRIDGE_ERROR.MISSING_WALLET.message);
26+ throw new Error(BRIDGE_ERROR.MISSING_WALLET);
2727});
2828
2929export const addressFromWallet = obj => wallet(obj).address;
3030
3131export const point = needBuilder(() => {
32− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
32+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
3333});
3434
3535export const pointCache = needBuilder(() => {
36− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
36+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
3737});
3838
3939export const fromPointCache = (cache, point) => {
4040 if (!(point in cache)) {
41− throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
41+ throw new Error(BRIDGE_ERROR.MISSING_POINT);
4242 }
4343
4444 return cache[point];
@@ -49,7 +49,7 @@ export const keystore = obj => {
4949 return ks.value.matchWith({
5050 Ok: result => result.value,
5151 Error: _ => {
52− throw BRIDGE_ERROR.MISSING_KEYSTORE;
52+ throw new Error(BRIDGE_ERROR.MISSING_KEYSTORE);
5353 },
5454 });
5555};
src/lib/tank.js+24−35
@@ -1,6 +1,7 @@
11// /lib/tank: functions for funding transactions
22
3−import { waitForTransactionConfirm } from './txn';
3+import * as retry from 'async-retry';
4+import { RETRY_OPTIONS, waitForTransactionConfirm } from './txn';
45
56//NOTE if accessing this in a localhost configuration fails with "CORS request
67// did not succeed", you might need to visit localhost:3001 or whatever
@@ -92,41 +93,29 @@ const ensureFundsFor = async (
9293 return false;
9394};
9495
95−// resolves when address has at least minBalance
96−//
97−//TODO should maybe do a "we got it" callback also, so clients can hide msg?
98−async function waitForBalance(
99− web3,
100− address,
101− minBalance,
102− askForFunding,
103− gotFunding
104−) {
105− console.log('tank: awaiting balance', address, minBalance);
106− return new Promise((resolve, reject) => {
107− let oldBalance = null;
108− const checkForBalance = async () => {
109− const balance = await web3.eth.getBalance(address);
110− if (balance >= minBalance) {
111− // if we ever asked for funding, retract that request now
112− if (gotFunding && oldBalance !== null) {
113− gotFunding();
114− }
115− resolve();
116− } else {
117− if (balance !== oldBalance) {
118− // TODO: minBalance is a `number` type
119− // but we want to display ETH, which will never accept a number
120− // but instead wants string or BN/BigNumber.
121− // this will be heavily refactored to correctly do BN math, but until
122− // then we'll manually convert this number to an integer string
123− askForFunding(address, minBalance.toFixed(), balance);
124− }
125− setTimeout(checkForBalance, 13000);
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();
126105 }
127− };
128− checkForBalance();
129− });
106+ return;
107+ } else {
108+ if (balance !== oldBalance) {
109+ // TODO: minBalance is a `number` type
110+ // but we want to display ETH, which will never accept a number
111+ // but instead wants string or BN/BigNumber.
112+ // this will be heavily refactored to correctly do BN math, but until
113+ // then we'll manually convert this number to an integer string
114+ askForFunding(address, minBalance.toFixed(), balance);
115+ }
116+ throw new Error('retrying');
117+ }
118+ }, RETRY_OPTIONS);
130119}
131120
132121export { remainingTransactions, fundTransactions, ensureFundsFor };
src/lib/txn.js+33−60
@@ -2,8 +2,8 @@ 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 * as retry from 'async-retry';
56
6−import { BRIDGE_ERROR } from './error';
77import { NETWORK_TYPES } from './network';
88import { ledgerSignTransaction } from './ledger';
99import { trezorSignTransaction } from './trezor';
@@ -12,6 +12,13 @@ import { WALLET_TYPES, addHexPrefix } from './wallet';
1212const CHECK_BLOCK_EVERY_MS =
1313 process.env.NODE_ENV === 'development' ? 1000 : 5000;
1414
15+const RETRY_OPTIONS = {
16+ retries: 99999,
17+ factor: 1,
18+ minTimeout: CHECK_BLOCK_EVERY_MS,
19+ randomize: false,
20+};
21+
1522const TXN_PURPOSE = {
1623 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
1724 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
@@ -56,6 +63,7 @@ const signTransaction = async config => {
5663 gasLimit,
5764 } = config;
5865
66+ //TODO require these in txn object
5967 nonce = toHex(nonce);
6068 chainId = toHex(chainId);
6169 gasPrice = toHex(toWei(gasPrice, 'gwei'));
@@ -106,45 +114,27 @@ const signTransaction = async config => {
106114 ? Object.assign(txParams, eip155Params)
107115 : txParams;
108116
109− const wal = wallet.matchWith({
110− Just: w => w.value,
111− Nothing: () => {
112− throw BRIDGE_ERROR.MISSING_WALLET;
113− },
114− });
115−
116− const sec = wal.privateKey;
117−
118− const utx = txn.matchWith({
119− Just: tx => Object.assign(tx.value, signingParams),
120− Nothing: () => {
121− throw BRIDGE_ERROR.MISSING_TXN;
122− },
123− });
117+ const utx = Object.assign(txn, signingParams);
124118
125119 const stx = new Tx(utx);
126120
121+ //TODO should try-catch and display error message to user,
122+ // ie ledger's "pls enable contract data"
123+ // needs to maybe happen at call-site though
127124 if (walletType === WALLET_TYPES.LEDGER) {
128125 await ledgerSignTransaction(stx, walletHdPath);
129126 } else if (walletType === WALLET_TYPES.TREZOR) {
130127 await trezorSignTransaction(stx, walletHdPath);
131128 } else {
132− stx.sign(sec);
129+ stx.sign(wallet.privateKey);
133130 }
134131
135132 setStx(Just(stx));
136133 return stx;
137134};
138135
139−const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
140− const txn = stx.matchWith({
141− Just: tx => tx.value,
142− Nothing: () => {
143− throw BRIDGE_ERROR.MISSING_TXN;
144− },
145− });
146−
147− const rawTx = hexify(txn.serialize());
136+const sendSignedTransaction = (web3, stx, doubtNonceError) => {
137+ const rawTx = hexify(stx.serialize());
148138
149139 return new Promise(async (resolve, reject) => {
150140 web3.eth
@@ -152,14 +142,6 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
152142 .on('transactionHash', hash => {
153143 resolve(hash);
154144 })
155− //TODO do we also reach this if network is slow? web3 only tries a set
156− // amount of times... should we instead do waitForTransactionConfirm
157− // in on-transactionHash? we don't care (much) about additional
158− // confirms anyway.
159− .on('confirmation', (confirmationNum, txn) => {
160− confirmationCb(txn.transactionHash, confirmationNum + 1);
161− resolve(txn.transactionHash);
162− })
163145 .on('error', err => {
164146 // if there's a nonce error, but we used the gas tank, it's likely
165147 // that it's because the tank already submitted our transaction.
@@ -176,16 +158,7 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
176158 'tx send error likely from gas tank submission, ignoring'
177159 );
178160 const txHash = web3.utils.keccak256(rawTx);
179− //TODO can we do does-exists check first?
180− //TODO max wait time before assume fail?
181− waitForTransactionConfirm(web3, txHash).then(res => {
182− if (res) {
183− resolve(txHash);
184− confirmationCb(txHash, 1);
185− } else {
186− reject('Unexpected tx failure');
187− }
188− });
161+ resolve(txHash);
189162 } else {
190163 reject(err.message || 'Transaction sending failed!');
191164 }
@@ -195,23 +168,22 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
195168
196169// returns a Promise<bool>, where the bool indicates tx success/failure
197170const waitForTransactionConfirm = (web3, txHash) => {
198− return new Promise((resolve, reject) => {
199− const checkForConfirm = async () => {
200− console.log('checking for confirm', txHash);
201− const receipt = await web3.eth.getTransactionReceipt(txHash);
202− console.log('tried, got', receipt);
203− let confirmed = receipt !== null;
204− if (confirmed) resolve(receipt.status === true);
205− else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
206− };
207− checkForConfirm();
208− });
171+ return retry(async (bail, n) => {
172+ const receipt = await web3.eth.getTransactionReceipt(txHash);
173+ const confirmed = receipt !== null;
174+ if (confirmed) return receipt.status === true;
175+ else throw new Error('retrying');
176+ }, RETRY_OPTIONS);
209177};
210178
211−const isTransactionConfirmed = async (web3, txHash) => {
212− const receipt = await web3.eth.getTransactionReceipt(txHash);
213− console.log('got confirm state', receipt !== null, receipt.confirmations);
214− return receipt !== null;
179+// returns a Promise that resolves when all stxs have been sent & confirmed
180+const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
181+ return Promise.all(
182+ stxs.map(async tx => {
183+ const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
184+ await waitForTransactionConfirm(web3, txHash);
185+ })
186+ );
215187};
216188
217189const hexify = val => addHexPrefix(val.toString('hex'));
@@ -245,10 +217,11 @@ const canDecodePatp = p => {
245217};
246218
247219export {
220+ RETRY_OPTIONS,
248221 signTransaction,
249222 sendSignedTransaction,
250223 waitForTransactionConfirm,
251− isTransactionConfirmed,
224+ sendAndAwaitAll,
252225 TXN_PURPOSE,
253226 getTxnInfo,
254227 renderTxnPurpose,
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/Invite/InviteEmail.js+3−4
@@ -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,
@@ -247,12 +246,12 @@ export default function InviteEmail() {
247246 owner.keys.address
248247 );
249248 const signedTx = await signTransaction({
250− wallet,
249+ wallet: _wallet,
251250 walletType,
252251 walletHdPath,
253252 networkType,
254253 // TODO: ^ make a useTransactionSigner to encapsulate this logic
255− txn: Just(inviteTx),
254+ txn: inviteTx,
256255 gasPrice: GAS_PRICE_GWEI.toString(),
257256 gasLimit: GAS_LIMIT.toString(),
258257 nonce: nonce + i,
@@ -320,7 +319,7 @@ export default function InviteEmail() {
320319 try {
321320 const txHash = await sendSignedTransaction(
322321 _web3,
323− Just(invite.signedTx),
322+ invite.signedTx,
324323 tankWasUsed,
325324 () => {}
326325 );
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 });