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