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