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