on d1230bb help sign in
bridge/src/lib/txn.js
234 lines 6.6 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};
107const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
108 const txn = stx.matchWith({
109 Just: tx => tx.value,
110 Nothing: () => {
111 throw BRIDGE_ERROR.MISSING_TXN;
112 },
113 });
115 const rawTx = hexify(txn.serialize());
117 return new Promise(async (resolve, reject) => {
118 web3.eth
119 .sendSignedTransaction(rawTx)
120 .on('transactionHash', hash => {
121 resolve(hash);
122 })
123 //TODO do we also reach this if network is slow? web3 only tries a set
124 // amount of times... should we instead do waitForTransactionConfirm
125 // in on-transactionHash? we don't care (much) about additional
126 // confirms anyway.
127 .on('confirmation', (confirmationNum, txn) => {
128 confirmationCb(txn.transactionHash, confirmationNum + 1);
129 resolve(txn.transactionHash);
130 })
131 .on('error', err => {
132 // if there's a nonce error, but we used the gas tank, it's likely
133 // that it's because the tank already submitted our transaction.
134 // we just wait for first confirmation here.
135 console.error(err);
136 if (
137 doubtNonceError &&
138 (err.message || '').includes("the tx doesn't have the correct nonce.")
139 ) {
140 console.log('nonce error, likely from gas tank submission, ignoring');
141 const txHash = web3.utils.keccak256(rawTx);
142 //TODO can we do does-exists check first?
143 //TODO max wait time before assume fail?
144 waitForTransactionConfirm(web3, txHash).then(res => {
145 if (res) {
146 resolve(txHash);
147 confirmationCb(txHash, 1);
148 } else {
149 reject('Unexpected tx failure');
150 }
151 });
152 } else {
153 reject(err.message);
154 }
155 });
156 });
157};
159// returns a Promise<bool>, where the bool indicates tx success/failure
160const waitForTransactionConfirm = (web3, txHash) => {
161 return new Promise((resolve, reject) => {
162 const checkForConfirm = async () => {
163 console.log('checking for confirm', txHash);
164 const receipt = await web3.eth.getTransactionReceipt(txHash);
165 console.log('tried, got', receipt);
166 let confirmed = receipt !== null;
167 if (confirmed) resolve(receipt.status === true);
168 else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
169 };
170 checkForConfirm();
171 });
172};
174const isTransactionConfirmed = async (web3, txHash) => {
175 const receipt = await web3.eth.getTransactionReceipt(txHash);
176 console.log('got confirm state', receipt !== null, receipt.confirmations);
177 return receipt !== null;
178};
180async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
181 await Promise.all(
182 signedTxs.map(tx => {
183 return new Promise((resolve, reject) => {
184 sendSignedTransaction(web3, tx, usedTank, resolve);
185 });
186 })
187 );
190const hexify = val => addHexPrefix(val.toString('hex'));
192const renderSignedTx = stx => ({
193 messageHash: hexify(stx.hash()),
194 v: hexify(stx.v),
195 s: hexify(stx.s),
196 r: hexify(stx.r),
197 rawTransaction: hexify(stx.serialize()),
198});
200const getTxnInfo = async (web3, addr) => {
201 let nonce = await web3.eth.getTransactionCount(addr);
202 let chainId = await web3.eth.net.getId();
203 let gasPrice = await web3.eth.getGasPrice();
204 return {
205 nonce: nonce,
206 chainId: chainId,
207 gasPrice: fromWei(gasPrice, 'gwei'),
208 };
209};
211// TODO(shrugs): deprecate, unifiy with other callsites
212const canDecodePatp = p => {
213 try {
214 ob.patp2dec(p);
215 return true;
216 } catch (_) {
217 return false;
218 }
219};
221export {
222 signTransaction,
223 sendSignedTransaction,
224 waitForTransactionConfirm,
225 sendAndAwaitConfirm,
226 isTransactionConfirmed,
227 getTxnInfo,
228 hexify,
229 renderSignedTx,
230 toHex,
231 toWei,
232 fromWei,
233 canDecodePatp,
234};