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