Skip to content

Commit

Permalink
Backend/UtxoCoin: chk blockchainInfo's feeRate too
Browse files Browse the repository at this point in the history
Rather than just having electrumservers and mempool.space API,
let's have a third source of truth.
  • Loading branch information
knocte committed May 19, 2024
1 parent 7035bc8 commit f3533e3
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 7 deletions.
27 changes: 27 additions & 0 deletions src/GWallet.Backend.Tests/ServerReference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ type ServerReference() =
TimeSpan = timeSpan
},dummy_now) |> Some

[<Test>]
member __.``averageBetween3DiscardingOutlier: basic test``() =
let res = TrustMinimizedEstimation.AverageBetween3DiscardingOutlier 1m 2m 3m
Assert.That(res, Is.EqualTo 2m)
()

[<Test>]
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)
()

[<Test>]
member __.``order of servers is kept if non-hostname details are same``() =
let serverWithHighestPriority =
Expand Down
18 changes: 18 additions & 0 deletions src/GWallet.Backend/Currency.fs
Original file line number Diff line number Diff line change
@@ -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
[<TypeConverter(typeof<StringTypeConverter>)>]
type Currency =
Expand Down
64 changes: 57 additions & 7 deletions src/GWallet.Backend/UtxoCoin/FeeRateEstimation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,6 +58,19 @@ module FeeRateEstimation =
return Some <| ToBrandedType satPerKB MoneyUnit.Satoshi
}

let private QueryFeeRateToBlockchainInfo (): Async<Option<FeeRate>> =
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>): decimal =
let avg = feesFromDifferentServers.Sum() / decimal feesFromDifferentServers.Length
avg
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f3533e3

Please sign in to comment.