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