on d82c74c help sign in
bridge/src/lib/txn.js
254 lines 6.9 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 * as retry from 'async-retry';
7import { BRIDGE_ERROR } from './error';
8import { NETWORK_TYPES } from './network';
9import { ledgerSignTransaction } from './ledger';
10import { trezorSignTransaction } from './trezor';
11import { WALLET_TYPES, addHexPrefix } from './wallet';
13const CHECK_BLOCK_EVERY_MS =
14 process.env.NODE_ENV === 'development' ? 1000 : 5000;
16const RETRY_OPTIONS = {
17 retries: 99999,
18 factor: 1,
19 minTimeout: CHECK_BLOCK_EVERY_MS,
20 randomize: false,
21};
23const TXN_PURPOSE = {
24 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
25 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
26 SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
27 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
28 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
29 SET_KEYS: Symbol('SET_KEYS'),
30 TRANSFER: Symbol('TRANSFER'),
31 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
32};
34const renderTxnPurpose = purpose =>
35 purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
36 ? 'set this management proxy'
37 : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
38 ? 'set this spawn proxy'
39 : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
40 ? 'set this transfer proxy'
41 : purpose === TXN_PURPOSE.CREATE_GALAXY
42 ? 'create this galaxy'
43 : purpose === TXN_PURPOSE.ISSUE_CHILD
44 ? 'issue this point'
45 : purpose === TXN_PURPOSE.SET_KEYS
46 ? 'set these network keys'
47 : purpose === TXN_PURPOSE.TRANSFER
48 ? 'transfer this point'
49 : purpose === TXN_PURPOSE.CANCEL_TRANSFER
50 ? 'cancel this transfer'
51 : 'perform this transaction';
53const signTransaction = async config => {
54 let {
55 wallet,
56 walletType,
57 walletHdPath,
58 networkType,
59 txn,
60 setStx,
61 nonce,
62 chainId,
63 gasPrice,
64 gasLimit,
65 } = config;
67 nonce = toHex(nonce);
68 chainId = toHex(chainId);
69 gasPrice = toHex(toWei(gasPrice, 'gwei'));
70 gasLimit = toHex(gasLimit);
72 const txParams = { nonce, chainId, gasPrice, gasLimit };
74 // NB (jtobin)
75 //
76 // Ledger does not seem to handle EIP-155 automatically. When using a Ledger,
77 // if the block number is at least FORK_BLKNUM = 2675000, one needs to
78 // pre-set the ECDSA signature parameters with r = 0, s = 0, and v = chainId
79 // prior to signing.
80 //
81 // The easiest way to handle this is to just branch on the network, since
82 // mainnet and Ropsten have obviously passed FORK_BLKNUM. This is somewhat
83 // awkward when dealing with offline transactions, since we might want to
84 // test them on a local network as well.
85 //
86 // The best thing to do is probably to add an 'advanced' tab to offline
87 // transaction generation where one can disable the defaulted-on EIP-155
88 // settings in this case. This is pretty low-priority, but is a
89 // comprehensive solution.
90 //
91 // See:
92 //
93 // See https://github.com/LedgerHQ/ledgerjs/issues/43#issuecomment-366984725
94 //
95 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
97 const eip155Params = {
98 r: '0x00',
99 s: '0x00',
100 v: chainId,
101 };
103 const defaultEip155Networks = [
104 NETWORK_TYPES.MAINNET,
105 NETWORK_TYPES.ROPSTEN,
106 NETWORK_TYPES.OFFLINE,
107 ];
109 const needEip155Params =
110 walletType === WALLET_TYPES.LEDGER &&
111 defaultEip155Networks.includes(networkType);
113 const signingParams = needEip155Params
114 ? Object.assign(txParams, eip155Params)
115 : txParams;
117 const wal = wallet.matchWith({
118 Just: w => w.value,
119 Nothing: () => {
120 throw BRIDGE_ERROR.MISSING_WALLET;
121 },
122 });
124 const sec = wal.privateKey;
126 const utx = txn.matchWith({
127 Just: tx => Object.assign(tx.value, signingParams),
128 Nothing: () => {
129 throw BRIDGE_ERROR.MISSING_TXN;
130 },
131 });
133 const stx = new Tx(utx);
135 if (walletType === WALLET_TYPES.LEDGER) {
136 await ledgerSignTransaction(stx, walletHdPath);
137 } else if (walletType === WALLET_TYPES.TREZOR) {
138 await trezorSignTransaction(stx, walletHdPath);
139 } else {
140 stx.sign(sec);
141 }
143 setStx(Just(stx));
144 return stx;
145};
147const sendSignedTransaction = (web3, stx, doubtNonceError) => {
148 const txn = stx.matchWith({
149 Just: tx => tx.value,
150 Nothing: () => {
151 throw BRIDGE_ERROR.MISSING_TXN;
152 },
153 });
155 const rawTx = hexify(txn.serialize());
157 return new Promise(async (resolve, reject) => {
158 web3.eth
159 .sendSignedTransaction(rawTx)
160 .on('transactionHash', hash => {
161 resolve(hash);
162 })
163 .on('error', err => {
164 // if there's a nonce error, but we used the gas tank, it's likely
165 // that it's because the tank already submitted our transaction.
166 // we just wait for first confirmation here.
167 console.error(err);
168 const isKnownError = (err.message || '').includes(
169 'known transaction: '
170 );
171 const isNonceError = (err.message || '').includes(
172 "the tx doesn't have the correct nonce."
173 );
174 if (isKnownError || (doubtNonceError && isNonceError)) {
175 console.log(
176 'tx send error likely from gas tank submission, ignoring'
177 );
178 const txHash = web3.utils.keccak256(rawTx);
179 resolve(txHash);
180 } else {
181 reject(err.message || 'Transaction sending failed!');
182 }
183 });
184 });
185};
187// returns a Promise<bool>, where the bool indicates tx success/failure
188const waitForTransactionConfirm = (web3, txHash) => {
189 return retry(async (bail, n) => {
190 const receipt = await web3.eth.getTransactionReceipt(txHash);
191 let confirmed = receipt !== null;
192 if (confirmed) return receipt.status === true;
193 else throw new Error('retrying');
194 }, RETRY_OPTIONS);
195};
197const sendAndAwaitAll = async (web3, stxs, doubtNonceError) => {
198 await Promise.all(
199 stxs.map(tx => {
200 return new Promise(async (resolve, reject) => {
201 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
202 await waitForTransactionConfirm(web3, txHash);
203 resolve();
204 });
205 })
206 );
207};
209const hexify = val => addHexPrefix(val.toString('hex'));
211const renderSignedTx = stx => ({
212 messageHash: hexify(stx.hash()),
213 v: hexify(stx.v),
214 s: hexify(stx.s),
215 r: hexify(stx.r),
216 rawTransaction: hexify(stx.serialize()),
217});
219const getTxnInfo = async (web3, addr) => {
220 let nonce = await web3.eth.getTransactionCount(addr);
221 let chainId = await web3.eth.net.getId();
222 let gasPrice = await web3.eth.getGasPrice();
223 return {
224 nonce: nonce,
225 chainId: chainId,
226 gasPrice: fromWei(gasPrice, 'gwei'),
227 };
228};
230const canDecodePatp = p => {
231 try {
232 ob.patp2dec(p);
233 return true;
234 } catch (_) {
235 return false;
236 }
237};
239export {
240 RETRY_OPTIONS,
241 signTransaction,
242 sendSignedTransaction,
243 waitForTransactionConfirm,
244 sendAndAwaitAll,
245 TXN_PURPOSE,
246 getTxnInfo,
247 renderTxnPurpose,
248 hexify,
249 renderSignedTx,
250 toHex,
251 toWei,
252 fromWei,
253 canDecodePatp,
254};