Skip to content

Commit

Permalink
Backend/UtxoCoin: check mempoolspace's feeRate too
Browse files Browse the repository at this point in the history
Historically, electrum servers return really shitty (as in, too
expensive) fee rates, so we mitigate this problem by querying
mempool.space API too and calculate the average.
  • Loading branch information
knocte committed May 18, 2024
1 parent 18084ea commit 7035bc8
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 21 deletions.
8 changes: 4 additions & 4 deletions src/GWallet.Backend/UtxoCoin/ElectrumClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ module ElectrumClient =
return raise <| ServerMisconfiguredException(SPrintF1 "Fee estimation returned an invalid non-positive value %M"
estimateFeeResult.Result)

let amountPerKB = estimateFeeResult.Result
let satPerKB = (NBitcoin.Money (amountPerKB, NBitcoin.MoneyUnit.BTC)).ToUnit NBitcoin.MoneyUnit.Satoshi
let btcPerKB = estimateFeeResult.Result
let satPerKB = (NBitcoin.Money (btcPerKB, NBitcoin.MoneyUnit.BTC)).ToUnit NBitcoin.MoneyUnit.Satoshi
let satPerB = satPerKB / (decimal 1000)
Infrastructure.LogDebug <| SPrintF2
"Electrum server gave us a fee rate of %M per KB = %M sat per B" amountPerKB satPerB
return amountPerKB
"Electrum server gave us a fee rate of %M per KB = %M sat per B" btcPerKB satPerB
return btcPerKB
}

let BroadcastTransaction (transactionInHex: string) (stratumServer: Async<StratumClient>) = async {
Expand Down
101 changes: 84 additions & 17 deletions src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,101 @@ open System.Linq
open GWallet.Backend
open GWallet.Backend.FSharpUtil.UwpHacks

open FSharp.Data
open NBitcoin

module FeeRateEstimation =

let private QueryFeeRateToElectrumServers (currency: Currency): Async<decimal> =
type MempoolSpaceProvider = JsonProvider<"""
{
"fastestFee": 41,
"halfHourFee": 38,
"hourFee": 35,
"economyFee": 12,
"minimumFee": 6
}
""">

let MempoolSpaceRestApiUri = Uri "https://mempool.space/api/v1/fees/recommended"

let private ToBrandedType(feeRatePerKB: decimal) (moneyUnit: MoneyUnit): FeeRate =
try
Money(feeRatePerKB, moneyUnit) |> FeeRate
with
| ex ->
// we need more info in case this bug shows again: https://gitlab.com/nblockchain/geewallet/issues/43
raise <| Exception(SPrintF2 "Could not create fee rate from %s %A"
(feeRatePerKB.ToString()) moneyUnit, ex)

let private QueryFeeRateToMempoolSpace (): Async<Option<FeeRate>> =
async {
let averageFee (feesFromDifferentServers: List<decimal>): decimal =
let avg = feesFromDifferentServers.Sum() / decimal feesFromDifferentServers.Length
avg
let! maybeJson = Networking.QueryRestApi MempoolSpaceRestApiUri
match maybeJson with
| None -> return None
| Some json ->
let recommendedFees = MempoolSpaceProvider.Parse json
let highPrioFeeSatsPerB = decimal recommendedFees.FastestFee
Infrastructure.LogDebug (SPrintF1 "mempool.space API gave us a fee rate of %M sat per B" highPrioFeeSatsPerB)
let satPerKB = highPrioFeeSatsPerB * (decimal 1000)
return Some <| ToBrandedType satPerKB MoneyUnit.Satoshi
}

let private AverageFee (feesFromDifferentServers: List<decimal>): decimal =
let avg = feesFromDifferentServers.Sum() / decimal feesFromDifferentServers.Length
avg

let private QueryFeeRateToElectrumServers (currency: Currency): Async<FeeRate> =
async {
//querying for 1 will always return -1 surprisingly...
let estimateFeeJob = ElectrumClient.EstimateFee 2
let numBlocksToWait = 2
let estimateFeeJob = ElectrumClient.EstimateFee numBlocksToWait
let! btcPerKiloByteForFastTrans =
Server.Query currency (QuerySettings.FeeEstimation averageFee) estimateFeeJob None
return btcPerKiloByteForFastTrans
Server.Query currency (QuerySettings.FeeEstimation AverageFee) estimateFeeJob None
return ToBrandedType (decimal btcPerKiloByteForFastTrans) MoneyUnit.BTC
}

let QueryFeeRateInternal currency =
let electrumJob =
async {
try
let! result = QueryFeeRateToElectrumServers currency
return Some result
with
| :? NoneAvailableException ->
return None
}

async {
match currency with
| Currency.LTC ->
let! electrumResult = electrumJob
return electrumResult
| Currency.BTC ->
let! bothJobs = Async.Parallel [electrumJob; QueryFeeRateToMempoolSpace()]
let electrumResult = bothJobs.ElementAt 0
let mempoolSpaceResult = bothJobs.ElementAt 1
match electrumResult, mempoolSpaceResult with
| None, None -> return None
| Some feeRate, None ->
Infrastructure.LogDebug "Only electrum servers available for feeRate estimation"
return Some feeRate
| None, Some feeRate ->
Infrastructure.LogDebug "Only mempool.space API available for feeRate estimation"
return Some feeRate
| Some electrumFeeRate, Some mempoolSpaceFeeRate ->
let average = AverageFee [decimal electrumFeeRate.FeePerK.Satoshi; decimal mempoolSpaceFeeRate.FeePerK.Satoshi]
let averageFeeRate = ToBrandedType average MoneyUnit.Satoshi
Infrastructure.LogDebug (SPrintF1 "Average fee rate of %M sat per B" averageFeeRate.SatoshiPerByte)
return Some averageFeeRate
| currency ->
return failwith <| SPrintF1 "UTXO currency not supported yet?: %A" currency
}

let internal EstimateFeeRate currency: Async<FeeRate> =
let toBrandedType(feeRate: decimal): FeeRate =
try
Money(feeRate, MoneyUnit.BTC) |> FeeRate
with
| ex ->
// we need more info in case this bug shows again: https://gitlab.com/nblockchain/geewallet/issues/43
raise <| Exception(SPrintF1 "Could not create fee rate from %s btc per KB"
(feeRate.ToString()), ex)
async {
let! btcPerKiloByteForFastTrans = QueryFeeRateToElectrumServers currency
return toBrandedType btcPerKiloByteForFastTrans
let! maybeFeeRate = QueryFeeRateInternal currency
match maybeFeeRate with
| None -> return failwith "Sending when offline not supported, try sign-off?"
| Some feeRate -> return feeRate
}

0 comments on commit 7035bc8

Please sign in to comment.