on e366ef3 help sign in
bridge/src/lib/txn.js
253 lines 7.3 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 TXN_PURPOSE = {
13 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
14 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
15 SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
16 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
17 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
18 SET_KEYS: Symbol('SET_KEYS'),
19 TRANSFER: Symbol('TRANSFER'),
20 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
21};
23const renderTxnPurpose = purpose =>
24 purpose === TXN_PURPOSE.SET_MANAGEMENT_PROXY
25 ? 'set this management proxy'
26 : purpose === TXN_PURPOSE.SET_SPAWN_PROXY
27 ? 'set this spawn proxy'
28 : purpose === TXN_PURPOSE.SET_TRANSFER_PROXY
29 ? 'set this transfer proxy'
30 : purpose === TXN_PURPOSE.CREATE_GALAXY
31 ? 'create this galaxy'
32 : purpose === TXN_PURPOSE.ISSUE_CHILD
33 ? 'issue this point'
34 : purpose === TXN_PURPOSE.SET_KEYS
35 ? 'set these network keys'
36 : purpose === TXN_PURPOSE.TRANSFER
37 ? 'transfer this point'
38 : purpose === TXN_PURPOSE.CANCEL_TRANSFER
39 ? 'cancel this transfer'
40 : 'perform this transaction';
42const signTransaction = async config => {
43 let {
44 wallet,
45 walletType,
46 walletHdPath,
47 networkType,
48 txn,
49 setStx,
50 nonce,
51 chainId,
52 gasPrice,
53 gasLimit,
54 } = config;
56 nonce = toHex(nonce);
57 chainId = toHex(chainId);
58 gasPrice = toHex(toWei(gasPrice, 'gwei'));
59 gasLimit = toHex(gasLimit);
61 const txParams = { nonce, chainId, gasPrice, gasLimit };
63 // NB (jtobin)
64 //
65 // Ledger does not seem to handle EIP-155 automatically. When using a Ledger,
66 // if the block number is at least FORK_BLKNUM = 2675000, one needs to
67 // pre-set the ECDSA signature parameters with r = 0, s = 0, and v = chainId
68 // prior to signing.
69 //
70 // The easiest way to handle this is to just branch on the network, since
71 // mainnet and Ropsten have obviously passed FORK_BLKNUM. This is somewhat
72 // awkward when dealing with offline transactions, since we might want to
73 // test them on a local network as well.
74 //
75 // The best thing to do is probably to add an 'advanced' tab to offline
76 // transaction generation where one can disable the defaulted-on EIP-155
77 // settings in this case. This is pretty low-priority, but is a
78 // comprehensive solution.
79 //
80 // See:
81 //
82 // See https://github.com/LedgerHQ/ledgerjs/issues/43#issuecomment-366984725
83 //
84 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
86 const eip155Params = {
87 r: '0x00',
88 s: '0x00',
89 v: chainId,
90 };
92 const defaultEip155Networks = [
93 NETWORK_TYPES.MAINNET,
94 NETWORK_TYPES.ROPSTEN,
95 NETWORK_TYPES.OFFLINE,
96 ];
98 const needEip155Params =
99 walletType === WALLET_TYPES.LEDGER &&
100 defaultEip155Networks.includes(networkType);
102 const signingParams = needEip155Params
103 ? Object.assign(txParams, eip155Params)
104 : txParams;
106 const wal = wallet.matchWith({
107 Just: w => w.value,
108 Nothing: () => {
109 throw BRIDGE_ERROR.MISSING_WALLET;
110 },
111 });
113 const sec = wal.privateKey;
115 const utx = txn.matchWith({
116 Just: tx => Object.assign(tx.value, signingParams),
117 Nothing: () => {
118 throw BRIDGE_ERROR.MISSING_TXN;
119 },
120 });
122 const stx = new Tx(utx);
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(sec);
130 }
132 setStx(Just(stx));
133 return stx;
134};
136const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
137 const txn = stx.matchWith({
138 Just: tx => tx.value,
139 Nothing: () => {
140 throw BRIDGE_ERROR.MISSING_TXN;
141 },
142 });
144 const rawTx = addHexPrefix(txn.serialize().toString('hex'));
146 return new Promise(async (resolve, reject) => {
147 web3.eth
148 .sendSignedTransaction(rawTx)
149 .on('transactionHash', hash => {
150 resolve(hash);
151 })
152 //TODO do we also reach this if network is slow? web3 only tries a set
153 // amount of times... should we instead do waitForTransactionConfirm
154 // in on-transactionHash? we don't care (much) about additional
155 // confirms anyway.
156 .on('confirmation', (confirmationNum, txn) => {
157 confirmationCb(txn.transactionHash, confirmationNum + 1);
158 resolve(txn.transactionHash);
159 })
160 .on('error', err => {
161 // if there's a nonce error, but we used the gas tank, it's likely
162 // that it's because the tank already submitted our transaction.
163 // we just wait for first confirmation here.
164 console.log('sent and got error', err);
165 if (
166 doubtNonceError &&
167 (err.message || '').includes("the tx doesn't have the correct nonce.")
168 ) {
169 console.log('nonce error, likely from gas tank submission, ignoring');
170 const txHash = web3.utils.keccak256(rawTx);
171 //TODO can we do does-exists check first?
172 //TODO max wait time before assume fail?
173 waitForTransactionConfirm(web3, txHash).then(res => {
174 if (res) {
175 resolve(txHash);
176 confirmationCb(txHash, 1);
177 } else {
178 reject('Unexpected tx failure');
179 }
180 });
181 } else {
182 reject(err.message);
183 }
184 });
185 });
186};
188// returns a Promise<bool>, where the bool indicates tx success/failure
189const waitForTransactionConfirm = (web3, txHash) => {
190 return new Promise((resolve, reject) => {
191 const checkForConfirm = async () => {
192 console.log('checking for confirm', txHash);
193 const receipt = await web3.eth.getTransactionReceipt(txHash);
194 console.log('tried, got', receipt);
195 let confirmed = receipt !== null;
196 if (confirmed) resolve(receipt.status === true);
197 else setTimeout(checkForConfirm, 13000);
198 };
199 checkForConfirm();
200 });
201};
203const isTransactionConfirmed = async (web3, txHash) => {
204 const receipt = await web3.eth.getTransactionReceipt(txHash);
205 console.log('got confirm state', receipt !== null, receipt.confirmations);
206 return receipt !== null;
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 signTransaction,
241 sendSignedTransaction,
242 waitForTransactionConfirm,
243 isTransactionConfirmed,
244 TXN_PURPOSE,
245 getTxnInfo,
246 renderTxnPurpose,
247 hexify,
248 renderSignedTx,
249 toHex,
250 toWei,
251 fromWei,
252 canDecodePatp,
253};