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