Skip to content

Commit

Permalink
Use mempool.space for effective fee rate + use it in CPFP flow (Walle…
Browse files Browse the repository at this point in the history
…tWasabi#13193)

* Use mempool.space as UnconfirmedTransactionChainProvider - non-breaking

* Use the updated UnconfirmedTransactionChain when CPFP

* Request unconf chain if an own input is unconfirmed

* Update cached requests every time a block is mined

* Code review suggestions

* Renamings

* Use await instead of GetAwaiter()

* More renaming

* Correct computation of fee paid by chain

* Fix NRE in CoinJoinDetails

* Use create from task and not create

* Remove line for debug

* Fail if we can't fetch

* Support TestNet and fail on RegTest

* Throw in ImmediateRequest if no need for CpfpInfo

* Use correctly mempool.space API by usising descendants

* Minor fixes

* Don't take descendant into account if they pay less

* Adjust to new version of mempool.space CPFP endpoint

* ShouldRequest adjustments

* Update WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs

Co-authored-by: yahiheb <[email protected]>

* Update WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs

Co-authored-by: yahiheb <[email protected]>

* Update WalletWasabi.Daemon/Global.cs

Co-authored-by: yahiheb <[email protected]>

* Update WalletWasabi/Wallets/CpfpInfoProvider.cs

Co-authored-by: yahiheb <[email protected]>

* Update WalletWasabi/Wallets/CpfpInfoProvider.cs

Co-authored-by: yahiheb <[email protected]>

* Code review suggestions

* Code review suggestion

* Revert JsonSerialization

* Use default

* Force requests to third-party while own mechanism not implemented

* Fix JsonDeserialization and reschedule if error

* Make GetCachedCpfpInfoAsync async

---------

Co-authored-by: Turbolay <[email protected]>
Co-authored-by: yahiheb <[email protected]>
Co-authored-by: Lucas Ontivero <[email protected]>
  • Loading branch information
4 people authored Aug 27, 2024
1 parent 67c9e10 commit 171f1b5
Show file tree
Hide file tree
Showing 34 changed files with 432 additions and 363 deletions.
17 changes: 15 additions & 2 deletions WalletWasabi.Daemon/Global.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,21 @@ public Global(string dataDir, string configFilePath, Config config)
trustedFullNodeBlockProviders: trustedFullNodeBlockProviders,
new P2PBlockProvider(P2PNodesManager));

HostedServices.Register<UnconfirmedTransactionChainProvider>(() => new UnconfirmedTransactionChainProvider(HttpClientFactory), friendlyName: "Unconfirmed Transaction Chain Provider");
WalletFactory walletFactory = new(DataDir, config.Network, BitcoinStore, wasabiSynchronizer, config.ServiceConfiguration, HostedServices.Get<HybridFeeProvider>(), BlockDownloadService, HostedServices.Get<UnconfirmedTransactionChainProvider>());
if (Network != Network.RegTest)
{
HostedServices.Register<CpfpInfoProvider>(() => new CpfpInfoProvider(HttpClientFactory, Network), friendlyName: "CPFP Info Provider");
}

WalletFactory walletFactory = new(
DataDir,
config.Network,
BitcoinStore,
wasabiSynchronizer,
config.ServiceConfiguration,
HostedServices.Get<HybridFeeProvider>(),
BlockDownloadService,
Network == Network.RegTest ? null : HostedServices.Get<CpfpInfoProvider>());

WalletManager = new WalletManager(config.Network, DataDir, new WalletDirectories(Config.Network, DataDir), walletFactory);
TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, HttpClientFactory, WalletManager);

Expand Down
8 changes: 4 additions & 4 deletions WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ public string BuildCancelTransaction(uint256 txId, string password = "")
}

[JsonRpcMethod("speeduptransaction")]
public string SpeedUpTransaction(uint256 txId, string password = "")
public async Task<string> SpeedUpTransactionAsync(uint256 txId, string password = "")
{
Guard.NotNull(nameof(txId), txId);
var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet);
Expand All @@ -398,7 +398,7 @@ public string SpeedUpTransaction(uint256 txId, string password = "")
throw new NotSupportedException($"Unknown transaction {txId}");
}

var speedUpResult = activeWallet.SpeedUpTransaction(smartTransactionToSpeedUp);
var speedUpResult = await activeWallet.SpeedUpTransactionAsync(smartTransactionToSpeedUp, null, CancellationToken.None).ConfigureAwait(false);
var speedUpSmartTransaction = speedUpResult.Transaction;
return speedUpSmartTransaction.Transaction.ToHex();
}
Expand All @@ -417,12 +417,12 @@ public async Task<JsonRpcResult> SendRawTransactionAsync(string txHex)
}

[JsonRpcMethod("gethistory")]
public JsonRpcResultList GetHistory()
public async Task<JsonRpcResultList> GetHistoryAsync()
{
var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet);

AssertWalletIsLoaded();
var summary = activeWallet.BuildHistorySummary();
var summary = await activeWallet.BuildHistorySummaryAsync();
return summary.Select(
x => new JsonRpcResult
{
Expand Down
13 changes: 13 additions & 0 deletions WalletWasabi.Fluent/Extensions/ObservableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,17 @@ public static IObservableCache<TObject, TKey> Fetch<TObject, TKey>(this IObserva
.DisposeMany()
.AsObservableCache();
}

public static IObservableCache<TObject, TKey> FetchAsync<TObject, TKey>(
this IObservable<Unit> signal,
Func<Task<IEnumerable<TObject>>> source,
Func<TObject, TKey> keySelector,
IEqualityComparer<TObject>? equalityComparer = null)
where TKey : notnull where TObject : notnull
{
return signal.SelectMany(_ => Observable.FromAsync(source))
.EditDiff(keySelector, equalityComparer)
.DisposeMany()
.AsObservableCache();
}
}
28 changes: 11 additions & 17 deletions WalletWasabi.Fluent/Helpers/TransactionFeeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,25 @@ public static async Task<AllFeeEstimate> GetFeeEstimatesWhenReadyAsync(Wallet wa
throw new InvalidOperationException("Couldn't get the fee estimations.");
}

public static bool TryEstimateConfirmationTime(HybridFeeProvider feeProvider, Network network, SmartTransaction tx, UnconfirmedTransactionChainProvider unconfirmedTxChainProvider, [NotNullWhen(true)] out TimeSpan? estimate)
public static async Task<TimeSpan?> EstimateConfirmationTimeAsync(HybridFeeProvider feeProvider, Network network, SmartTransaction tx, CpfpInfoProvider? cpfpInfoProvider, CancellationToken cancellationToken)
{
estimate = null;

if (TryGetFeeEstimates(feeProvider, network, out var feeEstimates) && feeEstimates.TryEstimateConfirmationTime(tx, out estimate))
if (TryGetFeeEstimates(feeProvider, network, out var feeEstimates) && feeEstimates.TryEstimateConfirmationTime(tx, out var estimate))
{
return true;
return estimate;
}

if (feeEstimates is not null)
if (feeEstimates is null)
{
var unconfirmedChain = unconfirmedTxChainProvider.GetUnconfirmedTransactionChain(tx.GetHash());

if (unconfirmedChain is null || unconfirmedChain.Count == 0)
{
return false;
}

var feeRate = FeeHelpers.CalculateEffectiveFeeRateOfUnconfirmedChain(unconfirmedChain);
return null;
}

estimate = feeEstimates.EstimateConfirmationTime(feeRate);
return true;
if (cpfpInfoProvider is null || await cpfpInfoProvider.GetCachedCpfpInfoAsync(tx.GetHash(), cancellationToken).ConfigureAwait(false) is not { } cpfpInfo)
{
return null;
}

return false;
var feeRate = new FeeRate(cpfpInfo.EffectiveFeePerVSize);
return feeEstimates.EstimateConfirmationTime(feeRate);;
}

public static bool TryEstimateConfirmationTime(Wallet wallet, FeeRate feeRate, [NotNullWhen(true)] out TimeSpan? estimate)
Expand Down
29 changes: 16 additions & 13 deletions WalletWasabi.Fluent/Models/Wallets/TransactionTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using WalletWasabi.Blockchain.Transactions;
using WalletWasabi.Fluent.Extensions;
using WalletWasabi.Fluent.Helpers;
Expand All @@ -21,7 +24,7 @@ public TransactionTreeBuilder(Wallet wallet)
_wallet = wallet;
}

public IEnumerable<TransactionModel> Build(List<TransactionSummary> summaries)
public async Task<IEnumerable<TransactionModel>> BuildAsync(List<TransactionSummary> summaries, CancellationToken cancellationToken)
{
TransactionModel? coinJoinGroup = default;

Expand All @@ -33,14 +36,14 @@ public IEnumerable<TransactionModel> Build(List<TransactionSummary> summaries)

if (!item.IsOwnCoinjoin())
{
result.Add(CreateRegular(i, item));
result.Add(await CreateRegularAsync(i, item, cancellationToken));
}

if (item.IsOwnCoinjoin())
{
coinJoinGroup ??= CreateCoinjoinGroup(i, item);
coinJoinGroup ??= await CreateCoinjoinGroupAsync(i, item, cancellationToken);

coinJoinGroup.Add(CreateCoinjoinTransaction(i, item));
coinJoinGroup.Add(await CreateCoinjoinTransactionAsync(i, item, cancellationToken));
}

if (coinJoinGroup is { } cjg &&
Expand Down Expand Up @@ -112,7 +115,7 @@ private bool TryFindHistoryItem(uint256 txid, IEnumerable<TransactionModel> hist
return found is not null;
}

private TransactionModel CreateRegular(int index, TransactionSummary transactionSummary)
private async Task<TransactionModel> CreateRegularAsync(int index, TransactionSummary transactionSummary, CancellationToken cancellationToken)
{
var itemType = GetItemType(transactionSummary);
var date = transactionSummary.FirstSeen.ToLocalTime();
Expand All @@ -138,11 +141,11 @@ private TransactionModel CreateRegular(int index, TransactionSummary transaction
BlockHash = transactionSummary.BlockHash,
Fee = transactionSummary.GetFee(),
FeeRate = transactionSummary.FeeRate(),
ConfirmedTooltip = GetConfirmationToolTip(status, confirmations, transactionSummary.Transaction),
ConfirmedTooltip = await GetConfirmationToolTipAsync(status, confirmations, transactionSummary.Transaction, cancellationToken),
};
}

private TransactionModel CreateCoinjoinGroup(int index, TransactionSummary transactionSummary)
private async Task<TransactionModel> CreateCoinjoinGroupAsync(int index, TransactionSummary transactionSummary, CancellationToken cancellationToken)
{
var date = transactionSummary.FirstSeen.ToLocalTime();
var confirmations = transactionSummary.GetConfirmations();
Expand All @@ -153,7 +156,7 @@ private TransactionModel CreateCoinjoinGroup(int index, TransactionSummary trans
Amount = Money.Zero,
Labels = transactionSummary.Labels,
Confirmations = confirmations,
ConfirmedTooltip = GetConfirmationToolTip(status, confirmations, transactionSummary.Transaction),
ConfirmedTooltip = await GetConfirmationToolTipAsync(status, confirmations, transactionSummary.Transaction, cancellationToken),
Id = transactionSummary.GetHash(),
Date = date,
DateString = date.ToUserFacingFriendlyString(),
Expand Down Expand Up @@ -255,7 +258,7 @@ private void UpdateCoinjoinGroup(TransactionModel coinjoinGroup)
}
}

private TransactionModel CreateCoinjoinTransaction(int index, TransactionSummary transactionSummary)
private async Task<TransactionModel> CreateCoinjoinTransactionAsync(int index, TransactionSummary transactionSummary, CancellationToken cancellationToken)
{
var date = transactionSummary.FirstSeen.ToLocalTime();
var confirmations = transactionSummary.GetConfirmations();
Expand All @@ -275,7 +278,7 @@ private TransactionModel CreateCoinjoinTransaction(int index, TransactionSummary
Confirmations = confirmations,
BlockHeight = transactionSummary.Height.Type == HeightType.Chain ? transactionSummary.Height.Value : 0,
BlockHash = transactionSummary.BlockHash,
ConfirmedTooltip = GetConfirmationToolTip(status, confirmations, transactionSummary.Transaction),
ConfirmedTooltip = await GetConfirmationToolTipAsync(status, confirmations, transactionSummary.Transaction, cancellationToken),
Fee = transactionSummary.GetFee(),
FeeRate = transactionSummary.FeeRate()
};
Expand Down Expand Up @@ -317,15 +320,15 @@ private static TransactionStatus GetItemStatus(TransactionSummary transactionSum
return transactionSummary.IsConfirmed() ? TransactionStatus.Confirmed : TransactionStatus.Pending;
}

private string GetConfirmationToolTip(TransactionStatus status, int confirmations, SmartTransaction smartTransaction)
private async Task<string> GetConfirmationToolTipAsync(TransactionStatus status, int confirmations, SmartTransaction smartTransaction, CancellationToken cancellationToken)
{
if (status == TransactionStatus.Confirmed)
{
return TextHelpers.GetConfirmationText(confirmations);
}

var friendlyString = TransactionFeeHelper.TryEstimateConfirmationTime(_wallet.FeeProvider, _wallet.Network, smartTransaction, _wallet.UnconfirmedTransactionChainProvider, out var estimate)
? TextHelpers.TimeSpanToFriendlyString(estimate.Value)
var friendlyString = await TransactionFeeHelper.EstimateConfirmationTimeAsync(_wallet.FeeProvider, _wallet.Network, smartTransaction, _wallet.CpfpInfoProvider, cancellationToken) is { } estimate
? TextHelpers.TimeSpanToFriendlyString(estimate)
: "";

return (status, friendlyString != "") switch
Expand Down
33 changes: 15 additions & 18 deletions WalletWasabi.Fluent/Models/Wallets/WalletTransactionsModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using DynamicData;
using NBitcoin;
using ReactiveUI;
Expand Down Expand Up @@ -46,14 +48,12 @@ public WalletTransactionsModel(IWalletModel walletModel, Wallet wallet)
.Select(x => (walletModel, x.EventArgs))
.ObserveOn(RxApp.MainThreadScheduler);

RequestedUnconfirmedTxChainArrived =
Observable.FromEventPattern<EventArgs>(wallet.UnconfirmedTransactionChainProvider, nameof(wallet.UnconfirmedTransactionChainProvider.RequestedUnconfirmedChainArrived)).ToSignal()
RequestedCpfpInfoArrived = wallet.CpfpInfoProvider is null ? null :
Observable.FromEventPattern<EventArgs>(wallet.CpfpInfoProvider, nameof(wallet.CpfpInfoProvider.RequestedCpfpInfoArrived)).ToSignal()
.ObserveOn(RxApp.MainThreadScheduler);

Cache =
TransactionProcessed
.Merge(RequestedUnconfirmedTxChainArrived)
.Fetch(BuildSummary, model => model.Id)
Cache = (RequestedCpfpInfoArrived is null ? TransactionProcessed : TransactionProcessed.Merge(RequestedCpfpInfoArrived))
.FetchAsync(() => BuildSummaryAsync(CancellationToken.None), model => model.Id)
.DisposeWith(_disposable);

IsEmpty = Cache.Empty();
Expand All @@ -66,7 +66,7 @@ public WalletTransactionsModel(IWalletModel walletModel, Wallet wallet)
public IObservable<Unit> TransactionProcessed { get; }

public IObservable<(IWalletModel Wallet, ProcessedResult EventArgs)> NewTransactionArrived { get; }
public IObservable<Unit> RequestedUnconfirmedTxChainArrived { get; }
public IObservable<Unit>? RequestedCpfpInfoArrived { get; }

public bool TryGetById(uint256 transactionId, bool isChild, [NotNullWhen(true)] out TransactionModel? transaction)
{
Expand All @@ -90,20 +90,17 @@ public async Task<SmartTransaction> LoadFromFileAsync(string path)
return txn;
}

public TimeSpan? TryEstimateConfirmationTime(uint256 id)
public async Task<TimeSpan?> TryEstimateConfirmationTimeAsync(uint256 id, CancellationToken cancellationToken)
{
if (!_wallet.BitcoinStore.TransactionStore.TryGetTransaction(id, out var smartTransaction))
{
throw new InvalidOperationException($"Transaction not found! ID: {id}");
}

return
TransactionFeeHelper.TryEstimateConfirmationTime(_wallet.FeeProvider, _wallet.Network, smartTransaction, _wallet.UnconfirmedTransactionChainProvider, out var estimate)
? estimate
: null;
return await TransactionFeeHelper.EstimateConfirmationTimeAsync(_wallet.FeeProvider, _wallet.Network, smartTransaction, _wallet.CpfpInfoProvider, cancellationToken);
}

public TimeSpan? TryEstimateConfirmationTime(TransactionModel model) => TryEstimateConfirmationTime(model.Id);
public async Task<TimeSpan?> TryEstimateConfirmationTimeAsync(TransactionModel model, CancellationToken cancellationToken) => await TryEstimateConfirmationTimeAsync(model.Id, cancellationToken);

public TimeSpan? TryEstimateConfirmationTime(TransactionInfo info)
{
Expand All @@ -126,7 +123,7 @@ public TransactionInfo Create(string address, decimal amount, LabelsArray labels
return transactionInfo;
}

public SpeedupTransaction CreateSpeedUpTransaction(TransactionModel transaction)
public async Task<SpeedupTransaction> CreateSpeedUpTransactionAsync(TransactionModel transaction, CancellationToken cancellationToken)
{
if (!_wallet.BitcoinStore.TransactionStore.TryGetTransaction(transaction.Id, out var targetTransaction))
{
Expand All @@ -139,7 +136,7 @@ public SpeedupTransaction CreateSpeedUpTransaction(TransactionModel transaction)
{
targetTransaction = largestCpfp;
}
var boostingTransaction = _wallet.SpeedUpTransaction(targetTransaction);
var boostingTransaction = await _wallet.SpeedUpTransactionAsync(targetTransaction, null, cancellationToken);

var fee = _walletModel.AmountProvider.Create(GetFeeDifference(targetTransaction, boostingTransaction));

Expand Down Expand Up @@ -206,10 +203,10 @@ public async Task SendAsync(BuildTransactionResult transaction)
_wallet.UpdateUsedHdPubKeysLabels(transaction.HdPubKeysWithNewLabels);
}

private IEnumerable<TransactionModel> BuildSummary()
private async Task<IEnumerable<TransactionModel>> BuildSummaryAsync(CancellationToken cancellationToken)
{
var orderedRawHistoryList = _wallet.BuildHistorySummary(sortForUi: true);
var transactionModels = _treeBuilder.Build(orderedRawHistoryList);
var orderedRawHistoryList = await _wallet.BuildHistorySummaryAsync(sortForUi: true, cancellationToken: cancellationToken);
var transactionModels = await _treeBuilder.BuildAsync(orderedRawHistoryList, cancellationToken);
return transactionModels;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using WalletWasabi.Fluent.Extensions;
using WalletWasabi.Fluent.Models.UI;
using WalletWasabi.Fluent.Models.Wallets;
using WalletWasabi.Fluent.ViewModels.Navigation;
Expand Down Expand Up @@ -39,11 +42,11 @@ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disp

_wallet.Transactions.Cache
.Connect()
.Subscribe(_ => Update())
.SubscribeAsync(async _ => await UpdateAsync(CancellationToken.None))
.DisposeWith(disposables);
}

private void Update()
private async Task UpdateAsync(CancellationToken cancellationToken)
{
if (_wallet.Transactions.TryGetById(_transaction.Id, _transaction.IsChild, out var transaction))
{
Expand All @@ -52,10 +55,10 @@ private void Update()
Confirmations = transaction.Confirmations;
IsConfirmed = Confirmations > 0;
TransactionId = transaction.Id;
ConfirmationTime = _wallet.Transactions.TryEstimateConfirmationTime(transaction.Id);
ConfirmationTime = await _wallet.Transactions.TryEstimateConfirmationTimeAsync(transaction.Id, cancellationToken);
IsConfirmationTimeVisible = ConfirmationTime.HasValue && ConfirmationTime != TimeSpan.Zero;
FeeRate = transaction.FeeRate;
FeeRateVisible = FeeRate != FeeRate.Zero;
FeeRateVisible = FeeRate is not null && FeeRate != FeeRate.Zero;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using WalletWasabi.Fluent.Models.Wallets;
using WalletWasabi.Fluent.Models.UI;
Expand Down Expand Up @@ -34,7 +36,7 @@ public CoinJoinsDetailsViewModel(UiContext uiContext, IWalletModel wallet, Trans
SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: true);
NextCommand = CancelCommand;

ConfirmationTime = wallet.Transactions.TryEstimateConfirmationTime(transaction);
ConfirmationTime = Task.Run(() => wallet.Transactions.TryEstimateConfirmationTimeAsync(transaction, CancellationToken.None)).Result;
IsConfirmationTimeVisible = ConfirmationTime.HasValue && ConfirmationTime != TimeSpan.Zero;
}

Expand Down
Loading

0 comments on commit 171f1b5

Please sign in to comment.