on e7ae14d help sign in
bridge/src/lib/txn.js
203 lines 5.8 kB history raw
1import * as ob from 'urbit-ob';
2import Tx from 'ethereumjs-tx';
3import { toWei, fromWei, toHex } from 'web3-utils';
4import retry from 'async-retry';
6import { NETWORK_TYPES } from './network';
7import { ledgerSignTransaction } from './ledger';
8import { trezorSignTransaction } from './trezor';
9import { WALLET_TYPES, addHexPrefix } from './wallet';
10import { CHECK_BLOCK_EVERY_MS } from './constants';
12const RETRY_OPTIONS = {
13 retries: 99999,
14 factor: 1,
15 minTimeout: CHECK_BLOCK_EVERY_MS,
16 randomize: false,
17};
19const signTransaction = async config => {
20 let {
21 wallet,
22 walletType,
23 walletHdPath,
24 networkType,
25 txn,
26 nonce,
27 chainId,
28 gasPrice,
29 gasLimit,
30 } = config;
32 //TODO require these in txn object
33 nonce = toHex(nonce);
34 chainId = toHex(chainId);
35 gasPrice = toHex(toWei(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 {
95 stx.sign(wallet.privateKey);
96 }
98 return stx;
99};
101const sendSignedTransaction = (web3, stx, doubtNonceError) => {
102 const rawTx = hexify(stx.serialize());
104 return new Promise(async (resolve, reject) => {
105 web3.eth
106 .sendSignedTransaction(rawTx)
107 .on('transactionHash', hash => {
108 resolve(hash);
109 })
110 .on('error', err => {
111 // if there's a nonce error, but we used the gas tank, it's likely
112 // that it's because the tank already submitted our transaction.
113 // we just wait for first confirmation here.
114 console.error(err);
115 const isKnownError = (err.message || '').includes(
116 'known transaction: '
117 );
118 const isNonceError = (err.message || '').includes(
119 "the tx doesn't have the correct nonce."
120 );
121 if (isKnownError || (doubtNonceError && isNonceError)) {
122 console.log(
123 'tx send error likely from gas tank submission, ignoring'
124 );
125 const txHash = web3.utils.keccak256(rawTx);
126 resolve(txHash);
127 } else {
128 reject(err.message || 'Transaction sending failed!');
129 }
130 });
131 });
132};
134// returns a Promise<bool>, where the bool indicates tx success/failure
135const waitForTransactionConfirm = (web3, txHash) => {
136 return retry(async (bail, n) => {
137 const receipt = await web3.eth.getTransactionReceipt(txHash);
138 const confirmed = receipt !== null;
139 if (confirmed) return receipt.status === true;
140 else throw new Error('retrying');
141 }, RETRY_OPTIONS);
142};
144// returns a Promise that resolves when all stxs have been sent & confirmed
145const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
146 return Promise.all(
147 stxs.map(async tx => {
148 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
149 await waitForTransactionConfirm(web3, txHash);
150 })
151 );
152};
154const sendTransactionsAndAwaitConfirm = async (web3, signedTxs, usedTank) =>
155 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
157const hexify = val => addHexPrefix(val.toString('hex'));
159const renderSignedTx = stx => ({
160 messageHash: hexify(stx.hash()),
161 v: hexify(stx.v),
162 s: hexify(stx.s),
163 r: hexify(stx.r),
164 rawTransaction: hexify(stx.serialize()),
165});
167const getTxnInfo = async (web3, addr) => {
168 let nonce = await web3.eth.getTransactionCount(addr);
169 let chainId = await web3.eth.net.getId();
170 let gasPrice = await web3.eth.getGasPrice();
172 return {
173 nonce: nonce,
174 chainId: chainId,
175 gasPrice: fromWei(gasPrice, 'gwei'),
176 };
177};
179// TODO(shrugs): deprecate, unifiy with other callsites
180const canDecodePatp = p => {
181 try {
182 ob.patp2dec(p);
183 return true;
184 } catch (_) {
185 return false;
186 }
187};
189export {
190 RETRY_OPTIONS,
191 signTransaction,
192 sendSignedTransaction,
193 waitForTransactionConfirm,
194 sendTransactionsAndAwaitConfirm,
195 sendAndAwaitAll,
196 getTxnInfo,
197 hexify,
198 renderSignedTx,
199 toHex,
200 toWei,
201 fromWei,
202 canDecodePatp,
203};