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