edge-currency-plugins 3.12.0 → 3.14.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.
Files changed (37) hide show
  1. package/android/src/main/assets/edge-currency-plugins/edge-currency-plugins.js +48 -48
  2. package/lib/common/constants.js +4 -1
  3. package/lib/common/fees/calcMinerFeePerByte.js +19 -5
  4. package/lib/common/fees/calcReplacementFeeRate.js +25 -0
  5. package/lib/common/fees/processMempoolSpaceFees.js +12 -16
  6. package/lib/common/plugin/types.js +6 -0
  7. package/lib/common/utxobased/engine/ServerStates.js +196 -95
  8. package/lib/common/utxobased/engine/UtxoEngine.js +16 -3
  9. package/lib/common/utxobased/info/bitcoin.js +4 -4
  10. package/lib/common/utxobased/info/bitcoincash.js +1 -1
  11. package/lib/common/utxobased/info/bitcointestnet.js +1 -1
  12. package/lib/common/utxobased/info/bitcointestnet4.js +1 -1
  13. package/lib/common/utxobased/info/dash.js +1 -1
  14. package/lib/common/utxobased/info/digibyte.js +1 -1
  15. package/lib/common/utxobased/info/dogecoin.js +1 -1
  16. package/lib/common/utxobased/info/ecash.js +4 -1
  17. package/lib/common/utxobased/info/litecoin.js +3 -3
  18. package/lib/common/utxobased/info/pivx.js +5 -5
  19. package/lib/common/utxobased/info/qtum.js +1 -1
  20. package/lib/common/utxobased/info/vertcoin.js +1 -1
  21. package/lib/common/utxobased/info/zcoin.js +1 -1
  22. package/lib/common/utxobased/keymanager/utxopicker/accumulative.js +4 -4
  23. package/lib/common/utxobased/keymanager/utxopicker/forceUseUtxo.js +5 -5
  24. package/lib/common/utxobased/keymanager/utxopicker/subtractFee.js +1 -1
  25. package/lib/common/utxobased/keymanager/utxopicker/utils.js +27 -3
  26. package/lib/common/utxobased/network/Blockbook.js +10 -2
  27. package/lib/src/common/constants.d.ts +1 -1
  28. package/lib/src/common/fees/calcReplacementFeeRate.d.ts +15 -0
  29. package/lib/src/common/plugin/types.d.ts +7 -1
  30. package/lib/src/common/utxobased/engine/ServerStates.d.ts +21 -0
  31. package/lib/src/common/utxobased/keymanager/utxopicker/utils.d.ts +12 -0
  32. package/lib/test/common/fees/calcReplacementFeeRate.spec.d.ts +1 -0
  33. package/lib/test/common/fees/processMempoolSpaceFees.spec.d.ts +1 -0
  34. package/lib/test/common/utxobased/engine/ServerStates.spec.d.ts +1 -0
  35. package/lib/test/common/utxobased/keymanager/utxopicker/decimalFeeRate.spec.d.ts +1 -0
  36. package/lib/test/common/utxobased/keymanager/utxopicker/utils.spec.d.ts +1 -0
  37. package/package.json +1 -1
@@ -3,7 +3,10 @@
3
3
  // Fees
4
4
  const FEES_PATH = 'fees.json'; exports.FEES_PATH = FEES_PATH
5
5
  const MAX_FEE = 999999999.0; exports.MAX_FEE = MAX_FEE
6
- const LOW_FEE = 2; exports.LOW_FEE = LOW_FEE
6
+ // Fee rates are sat/vB and may be fractional. Three decimal places matches the
7
+ // precision of the mempool.space `/fees/precise` endpoint, which is the most
8
+ // precise rate source we consume.
9
+ const FEE_RATE_DECIMALS = 3; exports.FEE_RATE_DECIMALS = FEE_RATE_DECIMALS
7
10
  const MAX_HIGH_DELAY = 200; exports.MAX_HIGH_DELAY = MAX_HIGH_DELAY
8
11
  const MAX_STANDARD_DELAY = 6; exports.MAX_STANDARD_DELAY = MAX_STANDARD_DELAY
9
12
  const MIN_STANDARD_DELAY = 2; exports.MIN_STANDARD_DELAY = MIN_STANDARD_DELAY
@@ -1,6 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _biggystring = require('biggystring');
2
2
 
3
3
 
4
+ var _constants = require('../constants');
4
5
 
5
6
 
6
7
 
@@ -29,10 +30,20 @@
29
30
  } = feeInfo
30
31
  let { highFee, lowFee, standardFeeHigh, standardFeeLow } = feeInfo
31
32
 
32
- highFee = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, highFee, highFeeFudgeFactor), 0)
33
- lowFee = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, lowFee, lowFeeFudgeFactor), 0)
34
- standardFeeHigh = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, standardFeeHigh, standardFeeHighFudgeFactor), 0)
35
- standardFeeLow = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, standardFeeLow, standardFeeLowFudgeFactor), 0)
33
+ // Fee rates may be fractional, so these round to FEE_RATE_DECIMALS places
34
+ // rather than to a whole sat/vB. biggystring's `round` takes the power of ten
35
+ // to round at, so the precision is negated: -3 means three decimal places.
36
+ const feeRatePlaces = -_constants.FEE_RATE_DECIMALS
37
+ highFee = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, highFee, highFeeFudgeFactor), feeRatePlaces)
38
+ lowFee = _biggystring.round.call(void 0, _biggystring.mul.call(void 0, lowFee, lowFeeFudgeFactor), feeRatePlaces)
39
+ standardFeeHigh = _biggystring.round.call(void 0,
40
+ _biggystring.mul.call(void 0, standardFeeHigh, standardFeeHighFudgeFactor),
41
+ feeRatePlaces
42
+ )
43
+ standardFeeLow = _biggystring.round.call(void 0,
44
+ _biggystring.mul.call(void 0, standardFeeLow, standardFeeLowFudgeFactor),
45
+ feeRatePlaces
46
+ )
36
47
 
37
48
  switch (networkFeeOption) {
38
49
  case 'low':
@@ -55,7 +66,10 @@
55
66
 
56
67
  // Add this much to the low fee = (amountDiffFromLow * lowHighFeeDiff) / lowHighAmountDiff)
57
68
  const temp1 = _biggystring.mul.call(void 0, amountDiffFromLow, lowHighFeeDiff)
58
- const addFeeToLow = _biggystring.div.call(void 0, temp1, lowHighAmountDiff)
69
+ // `div` defaults to zero decimal places, which would truncate the
70
+ // interpolated rate back to a whole sat/vB. Unlike `round` above, its
71
+ // precision argument is the number of decimal places, not a power of ten.
72
+ const addFeeToLow = _biggystring.div.call(void 0, temp1, lowHighAmountDiff, _constants.FEE_RATE_DECIMALS)
59
73
  return _biggystring.add.call(void 0, standardFeeLow, addFeeToLow)
60
74
  }
61
75
 
@@ -0,0 +1,25 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _constants = require('../constants');
2
+
3
+ /**
4
+ * Calculate the sat/vB fee rate for a transaction that replaces another by fee:
5
+ * double the rate the replaced transaction paid.
6
+ *
7
+ * The rate rounds up to FEE_RATE_DECIMALS places. Rounding up means the
8
+ * replacement never pays less than double, and a positive rate never rounds
9
+ * down to zero, which the UTXO picker would accept as a valid rate. Rounding
10
+ * to a fixed number of places keeps the rate readable where it is reported as
11
+ * `feeRateUsed`.
12
+ *
13
+ * @param replacedFee The fee the replaced transaction paid, in satoshis
14
+ * @param replacedVBytes The replaced transaction's virtual size
15
+ * @returns {number}
16
+ */
17
+ const calcReplacementFeeRate = (
18
+ replacedFee,
19
+ replacedVBytes
20
+ ) => {
21
+ const scale = 10 ** _constants.FEE_RATE_DECIMALS
22
+ // The fee and size are whole numbers, so this division is exact whenever the
23
+ // scaled rate is, and Math.ceil sees no floating point noise.
24
+ return Math.ceil((replacedFee * 2 * scale) / replacedVBytes) / scale
25
+ }; exports.calcReplacementFeeRate = calcReplacementFeeRate
@@ -1,7 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }var _biggystring = require('biggystring'); var bs = _interopRequireWildcard(_biggystring);
2
2
  var _cleaners = require('cleaners');
3
3
 
4
- var _constants = require('../constants');
5
4
 
6
5
 
7
6
  const asMempoolSpaceFees = _cleaners.asObject.call(void 0, {
@@ -12,13 +11,16 @@ var _constants = require('../constants');
12
11
 
13
12
  /**
14
13
  * Calculates the FeeRates object from MempoolSpace
15
- * Sets the minimum of LOW_FEE on all fee levels
16
14
  * @param fees
17
15
  * @returns Partial<FeeRates>
18
16
  */
19
17
 
20
- const STANDARD_FEE_HIGH_MULTIPLIER = 1.3
21
- const HIGH_FEE_MULTIPLIER = 2
18
+ // The `/fees/precise` endpoint reports rates with up to three decimal places,
19
+ // so the multipliers run through biggystring rather than JS numbers. A float
20
+ // multiply would reintroduce noise (0.575 * 1.3 is 0.7474999999999999) that
21
+ // biggystring carries exactly.
22
+ const STANDARD_FEE_HIGH_MULTIPLIER = '1.3'
23
+ const HIGH_FEE_MULTIPLIER = '2'
22
24
 
23
25
  const processMempoolSpaceFees = (fees) => {
24
26
  let mempoolFees
@@ -28,19 +30,13 @@ const HIGH_FEE_MULTIPLIER = 2
28
30
  return null
29
31
  }
30
32
 
31
- const { fastestFee, halfHourFee } = mempoolFees
32
-
33
- const lowFee = halfHourFee.toString()
34
- const standardFeeLow = fastestFee.toString()
35
- const standardFeeHigh = Math.round(
36
- fastestFee * STANDARD_FEE_HIGH_MULTIPLIER
37
- ).toString()
38
- const highFee = Math.round(fastestFee * HIGH_FEE_MULTIPLIER).toString()
33
+ const fastestFee = mempoolFees.fastestFee.toString()
34
+ const halfHourFee = mempoolFees.halfHourFee.toString()
39
35
 
40
36
  return {
41
- lowFee: bs.max(lowFee, _constants.LOW_FEE.toString()),
42
- standardFeeLow: bs.max(standardFeeLow, _constants.LOW_FEE.toString()),
43
- standardFeeHigh: bs.max(standardFeeHigh, _constants.LOW_FEE.toString()),
44
- highFee: bs.max(highFee, _constants.LOW_FEE.toString())
37
+ lowFee: halfHourFee,
38
+ standardFeeLow: fastestFee,
39
+ standardFeeHigh: bs.mul(fastestFee, STANDARD_FEE_HIGH_MULTIPLIER),
40
+ highFee: bs.mul(fastestFee, HIGH_FEE_MULTIPLIER)
45
41
  }
46
42
  }; exports.processMempoolSpaceFees = processMempoolSpaceFees
@@ -313,6 +313,12 @@ var _types3 = require('../utxobased/db/types');
313
313
 
314
314
 
315
315
 
316
+
317
+
318
+
319
+
320
+
321
+
316
322
 
317
323
 
318
324
 
@@ -91,6 +91,47 @@ var _constants = require('./constants');
91
91
 
92
92
 
93
93
 
94
+
95
+
96
+
97
+
98
+
99
+
100
+ /**
101
+ * How long broadcastTx gives the first wave (our sockets and Edge's own HTTP
102
+ * servers) before it also sends the transaction to the NOWNodes HTTP servers.
103
+ * NOWNodes is a third party, so it should only see a transaction when our own
104
+ * infrastructure has not already carried it. A first wave that fails outright
105
+ * does not wait this long; the second wave fires as soon as it has all failed.
106
+ */
107
+ const NOWNODES_BROADCAST_DELAY_MS = 2000; exports.NOWNODES_BROADCAST_DELAY_MS = NOWNODES_BROADCAST_DELAY_MS
108
+
109
+ /**
110
+ * Every broadcast attempt is counted toward "all attempts failed", so every
111
+ * attempt must settle. Sockets already expire a request after 30 seconds
112
+ * (Socket.ts); HTTP has no such bound of its own, and a server that accepts
113
+ * the connection and never answers would otherwise hold the whole broadcast
114
+ * open. This mirrors the socket figure.
115
+ */
116
+ const BROADCAST_ATTEMPT_TIMEOUT_MS = 30000; exports.BROADCAST_ATTEMPT_TIMEOUT_MS = BROADCAST_ATTEMPT_TIMEOUT_MS
117
+
118
+ const withTimeout = async (
119
+ promise,
120
+ ms,
121
+ message
122
+ ) => {
123
+ let timer
124
+ const timeout = new Promise((resolve, reject) => {
125
+ timer = setTimeout(() => {
126
+ reject(new Error(message))
127
+ }, ms)
128
+ })
129
+ try {
130
+ return await Promise.race([promise, timeout])
131
+ } finally {
132
+ if (timer != null) clearTimeout(timer)
133
+ }
134
+ }
94
135
 
95
136
  function makeServerStates(config) {
96
137
  const {
@@ -103,6 +144,7 @@ var _constants = require('./constants');
103
144
  walletInfo
104
145
  } = config
105
146
  const { serverConfigs = [] } = pluginInfo.engineInfo
147
+ const { broadcastTimeoutMs = exports.BROADCAST_ATTEMPT_TIMEOUT_MS } = config
106
148
  log('Making server states')
107
149
 
108
150
  const serverStatesCache = {}
@@ -366,112 +408,171 @@ var _constants = require('./constants');
366
408
  }
367
409
 
368
410
  const instance = {
411
+ /**
412
+ * Broadcast in two waves:
413
+ *
414
+ * 1. Immediately: every blockbook in the connection cache (whether or not
415
+ * its socket has finished connecting; a queued request transmits as
416
+ * soon as the socket opens, and times out otherwise) and every
417
+ * `blockbook` HTTP server, which are Edge's own.
418
+ * 2. After NOWNODES_BROADCAST_DELAY_MS, or as soon as every first-wave
419
+ * attempt has failed, whichever comes first: the `blockbook-nownode`
420
+ * HTTP servers. These are a third party and only see the transaction
421
+ * when our own infrastructure has not already carried it.
422
+ *
423
+ * The first success wins. The promise rejects only after every attempt
424
+ * has failed. Earlier versions only used HTTP when no socket was
425
+ * connected, so a single socket that looked connected but never answered
426
+ * could fail the whole broadcast without any HTTP attempt.
427
+ */
369
428
  async broadcastTx(transaction) {
370
429
  return await new Promise((resolve, reject) => {
371
- let resolved = false
372
- let bad = 0
373
-
374
- const wsUris = Object.keys(serverStatesCache).filter(
375
- uri => serverStatesCache[uri].blockbook != null
376
- )
377
-
378
- // Determine if there are any connected blockbook instances
379
- const isAnyBlockbookConnected = wsUris.some(
380
- uri => serverStatesCache[uri].blockbook.isConnected
381
- )
382
-
383
- if (isAnyBlockbookConnected) {
384
- for (const uri of wsUris) {
385
- const { blockbook } = serverStatesCache[uri]
386
- if (blockbook == null) continue
387
- blockbook
388
- .broadcastTx(transaction)
389
- .then(response => {
390
- if (!resolved) {
391
- resolved = true
392
- resolve(response.result)
393
- }
394
- })
395
- .catch((e) => {
396
- if (++bad === wsUris.length) {
397
- const msg = e != null ? `With error ${e.message}` : ''
398
- log.error(
399
- `broadcastTx fail: ${JSON.stringify(transaction)}\n${msg}`
400
- )
401
- reject(e)
402
- }
403
- })
430
+
431
+
432
+
433
+
434
+
435
+ const httpAttempt = (
436
+ uri,
437
+ headers
438
+ ) => ({
439
+ uri,
440
+ run: async () => {
441
+ const response = await io.fetchCors(`${uri}/api/v2/sendtx/`, {
442
+ method: 'POST',
443
+ headers,
444
+ body: transaction.signedTx
445
+ })
446
+ if (!response.ok) {
447
+ throw new Error(
448
+ `Failed to broadcast transaction via Blockbook: HTTP ${response.status}`
449
+ )
450
+ }
451
+ const json = await response.json()
452
+ return _blockbookApi.asBlockbookResponse.call(void 0, (0, _blockbookApi.asBroadcastTxResponse))(json).result
404
453
  }
454
+ })
455
+
456
+ //
457
+ // Build both waves up front, so `attempts` is the full count and an
458
+ // all-fail first wave cannot reject before the second has run.
459
+ //
460
+ const firstWave = []
461
+ const secondWave = []
462
+
463
+ for (const uri of Object.keys(serverStatesCache)) {
464
+ const { blockbook } = serverStatesCache[uri]
465
+ if (blockbook == null) continue
466
+ firstWave.push({
467
+ uri,
468
+ run: async () => (await blockbook.broadcastTx(transaction)).result
469
+ })
405
470
  }
406
471
 
407
- // Broadcast through any HTTP URI that may be configured, only if no
408
- // blockbook instances are connected.
409
- if (!isAnyBlockbookConnected) {
410
- // This is for the future when we want to get HTTP servers from the user
411
- // settings:
412
- // const httpUris = pluginState.getLocalServers(Infinity, [
413
- // /^http(?:s)?:/i
414
- // ])
415
-
416
- const { nowNodesApiKey } = initOptions
417
- const nowNodeUris = serverConfigs
418
- .filter(config => config.type === 'blockbook-nownode')
419
- .map(config => config.uris)
420
- .flat(1)
421
-
422
- // If there are no HTTP servers, reject the promise
423
- if (nowNodeUris.length < 1) {
424
- // If no HTTP servers are available, and we had no connected blockbook
425
- // instances, reject the promise with a message indicating no
426
- // available connections.
472
+ // This is for the future when we want to get HTTP servers from the user
473
+ // settings:
474
+ // const httpUris = pluginState.getLocalServers(Infinity, [
475
+ // /^http(?:s)?:/i
476
+ // ])
477
+ const { nowNodesApiKey } = initOptions
478
+ for (const config of serverConfigs) {
479
+ if (config.type === 'blockbook-nownode') {
480
+ // NOWNodes requires the key, and the key must not go anywhere else:
481
+ if (nowNodesApiKey == null) {
482
+ log.warn(
483
+ 'broadcastTx: skipping NOWNodes HTTP servers (no nowNodesApiKey)'
484
+ )
485
+ continue
486
+ }
487
+ for (const uri of config.uris) {
488
+ secondWave.push(httpAttempt(uri, { 'api-key': nowNodesApiKey }))
489
+ }
490
+ } else {
491
+ for (const uri of config.uris) {
492
+ firstWave.push(httpAttempt(uri, {}))
493
+ }
494
+ }
495
+ }
496
+
497
+ const attempts = firstWave.length + secondWave.length
498
+ if (attempts === 0) {
499
+ reject(
500
+ new Error('No available connections. Check your internet signal.')
501
+ )
502
+ return
503
+ }
504
+
505
+ let resolved = false
506
+ let failures = 0
507
+ const failureMessages = []
508
+ let secondWaveFired = false
509
+ let secondWaveTimer
510
+
511
+ const onSuccess = (uri, txid) => {
512
+ if (resolved) return
513
+ resolved = true
514
+ if (secondWaveTimer != null) clearTimeout(secondWaveTimer)
515
+ log(`broadcastTx succeeded via ${uri}: ${txid}`)
516
+ resolve(txid)
517
+ }
518
+ const onFailure = (uri, error) => {
519
+ const message = error instanceof Error ? error.message : String(error)
520
+ failureMessages.push(`${uri}: ${message}`)
521
+ log.warn(`broadcastTx attempt failed for ${uri}: ${message}`)
522
+ failures++
523
+ if (resolved) return
524
+ if (failures === attempts) {
525
+ log.error(
526
+ `broadcastTx fail: ${JSON.stringify(
527
+ transaction
528
+ )}\n${failureMessages.join('\n')}`
529
+ )
427
530
  reject(
428
- new Error('No available connections. Check your internet signal.')
531
+ error instanceof Error
532
+ ? error
533
+ : new Error(`Broadcast failed: ${failureMessages.join('; ')}`)
429
534
  )
430
535
  return
431
536
  }
432
-
433
- // If there is no key for the NowNode servers:
434
- if (nowNodesApiKey == null) {
435
- reject(new Error('Missing connection key for fallback servers.'))
436
- return
537
+ // Every first-wave attempt has failed, so there is nothing left to
538
+ // wait for. Fire the second wave now rather than sit out the delay.
539
+ if (!secondWaveFired && failures >= firstWave.length) {
540
+ fireSecondWave()
437
541
  }
438
-
439
- for (const uri of nowNodeUris) {
440
- log.warn('Falling back to NOWNode server broadcast over HTTP:', uri)
441
-
442
- // HTTP Fallback
443
- io.fetchCors(`${uri}/api/v2/sendtx/`, {
444
- method: 'POST',
445
- headers: {
446
- 'api-key': nowNodesApiKey
447
- },
448
- body: transaction.signedTx
542
+ }
543
+ const fire = (attempt) => {
544
+ withTimeout(
545
+ attempt.run(),
546
+ broadcastTimeoutMs,
547
+ `Timeout for broadcast to ${attempt.uri}`
548
+ )
549
+ .then(txid => {
550
+ onSuccess(attempt.uri, txid)
449
551
  })
450
- .then(async response => {
451
- if (!response.ok) {
452
- throw new Error(
453
- `Failed to broadcast transaction via Blockbook: HTTP ${response.status}`
454
- )
455
- }
456
- const json = await response.json()
457
- return _blockbookApi.asBlockbookResponse.call(void 0, (0, _blockbookApi.asBroadcastTxResponse))(json)
458
- })
459
- .then(response => {
460
- if (!resolved) {
461
- resolved = true
462
- resolve(response.result)
463
- }
464
- })
465
- .catch((e) => {
466
- if (++bad === nowNodeUris.length) {
467
- const msg = e != null ? `With error ${e.message}` : ''
468
- log.error(
469
- `broadcastTx fail: ${JSON.stringify(transaction)}\n${msg}`
470
- )
471
- reject(e)
472
- }
473
- })
474
- }
552
+ .catch((error) => {
553
+ onFailure(attempt.uri, error)
554
+ })
555
+ }
556
+ const fireSecondWave = () => {
557
+ if (secondWaveFired) return
558
+ secondWaveFired = true
559
+ if (secondWaveTimer != null) clearTimeout(secondWaveTimer)
560
+ if (secondWave.length === 0) return
561
+ log.warn(
562
+ 'broadcastTx: first wave has not resolved, trying NOWNodes servers'
563
+ )
564
+ for (const attempt of secondWave) fire(attempt)
565
+ }
566
+
567
+ for (const attempt of firstWave) fire(attempt)
568
+ if (firstWave.length === 0) {
569
+ // Nothing of our own to try first:
570
+ fireSecondWave()
571
+ } else if (secondWave.length > 0) {
572
+ secondWaveTimer = setTimeout(
573
+ fireSecondWave,
574
+ exports.NOWNODES_BROADCAST_DELAY_MS
575
+ )
475
576
  }
476
577
  })
477
578
  },
@@ -20,6 +20,7 @@ var _edgecorejs = require('edge-core-js');
20
20
 
21
21
  var _filterUndefined = require('../../../util/filterUndefined');
22
22
  var _unixTime = require('../../../util/unixTime');
23
+ var _calcReplacementFeeRate = require('../../fees/calcReplacementFeeRate');
23
24
  var _makeFees = require('../../fees/makeFees');
24
25
  var _EngineEmitter = require('../../plugin/EngineEmitter');
25
26
  var _Metadata = require('../../plugin/Metadata');
@@ -187,9 +188,12 @@ var _UtxoWalletTools = require('./UtxoWalletTools');
187
188
  // Must not be confirmed or dropped.
188
189
  if (replacedTx.blockHeight !== 0) return null
189
190
 
190
- // Double the fee used for the RBF transaction:
191
+ // Double the fee rate used by the replaced transaction:
191
192
  const vBytes = _utils.transactionSizeFromHex.call(void 0, replacedTx.hex)
192
- const newFeeRate = Math.round((parseInt(replacedTx.fees) / vBytes) * 2)
193
+ const newFeeRate = _calcReplacementFeeRate.calcReplacementFeeRate.call(void 0,
194
+ parseInt(replacedTx.fees),
195
+ vBytes
196
+ )
193
197
 
194
198
  const replacedTxInputs = replacedTx.inputs
195
199
  // Recreate UTXOs from DataLayer transaction and mark them as unspent:
@@ -631,7 +635,16 @@ var _UtxoWalletTools = require('./UtxoWalletTools');
631
635
  freshAddress.segwitAddress)), () => (
632
636
  freshAddress.publicAddress))
633
637
 
634
- const feeRate = parseInt(await fees.getRate(edgeSpendInfo))
638
+ // Fee rates may be fractional (e.g. a 1.8 sat/vB custom fee), so this
639
+ // must not be truncated to an integer. The picker rounds the satoshi
640
+ // amounts it derives from this rate up to whole satoshis. Number rejects
641
+ // any string that is not entirely a number, where parseFloat would read a
642
+ // comma-decimal '1,8' as 1 and charge the wrong rate without an error.
643
+ const feeRateString = await fees.getRate(edgeSpendInfo)
644
+ const feeRate = Number(feeRateString)
645
+ if (!isFinite(feeRate) || feeRate <= 0) {
646
+ throw new Error(`Invalid fee rate: ${feeRateString}`)
647
+ }
635
648
 
636
649
  let maxUtxo
637
650
  if (txOptions.CPFP != null) {
@@ -20,8 +20,8 @@ const currencyInfo = {
20
20
 
21
21
  // Explorers:
22
22
  blockExplorer: 'https://blockchair.com/bitcoin/block/%s',
23
- addressExplorer: 'https://blockchair.com/bitcoin/address/%s',
24
- transactionExplorer: 'https://blockchair.com/bitcoin/transaction/%s',
23
+ addressExplorer: 'https://mempool.space/address/%s',
24
+ transactionExplorer: 'https://mempool.space/tx/%s',
25
25
 
26
26
  denominations: [
27
27
  { name: 'BTC', multiplier: '100000000', symbol: '₿' },
@@ -53,7 +53,7 @@ const currencyInfo = {
53
53
  const engineInfo = {
54
54
  serverConfigs: [
55
55
  {
56
- type: 'blockbook-nownode',
56
+ type: 'blockbook',
57
57
  uris: ['https://btc-wusa1.edge.app', 'https://btc-eu1.edge.app']
58
58
  },
59
59
  {
@@ -65,7 +65,7 @@ const engineInfo = {
65
65
  forks: ['bitcoincash', 'bitcoingold'],
66
66
  gapLimit: 25,
67
67
  feeUpdateInterval: 60000,
68
- mempoolSpaceFeeInfoServer: 'https://mempool.space/api/v1/fees/recommended',
68
+ mempoolSpaceFeeInfoServer: 'https://mempool.space/api/v1/fees/precise',
69
69
  defaultFeeInfo: {
70
70
  lowFeeFudgeFactor: undefined,
71
71
  standardFeeLowFudgeFactor: undefined,
@@ -53,7 +53,7 @@ const currencyInfo = {
53
53
  const engineInfo = {
54
54
  serverConfigs: [
55
55
  {
56
- type: 'blockbook-nownode',
56
+ type: 'blockbook',
57
57
  uris: ['https://bch-eusa1.edge.app']
58
58
  },
59
59
  {
@@ -45,7 +45,7 @@ var _commonInfo = require('./commonInfo');
45
45
  forks: ['bitcoincash', 'bitcoingold'],
46
46
  gapLimit: 25,
47
47
  feeUpdateInterval: 60000,
48
- mempoolSpaceFeeInfoServer: 'https://mempool.space/api/v1/fees/recommended',
48
+ mempoolSpaceFeeInfoServer: 'https://mempool.space/api/v1/fees/precise',
49
49
  defaultFeeInfo: {
50
50
  lowFeeFudgeFactor: undefined,
51
51
  standardFeeLowFudgeFactor: undefined,
@@ -49,7 +49,7 @@ var _commonInfo = require('./commonInfo');
49
49
  gapLimit: 25,
50
50
  feeUpdateInterval: 60000,
51
51
  mempoolSpaceFeeInfoServer:
52
- 'https://mempool.space/testnet4/api/v1/fees/recommended',
52
+ 'https://mempool.space/testnet4/api/v1/fees/precise',
53
53
  defaultFeeInfo: {
54
54
  lowFeeFudgeFactor: undefined,
55
55
  standardFeeLowFudgeFactor: undefined,
@@ -52,7 +52,7 @@ const currencyInfo = {
52
52
  const engineInfo = {
53
53
  serverConfigs: [
54
54
  {
55
- type: 'blockbook-nownode',
55
+ type: 'blockbook',
56
56
  uris: ['https://dash-wusa1.edge.app']
57
57
  },
58
58
  {
@@ -46,7 +46,7 @@ const currencyInfo = {
46
46
  const engineInfo = {
47
47
  serverConfigs: [
48
48
  {
49
- type: 'blockbook-nownode',
49
+ type: 'blockbook',
50
50
  uris: ['https://dgb-eu1.edge.app']
51
51
  },
52
52
  {
@@ -48,7 +48,7 @@ const currencyInfo = {
48
48
  const engineInfo = {
49
49
  serverConfigs: [
50
50
  {
51
- type: 'blockbook-nownode',
51
+ type: 'blockbook',
52
52
  uris: ['https://doge-eusa1.edge.app']
53
53
  },
54
54
  {
@@ -32,7 +32,10 @@ const currencyInfo = {
32
32
  ..._commonInfo.legacyMemoInfo,
33
33
  defaultSettings: {
34
34
  customFeeSettings: ['satPerByte'],
35
- blockbookServers: ['wss://blockbook.fabien.cash/websocket'],
35
+ blockbookServers: [
36
+ 'wss://blockbook.fabien.cash/websocket',
37
+ 'wss://xec-blockbook.nownodes.io/wss/%{nowNodesApiKey}'
38
+ ],
36
39
  enableCustomServers: false
37
40
  },
38
41
  displayName: 'eCash',
@@ -20,8 +20,8 @@ var _commonInfo = require('./commonInfo');
20
20
 
21
21
  // Explorers:
22
22
  blockExplorer: 'https://blockchair.com/litecoin/block/%s',
23
- addressExplorer: 'https://blockchair.com/litecoin/address/%s',
24
- transactionExplorer: 'https://blockchair.com/litecoin/transaction/%s',
23
+ addressExplorer: 'https://litecoinspace.org/address/%s',
24
+ transactionExplorer: 'https://litecoinspace.org/tx/%s',
25
25
 
26
26
  denominations: [
27
27
  { name: 'LTC', multiplier: '100000000', symbol: 'Ł' },
@@ -50,7 +50,7 @@ var _commonInfo = require('./commonInfo');
50
50
  const engineInfo = {
51
51
  serverConfigs: [
52
52
  {
53
- type: 'blockbook-nownode',
53
+ type: 'blockbook',
54
54
  uris: ['https://ltc-wusa1.edge.app']
55
55
  },
56
56
  {