on 1597ccd help sign in
bridge/src/lib/txn.js
259 lines 7.5 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 TXN_PURPOSE = {
16 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
17 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
18 SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
19 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
20 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
21 SET_KEYS: Symbol('SET_KEYS'),
22 TRANSFER: Symbol('TRANSFER'),
23 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
24};
26const renderTxnPurpose = purpose =>
27 purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
28 ? 'set this management proxy'
29 : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
30 ? 'set this spawn proxy'
31 : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
32 ? 'set this transfer proxy'
33 : purpose === TXN_PURPOSE.CREATE_GALAXY
34 ? 'create this galaxy'
35 : purpose === TXN_PURPOSE.ISSUE_CHILD
36 ? 'issue this point'
37 : purpose === TXN_PURPOSE.SET_KEYS
38 ? 'set these network keys'
39 : purpose === TXN_PURPOSE.TRANSFER
40 ? 'transfer this point'
41 : purpose === TXN_PURPOSE.CANCEL_TRANSFER
42 ? 'cancel this transfer'
43 : 'perform this transaction';
45const signTransaction = async config => {
46 let {
47 wallet,
48 walletType,
49 walletHdPath,
50 networkType,
51 txn,
52 setStx,
53 nonce,
54 chainId,
55 gasPrice,
56 gasLimit,
57 } = config;
59 nonce = toHex(nonce);
60 chainId = toHex(chainId);
61 gasPrice = toHex(toWei(gasPrice, 'gwei'));
62 gasLimit = toHex(gasLimit);
64 const txParams = { nonce, chainId, gasPrice, gasLimit };
66 // NB (jtobin)
67 //
68 // Ledger does not seem to handle EIP-155 automatically. When using a Ledger,
69 // if the block number is at least FORK_BLKNUM = 2675000, one needs to
70 // pre-set the ECDSA signature parameters with r = 0, s = 0, and v = chainId
71 // prior to signing.
72 //
73 // The easiest way to handle this is to just branch on the network, since
74 // mainnet and Ropsten have obviously passed FORK_BLKNUM. This is somewhat
75 // awkward when dealing with offline transactions, since we might want to
76 // test them on a local network as well.
77 //
78 // The best thing to do is probably to add an 'advanced' tab to offline
79 // transaction generation where one can disable the defaulted-on EIP-155
80 // settings in this case. This is pretty low-priority, but is a
81 // comprehensive solution.
82 //
83 // See:
84 //
85 // See https://github.com/LedgerHQ/ledgerjs/issues/43#issuecomment-366984725
86 //
87 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
89 const eip155Params = {
90 r: '0x00',
91 s: '0x00',
92 v: chainId,
93 };
95 const defaultEip155Networks = [
96 NETWORK_TYPES.MAINNET,
97 NETWORK_TYPES.ROPSTEN,
98 NETWORK_TYPES.OFFLINE,
99 ];
101 const needEip155Params =
102 walletType === WALLET_TYPES.LEDGER &&
103 defaultEip155Networks.includes(networkType);
105 const signingParams = needEip155Params
106 ? Object.assign(txParams, eip155Params)
107 : txParams;
109 const wal = wallet.matchWith({
110 Just: w => w.value,
111 Nothing: () => {
112 throw BRIDGE_ERROR.MISSING_WALLET;
113 },
114 });
116 const sec = wal.privateKey;
118 const utx = txn.matchWith({
119 Just: tx => Object.assign(tx.value, signingParams),
120 Nothing: () => {
121 throw BRIDGE_ERROR.MISSING_TXN;
122 },
123 });
125 const stx = new Tx(utx);
127 if (walletType === WALLET_TYPES.LEDGER) {
128 await ledgerSignTransaction(stx, walletHdPath);
129 } else if (walletType === WALLET_TYPES.TREZOR) {
130 await trezorSignTransaction(stx, walletHdPath);
131 } else {
132 stx.sign(sec);
133 }
135 setStx(Just(stx));
136 return stx;
137};
139const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
140 const txn = stx.matchWith({
141 Just: tx => tx.value,
142 Nothing: () => {
143 throw BRIDGE_ERROR.MISSING_TXN;
144 },
145 });
147 const rawTx = hexify(txn.serialize());
149 return new Promise(async (resolve, reject) => {
150 web3.eth
151 .sendSignedTransaction(rawTx)
152 .on('transactionHash', hash => {
153 resolve(hash);
154 })
155 //TODO do we also reach this if network is slow? web3 only tries a set
156 // amount of times... should we instead do waitForTransactionConfirm
157 // in on-transactionHash? we don't care (much) about additional
158 // confirms anyway.
159 .on('confirmation', (confirmationNum, txn) => {
160 confirmationCb(txn.transactionHash, confirmationNum + 1);
161 resolve(txn.transactionHash);
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 if (
169 (err.message || '').includes('known transaction: ') ||
170 (doubtNonceError &&
171 (err.message || '').includes(
172 "the tx doesn't have the correct nonce."
173 ))
174 ) {
175 console.log('nonce error, likely from gas tank submission, ignoring');
176 const txHash = web3.utils.keccak256(rawTx);
177 //TODO can we do does-exists check first?
178 //TODO max wait time before assume fail?
179 waitForTransactionConfirm(web3, txHash).then(res => {
180 if (res) {
181 resolve(txHash);
182 confirmationCb(txHash, 1);
183 } else {
184 reject('Unexpected tx failure');
185 }
186 });
187 } else {
188 reject(err.message || 'Transaction sending failed!');
189 }
190 });
191 });
192};
194// returns a Promise<bool>, where the bool indicates tx success/failure
195const waitForTransactionConfirm = (web3, txHash) => {
196 return new Promise((resolve, reject) => {
197 const checkForConfirm = async () => {
198 console.log('checking for confirm', txHash);
199 const receipt = await web3.eth.getTransactionReceipt(txHash);
200 console.log('tried, got', receipt);
201 let confirmed = receipt !== null;
202 if (confirmed) resolve(receipt.status === true);
203 else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
204 };
205 checkForConfirm();
206 });
207};
209const isTransactionConfirmed = async (web3, txHash) => {
210 const receipt = await web3.eth.getTransactionReceipt(txHash);
211 console.log('got confirm state', receipt !== null, receipt.confirmations);
212 return receipt !== null;
213};
215const hexify = val => addHexPrefix(val.toString('hex'));
217const renderSignedTx = stx => ({
218 messageHash: hexify(stx.hash()),
219 v: hexify(stx.v),
220 s: hexify(stx.s),
221 r: hexify(stx.r),
222 rawTransaction: hexify(stx.serialize()),
223});
225const getTxnInfo = async (web3, addr) => {
226 let nonce = await web3.eth.getTransactionCount(addr);
227 let chainId = await web3.eth.net.getId();
228 let gasPrice = await web3.eth.getGasPrice();
229 return {
230 nonce: nonce,
231 chainId: chainId,
232 gasPrice: fromWei(gasPrice, 'gwei'),
233 };
234};
236const canDecodePatp = p => {
237 try {
238 ob.patp2dec(p);
239 return true;
240 } catch (_) {
241 return false;
242 }
243};
245export {
246 signTransaction,
247 sendSignedTransaction,
248 waitForTransactionConfirm,
249 isTransactionConfirmed,
250 TXN_PURPOSE,
251 getTxnInfo,
252 renderTxnPurpose,
253 hexify,
254 renderSignedTx,
255 toHex,
256 toWei,
257 fromWei,
258 canDecodePatp,
259};