@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.
- package/package.json +9 -4
- package/src/address-has-history.js +6 -0
- package/src/create-asset.js +216 -0
- package/src/create-token-factory.js +72 -0
- package/src/eth-like-util.js +2 -3
- package/src/exodus-eth-server/api-coin-nodes.js +2 -3
- package/src/exodus-eth-server/api.js +2 -3
- package/src/exodus-eth-server/clarity.js +6 -4
- package/src/get-balances.js +35 -13
- package/src/get-fee.js +97 -0
- package/src/hooks/index.js +1 -0
- package/src/hooks/monitor.js +88 -0
- package/src/index.js +3 -0
- package/src/number-utils.js +8 -0
- package/src/optimism-gas/index.js +4 -4
- package/src/staking/ethereum/service.js +4 -4
- package/src/staking-api.js +5 -0
- package/src/tx-log/clarity-monitor.js +4 -9
- package/src/tx-log/ethereum-monitor.js +2 -3
- package/src/tx-log/ethereum-no-history-monitor.js +2 -3
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +48 -0
- package/src/tx-log-staking-processor/derive-txs.js +59 -0
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +26 -0
- package/src/tx-log-staking-processor/index.js +36 -0
- package/src/tx-log-staking-processor/utils.js +70 -0
- package/src/tx-send/get-fee-info.js +41 -0
- package/src/tx-send/index.js +2 -0
- package/src/tx-send/tx-send.js +300 -0
|
@@ -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
|