mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
start refactor to use btcpayserver directly
This commit is contained in:
@@ -128,7 +128,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
||||
//still good to have a chance to proceed with a join to reduce timing analysis
|
||||
|
||||
var rand = Random.Shared.Next(1, 101);
|
||||
if (rand > 5)
|
||||
if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability)
|
||||
{
|
||||
_logger.LogInformation($"All coins are private and we have no pending payments. Skipping join.");
|
||||
return solution;
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**"/>
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj"/>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
<ProjectReference Include="..\..\submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj">
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>true</Private>
|
||||
@@ -53,12 +53,12 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources"/>
|
||||
<Folder Include="Resources" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.17"/>
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.17" />
|
||||
</ItemGroup>
|
||||
<Target Name="DeleteExampleFile" AfterTargets="Publish">
|
||||
<RemoveDir Directories="$(PublishDir)\Microservices"/>
|
||||
<RemoveDir Directories="$(PublishDir)\Microservices" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -9,27 +8,40 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WalletWasabi.Blockchain.Analysis;
|
||||
using WalletWasabi.Blockchain.Analysis.Clustering;
|
||||
using WalletWasabi.Blockchain.Keys;
|
||||
using WalletWasabi.Blockchain.TransactionOutputs;
|
||||
using WalletWasabi.Blockchain.Transactions;
|
||||
using WalletWasabi.Extensions;
|
||||
using WalletWasabi.Models;
|
||||
using WalletWasabi.WabiSabi.Backend.Rounds;
|
||||
using WalletWasabi.WabiSabi.Client;
|
||||
using WalletWasabi.Wallets;
|
||||
|
||||
namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
|
||||
public class BTCPayWallet : IWallet
|
||||
public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
{
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private readonly BitcoinLikePayoutHandler _bitcoinLikePayoutHandler;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly Services.Wallets.BTCPayWallet _btcPayWallet;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
public OnChainPaymentMethodData OnChainPaymentMethodData;
|
||||
public readonly DerivationStrategyBase DerivationScheme;
|
||||
public readonly ExplorerClient ExplorerClient;
|
||||
@@ -39,15 +51,30 @@ public class BTCPayWallet : IWallet
|
||||
public readonly ILogger Logger;
|
||||
private static readonly BlockchainAnalyzer BlockchainAnalyzer = new();
|
||||
|
||||
public BTCPayWallet(OnChainPaymentMethodData onChainPaymentMethodData, DerivationStrategyBase derivationScheme,
|
||||
ExplorerClient explorerClient, BTCPayKeyChain keyChain,
|
||||
IDestinationProvider destinationProvider, IBTCPayServerClientFactory btcPayServerClientFactory, string storeId,
|
||||
WabisabiStoreSettings wabisabiStoreSettings, IUTXOLocker utxoLocker,
|
||||
ILoggerFactory loggerFactory, Smartifier smartifier,
|
||||
public BTCPayWallet(
|
||||
WalletRepository walletRepository,
|
||||
BitcoinLikePayoutHandler bitcoinLikePayoutHandler,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
Services.Wallets.BTCPayWallet btcPayWallet,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
OnChainPaymentMethodData onChainPaymentMethodData,
|
||||
DerivationStrategyBase derivationScheme,
|
||||
ExplorerClient explorerClient,
|
||||
BTCPayKeyChain keyChain,
|
||||
IBTCPayServerClientFactory btcPayServerClientFactory,
|
||||
string storeId,
|
||||
WabisabiStoreSettings wabisabiStoreSettings,
|
||||
IUTXOLocker utxoLocker,
|
||||
ILoggerFactory loggerFactory,
|
||||
Smartifier smartifier,
|
||||
ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins)
|
||||
{
|
||||
KeyChain = keyChain;
|
||||
DestinationProvider = destinationProvider;
|
||||
_walletRepository = walletRepository;
|
||||
_bitcoinLikePayoutHandler = bitcoinLikePayoutHandler;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_btcPayWallet = btcPayWallet;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
OnChainPaymentMethodData = onChainPaymentMethodData;
|
||||
DerivationScheme = derivationScheme;
|
||||
ExplorerClient = explorerClient;
|
||||
@@ -72,7 +99,7 @@ public class BTCPayWallet : IWallet
|
||||
}
|
||||
|
||||
public IKeyChain KeyChain { get; }
|
||||
public IDestinationProvider DestinationProvider { get; }
|
||||
public IDestinationProvider DestinationProvider => this;
|
||||
|
||||
public int AnonymitySetTarget => WabisabiStoreSettings.PlebMode? 2: WabisabiStoreSettings.AnonymitySetTarget;
|
||||
public bool ConsolidationMode => !WabisabiStoreSettings.PlebMode && WabisabiStoreSettings.ConsolidationMode;
|
||||
@@ -93,10 +120,11 @@ public class BTCPayWallet : IWallet
|
||||
public async Task<CoinsView> GetAllCoins()
|
||||
{
|
||||
await _savingProgress;
|
||||
var client = await BtcPayServerClientFactory.Create(null, StoreId);
|
||||
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC");
|
||||
await _smartifier.LoadCoins(utxos.ToList());
|
||||
var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.Outpoint == pair.Key))
|
||||
|
||||
var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme);
|
||||
var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos);
|
||||
await _smartifier.LoadCoins(utxos.ToList(), 1, utxoLabels);
|
||||
var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key))
|
||||
.Select(pair => pair.Value));
|
||||
|
||||
return new CoinsView(coins);
|
||||
@@ -140,31 +168,40 @@ public class BTCPayWallet : IWallet
|
||||
{
|
||||
return Array.Empty<SmartCoin>();
|
||||
}
|
||||
|
||||
var client = await BtcPayServerClientFactory.Create(null, StoreId);
|
||||
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC");
|
||||
var objs = client.GetOnChainWalletObjects(StoreId, "BTC",
|
||||
new GetWalletObjectsRequest()
|
||||
{
|
||||
Type = "utxo", Ids = utxos.Select(data => data.Outpoint.ToString()).ToArray()
|
||||
});
|
||||
|
||||
var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme, true, CancellationToken.None);
|
||||
var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos);
|
||||
if (!WabisabiStoreSettings.PlebMode)
|
||||
{
|
||||
if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true)
|
||||
{
|
||||
|
||||
utxos = utxos.Where(data =>
|
||||
!WabisabiStoreSettings.InputLabelsAllowed.Any(s => data.Labels.ContainsKey(s)));
|
||||
utxoLabels.TryGetValue(data.OutPoint, out var opLabels) &&
|
||||
opLabels.Any(
|
||||
attachment => WabisabiStoreSettings.InputLabelsAllowed.Any(s => attachment.Id == s))).ToArray();
|
||||
}
|
||||
|
||||
if (WabisabiStoreSettings.InputLabelsExcluded?.Any() is true)
|
||||
{
|
||||
|
||||
utxos = utxos.Where(data =>
|
||||
WabisabiStoreSettings.InputLabelsExcluded.All(s => !data.Labels.ContainsKey(s)));
|
||||
!utxoLabels.TryGetValue(data.OutPoint, out var opLabels) ||
|
||||
opLabels.All(
|
||||
attachment => WabisabiStoreSettings.InputLabelsExcluded.All(s => attachment.Id != s))).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
var locks = await UtxoLocker.FindLocks(utxos.Select(data => data.Outpoint).ToArray());
|
||||
utxos = utxos.Where(data => !locks.Contains(data.Outpoint)).Where(data => data.Confirmations > 0);
|
||||
if (WabisabiStoreSettings.PlebMode || !WabisabiStoreSettings.CrossMixBetweenCoordinators)
|
||||
{
|
||||
utxos = utxos.Where(data =>
|
||||
!utxoLabels.TryGetValue(data.OutPoint, out var opLabels) ||
|
||||
!opLabels.Any(attachment => attachment.Type == "coinjoin" && attachment.Data?.Value<string>("CoordinatorName") == coordinatorName))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var locks = await UtxoLocker.FindLocks(utxos.Select(data => data.OutPoint).ToArray());
|
||||
utxos = utxos.Where(data => !locks.Contains(data.OutPoint)).Where(data => data.Confirmations > 0).ToArray();
|
||||
if (_bannedCoins.TryGetValue(coordinatorName, out var bannedCoins))
|
||||
{
|
||||
var expired = bannedCoins.Where(pair => pair.Value < DateTimeOffset.Now).ToArray();
|
||||
@@ -174,16 +211,16 @@ public class BTCPayWallet : IWallet
|
||||
|
||||
}
|
||||
|
||||
utxos = utxos.Where(data => !bannedCoins.ContainsKey(data.Outpoint));
|
||||
utxos = utxos.Where(data => !bannedCoins.ContainsKey(data.OutPoint)).ToArray();
|
||||
}
|
||||
await _smartifier.LoadCoins(utxos.Where(data => data.Confirmations>0).ToList());
|
||||
await _smartifier.LoadCoins(utxos.Where(data => data.Confirmations>0).ToList(), 1, utxoLabels);
|
||||
|
||||
var resultX = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.Outpoint == pair.Key))
|
||||
var resultX = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key))
|
||||
.Select(pair => pair.Value));
|
||||
|
||||
foreach (SmartCoin c in resultX)
|
||||
{
|
||||
var utxo = utxos.Single(coin => coin.Outpoint == c.Outpoint);
|
||||
var utxo = utxos.Single(coin => coin.OutPoint == c.Outpoint);
|
||||
c.Height = new Height((uint) utxo.Confirmations);
|
||||
}
|
||||
|
||||
@@ -196,6 +233,28 @@ public class BTCPayWallet : IWallet
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<OutPoint, List<Attachment>>> GetUtxoLabels(WalletRepository walletRepository, string storeId ,ReceivedCoin[] utxos)
|
||||
{
|
||||
var walletTransactionsInfoAsync = await walletRepository.GetWalletTransactionsInfo(new WalletId(storeId, "BTC"),
|
||||
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
|
||||
|
||||
var utxoLabels = utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.ToString(), out var info3);
|
||||
var info = walletRepository.Merge(info1, info2, info3);
|
||||
if (info is null)
|
||||
{
|
||||
return (coin.OutPoint, null);
|
||||
}
|
||||
|
||||
return (coin.OutPoint, info.Attachments);
|
||||
}).Where(tuple => tuple.Attachments is not null)
|
||||
.ToDictionary(tuple => tuple.OutPoint, tuple => tuple.Attachments);
|
||||
return utxoLabels;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<SmartTransaction>> GetTransactionsAsync()
|
||||
{
|
||||
@@ -471,4 +530,141 @@ public class BTCPayWallet : IWallet
|
||||
Logger.LogInformation($"unlocked utxos: {string.Join(',', unlocked)}");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count, bool preferTaproot, bool mixedOutputs)
|
||||
{
|
||||
if (!WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet) && mixedOutputs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mixClient = await BtcPayServerClientFactory.Create(null, WabisabiStoreSettings.MixToOtherWallet);
|
||||
var pm = await mixClient.GetStoreOnChainPaymentMethod(WabisabiStoreSettings.MixToOtherWallet,
|
||||
"BTC");
|
||||
|
||||
var deriv = ExplorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme);
|
||||
if (deriv.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType())
|
||||
{
|
||||
return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ =>
|
||||
_btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, deriv, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address));
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
WabisabiStoreSettings.MixToOtherWallet = null;
|
||||
}
|
||||
}
|
||||
return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ =>
|
||||
_btcPayWallet.ReserveAddressAsync(StoreId ,DerivationScheme, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PendingPayment>> GetPendingPaymentsAsync( UtxoSelectionParameters roundParameters)
|
||||
{
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var payouts = (await _pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
States = new [] {PayoutState.AwaitingPayment},
|
||||
Stores = new []{StoreId},
|
||||
PaymentMethods = new []{"BTC"}
|
||||
})).Select(async data =>
|
||||
{
|
||||
|
||||
var claim = await _bitcoinLikePayoutHandler.ParseClaimDestination(new PaymentMethodId("BTC", PaymentTypes.BTCLike),
|
||||
data.Destination, CancellationToken.None);
|
||||
|
||||
if (!string.IsNullOrEmpty(claim.error) || claim.destination is not IBitcoinLikeClaimDestination bitcoinLikeClaimDestination )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var payoutBlob = data.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
var value = new Money(payoutBlob.CryptoAmount.Value, MoneyUnit.BTC);
|
||||
if (!roundParameters.AllowedOutputAmounts.Contains(value) ||
|
||||
!roundParameters.AllowedOutputScriptTypes.Contains(bitcoinLikeClaimDestination.Address.ScriptPubKey.GetScriptType()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new PendingPayment()
|
||||
{
|
||||
Identifier = data.Id,
|
||||
Destination = bitcoinLikeClaimDestination.Address,
|
||||
Value =value,
|
||||
PaymentStarted = PaymentStarted(data.Id),
|
||||
PaymentFailed = PaymentFailed(data.Id),
|
||||
PaymentSucceeded = PaymentSucceeded(data.Id),
|
||||
};
|
||||
}).Where(payment => payment is not null).ToArray();
|
||||
return await Task.WhenAll(payouts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Array.Empty<PendingPayment>();
|
||||
}
|
||||
}
|
||||
|
||||
private Action<(uint256 roundId, uint256 transactionId, int outputIndex)> PaymentSucceeded(string payoutId)
|
||||
{
|
||||
|
||||
return tuple =>
|
||||
_pullPaymentHostedService.MarkPaid( new HostedServices.MarkPayoutRequest()
|
||||
{
|
||||
PayoutId = payoutId,
|
||||
State = PayoutState.InProgress,
|
||||
Proof = JObject.FromObject(new PayoutTransactionOnChainBlob()
|
||||
{
|
||||
Candidates = new HashSet<uint256>()
|
||||
{
|
||||
tuple.transactionId
|
||||
},
|
||||
TransactionId = tuple.transactionId
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private Action PaymentFailed(string payoutId)
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
_pullPaymentHostedService.MarkPaid(new HostedServices.MarkPayoutRequest()
|
||||
{
|
||||
PayoutId = payoutId,
|
||||
State = PayoutState.AwaitingPayment
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private Func<Task<bool>> PaymentStarted(string payoutId)
|
||||
{
|
||||
return async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _pullPaymentHostedService.MarkPaid( new HostedServices.MarkPayoutRequest()
|
||||
{
|
||||
PayoutId = payoutId,
|
||||
State = PayoutState.InProgress,
|
||||
Proof = JObject.FromObject(new WabisabiPaymentProof())
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class WabisabiPaymentProof
|
||||
{
|
||||
[JsonProperty("proofType")]
|
||||
public string ProofType { get; set; } = "Wabisabi";
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 TransactionId { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)]
|
||||
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
|
||||
public string Link { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
11
Plugins/BTCPayServer.Plugins.Wabisabi/CoinjoinsViewModel.cs
Normal file
11
Plugins/BTCPayServer.Plugins.Wabisabi/CoinjoinsViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models;
|
||||
|
||||
namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
public class CoinjoinsViewModel : BasePagingViewModel
|
||||
{
|
||||
public List<BTCPayWallet.CoinjoinData> Coinjoins { get; set; } = new();
|
||||
public override int CurrentPageCount => Coinjoins.Count;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WalletWasabi.WabiSabi.Models.Serialization;
|
||||
|
||||
@@ -24,20 +23,19 @@ public static class CoordinatorExtensions
|
||||
|
||||
services.AddSingleton<IUIExtension>(new UIExtension("Wabisabi/WabisabiServerNavvExtension", "server-nav"));
|
||||
|
||||
Type t = Assembly.GetEntryAssembly().GetType("BTCPayServer.Services.Socks5HttpClientHandler");
|
||||
|
||||
services.AddHttpClient("wabisabi-coordinator-scripts-no-redirect.onion")
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t);
|
||||
|
||||
var handler = new Socks5HttpClientHandler(provider.GetRequiredService<BTCPayServerOptions>());
|
||||
handler.AllowAutoRedirect = false;
|
||||
return handler;
|
||||
});
|
||||
|
||||
services.AddHttpClient("wabisabi-coordinator-scripts.onion")
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t);
|
||||
var handler = new Socks5HttpClientHandler(provider.GetRequiredService<BTCPayServerOptions>());
|
||||
handler.AllowAutoRedirect = false;
|
||||
return handler;
|
||||
});
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WalletWasabi.Extensions;
|
||||
using WalletWasabi.WabiSabi.Backend.Rounds;
|
||||
using WalletWasabi.WabiSabi.Client;
|
||||
|
||||
namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
public class NBXInternalDestinationProvider : IDestinationProvider
|
||||
{
|
||||
private readonly ExplorerClient _explorerClient;
|
||||
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
||||
private readonly DerivationStrategyBase _derivationStrategy;
|
||||
private readonly BTCPayServerClient _client;
|
||||
private readonly string _storeId;
|
||||
private readonly WabisabiStoreSettings _wabisabiStoreSettings;
|
||||
|
||||
public NBXInternalDestinationProvider(ExplorerClient explorerClient, IBTCPayServerClientFactory btcPayServerClientFactory,
|
||||
DerivationStrategyBase derivationStrategy, BTCPayServerClient client, string storeId, WabisabiStoreSettings wabisabiStoreSettings)
|
||||
{
|
||||
_explorerClient = explorerClient;
|
||||
_btcPayServerClientFactory = btcPayServerClientFactory;
|
||||
_derivationStrategy = derivationStrategy;
|
||||
_client = client;
|
||||
_storeId = storeId;
|
||||
_wabisabiStoreSettings = wabisabiStoreSettings;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count, bool preferTaproot, bool mixedOutputs)
|
||||
{
|
||||
if (!_wabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(_wabisabiStoreSettings.MixToOtherWallet) && mixedOutputs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mixClient = await _btcPayServerClientFactory.Create(null, _wabisabiStoreSettings.MixToOtherWallet);
|
||||
var pm = await mixClient.GetStoreOnChainPaymentMethod(_wabisabiStoreSettings.MixToOtherWallet,
|
||||
"BTC");
|
||||
|
||||
var deriv = _explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme);
|
||||
if (deriv.ScriptPubKeyType() == _derivationStrategy.ScriptPubKeyType())
|
||||
{
|
||||
|
||||
return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ =>
|
||||
_explorerClient.GetUnusedAsync(deriv, DerivationFeature.Deposit, 0, true))).ContinueWith(task => task.Result.Select(information => information.Address));
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
_wabisabiStoreSettings.MixToOtherWallet = null;
|
||||
}
|
||||
}
|
||||
return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ =>
|
||||
_explorerClient.GetUnusedAsync(_derivationStrategy, DerivationFeature.Deposit, 0, true))).ContinueWith(task => task.Result.Select(information => information.Address));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PendingPayment>> GetPendingPaymentsAsync( UtxoSelectionParameters roundParameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payouts = await _client.GetStorePayouts(_storeId, false);
|
||||
return payouts.Where(data =>
|
||||
data.State == PayoutState.AwaitingPayment &&
|
||||
data.PaymentMethod.Equals("BTC", StringComparison.InvariantCultureIgnoreCase)).Select(data =>
|
||||
{
|
||||
IDestination destination = null;
|
||||
try
|
||||
{
|
||||
|
||||
var bip21 = new BitcoinUrlBuilder(data.Destination, _explorerClient.Network.NBitcoinNetwork);
|
||||
destination = bip21.Address;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
destination = BitcoinAddress.Create(data.Destination, _explorerClient.Network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
var value = new Money((decimal)data.PaymentMethodAmount, MoneyUnit.BTC);
|
||||
if (!roundParameters.AllowedOutputAmounts.Contains(value) ||
|
||||
!roundParameters.AllowedOutputScriptTypes.Contains(destination.ScriptPubKey.GetScriptType()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new PendingPayment()
|
||||
{
|
||||
Identifier = data.Id,
|
||||
Destination = destination,
|
||||
Value =value,
|
||||
PaymentStarted = PaymentStarted(_storeId, data.Id),
|
||||
PaymentFailed = PaymentFailed(_storeId, data.Id),
|
||||
PaymentSucceeded = PaymentSucceeded(_storeId, data.Id,
|
||||
_explorerClient.Network.NBitcoinNetwork.ChainName == ChainName.Mainnet),
|
||||
};
|
||||
}).Where(payment => payment is not null).ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return Array.Empty<PendingPayment>();
|
||||
}
|
||||
}
|
||||
|
||||
private Action<(uint256 roundId, uint256 transactionId, int outputIndex)> PaymentSucceeded(string storeId, string payoutId, bool mainnet)
|
||||
{
|
||||
return tuple =>
|
||||
_client.MarkPayout(storeId, payoutId, new MarkPayoutRequest()
|
||||
{
|
||||
State = PayoutState.InProgress,
|
||||
PaymentProof = JObject.FromObject(new WabisabiPaymentProof()
|
||||
{
|
||||
ProofType = "PayoutTransactionOnChainBlob",
|
||||
Candidates = new HashSet<uint256>()
|
||||
{
|
||||
tuple.transactionId
|
||||
},
|
||||
TransactionId = tuple.transactionId,
|
||||
Link = ComputeTxUrl(mainnet, tuple.transactionId.ToString(), tuple.outputIndex.ToString())
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static string ComputeTxUrl(bool mainnet, string tx, string outputIndex = null)
|
||||
{
|
||||
var path = $"tx/{(outputIndex is null ? tx : $"{outputIndex}:{tx}#flow")}";
|
||||
return mainnet
|
||||
? $"https://mempool.space/{path}"
|
||||
: $"https://mempool.space/testnet/{path}";
|
||||
}
|
||||
private Action PaymentFailed(string storeId, string payoutId)
|
||||
{
|
||||
return () =>
|
||||
_client.MarkPayout(storeId, payoutId, new MarkPayoutRequest() {State = PayoutState.AwaitingPayment});
|
||||
}
|
||||
|
||||
private Func<Task<bool>> PaymentStarted(string storeId, string payoutId)
|
||||
{
|
||||
return async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.MarkPayout(storeId, payoutId,
|
||||
new MarkPayoutRequest()
|
||||
{
|
||||
State = PayoutState.InProgress,
|
||||
PaymentProof = JObject.FromObject(new WabisabiPaymentProof())
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class WabisabiPaymentProof
|
||||
{
|
||||
[JsonProperty("proofType")]
|
||||
public string ProofType { get; set; } = "Wabisabi";
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 TransactionId { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)]
|
||||
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
|
||||
public string Link { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@@ -20,16 +22,20 @@ namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
public class Smartifier
|
||||
{
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private readonly ExplorerClient _explorerClient;
|
||||
public DerivationStrategyBase DerivationScheme { get; }
|
||||
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
||||
private readonly string _storeId;
|
||||
private readonly Action<object, PropertyChangedEventArgs> _coinOnPropertyChanged;
|
||||
|
||||
public Smartifier(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase,
|
||||
public Smartifier(
|
||||
WalletRepository walletRepository,
|
||||
ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase,
|
||||
IBTCPayServerClientFactory btcPayServerClientFactory, string storeId,
|
||||
Action<object, PropertyChangedEventArgs> coinOnPropertyChanged)
|
||||
{
|
||||
_walletRepository = walletRepository;
|
||||
_explorerClient = explorerClient;
|
||||
DerivationScheme = derivationStrategyBase;
|
||||
_btcPayServerClientFactory = btcPayServerClientFactory;
|
||||
@@ -45,25 +51,26 @@ public class Smartifier
|
||||
public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new();
|
||||
private readonly Task<RootedKeyPath> _accountKeyPath;
|
||||
|
||||
public async Task LoadCoins(List<OnChainWalletUTXOData> coins, int current = 1)
|
||||
public async Task LoadCoins(List<ReceivedCoin> coins, int current ,
|
||||
Dictionary<OutPoint, List<Attachment>> utxoLabels)
|
||||
{
|
||||
coins = coins.Where(data => data is not null).ToList();
|
||||
if (current > 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var txs = coins.Select(data => data.Outpoint.Hash).Distinct();
|
||||
var txs = coins.Select(data => data.OutPoint.Hash).Distinct();
|
||||
foreach (uint256 tx in txs)
|
||||
{
|
||||
cached.TryAdd(tx, _explorerClient.GetTransactionAsync(DerivationScheme, tx));
|
||||
}
|
||||
|
||||
foreach (OnChainWalletUTXOData coin in coins)
|
||||
foreach (var coin in coins)
|
||||
{
|
||||
var client = await _btcPayServerClientFactory.Create(null, _storeId);
|
||||
var tx = await Transactions.GetOrAdd(coin.Outpoint.Hash, async uint256 =>
|
||||
var tx = await Transactions.GetOrAdd(coin.OutPoint.Hash, async uint256 =>
|
||||
{
|
||||
var unsmartTx = await cached[coin.Outpoint.Hash];
|
||||
var unsmartTx = await cached[coin.OutPoint.Hash];
|
||||
if (unsmartTx is null)
|
||||
{
|
||||
return null;
|
||||
@@ -71,9 +78,6 @@ public class Smartifier
|
||||
var smartTx = new SmartTransaction(unsmartTx.Transaction,
|
||||
unsmartTx.Height is null ? Height.Mempool : new Height((uint)unsmartTx.Height.Value),
|
||||
unsmartTx.BlockHash, firstSeen: unsmartTx.Timestamp);
|
||||
//var indexesOfOurSpentInputs = unsmartTx.Inputs.Select(output => (uint)output.Inputndex).ToArray();
|
||||
// var ourSpentUtxos = unsmartTx.Transaction.Inputs.AsIndexedInputs()
|
||||
// .Where(@in => indexesOfOurSpentInputs.Contains(@in.Index)).ToDictionary(@in=> @in.Index,@in => @in);
|
||||
|
||||
|
||||
var ourSpentUtxos = new Dictionary<MatchedOutput, IndexedTxIn>();
|
||||
@@ -105,43 +109,29 @@ public class Smartifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var utxoObjects = await client.GetOnChainWalletObjects(_storeId, "BTC",
|
||||
new GetWalletObjectsRequest()
|
||||
{
|
||||
Ids = ourSpentUtxos.Select(point => point.Value.PrevOut.ToString()).ToArray(),
|
||||
Type = "utxo",
|
||||
IncludeNeighbourData = true
|
||||
});
|
||||
var labelsOfOurSpentUtxos =utxoObjects.ToDictionary(data => data.Id,
|
||||
data => data.Links.Where(link => link.Type == "label"));
|
||||
|
||||
|
||||
await LoadCoins(unsmartTx.Inputs.Select(output =>
|
||||
}
|
||||
var inputsToLoad = unsmartTx.Inputs.Select(output =>
|
||||
{
|
||||
if (!ourSpentUtxos.TryGetValue(output, out var outputtxin))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var outpoint = outputtxin.PrevOut;
|
||||
var labels = labelsOfOurSpentUtxos
|
||||
.GetValueOrDefault(outpoint.ToString(),
|
||||
new List<OnChainWalletObjectData.OnChainWalletObjectLink>())
|
||||
.ToDictionary(link => link.Id, link => new LabelData());
|
||||
return new OnChainWalletUTXOData()
|
||||
return new ReceivedCoin()
|
||||
{
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Address =
|
||||
output.Address?.ToString() ?? _explorerClient.Network
|
||||
.CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey)
|
||||
.ToString(),
|
||||
output.Address ?? _explorerClient.Network
|
||||
.CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey),
|
||||
KeyPath = output.KeyPath,
|
||||
Amount = ((Money)output.Value).ToDecimal(MoneyUnit.BTC),
|
||||
Outpoint = outpoint,
|
||||
Labels = labels,
|
||||
Value = output.Value,
|
||||
OutPoint = outpoint,
|
||||
Confirmations = unsmartTx.Confirmations
|
||||
};
|
||||
}).ToList(),current+1);
|
||||
}).ToList();
|
||||
|
||||
await LoadCoins(inputsToLoad,current+1, await BTCPayWallet.GetUtxoLabels(_walletRepository, _storeId, inputsToLoad.ToArray()));
|
||||
foreach (MatchedOutput input in unsmartTx.Inputs)
|
||||
{
|
||||
if (!ourSpentUtxos.TryGetValue(input, out var outputtxin))
|
||||
@@ -159,19 +149,18 @@ public class Smartifier
|
||||
return smartTx;
|
||||
});
|
||||
|
||||
var smartCoin = await Coins.GetOrAdd(coin.Outpoint, async point =>
|
||||
var smartCoin = await Coins.GetOrAdd(coin.OutPoint, async point =>
|
||||
{
|
||||
|
||||
var unsmartTx = await cached[coin.Outpoint.Hash];
|
||||
utxoLabels.TryGetValue(coin.OutPoint, out var labels);
|
||||
var unsmartTx = await cached[coin.OutPoint.Hash];
|
||||
var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey;
|
||||
var kp = (await _accountKeyPath).Derive(coin.KeyPath).KeyPath;
|
||||
var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(coin.Labels.Keys.ToList()),
|
||||
var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(labels?.Select(attachment => attachment.Id).ToArray()?? Array.Empty<string>()),
|
||||
current == 1 ? KeyState.Clean : KeyState.Used);
|
||||
var anonsetLabel = coin.Labels.Keys.FirstOrDefault(s => s.StartsWith("anonset-"))
|
||||
?.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1";
|
||||
var anonsetLabel = labels?.FirstOrDefault(s => s.Id.StartsWith("anonset-"))?.Id.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1";
|
||||
hdPubKey.SetAnonymitySet(double.Parse(anonsetLabel));
|
||||
|
||||
var c = new SmartCoin(tx, coin.Outpoint.N, hdPubKey);
|
||||
var c = new SmartCoin(tx, coin.OutPoint.N, hdPubKey);
|
||||
c.PropertyChanged += CoinPropertyChanged;
|
||||
return c;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Plugins.Wabisabi
|
||||
@using NBitcoin
|
||||
@using WalletWasabi.Blockchain.Analysis
|
||||
@model List<BTCPayServer.Plugins.Wabisabi.BTCPayWallet.CoinjoinData>
|
||||
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||
@{
|
||||
var network = BtcPayNetworkProvider.BTC;
|
||||
var mainnet = BtcPayNetworkProvider.NetworkType == ChainName.Mainnet;
|
||||
}
|
||||
|
||||
@functions
|
||||
{
|
||||
void PrintCoin(BTCPayWallet.CoinjoinData.CoinjoinDataCoin coin)
|
||||
{
|
||||
var op = OutPoint.Parse(coin.Outpoint);
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@PaymentTypes.BTCLike.GetTransactionLink(BtcPayNetworkProvider.BTC, op.Hash.ToString())" target="_blank" class="text-break">
|
||||
@coin.Outpoint
|
||||
</a>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
@coin.Amount
|
||||
</td>
|
||||
<td>
|
||||
@if (string.IsNullOrEmpty(coin.PayoutId))
|
||||
{
|
||||
@coin.AnonymitySet
|
||||
}
|
||||
else
|
||||
{
|
||||
@($"Payment ({coin.PayoutId})")
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Round</th>
|
||||
<th class="w-125px">Timestamp</th>
|
||||
<th class="w-125px">Coordinator</th>
|
||||
<th class="w-125px">Transaction</th>
|
||||
<th class="text-nowrap">In</th>
|
||||
<th class="text-nowrap">Out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var cjData in Model)
|
||||
{
|
||||
var cjInWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsIn.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC))));
|
||||
var cjOutWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsOut.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC))));
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-break" data-bs-toggle="collapse" data-bs-target="#txcoins-@cjData.Round">@cjData.Round</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-break">@cjData.Timestamp.ToTimeAgo()</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-break">@cjData.CoordinatorName</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@PaymentTypes.BTCLike.GetTransactionLink(network, cjData.Transaction)" target="_blank" class="text-break">
|
||||
@cjData.Transaction
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="">@cjData.CoinsIn.Length (@cjData.CoinsIn.Sum(coin => coin.Amount) BTC) (@cjInWeightedAverage anonset wavg)</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="">@cjData.CoinsOut.Length (@cjData.CoinsOut.Sum(coin => coin.Amount) BTC) (@cjOutWeightedAverage anonset wavg)</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr id="txcoins-@cjData.Round" class="collapse">
|
||||
<td colspan="6">
|
||||
<table class="table mb-0">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Inputs</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-125px">utxo</th>
|
||||
<th >Amount</th>
|
||||
<th >Anonset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var c in cjData.CoinsIn)
|
||||
{
|
||||
PrintCoin(c);
|
||||
}
|
||||
|
||||
|
||||
</table>
|
||||
<table class="table mb-0">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Outputs</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-125px">utxo</th>
|
||||
<th >Amount</th>
|
||||
<th >Anonset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var c in cjData.CoinsOut)
|
||||
{
|
||||
PrintCoin(c);
|
||||
}
|
||||
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -39,42 +39,10 @@
|
||||
}
|
||||
@if (available)
|
||||
{
|
||||
@functions
|
||||
{
|
||||
void PrintCoin(BTCPayWallet.CoinjoinData.CoinjoinDataCoin coin, bool mainnet)
|
||||
{
|
||||
var op = OutPoint.Parse(coin.Outpoint);
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@NBXInternalDestinationProvider.ComputeTxUrl(mainnet, op.Hash.ToString(), op.N.ToString())" target="_blank" class="text-break">
|
||||
@coin.Outpoint
|
||||
</a>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
@coin.Amount
|
||||
</td>
|
||||
<td>
|
||||
@if (string.IsNullOrEmpty(coin.PayoutId))
|
||||
{
|
||||
@coin.AnonymitySet
|
||||
}
|
||||
else
|
||||
{
|
||||
@($"Payment ({coin.PayoutId})")
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
{
|
||||
var settings = await WabisabiService.GetWabisabiForStore(storeId);
|
||||
var enabledSettings = settings.Settings.Where(coordinatorSettings => coordinatorSettings.Enabled);
|
||||
var cjHistory = await Client.GetOnChainWalletObjects(storeId, "BTC", new GetWalletObjectsRequest()
|
||||
{
|
||||
Type = "coinjoin"
|
||||
}).ContinueWith(task => task.Result.Select(data => (data, data.Data.ToObject<BTCPayWallet.CoinjoinData>())).OrderByDescending(tuple => tuple.Item2.Timestamp));
|
||||
|
||||
var cjHistory = (await WabisabiService.GetCoinjoinHistory(storeId)).Take(10);
|
||||
|
||||
@if (!enabledSettings.Any())
|
||||
{
|
||||
@@ -97,7 +65,11 @@
|
||||
{
|
||||
<div class="widget store-wallet-balance">
|
||||
<header>
|
||||
<h3>Coinjoin history</h3>
|
||||
<h3>Recent Coinjoins</h3>
|
||||
@if (cjHistory.Any())
|
||||
{
|
||||
<a asp-controller="WabisabiStore" asp-action="ListCoinjoins" asp-route-storeId="@storeId">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (!cjHistory.Any())
|
||||
{
|
||||
@@ -109,93 +81,8 @@
|
||||
{
|
||||
<div class="table-responsive-sm my-0">
|
||||
|
||||
<table class="table table-hover mt-3 mb-0 d-block" style="overflow-y: scroll; max-height: 500px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Round</th>
|
||||
<th class="w-125px">Timestamp</th>
|
||||
<th class="w-125px">Coordinator</th>
|
||||
<th class="w-125px">Transaction</th>
|
||||
<th class="text-nowrap">In</th>
|
||||
<th class="text-nowrap">Out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var cj in cjHistory)
|
||||
{
|
||||
var cjData = cj.Item2;
|
||||
var cjInWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsIn.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC))));
|
||||
var cjOutWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsOut.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC))));
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-break" data-bs-toggle="collapse" data-bs-target="#txcoins-@cjData.Round">@cjData.Round</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-break">@cjData.Timestamp</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-break">@cjData.CoordinatorName</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@NBXInternalDestinationProvider.ComputeTxUrl(mainnet, cjData.Transaction)" target="_blank" class="text-break">
|
||||
@cjData.Transaction
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="">@cjData.CoinsIn.Length (@cjData.CoinsIn.Sum(coin => coin.Amount) BTC) (@cjInWeightedAverage anonset wavg)</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="">@cjData.CoinsOut.Length (@cjData.CoinsOut.Sum(coin => coin.Amount) BTC) (@cjOutWeightedAverage anonset wavg)</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr id="txcoins-@cjData.Round" class="collapse">
|
||||
<td colspan="6">
|
||||
<table class="table mb-0">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Inputs</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-125px">utxo</th>
|
||||
<th >Amount</th>
|
||||
<th >Anonset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var c in cjData.CoinsIn)
|
||||
{
|
||||
PrintCoin(c, mainnet);
|
||||
}
|
||||
|
||||
|
||||
</table>
|
||||
<table class="table mb-0">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Outputs</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-125px">utxo</th>
|
||||
<th >Amount</th>
|
||||
<th >Anonset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var c in cjData.CoinsOut)
|
||||
{
|
||||
PrintCoin(c, mainnet);
|
||||
}
|
||||
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<partial name="Wabisabi/CoinjoinHistoryTable" model="cjHistory"/>
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -404,8 +291,7 @@
|
||||
@{
|
||||
foreach (var setting in enabledSettings)
|
||||
{
|
||||
|
||||
if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator))
|
||||
if (!WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -441,7 +327,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -457,4 +343,4 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
@using BTCPayServer.Components
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Plugins.Wabisabi.CoinjoinsViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(StoreNavPages.Plugins);
|
||||
}
|
||||
|
||||
<partial name="Wabisabi/CoinjoinHistoryTable" model="Model.Coinjoins" />
|
||||
<vc:pager view-model="Model"></vc:pager>
|
||||
@@ -118,7 +118,7 @@
|
||||
<div id="advanced" class="@(Model.PlebMode ? "d-none" : "")">
|
||||
<div class="form-group">
|
||||
|
||||
<label asp-for="AnonymitySetTarget" class="form-check-label">Use Anon score model</label>
|
||||
<label asp-for="AnonymitySetTarget" class="form-label">Use Anon score model</label>
|
||||
<input type="number" class="form-control" asp-for="AnonymitySetTarget" placeholder="target anon score">
|
||||
|
||||
<p class="text-muted">Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value.<br /> Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.</p>
|
||||
@@ -138,6 +138,16 @@
|
||||
<input asp-for="BatchPayments" type="checkbox" class="form-check-input" />
|
||||
<p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<label asp-for="CrossMixBetweenCoordinators" class="form-check-label">Mix funds between different coordinators</label>
|
||||
<input asp-for="CrossMixBetweenCoordinators" type="checkbox" class="form-check-input" />
|
||||
<p class="text-muted">Whether to allow mixed coins to be mixed within different coordinators for greater privacy (Warning: This will make your coins to lose the free remix within the same coordinator)</p>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<label asp-for="ExtraJoinProbability" class="form-label">ExtraJoin Probability</label>
|
||||
<input asp-for="ExtraJoinProbability" type="number" min="0" max="100" class="form-control" />
|
||||
<p class="text-muted">Percentage probability of joining a round even if you have no payments to batch and all coins are private (Warning: a high probability will quickly eat up your balance in mining fees) </p>
|
||||
</div>
|
||||
<div class="form-group ">
|
||||
<label asp-for="MixToOtherWallet" class="form-check-label">Send to other wallet</label>
|
||||
<select asp-for="MixToOtherWallet" asp-items="selectStores" class="form-select"></select>
|
||||
|
||||
@@ -158,8 +158,7 @@ public class WabisabiCoordinatorClientInstance
|
||||
var roundStateUpdaterHttpClient =
|
||||
WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit);
|
||||
sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient);
|
||||
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory,
|
||||
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
|
||||
|
||||
}
|
||||
|
||||
WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger);
|
||||
@@ -168,14 +167,12 @@ public class WabisabiCoordinatorClientInstance
|
||||
if (coordinatorName == "local")
|
||||
{
|
||||
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
|
||||
sharedWabisabiClient,
|
||||
sharedWabisabiClient, null,
|
||||
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
|
||||
WasabiHttpClientFactory,
|
||||
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater,null, WasabiHttpClientFactory,
|
||||
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,11 +70,13 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin
|
||||
provider.GetRequiredService<WabisabiCoordinatorClientInstanceManager>());
|
||||
applicationBuilder.AddSingleton<WabisabiService>();
|
||||
applicationBuilder.AddSingleton<WalletProvider>(provider => new(
|
||||
provider,
|
||||
provider.GetRequiredService<IStoreRepository>(),
|
||||
provider.GetRequiredService<IBTCPayServerClientFactory>(),
|
||||
provider.GetRequiredService<IExplorerClientProvider>(),
|
||||
provider.GetRequiredService<ILoggerFactory>(),
|
||||
utxoLocker
|
||||
utxoLocker,
|
||||
provider.GetRequiredService<EventAggregator>()
|
||||
));
|
||||
applicationBuilder.AddWabisabiCoordinator();
|
||||
applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>());
|
||||
|
||||
@@ -4,7 +4,9 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WalletWasabi.WabiSabi.Client;
|
||||
|
||||
namespace BTCPayServer.Plugins.Wabisabi
|
||||
@@ -14,15 +16,18 @@ namespace BTCPayServer.Plugins.Wabisabi
|
||||
private readonly IStoreRepository _storeRepository;
|
||||
private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager;
|
||||
private readonly WalletProvider _walletProvider;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private string[] _ids => _coordinatorClientInstanceManager.HostedServices.Keys.ToArray();
|
||||
|
||||
public WabisabiService( IStoreRepository storeRepository,
|
||||
WabisabiCoordinatorClientInstanceManager coordinatorClientInstanceManager,
|
||||
WalletProvider walletProvider)
|
||||
WalletProvider walletProvider,
|
||||
WalletRepository walletRepository)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_coordinatorClientInstanceManager = coordinatorClientInstanceManager;
|
||||
_walletProvider = walletProvider;
|
||||
_walletRepository = walletRepository;
|
||||
}
|
||||
|
||||
public async Task<WabisabiStoreSettings> GetWabisabiForStore(string storeId)
|
||||
@@ -68,6 +73,18 @@ namespace BTCPayServer.Plugins.Wabisabi
|
||||
await _walletProvider.SettingsUpdated(storeId, wabisabiSettings);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<BTCPayWallet.CoinjoinData>> GetCoinjoinHistory(string storeId)
|
||||
{
|
||||
return (await _walletRepository.GetWalletObjects(
|
||||
new GetWalletObjectsQuery(new WalletId(storeId, "BTC"))
|
||||
{
|
||||
Type = "coinjoin"
|
||||
})).Values.Where(data => !string.IsNullOrEmpty(data.Data))
|
||||
.Select(data => JObject.Parse(data.Data).ToObject<BTCPayWallet.CoinjoinData>())
|
||||
.OrderByDescending(tuple => tuple.Timestamp).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Common;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
@@ -176,7 +177,19 @@ namespace BTCPayServer.Plugins.Wabisabi
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
[Route("coinjoins")]
|
||||
public async Task<IActionResult> ListCoinjoins(string storeId, CoinjoinsViewModel viewModel, [FromServices] WalletRepository walletRepository)
|
||||
{
|
||||
var objects =await _WabisabiService.GetCoinjoinHistory(storeId);
|
||||
|
||||
viewModel ??= new CoinjoinsViewModel();
|
||||
viewModel.Coinjoins = objects
|
||||
.Skip(viewModel.Skip)
|
||||
.Take(viewModel.Count).ToList();
|
||||
viewModel.Total = objects.Count();
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet("spend")]
|
||||
public async Task<IActionResult> Spend(string storeId)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,8 @@ public class WabisabiStoreSettings
|
||||
public int AnonymitySetTarget { get; set; } = 5;
|
||||
|
||||
public bool BatchPayments { get; set; } = true;
|
||||
|
||||
public int ExtraJoinProbability { get; set; } = 0;
|
||||
public bool CrossMixBetweenCoordinators { get; set; } = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Common;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
@@ -16,29 +21,39 @@ using WalletWasabi.Bases;
|
||||
using WalletWasabi.Blockchain.TransactionOutputs;
|
||||
using WalletWasabi.WabiSabi.Client;
|
||||
using WalletWasabi.Wallets;
|
||||
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
|
||||
namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
{
|
||||
private Dictionary<string, WabisabiStoreSettings>? _cachedSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IStoreRepository _storeRepository;
|
||||
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
||||
private readonly IExplorerClientProvider _explorerClientProvider;
|
||||
public IUTXOLocker UtxoLocker { get; set; }
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public WalletProvider(IStoreRepository storeRepository, IBTCPayServerClientFactory btcPayServerClientFactory,
|
||||
IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker ) : base(TimeSpan.FromMinutes(5))
|
||||
public WalletProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
IStoreRepository storeRepository,
|
||||
IBTCPayServerClientFactory btcPayServerClientFactory,
|
||||
IExplorerClientProvider explorerClientProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IUTXOLocker utxoLocker,
|
||||
EventAggregator eventAggregator ) : base(TimeSpan.FromMinutes(5))
|
||||
{
|
||||
UtxoLocker = utxoLocker;
|
||||
_serviceProvider = serviceProvider;
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayServerClientFactory = btcPayServerClientFactory;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_loggerFactory = loggerFactory;
|
||||
initialLoad = Task.Run(async () =>
|
||||
{
|
||||
_cachedSettings =
|
||||
await storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
|
||||
});
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
}
|
||||
|
||||
public readonly ConcurrentDictionary<string, Task<IWallet?>> LoadedWallets = new();
|
||||
@@ -80,14 +95,17 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
|
||||
var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey);
|
||||
|
||||
var destinationProvider =
|
||||
new NBXInternalDestinationProvider(explorerClient, _btcPayServerClientFactory, derivationStrategy, client, name,
|
||||
wabisabiStoreSettings);
|
||||
|
||||
var smartifier = new Smartifier(explorerClient, derivationStrategy, _btcPayServerClientFactory, name,
|
||||
var smartifier = new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, _btcPayServerClientFactory, name,
|
||||
CoinOnPropertyChanged);
|
||||
|
||||
return (IWallet)new BTCPayWallet(pm, derivationStrategy, explorerClient, keychain, destinationProvider,
|
||||
return (IWallet)new BTCPayWallet(
|
||||
_serviceProvider.GetRequiredService<WalletRepository>(),
|
||||
_serviceProvider.GetRequiredService<BitcoinLikePayoutHandler>(),
|
||||
_serviceProvider.GetRequiredService<BTCPayNetworkJsonSerializerSettings>(),
|
||||
_serviceProvider.GetRequiredService<Services.Wallets.BTCPayWalletProvider>().GetWallet("BTC"),
|
||||
_serviceProvider.GetRequiredService<PullPaymentHostedService>(),
|
||||
pm, derivationStrategy, explorerClient, keychain,
|
||||
_btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker,
|
||||
_loggerFactory, smartifier, BannedCoins);
|
||||
|
||||
@@ -96,6 +114,8 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
}
|
||||
|
||||
private Task initialLoad = null;
|
||||
private IEventAggregatorSubscription _subscription;
|
||||
|
||||
public async Task<IEnumerable<IWallet>> GetWalletsAsync()
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient("BTC");
|
||||
@@ -135,26 +155,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
public async Task ResetWabisabiStuckPayouts()
|
||||
{
|
||||
var wallets = await GetWalletsAsync();
|
||||
foreach (BTCPayWallet wallet in wallets)
|
||||
|
||||
var pullPaymentHostedService = _serviceProvider.GetRequiredService<PullPaymentHostedService>();
|
||||
var payouts = await pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
var client = await _btcPayServerClientFactory.Create(null, wallet.StoreId);
|
||||
var payouts = await client.GetStorePayouts(wallet.StoreId);
|
||||
var inProgressPayouts = payouts.Where(data =>
|
||||
data.State == PayoutState.InProgress && data.PaymentMethod == "BTC" &&
|
||||
data.PaymentProof?.Value<string>("proofType") == "Wabisabi");
|
||||
foreach (PayoutData payout in inProgressPayouts)
|
||||
States = new PayoutState[]
|
||||
{
|
||||
try
|
||||
PayoutState.InProgress
|
||||
},
|
||||
PaymentMethods = new[] {"BTC"},
|
||||
Stores = wallets.Select(wallet => ((BTCPayWallet) wallet).StoreId).ToArray()
|
||||
});
|
||||
var inProgressPayouts = payouts
|
||||
.Where(data => data.GetProofBlobJson()?.Value<string>("proofType") == "Wabisabi").ToArray();
|
||||
foreach (PayoutData payout in inProgressPayouts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pullPaymentHostedService.MarkPaid(new HostedServices.MarkPayoutRequest()
|
||||
{
|
||||
var paymentproof =
|
||||
payout.PaymentProof.ToObject<NBXInternalDestinationProvider.WabisabiPaymentProof>();
|
||||
if (paymentproof.Candidates?.Any() is not true)
|
||||
await client.MarkPayout(wallet.StoreId, payout.Id,
|
||||
new MarkPayoutRequest() {State = PayoutState.AwaitingPayment});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
State = PayoutState.AwaitingPayment,
|
||||
PayoutId = payout.Id
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,4 +279,22 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
return offsets;
|
||||
});
|
||||
}
|
||||
|
||||
public override Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
initialLoad = Task.Run(async () =>
|
||||
{
|
||||
_cachedSettings =
|
||||
await _storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
|
||||
}, cancellationToken);
|
||||
_subscription = _eventAggregator.SubscribeAsync<WalletChangedEvent>(@event =>
|
||||
Check(@event.WalletId.StoreId, cancellationToken));
|
||||
return base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Submodule submodules/walletwasabi updated: 36b7fb4566...9fa9947c67
Reference in New Issue
Block a user