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