on dee2cd5 help sign in
bridge/src/lib/txn.js
232 lines 6.7 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';
6import { BRIDGE_ERROR } from './error';
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 signTransaction = async config => {
14 let {
15 wallet,
16 walletType,
17 walletHdPath,
18 networkType,
19 txn,
20 setStx,
21 nonce,
22 chainId,
23 gasPrice,
24 gasLimit,
25 } = config;
27 nonce = toHex(nonce);
28 chainId = toHex(chainId);
29 gasPrice = toHex(toWei(gasPrice, 'gwei'));
30 gasLimit = toHex(gasLimit);
32 const txParams = { nonce, chainId, gasPrice, gasLimit };
34 // NB (jtobin)
35 //
36 // Ledger does not seem to handle EIP-155 automatically. When using a Ledger,
37 // if the block number is at least FORK_BLKNUM = 2675000, one needs to
38 // pre-set the ECDSA signature parameters with r = 0, s = 0, and v = chainId
39 // prior to signing.
40 //
41 // The easiest way to handle this is to just branch on the network, since
42 // mainnet and Ropsten have obviously passed FORK_BLKNUM. This is somewhat
43 // awkward when dealing with offline transactions, since we might want to
44 // test them on a local network as well.
45 //
46 // The best thing to do is probably to add an 'advanced' tab to offline
47 // transaction generation where one can disable the defaulted-on EIP-155
48 // settings in this case. This is pretty low-priority, but is a
49 // comprehensive solution.
50 //
51 // See:
52 //
53 // See https://github.com/LedgerHQ/ledgerjs/issues/43#issuecomment-366984725
54 //
55 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
57 const eip155Params = {
58 r: '0x00',
59 s: '0x00',
60 v: chainId,
61 };
63 const defaultEip155Networks = [
64 NETWORK_TYPES.MAINNET,
65 NETWORK_TYPES.ROPSTEN,
66 NETWORK_TYPES.OFFLINE,
67 ];
69 const needEip155Params =
70 walletType === WALLET_TYPES.LEDGER &&
71 defaultEip155Networks.includes(networkType);
73 const signingParams = needEip155Params
74 ? Object.assign(txParams, eip155Params)
75 : txParams;
77 const wal = wallet.matchWith({
78 Just: w => w.value,
79 Nothing: () => {
80 throw BRIDGE_ERROR.MISSING_WALLET;
81 },
82 });
84 const sec = wal.privateKey;
86 const utx = txn.matchWith({
87 Just: tx => Object.assign(tx.value, signingParams),
88 Nothing: () => {
89 throw BRIDGE_ERROR.MISSING_TXN;
90 },
91 });
93 const stx = new Tx(utx);
95 if (walletType === WALLET_TYPES.LEDGER) {
96 await ledgerSignTransaction(stx, walletHdPath);
97 } else if (walletType === WALLET_TYPES.TREZOR) {
98 await trezorSignTransaction(stx, walletHdPath);
99 } else {
100 stx.sign(sec);
101 }
103 setStx(Just(stx));
104 return stx;
105};
107// TODO(shrugs): refactor the hell out of all of these functions
108// but especially this one
109const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
110 const txn = stx.matchWith({
111 Just: tx => tx.value,
112 Nothing: () => {
113 throw BRIDGE_ERROR.MISSING_TXN;
114 },
115 });
117 const rawTx = hexify(txn.serialize());
119 return new Promise(async (resolve, reject) => {
120 web3.eth
121 .sendSignedTransaction(rawTx)
122 .on('transactionHash', hash => {
123 resolve(hash);
124 })
125 //TODO do we also reach this if network is slow? web3 only tries a set
126 // amount of times... should we instead do waitForTransactionConfirm
127 // in on-transactionHash? we don't care (much) about additional
128 // confirms anyway.
129 .on('confirmation', (confirmationNum, txn) => {
130 if (confirmationCb) {
131 confirmationCb(txn.transactionHash, confirmationNum + 1);
132 }
133 resolve(txn.transactionHash);
134 })
135 .on('error', err => {
136 // if there's a nonce error, but we used the gas tank, it's likely
137 // that it's because the tank already submitted our transaction.
138 // we just wait for first confirmation here.
139 console.error(err);
140 if (
141 doubtNonceError &&
142 (err.message || '').includes("the tx doesn't have the correct nonce.")
143 ) {
144 console.log('nonce error, likely from gas tank submission, ignoring');
145 const txHash = web3.utils.keccak256(rawTx);
146 //TODO can we do does-exists check first?
147 //TODO max wait time before assume fail?
148 waitForTransactionConfirm(web3, txHash).then(res => {
149 if (res) {
150 resolve(txHash);
151 confirmationCb(txHash, 1);
152 } else {
153 reject('Unexpected tx failure');
154 }
155 });
156 } else {
157 reject(err.message);
158 }
159 });
160 });
161};
163// returns a Promise<bool>, where the bool indicates tx success/failure
164const waitForTransactionConfirm = (web3, txHash) => {
165 return new Promise((resolve, reject) => {
166 const checkForConfirm = async () => {
167 console.log('checking for confirm', txHash);
168 const receipt = await web3.eth.getTransactionReceipt(txHash);
169 console.log('tried, got', receipt);
170 let confirmed = receipt !== null;
171 if (confirmed) resolve(receipt.status === true);
172 else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
173 };
174 checkForConfirm();
175 });
176};
178const isTransactionConfirmed = async (web3, txHash) => {
179 const receipt = await web3.eth.getTransactionReceipt(txHash);
180 console.log('got confirm state', receipt !== null, receipt.confirmations);
181 return receipt !== null;
182};
184const sendTransactionsAndAwaitConfirm = async (web3, signedTxs, usedTank) =>
185 Promise.all(signedTxs.map(tx => sendSignedTransaction(web3, tx, usedTank)));
187const hexify = val => addHexPrefix(val.toString('hex'));
189const renderSignedTx = stx => ({
190 messageHash: hexify(stx.hash()),
191 v: hexify(stx.v),
192 s: hexify(stx.s),
193 r: hexify(stx.r),
194 rawTransaction: hexify(stx.serialize()),
195});
197const getTxnInfo = async (web3, addr) => {
198 let nonce = await web3.eth.getTransactionCount(addr);
199 let chainId = await web3.eth.net.getId();
200 let gasPrice = await web3.eth.getGasPrice();
202 return {
203 nonce: nonce,
204 chainId: chainId,
205 gasPrice: fromWei(gasPrice, 'gwei'),
206 };
207};
209// TODO(shrugs): deprecate, unifiy with other callsites
210const canDecodePatp = p => {
211 try {
212 ob.patp2dec(p);
213 return true;
214 } catch (_) {
215 return false;
216 }
217};
219export {
220 signTransaction,
221 sendSignedTransaction,
222 waitForTransactionConfirm,
223 sendTransactionsAndAwaitConfirm,
224 isTransactionConfirmed,
225 getTxnInfo,
226 hexify,
227 renderSignedTx,
228 toHex,
229 toWei,
230 fromWei,
231 canDecodePatp,
232};