edge-currency-accountbased 0.7.72
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/CHANGELOG.md +713 -0
- package/LICENSE +29 -0
- package/README.md +63 -0
- package/index.js +3 -0
- package/lib/binance/bnbEngine.js +591 -0
- package/lib/binance/bnbInfo.js +43 -0
- package/lib/binance/bnbPlugin.js +168 -0
- package/lib/binance/bnbSchema.js +83 -0
- package/lib/binance/bnbTypes.js +39 -0
- package/lib/common/engine.js +918 -0
- package/lib/common/plugin.js +152 -0
- package/lib/common/schema.js +108 -0
- package/lib/common/types.js +85 -0
- package/lib/common/utils.js +378 -0
- package/lib/eos/eosEngine.js +1216 -0
- package/lib/eos/eosInfo.js +98 -0
- package/lib/eos/eosPlugin.js +314 -0
- package/lib/eos/eosSchema.js +190 -0
- package/lib/eos/eosTypes.js +88 -0
- package/lib/eos/telosInfo.js +94 -0
- package/lib/eos/waxInfo.js +95 -0
- package/lib/ethereum/etcInfo.js +121 -0
- package/lib/ethereum/ethEngine.js +832 -0
- package/lib/ethereum/ethInfo.js +1300 -0
- package/lib/ethereum/ethMiningFees.js +157 -0
- package/lib/ethereum/ethNetwork.js +2195 -0
- package/lib/ethereum/ethPlugin.js +377 -0
- package/lib/ethereum/ethSchema.js +61 -0
- package/lib/ethereum/ethTypes.js +461 -0
- package/lib/ethereum/ftminfo.js +102 -0
- package/lib/ethereum/rskInfo.js +101 -0
- package/lib/fio/fioConst.js +38 -0
- package/lib/fio/fioEngine.js +1250 -0
- package/lib/fio/fioError.js +38 -0
- package/lib/fio/fioInfo.js +72 -0
- package/lib/fio/fioPlugin.js +486 -0
- package/lib/fio/fioSchema.js +56 -0
- package/lib/index.js +44 -0
- package/lib/pluginError.js +32 -0
- package/lib/react-native/edge-currency-accountbased.js +239635 -0
- package/lib/react-native/edge-currency-accountbased.js.map +1 -0
- package/lib/react-native-io.js +41 -0
- package/lib/stellar/stellarEngine.js +563 -0
- package/lib/stellar/stellarInfo.js +37 -0
- package/lib/stellar/stellarPlugin.js +215 -0
- package/lib/stellar/stellarSchema.js +54 -0
- package/lib/stellar/stellarTypes.js +66 -0
- package/lib/tezos/tezosEngine.js +497 -0
- package/lib/tezos/tezosInfo.js +60 -0
- package/lib/tezos/tezosPlugin.js +174 -0
- package/lib/tezos/tezosTypes.js +110 -0
- package/lib/xrp/xrpEngine.js +583 -0
- package/lib/xrp/xrpInfo.js +47 -0
- package/lib/xrp/xrpPlugin.js +229 -0
- package/lib/xrp/xrpSchema.js +74 -0
- package/lib/xrp/xrpTypes.js +38 -0
- package/package.json +139 -0
- package/postinstall.sh +7 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Created by paul on 7/7/17.
|
|
3
|
+
*/
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import Common from '@ethereumjs/common'
|
|
7
|
+
import { Transaction } from '@ethereumjs/tx'
|
|
8
|
+
import { bns } from 'biggystring'
|
|
9
|
+
import { asMaybe } from 'cleaners'
|
|
10
|
+
import {
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
InsufficientFundsError
|
|
18
|
+
} from 'edge-core-js/types'
|
|
19
|
+
import abi from 'ethereumjs-abi'
|
|
20
|
+
import EthereumUtil from 'ethereumjs-util'
|
|
21
|
+
import ethWallet from 'ethereumjs-wallet'
|
|
22
|
+
|
|
23
|
+
import { CurrencyEngine } from '../common/engine.js'
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
addHexPrefix,
|
|
27
|
+
bufToHex,
|
|
28
|
+
cleanTxLogs,
|
|
29
|
+
getEdgeInfoServer,
|
|
30
|
+
getOtherParams,
|
|
31
|
+
hexToDecimal,
|
|
32
|
+
isHex,
|
|
33
|
+
normalizeAddress,
|
|
34
|
+
toHex,
|
|
35
|
+
validateObject
|
|
36
|
+
} from '../common/utils'
|
|
37
|
+
import { calcMiningFee } from './ethMiningFees.js'
|
|
38
|
+
import { EthereumNetwork } from './ethNetwork'
|
|
39
|
+
import { EthereumPlugin } from './ethPlugin'
|
|
40
|
+
import { EthGasStationSchema } from './ethSchema.js'
|
|
41
|
+
import {
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
asEthereumFees
|
|
51
|
+
} from './ethTypes.js'
|
|
52
|
+
|
|
53
|
+
const NETWORKFEES_POLL_MILLISECONDS = 60 * 10 * 1000 // 10 minutes
|
|
54
|
+
const ETH_GAS_STATION_WEI_MULTIPLIER = 100000000 // 100 million is the multiplier for ethgassstation because it uses 10x gwei
|
|
55
|
+
const WEI_MULTIPLIER = 1000000000
|
|
56
|
+
const GAS_PRICE_SANITY_CHECK = 30000 // 3000 Gwei (ethgasstation api reports gas prices with additional decimal place)
|
|
57
|
+
|
|
58
|
+
export class EthereumEngine extends CurrencyEngine {
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
currencyPlugin,
|
|
67
|
+
walletInfo,
|
|
68
|
+
initOptions,
|
|
69
|
+
opts,
|
|
70
|
+
currencyInfo,
|
|
71
|
+
fetchCors
|
|
72
|
+
) {
|
|
73
|
+
super(currencyPlugin, walletInfo, opts)
|
|
74
|
+
const { pluginId } = this.currencyInfo
|
|
75
|
+
if (typeof this.walletInfo.keys[`${pluginId}Key`] !== 'string') {
|
|
76
|
+
if (walletInfo.keys.keys && walletInfo.keys.keys[`${pluginId}Key`]) {
|
|
77
|
+
this.walletInfo.keys[`${pluginId}Key`] =
|
|
78
|
+
walletInfo.keys.keys[`${pluginId}Key`]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.currencyPlugin = currencyPlugin
|
|
82
|
+
this.initOptions = initOptions
|
|
83
|
+
this.ethNetwork = new EthereumNetwork(this, this.currencyInfo)
|
|
84
|
+
this.lastEstimatedGasLimit = {
|
|
85
|
+
publicAddress: '',
|
|
86
|
+
contractAddress: '',
|
|
87
|
+
gasLimit: ''
|
|
88
|
+
}
|
|
89
|
+
this.fetchCors = fetchCors
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateBalance(tk, balance) {
|
|
93
|
+
if (typeof this.walletLocalData.totalBalances[tk] === 'undefined') {
|
|
94
|
+
this.walletLocalData.totalBalances[tk] = '0'
|
|
95
|
+
}
|
|
96
|
+
if (!bns.eq(balance, this.walletLocalData.totalBalances[tk])) {
|
|
97
|
+
this.walletLocalData.totalBalances[tk] = balance
|
|
98
|
+
this.log.warn(tk + ': token Address balance: ' + balance)
|
|
99
|
+
this.currencyEngineCallbacks.onBalanceChanged(tk, balance)
|
|
100
|
+
}
|
|
101
|
+
this.tokenCheckBalanceStatus[tk] = 1
|
|
102
|
+
this.updateOnAddressesChecked()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
processUnconfirmedTransaction(tx) {
|
|
106
|
+
const fromAddress = '0x' + tx.inputs[0].addresses[0]
|
|
107
|
+
const toAddress = '0x' + tx.outputs[0].addresses[0]
|
|
108
|
+
const epochTime = Date.parse(tx.received) / 1000
|
|
109
|
+
const ourReceiveAddresses = []
|
|
110
|
+
|
|
111
|
+
let nativeAmount
|
|
112
|
+
if (
|
|
113
|
+
normalizeAddress(fromAddress) ===
|
|
114
|
+
normalizeAddress(this.walletLocalData.publicKey)
|
|
115
|
+
) {
|
|
116
|
+
if (fromAddress === toAddress) {
|
|
117
|
+
// Spend to self
|
|
118
|
+
nativeAmount = bns.sub('0', tx.fees.toString(10))
|
|
119
|
+
} else {
|
|
120
|
+
nativeAmount = (0 - tx.total).toString(10)
|
|
121
|
+
nativeAmount = bns.sub(nativeAmount, tx.fees.toString(10))
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
nativeAmount = tx.total.toString(10)
|
|
125
|
+
ourReceiveAddresses.push(this.walletLocalData.publicKey)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const otherParams = {
|
|
129
|
+
from: [fromAddress],
|
|
130
|
+
to: [toAddress],
|
|
131
|
+
gas: '',
|
|
132
|
+
gasPrice: '',
|
|
133
|
+
gasUsed: tx.fees.toString(10),
|
|
134
|
+
cumulativeGasUsed: '',
|
|
135
|
+
errorVal: 0,
|
|
136
|
+
tokenRecipientAddress: null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const edgeTransaction = {
|
|
140
|
+
txid: addHexPrefix(tx.hash),
|
|
141
|
+
date: epochTime,
|
|
142
|
+
currencyCode: this.currencyInfo.currencyCode,
|
|
143
|
+
blockHeight: 0,
|
|
144
|
+
nativeAmount,
|
|
145
|
+
networkFee: tx.fees.toString(10),
|
|
146
|
+
ourReceiveAddresses,
|
|
147
|
+
signedTx: '',
|
|
148
|
+
otherParams
|
|
149
|
+
}
|
|
150
|
+
this.addTransaction(this.currencyInfo.currencyCode, edgeTransaction)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// curreently for Ethereum but should allow other currencies
|
|
154
|
+
async checkUpdateNetworkFees() {
|
|
155
|
+
// Get the network fees from the info server
|
|
156
|
+
try {
|
|
157
|
+
const infoServer = getEdgeInfoServer()
|
|
158
|
+
const url = `${infoServer}/v1/networkFees/${this.currencyInfo.currencyCode}`
|
|
159
|
+
const jsonObj = await this.ethNetwork.fetchGet(url)
|
|
160
|
+
const valid = asMaybe(asEthereumFees)(jsonObj) != null
|
|
161
|
+
|
|
162
|
+
if (valid) {
|
|
163
|
+
if (
|
|
164
|
+
JSON.stringify(this.walletLocalData.otherData.networkFees) !==
|
|
165
|
+
JSON.stringify(jsonObj)
|
|
166
|
+
) {
|
|
167
|
+
this.walletLocalData.otherData.networkFees = jsonObj
|
|
168
|
+
this.walletLocalDataDirty = true
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
this.log.error(
|
|
172
|
+
`Error: Fetched invalid networkFees ${JSON.stringify(jsonObj)}`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.log.error(
|
|
177
|
+
`Error fetching ${this.currencyInfo.currencyCode} networkFees from Edge info server`
|
|
178
|
+
)
|
|
179
|
+
this.log.error(err)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { baseFeePerGas } = await this.ethNetwork.getBaseFeePerGas()
|
|
183
|
+
|
|
184
|
+
// If base fee is not suppported, update network fees fromethgasstation.info
|
|
185
|
+
if (baseFeePerGas == null) {
|
|
186
|
+
this.log.warn(`Updating networkFees from ethgasstation.info`)
|
|
187
|
+
this.updateNetworkFeesFromEthGasStation()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update the network fees from network base fee
|
|
192
|
+
this.updateNetworkFeesFromBaseFeePerGas(hexToDecimal(baseFeePerGas))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async updateNetworkFeesFromBaseFeePerGas(baseFeePerGas) {
|
|
196
|
+
/*
|
|
197
|
+
This algorithm calculates fee amounts using the base multiplier from the
|
|
198
|
+
info server.
|
|
199
|
+
|
|
200
|
+
Formula:
|
|
201
|
+
fee = baseMultiplier * baseFee + minPriorityFee
|
|
202
|
+
|
|
203
|
+
Where:
|
|
204
|
+
minPriorityFee = <minimum priority fee from info server>
|
|
205
|
+
baseFee = <latest block's base fee>
|
|
206
|
+
baseMultiplier = <multiplier from info server for low, standard, high, etc>
|
|
207
|
+
|
|
208
|
+
Reference analysis for choosing 2 gwei minimum priority fee:
|
|
209
|
+
https://hackmd.io/@q8X_WM2nTfu6nuvAzqXiTQ/1559-wallets#:~:text=2%20gwei%20is%20probably%20a%20very%20good%20default
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
const networkFees = this.walletLocalData.otherData.networkFees
|
|
213
|
+
|
|
214
|
+
// Make sure there is a default network fee entry and gasPrice entry
|
|
215
|
+
if (networkFees.default == null || networkFees.default.gasPrice == null) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const defaultNetworkFee =
|
|
220
|
+
this.currencyInfo.defaultSettings.otherSettings.defaultNetworkFees.default
|
|
221
|
+
|
|
222
|
+
// The minimum priority fee for slow transactions
|
|
223
|
+
const minPriorityFee =
|
|
224
|
+
networkFees.default.minPriorityFee || defaultNetworkFee.minPriorityFee
|
|
225
|
+
// This is how much we will multiply the base fee by
|
|
226
|
+
const baseMultiplier =
|
|
227
|
+
networkFees.default.baseFeeMultiplier ||
|
|
228
|
+
defaultNetworkFee.baseFeeMultiplier
|
|
229
|
+
|
|
230
|
+
// Make sure the properties exist
|
|
231
|
+
if (minPriorityFee == null || baseMultiplier == null) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { gasPrice } = networkFees.default
|
|
236
|
+
|
|
237
|
+
const formula = (baseMultiplier) =>
|
|
238
|
+
bns.div(
|
|
239
|
+
bns.add(bns.mul(baseMultiplier, baseFeePerGas), minPriorityFee),
|
|
240
|
+
'1'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Update default network fees
|
|
244
|
+
networkFees.default = {
|
|
245
|
+
...networkFees.default,
|
|
246
|
+
gasPrice: {
|
|
247
|
+
...gasPrice,
|
|
248
|
+
...Object.keys(baseMultiplier).reduce(
|
|
249
|
+
(fees, key) => ({ ...fees, [key]: formula(baseMultiplier[key]) }),
|
|
250
|
+
{}
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// deprecate after london hardfork because of EIP 1559
|
|
257
|
+
async updateNetworkFeesFromEthGasStation() {
|
|
258
|
+
let jsonObj
|
|
259
|
+
try {
|
|
260
|
+
const { ethGasStationUrl } =
|
|
261
|
+
this.currencyInfo.defaultSettings.otherSettings
|
|
262
|
+
if (ethGasStationUrl == null) return
|
|
263
|
+
const { ethGasStationApiKey } = this.initOptions
|
|
264
|
+
jsonObj = await this.ethNetwork.fetchGet(
|
|
265
|
+
`${ethGasStationUrl}?api-key=${ethGasStationApiKey || ''}`
|
|
266
|
+
)
|
|
267
|
+
const valid = validateObject(jsonObj, EthGasStationSchema)
|
|
268
|
+
|
|
269
|
+
if (valid) {
|
|
270
|
+
const fees = this.walletLocalData.otherData.networkFees
|
|
271
|
+
const ethereumFee = fees.default
|
|
272
|
+
if (!ethereumFee.gasPrice) {
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
const gasPrice = ethereumFee.gasPrice
|
|
276
|
+
|
|
277
|
+
const safeLow = jsonObj.safeLow
|
|
278
|
+
let average = jsonObj.average
|
|
279
|
+
let fast = jsonObj.fast
|
|
280
|
+
let fastest = jsonObj.fastest
|
|
281
|
+
|
|
282
|
+
// Sanity checks
|
|
283
|
+
if (safeLow <= 0 || safeLow > GAS_PRICE_SANITY_CHECK) {
|
|
284
|
+
throw new Error('Invalid safeLow value from EthGasStation')
|
|
285
|
+
}
|
|
286
|
+
if (average < 1 || average > GAS_PRICE_SANITY_CHECK) {
|
|
287
|
+
throw new Error('Invalid average value from EthGasStation')
|
|
288
|
+
}
|
|
289
|
+
if (fast < 1 || fast > GAS_PRICE_SANITY_CHECK) {
|
|
290
|
+
throw new Error('Invalid fastest value from EthGasStation')
|
|
291
|
+
}
|
|
292
|
+
if (fastest < 1 || fastest > GAS_PRICE_SANITY_CHECK) {
|
|
293
|
+
throw new Error('Invalid fastest value from EthGasStation')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Correct inconsistencies
|
|
297
|
+
if (average <= safeLow) average = safeLow + 1
|
|
298
|
+
if (fast <= average) fast = average + 1
|
|
299
|
+
if (fastest <= fast) fastest = fast + 1
|
|
300
|
+
|
|
301
|
+
let lowFee = safeLow
|
|
302
|
+
let standardFeeLow = fast
|
|
303
|
+
let standardFeeHigh = (fast + fastest) * 0.75
|
|
304
|
+
let highFee = fastest
|
|
305
|
+
|
|
306
|
+
lowFee = (
|
|
307
|
+
Math.round(lowFee) * ETH_GAS_STATION_WEI_MULTIPLIER
|
|
308
|
+
).toString()
|
|
309
|
+
standardFeeLow = (
|
|
310
|
+
Math.round(standardFeeLow) * ETH_GAS_STATION_WEI_MULTIPLIER
|
|
311
|
+
).toString()
|
|
312
|
+
standardFeeHigh = (
|
|
313
|
+
Math.round(standardFeeHigh) * ETH_GAS_STATION_WEI_MULTIPLIER
|
|
314
|
+
).toString()
|
|
315
|
+
highFee = (
|
|
316
|
+
Math.round(highFee) * ETH_GAS_STATION_WEI_MULTIPLIER
|
|
317
|
+
).toString()
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
gasPrice.lowFee !== lowFee ||
|
|
321
|
+
gasPrice.standardFeeLow !== standardFeeLow ||
|
|
322
|
+
gasPrice.highFee !== highFee ||
|
|
323
|
+
gasPrice.standardFeeHigh !== standardFeeHigh
|
|
324
|
+
) {
|
|
325
|
+
gasPrice.lowFee = lowFee
|
|
326
|
+
gasPrice.standardFeeLow = standardFeeLow
|
|
327
|
+
gasPrice.highFee = highFee
|
|
328
|
+
gasPrice.standardFeeHigh = standardFeeHigh
|
|
329
|
+
this.walletLocalDataDirty = true
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
throw new Error(`Error: Fetched invalid networkFees from EthGasStation`)
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
this.log.error(
|
|
336
|
+
`Error fetching ${this.currencyInfo.currencyCode} networkFees from EthGasStation`
|
|
337
|
+
)
|
|
338
|
+
this.log.error(err)
|
|
339
|
+
this.log.crash(err, { rawData: jsonObj })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async clearBlockchainCache() {
|
|
344
|
+
await super.clearBlockchainCache()
|
|
345
|
+
this.otherData.nextNonce = '0'
|
|
346
|
+
this.otherData.unconfirmedNextNonce = '0'
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ****************************************************************************
|
|
350
|
+
// Public methods
|
|
351
|
+
// ****************************************************************************
|
|
352
|
+
|
|
353
|
+
async startEngine() {
|
|
354
|
+
this.engineOn = true
|
|
355
|
+
this.addToLoop('checkUpdateNetworkFees', NETWORKFEES_POLL_MILLISECONDS)
|
|
356
|
+
|
|
357
|
+
this.ethNetwork.needsLoop()
|
|
358
|
+
|
|
359
|
+
super.startEngine()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async resyncBlockchain() {
|
|
363
|
+
await this.killEngine()
|
|
364
|
+
await this.clearBlockchainCache()
|
|
365
|
+
await this.startEngine()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async makeSpend(edgeSpendInfoIn) {
|
|
369
|
+
const { edgeSpendInfo, currencyCode } = super.makeSpend(edgeSpendInfoIn)
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
For RBF transactions, get the gas price and limit (fees) of the existing
|
|
373
|
+
transaction as well as the current nonce. The fees and the nonce will be
|
|
374
|
+
used instead of the calculated equivalents.
|
|
375
|
+
*/
|
|
376
|
+
let rbfGasPrice
|
|
377
|
+
let rbfGasLimit
|
|
378
|
+
let rbfNonce
|
|
379
|
+
const rbfTxid =
|
|
380
|
+
edgeSpendInfo.rbfTxid && normalizeAddress(edgeSpendInfo.rbfTxid)
|
|
381
|
+
if (rbfTxid) {
|
|
382
|
+
const rbfTxIndex = this.findTransaction(currencyCode, rbfTxid)
|
|
383
|
+
|
|
384
|
+
if (rbfTxIndex > -1) {
|
|
385
|
+
const rbfTrx = this.transactionList[currencyCode][rbfTxIndex]
|
|
386
|
+
|
|
387
|
+
if (rbfTrx.otherParams) {
|
|
388
|
+
const { gasPrice, gas, nonceUsed } = rbfTrx.otherParams
|
|
389
|
+
rbfGasPrice = bns.mul(gasPrice, '2')
|
|
390
|
+
rbfGasLimit = gas
|
|
391
|
+
rbfNonce = nonceUsed
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!rbfGasPrice || !rbfGasLimit || !rbfNonce) {
|
|
396
|
+
throw new Error('Missing data to complete RBF transaction.')
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Ethereum can only have one output
|
|
401
|
+
if (edgeSpendInfo.spendTargets.length !== 1) {
|
|
402
|
+
throw new Error('Error: only one output allowed')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const spendTarget = edgeSpendInfo.spendTargets[0]
|
|
406
|
+
const publicAddress = spendTarget.publicAddress
|
|
407
|
+
if (!EthereumUtil.isValidAddress(publicAddress)) {
|
|
408
|
+
throw new TypeError(`Invalid ${this.currencyInfo.pluginId} address`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let data =
|
|
412
|
+
spendTarget.otherParams != null ? spendTarget.otherParams.data : undefined
|
|
413
|
+
|
|
414
|
+
let otherParams = {}
|
|
415
|
+
|
|
416
|
+
let gasPrice
|
|
417
|
+
let gasLimit
|
|
418
|
+
let useDefaults = false
|
|
419
|
+
|
|
420
|
+
// Use RBF gas price and gas limit when present, otherwise, calculate mining fees
|
|
421
|
+
if (rbfGasPrice && rbfGasLimit) {
|
|
422
|
+
gasPrice = rbfGasPrice
|
|
423
|
+
gasLimit = rbfGasLimit
|
|
424
|
+
} else {
|
|
425
|
+
const miningFees = calcMiningFee(
|
|
426
|
+
edgeSpendInfo,
|
|
427
|
+
this.walletLocalData.otherData.networkFees,
|
|
428
|
+
this.currencyInfo
|
|
429
|
+
)
|
|
430
|
+
gasPrice = miningFees.gasPrice
|
|
431
|
+
gasLimit = miningFees.gasLimit
|
|
432
|
+
useDefaults = miningFees.useDefaults
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const defaultGasLimit = gasLimit
|
|
436
|
+
let nativeAmount = edgeSpendInfo.spendTargets[0].nativeAmount
|
|
437
|
+
|
|
438
|
+
let contractAddress
|
|
439
|
+
let value
|
|
440
|
+
if (currencyCode === this.currencyInfo.currencyCode) {
|
|
441
|
+
const ethParams = {
|
|
442
|
+
from: [this.walletLocalData.publicKey],
|
|
443
|
+
to: [publicAddress],
|
|
444
|
+
gas: gasLimit,
|
|
445
|
+
gasPrice: gasPrice,
|
|
446
|
+
gasUsed: '0',
|
|
447
|
+
cumulativeGasUsed: '0',
|
|
448
|
+
errorVal: 0,
|
|
449
|
+
tokenRecipientAddress: null,
|
|
450
|
+
nonceArg: rbfNonce,
|
|
451
|
+
rbfTxid,
|
|
452
|
+
data
|
|
453
|
+
}
|
|
454
|
+
otherParams = ethParams
|
|
455
|
+
value = bns.add(nativeAmount, '0', 16)
|
|
456
|
+
} else {
|
|
457
|
+
if (data) {
|
|
458
|
+
contractAddress = publicAddress
|
|
459
|
+
} else {
|
|
460
|
+
const tokenInfo = this.getTokenInfo(currencyCode)
|
|
461
|
+
if (!tokenInfo || typeof tokenInfo.contractAddress !== 'string') {
|
|
462
|
+
throw new Error(
|
|
463
|
+
'Error: Token not supported or invalid contract address'
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
contractAddress = tokenInfo.contractAddress
|
|
468
|
+
value = '0x0'
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const ethParams = {
|
|
472
|
+
from: [this.walletLocalData.publicKey],
|
|
473
|
+
to: [contractAddress],
|
|
474
|
+
gas: gasLimit,
|
|
475
|
+
gasPrice: gasPrice,
|
|
476
|
+
gasUsed: '0',
|
|
477
|
+
cumulativeGasUsed: '0',
|
|
478
|
+
errorVal: 0,
|
|
479
|
+
tokenRecipientAddress: publicAddress,
|
|
480
|
+
nonceArg: rbfNonce,
|
|
481
|
+
rbfTxid,
|
|
482
|
+
data
|
|
483
|
+
}
|
|
484
|
+
otherParams = ethParams
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// If the recipient or contractaddress has changed from previous makeSpend(), calculate the gasLimit
|
|
488
|
+
if (
|
|
489
|
+
useDefaults &&
|
|
490
|
+
(this.lastEstimatedGasLimit.publicAddress !== publicAddress ||
|
|
491
|
+
this.lastEstimatedGasLimit.contractAddress !== contractAddress ||
|
|
492
|
+
this.lastEstimatedGasLimit.gasLimit === '')
|
|
493
|
+
) {
|
|
494
|
+
if (!data) {
|
|
495
|
+
const dataArray = abi.simpleEncode(
|
|
496
|
+
'transfer(address,uint256):(uint256)',
|
|
497
|
+
contractAddress || publicAddress,
|
|
498
|
+
value
|
|
499
|
+
)
|
|
500
|
+
data = '0x' + Buffer.from(dataArray).toString('hex')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const estimateGasParams = [
|
|
504
|
+
{
|
|
505
|
+
to: contractAddress || publicAddress,
|
|
506
|
+
from: this.walletLocalData.publicKey,
|
|
507
|
+
gas: '0xffffff',
|
|
508
|
+
value,
|
|
509
|
+
data
|
|
510
|
+
},
|
|
511
|
+
'latest'
|
|
512
|
+
]
|
|
513
|
+
try {
|
|
514
|
+
// Determine if recipient is a normal or contract address
|
|
515
|
+
const getCodeResult = await this.ethNetwork.multicastServers(
|
|
516
|
+
'eth_getCode',
|
|
517
|
+
[contractAddress || publicAddress, 'latest']
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
if (getCodeResult.result.result !== '0x') {
|
|
522
|
+
const estimateGasResult = await this.ethNetwork.multicastServers(
|
|
523
|
+
'eth_estimateGas',
|
|
524
|
+
estimateGasParams
|
|
525
|
+
)
|
|
526
|
+
this.log.warn(
|
|
527
|
+
'lookhere estimateGas estimateGasResult',
|
|
528
|
+
JSON.stringify(estimateGasResult)
|
|
529
|
+
)
|
|
530
|
+
gasLimit = bns.add(
|
|
531
|
+
parseInt(estimateGasResult.result.result, 16).toString(),
|
|
532
|
+
'0'
|
|
533
|
+
)
|
|
534
|
+
// Overestimate gas limit to reduce chance of failure when sending to a contract
|
|
535
|
+
if (currencyCode === this.currencyInfo.currencyCode) {
|
|
536
|
+
// Double gas limit estimate when sending ETH to contract
|
|
537
|
+
gasLimit = bns.mul(gasLimit, '2')
|
|
538
|
+
} else {
|
|
539
|
+
// For tokens, double estimate if it's less than half of default, otherwise use default. For estimates beyond default value, use the estimate as-is.
|
|
540
|
+
gasLimit = bns.lt(gasLimit, bns.div(defaultGasLimit, '2'))
|
|
541
|
+
? bns.mul(gasLimit, '2')
|
|
542
|
+
: bns.lt(gasLimit, defaultGasLimit)
|
|
543
|
+
? defaultGasLimit
|
|
544
|
+
: gasLimit
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
gasLimit = '21000'
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
// If we know the address is a contract but estimateGas fails use the default token gas limit
|
|
551
|
+
if (
|
|
552
|
+
this.currencyInfo.defaultSettings.otherSettings.defaultNetworkFees
|
|
553
|
+
.default.gasLimit.tokenTransaction != null
|
|
554
|
+
)
|
|
555
|
+
gasLimit =
|
|
556
|
+
this.currencyInfo.defaultSettings.otherSettings.defaultNetworkFees
|
|
557
|
+
.default.gasLimit.tokenTransaction
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Sanity check calculated value
|
|
561
|
+
if (bns.lt(gasLimit, '21000')) {
|
|
562
|
+
gasLimit = defaultGasLimit
|
|
563
|
+
this.lastEstimatedGasLimit.gasLimit = ''
|
|
564
|
+
throw new Error('Calculated gasLimit less than minimum')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Save locally to compare for future makeSpend() calls
|
|
568
|
+
this.lastEstimatedGasLimit = {
|
|
569
|
+
publicAddress,
|
|
570
|
+
contractAddress,
|
|
571
|
+
gasLimit
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
this.log.error(`makeSpend Error determining gas limit ${err}`)
|
|
575
|
+
}
|
|
576
|
+
} else if (useDefaults) {
|
|
577
|
+
// If recipient and contract address are the same from the previous makeSpend(), use the previously calculated gasLimit
|
|
578
|
+
gasLimit = this.lastEstimatedGasLimit.gasLimit
|
|
579
|
+
}
|
|
580
|
+
otherParams.gas = gasLimit
|
|
581
|
+
|
|
582
|
+
const nativeBalance =
|
|
583
|
+
this.walletLocalData.totalBalances[this.currencyInfo.currencyCode]
|
|
584
|
+
|
|
585
|
+
let nativeNetworkFee = bns.mul(gasPrice, gasLimit)
|
|
586
|
+
let totalTxAmount = '0'
|
|
587
|
+
let parentNetworkFee = null
|
|
588
|
+
|
|
589
|
+
if (currencyCode === this.currencyInfo.currencyCode) {
|
|
590
|
+
totalTxAmount = bns.add(nativeNetworkFee, nativeAmount)
|
|
591
|
+
if (bns.gt(totalTxAmount, nativeBalance)) {
|
|
592
|
+
throw new InsufficientFundsError()
|
|
593
|
+
}
|
|
594
|
+
nativeAmount = bns.mul(totalTxAmount, '-1')
|
|
595
|
+
} else {
|
|
596
|
+
parentNetworkFee = nativeNetworkFee
|
|
597
|
+
// Check if there's enough parent currency to pay the transaction fee, and if not return the parent currency code and amount
|
|
598
|
+
if (bns.gt(nativeNetworkFee, nativeBalance)) {
|
|
599
|
+
throw new InsufficientFundsError({
|
|
600
|
+
currencyCode: this.currencyInfo.currencyCode,
|
|
601
|
+
networkFee: nativeNetworkFee
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
const balanceToken = this.walletLocalData.totalBalances[currencyCode]
|
|
605
|
+
if (bns.gt(nativeAmount, balanceToken)) {
|
|
606
|
+
throw new InsufficientFundsError()
|
|
607
|
+
}
|
|
608
|
+
nativeNetworkFee = '0' // Do not show a fee for token transactions.
|
|
609
|
+
nativeAmount = bns.mul(nativeAmount, '-1')
|
|
610
|
+
}
|
|
611
|
+
// **********************************
|
|
612
|
+
// Create the unsigned EdgeTransaction
|
|
613
|
+
|
|
614
|
+
// This is used for display purposes in the GUI
|
|
615
|
+
const feeRateUsed = {
|
|
616
|
+
// Convert gasPrice from wei to gwei
|
|
617
|
+
gasPrice: bns.div(
|
|
618
|
+
gasPrice,
|
|
619
|
+
WEI_MULTIPLIER.toString(),
|
|
620
|
+
WEI_MULTIPLIER.toString().length - 1
|
|
621
|
+
),
|
|
622
|
+
gasLimit
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const edgeTransaction = {
|
|
626
|
+
txid: '', // txid
|
|
627
|
+
date: 0, // date
|
|
628
|
+
currencyCode, // currencyCode
|
|
629
|
+
blockHeight: 0, // blockHeight
|
|
630
|
+
nativeAmount, // nativeAmount
|
|
631
|
+
networkFee: nativeNetworkFee, // networkFee
|
|
632
|
+
feeRateUsed,
|
|
633
|
+
ourReceiveAddresses: [], // ourReceiveAddresses
|
|
634
|
+
signedTx: '', // signedTx
|
|
635
|
+
otherParams // otherParams
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (parentNetworkFee) {
|
|
639
|
+
edgeTransaction.parentNetworkFee = parentNetworkFee
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return edgeTransaction
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async signTx(edgeTransaction) {
|
|
646
|
+
const otherParams = getOtherParams(edgeTransaction)
|
|
647
|
+
|
|
648
|
+
// Do signing
|
|
649
|
+
const gasLimitHex = toHex(otherParams.gas)
|
|
650
|
+
const gasPriceHex = toHex(otherParams.gasPrice)
|
|
651
|
+
let nativeAmountHex
|
|
652
|
+
|
|
653
|
+
if (edgeTransaction.currencyCode === this.currencyInfo.currencyCode) {
|
|
654
|
+
// Remove the networkFee from the nativeAmount
|
|
655
|
+
const nativeAmount = bns.add(
|
|
656
|
+
edgeTransaction.nativeAmount,
|
|
657
|
+
edgeTransaction.networkFee
|
|
658
|
+
)
|
|
659
|
+
nativeAmountHex = bns.mul('-1', nativeAmount, 16)
|
|
660
|
+
} else {
|
|
661
|
+
nativeAmountHex = bns.mul('-1', edgeTransaction.nativeAmount, 16)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Nonce:
|
|
665
|
+
|
|
666
|
+
const nonceArg = otherParams.nonceArg
|
|
667
|
+
let nonce = nonceArg
|
|
668
|
+
if (!nonce) {
|
|
669
|
+
// Use an unconfirmed nonce if
|
|
670
|
+
// 1. We have unconfirmed spending txs in the transaction list
|
|
671
|
+
// 2. It is greater than the confirmed nonce
|
|
672
|
+
// 3. Is no more than 5 higher than confirmed nonce
|
|
673
|
+
// Othewise, use the next nonce
|
|
674
|
+
if (
|
|
675
|
+
this.walletLocalData.numUnconfirmedSpendTxs &&
|
|
676
|
+
bns.gt(
|
|
677
|
+
this.walletLocalData.otherData.unconfirmedNextNonce,
|
|
678
|
+
this.walletLocalData.otherData.nextNonce
|
|
679
|
+
)
|
|
680
|
+
) {
|
|
681
|
+
const diff = bns.sub(
|
|
682
|
+
this.walletLocalData.otherData.unconfirmedNextNonce,
|
|
683
|
+
this.walletLocalData.otherData.nextNonce
|
|
684
|
+
)
|
|
685
|
+
if (bns.lte(diff, '5')) {
|
|
686
|
+
nonce = this.walletLocalData.otherData.unconfirmedNextNonce
|
|
687
|
+
this.walletLocalData.otherData.unconfirmedNextNonce = bns.add(
|
|
688
|
+
this.walletLocalData.otherData.unconfirmedNextNonce,
|
|
689
|
+
'1'
|
|
690
|
+
)
|
|
691
|
+
this.walletLocalDataDirty = true
|
|
692
|
+
} else {
|
|
693
|
+
const e = new Error('Excessive pending spend transactions')
|
|
694
|
+
e.name = 'ErrorExcessivePendingSpends'
|
|
695
|
+
throw e
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
nonce = this.walletLocalData.otherData.nextNonce
|
|
699
|
+
this.walletLocalData.otherData.unconfirmedNextNonce = bns.add(
|
|
700
|
+
this.walletLocalData.otherData.nextNonce,
|
|
701
|
+
'1'
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Convert nonce to hex for tsParams
|
|
706
|
+
const nonceHex = toHex(nonce)
|
|
707
|
+
|
|
708
|
+
// Data:
|
|
709
|
+
|
|
710
|
+
let data
|
|
711
|
+
if (otherParams.data != null) {
|
|
712
|
+
data = otherParams.data
|
|
713
|
+
} else if (
|
|
714
|
+
edgeTransaction.currencyCode === this.currencyInfo.currencyCode
|
|
715
|
+
) {
|
|
716
|
+
data = ''
|
|
717
|
+
} else {
|
|
718
|
+
const dataArray = abi.simpleEncode(
|
|
719
|
+
'transfer(address,uint256):(uint256)',
|
|
720
|
+
otherParams.tokenRecipientAddress,
|
|
721
|
+
nativeAmountHex
|
|
722
|
+
)
|
|
723
|
+
data = '0x' + Buffer.from(dataArray).toString('hex')
|
|
724
|
+
nativeAmountHex = '0x00'
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Select the chain
|
|
728
|
+
const otherSettings =
|
|
729
|
+
this.currencyInfo.defaultSettings.otherSettings
|
|
730
|
+
const { chainParams } = otherSettings
|
|
731
|
+
const common = Common.custom(chainParams)
|
|
732
|
+
|
|
733
|
+
// Transaction Parameters
|
|
734
|
+
const txParams = {
|
|
735
|
+
nonce: nonceHex,
|
|
736
|
+
gasPrice: gasPriceHex,
|
|
737
|
+
gasLimit: gasLimitHex,
|
|
738
|
+
to: otherParams.to[0],
|
|
739
|
+
value: nativeAmountHex,
|
|
740
|
+
data
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const privKey = Buffer.from(
|
|
744
|
+
this.walletInfo.keys[`${this.currencyInfo.pluginId}Key`],
|
|
745
|
+
'hex'
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
// Log the private key address
|
|
749
|
+
const wallet = ethWallet.fromPrivateKey(privKey)
|
|
750
|
+
this.log.warn(`signTx getAddressString ${wallet.getAddressString()}`)
|
|
751
|
+
|
|
752
|
+
// Create and sign transaction
|
|
753
|
+
const unsignedTx = Transaction.fromTxData(txParams, { common })
|
|
754
|
+
const signedTx = unsignedTx.sign(privKey)
|
|
755
|
+
|
|
756
|
+
edgeTransaction.signedTx = bufToHex(signedTx.serialize())
|
|
757
|
+
edgeTransaction.txid = bufToHex(signedTx.hash())
|
|
758
|
+
edgeTransaction.date = Date.now() / 1000
|
|
759
|
+
if (edgeTransaction.otherParams) {
|
|
760
|
+
edgeTransaction.otherParams.nonceUsed = nonce
|
|
761
|
+
}
|
|
762
|
+
this.log.warn(`signTx\n${cleanTxLogs(edgeTransaction)}`)
|
|
763
|
+
return edgeTransaction
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async broadcastTx(
|
|
767
|
+
edgeTransaction
|
|
768
|
+
) {
|
|
769
|
+
await this.ethNetwork.multicastServers('broadcastTx', edgeTransaction)
|
|
770
|
+
|
|
771
|
+
// Success
|
|
772
|
+
this.log.warn(`SUCCESS broadcastTx\n${cleanTxLogs(edgeTransaction)}`)
|
|
773
|
+
|
|
774
|
+
return edgeTransaction
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
getDisplayPrivateSeed() {
|
|
778
|
+
if (
|
|
779
|
+
this.walletInfo.keys &&
|
|
780
|
+
this.walletInfo.keys[`${this.currencyInfo.pluginId}Key`]
|
|
781
|
+
) {
|
|
782
|
+
return this.walletInfo.keys[`${this.currencyInfo.pluginId}Key`]
|
|
783
|
+
}
|
|
784
|
+
return ''
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
getDisplayPublicSeed() {
|
|
788
|
+
if (this.walletInfo.keys && this.walletInfo.keys.publicKey) {
|
|
789
|
+
return this.walletInfo.keys.publicKey
|
|
790
|
+
}
|
|
791
|
+
return ''
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Overload saveTx to mutate replaced transactions by RBF
|
|
795
|
+
async saveTx(edgeTransaction) {
|
|
796
|
+
// We must check if this transaction replaces another transaction
|
|
797
|
+
if (edgeTransaction.otherParams && edgeTransaction.otherParams.rbfTxid) {
|
|
798
|
+
const { currencyCode } = edgeTransaction
|
|
799
|
+
|
|
800
|
+
// Get the replaced transaction using the rbfTxid
|
|
801
|
+
const txid = edgeTransaction.otherParams.rbfTxid
|
|
802
|
+
const idx = this.findTransaction(currencyCode, txid)
|
|
803
|
+
const replacedEdgeTransaction = this.transactionList[currencyCode][idx]
|
|
804
|
+
|
|
805
|
+
// Use the RBF metadata because metadata for replaced transaction is not
|
|
806
|
+
// present in edge-currency-accountbased state
|
|
807
|
+
const metadata = edgeTransaction.metadata
|
|
808
|
+
|
|
809
|
+
// Update the transaction's blockHeight to -1 (drops the transaction)
|
|
810
|
+
const updatedEdgeTransaction = {
|
|
811
|
+
...replacedEdgeTransaction,
|
|
812
|
+
metadata,
|
|
813
|
+
blockHeight: -1
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
this.addTransaction(currencyCode, updatedEdgeTransaction)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
super.saveTx(edgeTransaction)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async addCustomToken(obj) {
|
|
823
|
+
let contractAddress = obj.contractAddress.replace('0x', '').toLowerCase()
|
|
824
|
+
if (!isHex(contractAddress) || contractAddress.length !== 40) {
|
|
825
|
+
throw new Error('ErrorInvalidContractAddress')
|
|
826
|
+
}
|
|
827
|
+
contractAddress = '0x' + contractAddress
|
|
828
|
+
super.addCustomToken(obj, contractAddress)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export { CurrencyEngine }
|