on 7fcbd29 help sign in
bridge/src/lib/txn.js
234 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';
5import * as retry from 'async-retry';
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 RETRY_OPTIONS = {
16 retries: 99999,
17 factor: 1,
18 minTimeout: CHECK_BLOCK_EVERY_MS,
19 randomize: false,
20};
22const TXN_PURPOSE = {
23 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
24 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
25 SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
26 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
27 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
28 SET_KEYS: Symbol('SET_KEYS'),
29 TRANSFER: Symbol('TRANSFER'),
30 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
31};
33const renderTxnPurpose = purpose =>
34 purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
35 ? 'set this management proxy'
36 : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
37 ? 'set this spawn proxy'
38 : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
39 ? 'set this transfer proxy'
40 : purpose === TXN_PURPOSE.CREATE_GALAXY
41 ? 'create this galaxy'
42 : purpose === TXN_PURPOSE.ISSUE_CHILD
43 ? 'issue this point'
44 : purpose === TXN_PURPOSE.SET_KEYS
45 ? 'set these network keys'
46 : purpose === TXN_PURPOSE.TRANSFER
47 ? 'transfer this point'
48 : purpose === TXN_PURPOSE.CANCEL_TRANSFER
49 ? 'cancel this transfer'
50 : 'perform this transaction';
52const signTransaction = async config => {
53 let {
54 wallet,
55 walletType,
56 walletHdPath,
57 networkType,
58 txn,
59 setStx,
60 nonce,
61 chainId,
62 gasPrice,
63 gasLimit,
64 } = config;
66 //TODO require these in txn object
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 utx = Object.assign(txn, signingParams);
119 const stx = new Tx(utx);
121 //TODO should try-catch and display error message to user,
122 // ie ledger's "pls enable contract data"
123 // needs to maybe happen at call-site though
124 if (walletType === WALLET_TYPES.LEDGER) {
125 await ledgerSignTransaction(stx, walletHdPath);
126 } else if (walletType === WALLET_TYPES.TREZOR) {
127 await trezorSignTransaction(stx, walletHdPath);
128 } else {
129 stx.sign(wallet.privateKey);
130 }
132 setStx(Just(stx));
133 return stx;
134};
136const sendSignedTransaction = (web3, stx, doubtNonceError) => {
137 const rawTx = hexify(stx.serialize());
139 return new Promise(async (resolve, reject) => {
140 web3.eth
141 .sendSignedTransaction(rawTx)
142 .on('transactionHash', hash => {
143 resolve(hash);
144 })
145 .on('error', err => {
146 // if there's a nonce error, but we used the gas tank, it's likely
147 // that it's because the tank already submitted our transaction.
148 // we just wait for first confirmation here.
149 console.error(err);
150 const isKnownError = (err.message || '').includes(
151 'known transaction: '
152 );
153 const isNonceError = (err.message || '').includes(
154 "the tx doesn't have the correct nonce."
155 );
156 if (isKnownError || (doubtNonceError && isNonceError)) {
157 console.log(
158 'tx send error likely from gas tank submission, ignoring'
159 );
160 const txHash = web3.utils.keccak256(rawTx);
161 resolve(txHash);
162 } else {
163 reject(err.message || 'Transaction sending failed!');
164 }
165 });
166 });
167};
169// returns a Promise<bool>, where the bool indicates tx success/failure
170const waitForTransactionConfirm = (web3, txHash) => {
171 return retry(async (bail, n) => {
172 const receipt = await web3.eth.getTransactionReceipt(txHash);
173 const confirmed = receipt !== null;
174 if (confirmed) return receipt.status === true;
175 else throw new Error('retrying');
176 }, RETRY_OPTIONS);
177};
179// returns a Promise that resolves when all stxs have been sent & confirmed
180const sendAndAwaitAll = (web3, stxs, doubtNonceError) => {
181 return Promise.all(
182 stxs.map(async tx => {
183 const txHash = await sendSignedTransaction(web3, tx, doubtNonceError);
184 await waitForTransactionConfirm(web3, txHash);
185 })
186 );
187};
189const hexify = val => addHexPrefix(val.toString('hex'));
191const renderSignedTx = stx => ({
192 messageHash: hexify(stx.hash()),
193 v: hexify(stx.v),
194 s: hexify(stx.s),
195 r: hexify(stx.r),
196 rawTransaction: hexify(stx.serialize()),
197});
199const getTxnInfo = async (web3, addr) => {
200 let nonce = await web3.eth.getTransactionCount(addr);
201 let chainId = await web3.eth.net.getId();
202 let gasPrice = await web3.eth.getGasPrice();
203 return {
204 nonce: nonce,
205 chainId: chainId,
206 gasPrice: fromWei(gasPrice, 'gwei'),
207 };
208};
210const canDecodePatp = p => {
211 try {
212 ob.patp2dec(p);
213 return true;
214 } catch (_) {
215 return false;
216 }
217};
219export {
220 RETRY_OPTIONS,
221 signTransaction,
222 sendSignedTransaction,
223 waitForTransactionConfirm,
224 sendAndAwaitAll,
225 TXN_PURPOSE,
226 getTxnInfo,
227 renderTxnPurpose,
228 hexify,
229 renderSignedTx,
230 toHex,
231 toWei,
232 fromWei,
233 canDecodePatp,
234};