on 64dc313 help sign in
bridge/src/lib/txn.js
204 lines 5.8 kB history raw
1import Tx from 'ethereumjs-tx';
2import { toWei, fromWei, toHex } from 'web3-utils';
3import retry from 'async-retry';
5import { NETWORK_TYPES } from './network';
6import { ledgerSignTransaction } from './ledger';
7import { trezorSignTransaction } from './trezor';
8import { WALLET_TYPES, addHexPrefix } from './wallet';
9import { CHECK_BLOCK_EVERY_MS } from './constants';
10import patp2dec from './patp2dec';
12const RETRY_OPTIONS = {
13 retries: 99999,
14 factor: 1,
15 minTimeout: CHECK_BLOCK_EVERY_MS,
16 randomize: false,
17};
19const signTransaction = async ({
20 wallet,
21 walletType,
22 walletHdPath,
23 networkType,
24 txn,
25 nonce, // number
26 chainId, // number
27 gasPrice, // string, in gwei
28 gasLimit, // string | number
29}) => {
30 // TODO: require these in txn object
31 nonce = toHex(nonce);
32 chainId = toHex(chainId);
33 gasPrice = toHex(toWei(gasPrice, 'gwei'));
34 gasLimit = toHex(gasLimit);
36 const txParams = { nonce, chainId, gasPrice, gasLimit };
38 // NB (jtobin)
39 //
40 // Ledger does not seem to handle EIP-155 automatically. When using a Ledger,
41 // if the block number is at least FORK_BLKNUM = 2675000, one needs to
42 // pre-set the ECDSA signature parameters with r = 0, s = 0, and v = chainId
43 // prior to signing.
44 //
45 // The easiest way to handle this is to just branch on the network, since
46 // mainnet and Ropsten have obviously passed FORK_BLKNUM. This is somewhat
47 // awkward when dealing with offline transactions, since we might want to
48 // test them on a local network as well.
49 //
50 // The best thing to do is probably to add an 'advanced' tab to offline
51 // transaction generation where one can disable the defaulted-on EIP-155
52 // settings in this case. This is pretty low-priority, but is a
53 // comprehensive solution.
54 //
55 // See:
56 //
57 // See https://github.com/LedgerHQ/ledgerjs/issues/43#issuecomment-366984725
58 //
59 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
61 const eip155Params = {
62 r: '0x00',
63 s: '0x00',
64 v: chainId,
65 };
67 const defaultEip155Networks = [
68 NETWORK_TYPES.MAINNET,
69 NETWORK_TYPES.ROPSTEN,
70 NETWORK_TYPES.OFFLINE,
71 ];
73 const needEip155Params =
74 walletType === WALLET_TYPES.LEDGER &&
75 defaultEip155Networks.includes(networkType);
77 const signingParams = needEip155Params
78 ? Object.assign(txParams, eip155Params)
79 : txParams;
81 const utx = Object.assign(txn, signingParams);
83 const stx = new Tx(utx);
85 //TODO should try-catch and display error message to user,
86 // ie ledger's "pls enable contract data"
87 // needs to maybe happen at call-site though
88 if (walletType === WALLET_TYPES.LEDGER) {
89 await ledgerSignTransaction(stx, walletHdPath);
90 } else if (walletType === WALLET_TYPES.TREZOR) {
91 await trezorSignTransaction(stx, walletHdPath);
92 } else {
93 stx.sign(wallet.privateKey);
94 }
96 return stx;
97};
99const sendSignedTransaction = (web3, stx, doubtNonceError) => {
100 const rawTx = hexify(stx.serialize());
102 return new Promise(async (resolve, reject) => {
103 web3.eth
104 .sendSignedTransaction(rawTx)
105 .on('transactionHash', hash => {
106 resolve(hash);
107 })
108 .on('error', err => {
109 // if there's a nonce error, but we used the gas tank, it's likely
110 // that it's because the tank already submitted our transaction.
111 // we just wait for first confirmation here.
112 console.error(err);
113 const isKnownError = (err.message || '').includes(
114 'known transaction: '
115 );
116 const isNonceError = (err.message || '').includes(
117 "the tx doesn't have the correct nonce."
118 );
119 if (isKnownError || (doubtNonceError && isNonceError)) {
120 console.log(
121 'tx send error likely from gas tank submission, ignoring'
122 );
123 const txHash = web3.utils.keccak256(rawTx);
124 resolve(txHash);
125 } else {
126 reject(err.message || 'Transaction sending failed!');
127 }
128 });
129 });
130};
132// returns a Promise<void>, throwing on tx failure
133const waitForTransactionConfirm = (web3, txHash) => {
134 return retry(async (bail, n) => {
135 const receipt = await web3.eth.getTransactionReceipt(txHash);
136 const confirmed = receipt !== null;
137 if (!confirmed) {
138 throw new Error('Transaction not confirmed.');
139 }
141 const success = receipt.status === true;
142 if (!success) {
143 return bail(new Error('Transaction failed.'));
144 }
145 }, RETRY_OPTIONS);
146};
148// returns a Promise that resolves when all stxs have been sent & confirmed
149const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
150 return Promise.all(
151 stxs.map(async tx => {
152 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
153 await waitForTransactionConfirm(web3, txHash);
154 })
155 );
156};
158const sendTransactionsAndAwaitConfirm = async (web3, signedTxs, usedTank) =>
159 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
161const hexify = val => addHexPrefix(val.toString('hex'));
163const renderSignedTx = stx => ({
164 messageHash: hexify(stx.hash()),
165 v: hexify(stx.v),
166 s: hexify(stx.s),
167 r: hexify(stx.r),
168 rawTransaction: hexify(stx.serialize()),
169});
171const getTxnInfo = async (web3, addr) => {
172 let nonce = await web3.eth.getTransactionCount(addr);
173 let chainId = await web3.eth.net.getId();
174 let gasPrice = await web3.eth.getGasPrice();
176 return {
177 nonce: nonce,
178 chainId: chainId,
179 gasPrice: fromWei(gasPrice, 'gwei'),
180 };
181};
183// TODO(shrugs): deprecate, unifiy with other callsites
184const canDecodePatp = p => {
185 try {
186 patp2dec(p);
187 return true;
188 } catch (_) {
189 return false;
190 }
191};
193export {
194 RETRY_OPTIONS,
195 signTransaction,
196 sendSignedTransaction,
197 waitForTransactionConfirm,
198 sendTransactionsAndAwaitConfirm,
199 sendAndAwaitAll,
200 getTxnInfo,
201 hexify,
202 renderSignedTx,
203 canDecodePatp,
204};