@exodus/ethereum-api 7.2.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ /* eslint-disable @exodus/export-default/last */
2
+
3
+ import { getNonce, transactionExists } from '../eth-like-util'
4
+ import {
5
+ createUnsignedTx,
6
+ calculateBumpedGasPrice,
7
+ isToken as checkIsToken,
8
+ normalizeTxId,
9
+ } from '@exodus/ethereum-lib'
10
+ import assert from 'minimalistic-assert'
11
+ import getFeeInfo from './get-fee-info'
12
+
13
+ const txSendFactory = ({ assetClientInterface }) => {
14
+ assert(assetClientInterface, 'assetClientInterface is required')
15
+ return async ({
16
+ asset,
17
+ walletAccount,
18
+ amount,
19
+ address,
20
+ feeAmount,
21
+ shouldLog = true,
22
+ keepTxInput,
23
+ txInput,
24
+ nonce: _nonce,
25
+ bumpTxId,
26
+ customFee,
27
+ isSendAll,
28
+ isExchange,
29
+ feeOpts: feeOpts_ = {},
30
+ }) => {
31
+ const assetName = asset.name
32
+ const baseAsset = asset.baseAsset
33
+ const feeOpts = { ...feeOpts_ }
34
+ const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
35
+ let eip1559Enabled = baseAsset.name === 'ethereum' // TODO: temp override, clean up use of eip1559Enabled flag and default to always true
36
+
37
+ const fromAddress = await assetClientInterface.getReceiveAddress({
38
+ assetName: baseAsset.name,
39
+ walletAccount,
40
+ })
41
+
42
+ let nonceParam = _nonce
43
+
44
+ // `replacedTx` is always an ETH/ETC transaction (not a token)
45
+ let replacedTx, replacedTokenTx
46
+ if (bumpTxId) {
47
+ const baseAssetTxLog = await assetClientInterface.getTxLog({
48
+ assetName: baseAsset.name,
49
+ walletAccount,
50
+ })
51
+ replacedTx = baseAssetTxLog.get(bumpTxId)
52
+ if (!replacedTx || !replacedTx.pending) {
53
+ throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
54
+ }
55
+
56
+ if (replacedTx.tokens.length > 0) {
57
+ const tokenTxSet = await assetClientInterface.getTxLog({
58
+ assetName: replacedTx.tokens[0],
59
+ walletAccount,
60
+ })
61
+ replacedTokenTx = tokenTxSet.get(bumpTxId)
62
+
63
+ if (replacedTokenTx) {
64
+ asset = assets[replacedTx.tokens[0]]
65
+ }
66
+ }
67
+
68
+ address = (replacedTokenTx || replacedTx).to
69
+ amount = (replacedTokenTx || replacedTx).coinAmount.negate()
70
+ feeOpts.gasLimit = replacedTx.data.gasLimit
71
+ const { gasPrice: currentGasPrice, eip1559Enabled: _eip1559Enabled } =
72
+ await assetClientInterface.getFeeData({
73
+ assetName,
74
+ })
75
+ const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
76
+ baseAsset,
77
+ tx: replacedTx,
78
+ currentGasPrice,
79
+ eip1559Enabled: _eip1559Enabled,
80
+ })
81
+ feeOpts.gasPrice = bumpedGasPrice
82
+ feeOpts.tipGasPrice = bumpedTipGasPrice
83
+ eip1559Enabled = _eip1559Enabled && feeOpts.tipGasPrice
84
+ nonceParam = replacedTx.data.nonce
85
+ txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
86
+ feeAmount = feeOpts.gasPrice.mul(feeOpts.gasLimit)
87
+ if (nonceParam === undefined) {
88
+ throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
89
+ }
90
+ }
91
+
92
+ const createTxParams = {
93
+ assetClientInterface,
94
+ asset,
95
+ walletAccount,
96
+ toAddress: address,
97
+ amount,
98
+ nonce: nonceParam,
99
+ fromAddress,
100
+ eip1559Enabled,
101
+ customFee,
102
+ feeOpts,
103
+ txInput,
104
+ keepTxInput,
105
+ isSendAll,
106
+ isExchange,
107
+ }
108
+ let { txId, rawTx, nonce, gasLimit, tipGasPrice } = await createTx(createTxParams)
109
+
110
+ try {
111
+ await baseAsset.api.broadcastTx(rawTx.toString('hex'))
112
+ } catch (err) {
113
+ const nonceTooLowErr = err.message.match(/nonce (is |)too low/i)
114
+ const txAlreadyExists = nonceTooLowErr
115
+ ? await transactionExists({ asset, txId })
116
+ : err.message.match(/already known/i)
117
+
118
+ if (txAlreadyExists) {
119
+ console.info('tx already broadcast') // inject logger factory from platform
120
+ } else if (bumpTxId || !nonceTooLowErr) {
121
+ throw err
122
+ } else if (nonceTooLowErr) {
123
+ console.info('trying to send again...') // inject logger factory from platform
124
+ // let's try to fix the nonce issue
125
+ nonce = await getNonce({ asset: baseAsset, address: fromAddress })
126
+ ;({ txId, rawTx } = await createTx({ ...createTxParams, nonce }))
127
+ await baseAsset.api.broadcastTx(rawTx.toString('hex'))
128
+ }
129
+ }
130
+
131
+ const selfSend = fromAddress === address
132
+
133
+ await assetClientInterface.updateTxLogAndNotify({
134
+ assetName: asset.name,
135
+ walletAccount,
136
+ txs: [
137
+ {
138
+ txId,
139
+ confirmations: 0,
140
+ coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
141
+ coinName: asset.name,
142
+ feeAmount,
143
+ feeCoinName: asset.feeAsset.name,
144
+ selfSend,
145
+ to: address,
146
+ currencies: {
147
+ [assetName]: asset.currency,
148
+ [asset.feeAsset.name]: asset.feeAsset.currency,
149
+ },
150
+ data: eip1559Enabled
151
+ ? {
152
+ gasLimit,
153
+ replacedTxId: bumpTxId,
154
+ nonce,
155
+ tipGasPrice: tipGasPrice.toBaseString(),
156
+ }
157
+ : { gasLimit, replacedTxId: bumpTxId, nonce },
158
+ },
159
+ ],
160
+ })
161
+
162
+ const isToken = checkIsToken(asset)
163
+ if (isToken) {
164
+ await assetClientInterface.updateTxLogAndNotify({
165
+ assetName: baseAsset.name,
166
+ walletAccount,
167
+ txs: [
168
+ {
169
+ txId,
170
+ coinAmount: baseAsset.currency.ZERO,
171
+ coinName: baseAsset.name,
172
+ feeAmount,
173
+ feeCoinName: baseAsset.name,
174
+ selfSend,
175
+ to: address,
176
+ token: asset.name,
177
+ currencies: {
178
+ [baseAsset.name]: baseAsset.currency,
179
+ [asset.feeAsset.name]: asset.feeAsset.currency,
180
+ },
181
+ data: eip1559Enabled
182
+ ? {
183
+ gasLimit,
184
+ replacedTxId: bumpTxId,
185
+ nonce,
186
+ tipGasPrice: tipGasPrice.toBaseString(),
187
+ }
188
+ : { gasLimit, replacedTxId: bumpTxId, nonce },
189
+ },
190
+ ],
191
+ })
192
+ }
193
+
194
+ return { txId }
195
+ }
196
+ }
197
+
198
+ const createTx = async ({
199
+ assetClientInterface,
200
+ asset,
201
+ walletAccount,
202
+ toAddress,
203
+ amount,
204
+ nonce,
205
+ txInput,
206
+ eip1559Enabled = true,
207
+ keepTxInput = false,
208
+ customFee: customGasPrice,
209
+ isSendAll,
210
+ isExchange,
211
+ fromAddress,
212
+ feeOpts,
213
+ }) => {
214
+ const isToken = checkIsToken(asset)
215
+
216
+ if (txInput && isToken && !keepTxInput)
217
+ throw new Error(`Additional data for Ethereum Token (${asset.name}) is not allowed`)
218
+
219
+ txInput =
220
+ isToken && !keepTxInput
221
+ ? asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
222
+ : txInput
223
+
224
+ let { gasLimit, gasPrice, tipGasPrice } = await getFeeInfo({
225
+ assetClientInterface,
226
+ asset,
227
+ fromAddress,
228
+ toAddress,
229
+ amount,
230
+ isExchange,
231
+ txInput,
232
+ feeOpts,
233
+ })
234
+
235
+ if (eip1559Enabled) {
236
+ if (customGasPrice) {
237
+ gasPrice = customGasPrice
238
+ }
239
+
240
+ if (isSendAll && !isToken) {
241
+ // force consuming all gas
242
+ tipGasPrice = gasPrice
243
+ }
244
+
245
+ // gasLimit = customGasLimit
246
+ }
247
+
248
+ if (asset.baseAsset?.api?.hasFeature?.('noHistory')) {
249
+ nonce = await getNonce({ asset: asset.baseAsset, address: fromAddress })
250
+ }
251
+
252
+ if (nonce === undefined) {
253
+ // Calculate latest nonce from base asset's TX log
254
+ const baseAssetTxLog = await assetClientInterface.getTxLog({
255
+ assetName: asset.baseAsset.name,
256
+ walletAccount,
257
+ })
258
+ nonce = [...baseAssetTxLog]
259
+ .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
260
+ .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
261
+ }
262
+
263
+ const unsignedTx = await createUnsignedTx({
264
+ asset,
265
+ walletAccount,
266
+ address: toAddress,
267
+ amount,
268
+ nonce,
269
+ txInput,
270
+ gasLimit,
271
+ gasPrice,
272
+ tipGasPrice,
273
+ fromAddress,
274
+ eip1559Enabled,
275
+ })
276
+
277
+ // TODO: move into createUnsignedTx()
278
+ if (keepTxInput && !isToken) {
279
+ unsignedTx.txData.to = toAddress
280
+ }
281
+
282
+ unsignedTx.txMeta.eip1559Enabled = eip1559Enabled
283
+
284
+ const { txId, rawTx } = await assetClientInterface.signTransaction({
285
+ assetName: asset.baseAsset.name,
286
+ unsignedTx,
287
+ walletAccount,
288
+ })
289
+
290
+ return {
291
+ txId: normalizeTxId(txId),
292
+ rawTx,
293
+ nonce,
294
+ gasLimit,
295
+ gasPrice,
296
+ tipGasPrice,
297
+ }
298
+ }
299
+
300
+ export default txSendFactory