diff --git a/src/GWallet.Backend.Tests/ServerReference.fs b/src/GWallet.Backend.Tests/ServerReference.fs index a87110563..a44045b06 100644 --- a/src/GWallet.Backend.Tests/ServerReference.fs +++ b/src/GWallet.Backend.Tests/ServerReference.fs @@ -50,6 +50,33 @@ type ServerReference() = TimeSpan = timeSpan },dummy_now) |> Some + [] + member __.``averageBetween3DiscardingOutlier: basic test``() = + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 1m 2m 3m + Assert.That(res, Is.EqualTo 2m) + () + + [] + member __.``averageBetween3DiscardingOutlier: nuanced tests``() = + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 0m 2m 3m + Assert.That(res, Is.EqualTo 2.5m) + + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 2m 0m 3m + Assert.That(res, Is.EqualTo 2.5m) + () + + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 0m 3m 2m + Assert.That(res, Is.EqualTo 2.5m) + () + + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 3m 0m 2m + Assert.That(res, Is.EqualTo 2.5m) + () + + let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 3m 2m 0m + Assert.That(res, Is.EqualTo 2.5m) + () + [] member __.``order of servers is kept if non-hostname details are same``() = let serverWithHighestPriority = diff --git a/src/GWallet.Backend/Currency.fs b/src/GWallet.Backend/Currency.fs index 89b874e88..647210179 100644 --- a/src/GWallet.Backend/Currency.fs +++ b/src/GWallet.Backend/Currency.fs @@ -1,10 +1,28 @@ namespace GWallet.Backend +open System open System.Linq open System.ComponentModel open GWallet.Backend.FSharpUtil.UwpHacks +module TrustMinimizedEstimation = + let AverageBetween3DiscardingOutlier (one: decimal) (two: decimal) (three: decimal): decimal = + let sorted = List.sort [one; two; three] + let first = sorted.Item 0 + let last = sorted.Item 2 + let higher = Math.Max(first, last) + let intermediate = sorted.Item 1 + let lower = Math.Min(first, last) + + if (higher - intermediate = intermediate - lower) then + (higher + intermediate + lower) / 3m + // choose the two that are closest + elif (higher - intermediate) < (intermediate - lower) then + (higher + intermediate) / 2m + else + (lower + intermediate) / 2m + // this attribute below is for Json.NET (Newtonsoft.Json) to be able to deserialize this as a dict key [)>] type Currency = diff --git a/src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs b/src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs index c2a7214c2..8766f70dd 100644 --- a/src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs +++ b/src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs @@ -23,6 +23,19 @@ module FeeRateEstimation = let MempoolSpaceRestApiUri = Uri "https://mempool.space/api/v1/fees/recommended" + type BlockchainInfoProvider = JsonProvider<""" + { + "limits": { + "min": 4, + "max": 16 + }, + "regular": 9, + "priority": 11 + } + """> + + let BlockchainInfoRestApiUri = Uri "https://api.blockchain.info/mempool/fees" + let private ToBrandedType(feeRatePerKB: decimal) (moneyUnit: MoneyUnit): FeeRate = try Money(feeRatePerKB, moneyUnit) |> FeeRate @@ -45,6 +58,19 @@ module FeeRateEstimation = return Some <| ToBrandedType satPerKB MoneyUnit.Satoshi } + let private QueryFeeRateToBlockchainInfo (): Async> = + async { + let! maybeJson = Networking.QueryRestApi BlockchainInfoRestApiUri + match maybeJson with + | None -> return None + | Some json -> + let recommendedFees = BlockchainInfoProvider.Parse json + let highPrioFeeSatsPerB = decimal recommendedFees.Priority + Infrastructure.LogDebug (SPrintF1 "blockchain.info 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 = let avg = feesFromDifferentServers.Sum() / decimal feesFromDifferentServers.Length avg @@ -76,19 +102,43 @@ module FeeRateEstimation = let! electrumResult = electrumJob return electrumResult | Currency.BTC -> - let! bothJobs = Async.Parallel [electrumJob; QueryFeeRateToMempoolSpace()] + let! bothJobs = Async.Parallel [electrumJob; QueryFeeRateToMempoolSpace(); QueryFeeRateToBlockchainInfo()] let electrumResult = bothJobs.ElementAt 0 let mempoolSpaceResult = bothJobs.ElementAt 1 - match electrumResult, mempoolSpaceResult with - | None, None -> return None - | Some feeRate, None -> + let blockchainInfoResult = bothJobs.ElementAt 2 + match electrumResult, mempoolSpaceResult, blockchainInfoResult with + | None, None, None -> return None + | Some feeRate, None, None -> Infrastructure.LogDebug "Only electrum servers available for feeRate estimation" return Some feeRate - | None, Some feeRate -> + | None, Some feeRate, None -> 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] + | None, None, Some feeRate -> + Infrastructure.LogDebug "Only blockchain.info API available for feeRate estimation" + return Some feeRate + | None, Some restApiFeeRate1, Some restApiFeeRate2 -> + Infrastructure.LogDebug "Only REST APIs available for feeRate estimation" + let average = AverageFee [decimal restApiFeeRate1.FeePerK.Satoshi; decimal restApiFeeRate2.FeePerK.Satoshi] + let averageFeeRate = ToBrandedType average MoneyUnit.Satoshi + Infrastructure.LogDebug (SPrintF1 "Average fee rate of %M sat per B" averageFeeRate.SatoshiPerByte) + return Some averageFeeRate + | Some electrumFeeRate, Some restApiFeeRate, None -> + let average = AverageFee [decimal electrumFeeRate.FeePerK.Satoshi; decimal restApiFeeRate.FeePerK.Satoshi] + let averageFeeRate = ToBrandedType average MoneyUnit.Satoshi + Infrastructure.LogDebug (SPrintF1 "Average fee rate of %M sat per B" averageFeeRate.SatoshiPerByte) + return Some averageFeeRate + | Some electrumFeeRate, None, Some restApiFeeRate -> + let average = AverageFee [decimal electrumFeeRate.FeePerK.Satoshi; decimal restApiFeeRate.FeePerK.Satoshi] + let averageFeeRate = ToBrandedType average MoneyUnit.Satoshi + Infrastructure.LogDebug (SPrintF1 "Average fee rate of %M sat per B" averageFeeRate.SatoshiPerByte) + return Some averageFeeRate + | Some electrumFeeRate, Some restApiFeeRate1, Some restApiFeeRate2 -> + let average = + TrustMinimizedEstimation.AverageBetween3DiscardingOutlier + (decimal electrumFeeRate.FeePerK.Satoshi) + (decimal restApiFeeRate1.FeePerK.Satoshi) + (decimal restApiFeeRate2.FeePerK.Satoshi) let averageFeeRate = ToBrandedType average MoneyUnit.Satoshi Infrastructure.LogDebug (SPrintF1 "Average fee rate of %M sat per B" averageFeeRate.SatoshiPerByte) return Some averageFeeRate