start refactor to use btcpayserver directly

This commit is contained in:
Kukks
2023-01-19 14:44:12 +01:00
parent 07952a4d44
commit 5ca18c5dc8
18 changed files with 552 additions and 427 deletions

View File

@@ -128,7 +128,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
//still good to have a chance to proceed with a join to reduce timing analysis //still good to have a chance to proceed with a join to reduce timing analysis
var rand = Random.Shared.Next(1, 101); 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."); _logger.LogInformation($"All coins are private and we have no pending payments. Skipping join.");
return solution; return solution;

View File

@@ -44,8 +44,8 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\**"/> <EmbeddedResource Include="Resources\**" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj"/> <ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<ProjectReference Include="..\..\submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj"> <ProjectReference Include="..\..\submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj">
<Properties>StaticWebAssetsEnabled=false</Properties> <Properties>StaticWebAssetsEnabled=false</Properties>
<Private>true</Private> <Private>true</Private>
@@ -53,12 +53,12 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources"/> <Folder Include="Resources" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NNostr.Client" Version="0.0.17"/> <PackageReference Include="NNostr.Client" Version="0.0.17" />
</ItemGroup> </ItemGroup>
<Target Name="DeleteExampleFile" AfterTargets="Publish"> <Target Name="DeleteExampleFile" AfterTargets="Publish">
<RemoveDir Directories="$(PublishDir)\Microservices"/> <RemoveDir Directories="$(PublishDir)\Microservices" />
</Target> </Target>
</Project> </Project>

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -9,27 +8,40 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using WalletWasabi.Blockchain.Analysis; using WalletWasabi.Blockchain.Analysis;
using WalletWasabi.Blockchain.Analysis.Clustering; using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.Keys; using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Blockchain.Transactions; using WalletWasabi.Blockchain.Transactions;
using WalletWasabi.Extensions;
using WalletWasabi.Models; using WalletWasabi.Models;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client;
using WalletWasabi.Wallets; using WalletWasabi.Wallets;
namespace BTCPayServer.Plugins.Wabisabi; 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 OnChainPaymentMethodData OnChainPaymentMethodData;
public readonly DerivationStrategyBase DerivationScheme; public readonly DerivationStrategyBase DerivationScheme;
public readonly ExplorerClient ExplorerClient; public readonly ExplorerClient ExplorerClient;
@@ -39,15 +51,30 @@ public class BTCPayWallet : IWallet
public readonly ILogger Logger; public readonly ILogger Logger;
private static readonly BlockchainAnalyzer BlockchainAnalyzer = new(); private static readonly BlockchainAnalyzer BlockchainAnalyzer = new();
public BTCPayWallet(OnChainPaymentMethodData onChainPaymentMethodData, DerivationStrategyBase derivationScheme, public BTCPayWallet(
ExplorerClient explorerClient, BTCPayKeyChain keyChain, WalletRepository walletRepository,
IDestinationProvider destinationProvider, IBTCPayServerClientFactory btcPayServerClientFactory, string storeId, BitcoinLikePayoutHandler bitcoinLikePayoutHandler,
WabisabiStoreSettings wabisabiStoreSettings, IUTXOLocker utxoLocker, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
ILoggerFactory loggerFactory, Smartifier smartifier, 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) ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins)
{ {
KeyChain = keyChain; KeyChain = keyChain;
DestinationProvider = destinationProvider; _walletRepository = walletRepository;
_bitcoinLikePayoutHandler = bitcoinLikePayoutHandler;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_btcPayWallet = btcPayWallet;
_pullPaymentHostedService = pullPaymentHostedService;
OnChainPaymentMethodData = onChainPaymentMethodData; OnChainPaymentMethodData = onChainPaymentMethodData;
DerivationScheme = derivationScheme; DerivationScheme = derivationScheme;
ExplorerClient = explorerClient; ExplorerClient = explorerClient;
@@ -72,7 +99,7 @@ public class BTCPayWallet : IWallet
} }
public IKeyChain KeyChain { get; } public IKeyChain KeyChain { get; }
public IDestinationProvider DestinationProvider { get; } public IDestinationProvider DestinationProvider => this;
public int AnonymitySetTarget => WabisabiStoreSettings.PlebMode? 2: WabisabiStoreSettings.AnonymitySetTarget; public int AnonymitySetTarget => WabisabiStoreSettings.PlebMode? 2: WabisabiStoreSettings.AnonymitySetTarget;
public bool ConsolidationMode => !WabisabiStoreSettings.PlebMode && WabisabiStoreSettings.ConsolidationMode; public bool ConsolidationMode => !WabisabiStoreSettings.PlebMode && WabisabiStoreSettings.ConsolidationMode;
@@ -93,10 +120,11 @@ public class BTCPayWallet : IWallet
public async Task<CoinsView> GetAllCoins() public async Task<CoinsView> GetAllCoins()
{ {
await _savingProgress; await _savingProgress;
var client = await BtcPayServerClientFactory.Create(null, StoreId);
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme);
await _smartifier.LoadCoins(utxos.ToList()); var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos);
var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.Outpoint == pair.Key)) 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)); .Select(pair => pair.Value));
return new CoinsView(coins); return new CoinsView(coins);
@@ -141,30 +169,39 @@ public class BTCPayWallet : IWallet
return Array.Empty<SmartCoin>(); return Array.Empty<SmartCoin>();
} }
var client = await BtcPayServerClientFactory.Create(null, StoreId); var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme, true, CancellationToken.None);
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos);
var objs = client.GetOnChainWalletObjects(StoreId, "BTC",
new GetWalletObjectsRequest()
{
Type = "utxo", Ids = utxos.Select(data => data.Outpoint.ToString()).ToArray()
});
if (!WabisabiStoreSettings.PlebMode) if (!WabisabiStoreSettings.PlebMode)
{ {
if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true) if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true)
{ {
utxos = utxos.Where(data => 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) if (WabisabiStoreSettings.InputLabelsExcluded?.Any() is true)
{ {
utxos = utxos.Where(data => 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()); if (WabisabiStoreSettings.PlebMode || !WabisabiStoreSettings.CrossMixBetweenCoordinators)
utxos = utxos.Where(data => !locks.Contains(data.Outpoint)).Where(data => data.Confirmations > 0); {
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)) if (_bannedCoins.TryGetValue(coordinatorName, out var bannedCoins))
{ {
var expired = bannedCoins.Where(pair => pair.Value < DateTimeOffset.Now).ToArray(); 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)); .Select(pair => pair.Value));
foreach (SmartCoin c in resultX) 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); 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() public async Task<IEnumerable<SmartTransaction>> GetTransactionsAsync()
{ {
@@ -471,4 +530,141 @@ public class BTCPayWallet : IWallet
Logger.LogInformation($"unlocked utxos: {string.Join(',', unlocked)}"); 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; }
}
} }

View 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;
}

View File

@@ -1,8 +1,7 @@
using System; using BTCPayServer.Abstractions.Contracts;
using System.Net.Http;
using System.Reflection;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Services; using BTCPayServer.Abstractions.Services;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using WalletWasabi.WabiSabi.Models.Serialization; using WalletWasabi.WabiSabi.Models.Serialization;
@@ -24,20 +23,19 @@ public static class CoordinatorExtensions
services.AddSingleton<IUIExtension>(new UIExtension("Wabisabi/WabisabiServerNavvExtension", "server-nav")); 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") services.AddHttpClient("wabisabi-coordinator-scripts-no-redirect.onion")
.ConfigurePrimaryHttpMessageHandler(provider => .ConfigurePrimaryHttpMessageHandler(provider =>
{ {
var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t);
var handler = new Socks5HttpClientHandler(provider.GetRequiredService<BTCPayServerOptions>());
handler.AllowAutoRedirect = false; handler.AllowAutoRedirect = false;
return handler; return handler;
}); });
services.AddHttpClient("wabisabi-coordinator-scripts.onion") services.AddHttpClient("wabisabi-coordinator-scripts.onion")
.ConfigurePrimaryHttpMessageHandler(provider => .ConfigurePrimaryHttpMessageHandler(provider =>
{ {
var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t); var handler = new Socks5HttpClientHandler(provider.GetRequiredService<BTCPayServerOptions>());
handler.AllowAutoRedirect = false; handler.AllowAutoRedirect = false;
return handler; return handler;
}); });

View File

@@ -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; }
}
}

View File

@@ -6,6 +6,8 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
@@ -20,16 +22,20 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class Smartifier public class Smartifier
{ {
private readonly WalletRepository _walletRepository;
private readonly ExplorerClient _explorerClient; private readonly ExplorerClient _explorerClient;
public DerivationStrategyBase DerivationScheme { get; } public DerivationStrategyBase DerivationScheme { get; }
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
private readonly string _storeId; private readonly string _storeId;
private readonly Action<object, PropertyChangedEventArgs> _coinOnPropertyChanged; private readonly Action<object, PropertyChangedEventArgs> _coinOnPropertyChanged;
public Smartifier(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, public Smartifier(
WalletRepository walletRepository,
ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase,
IBTCPayServerClientFactory btcPayServerClientFactory, string storeId, IBTCPayServerClientFactory btcPayServerClientFactory, string storeId,
Action<object, PropertyChangedEventArgs> coinOnPropertyChanged) Action<object, PropertyChangedEventArgs> coinOnPropertyChanged)
{ {
_walletRepository = walletRepository;
_explorerClient = explorerClient; _explorerClient = explorerClient;
DerivationScheme = derivationStrategyBase; DerivationScheme = derivationStrategyBase;
_btcPayServerClientFactory = btcPayServerClientFactory; _btcPayServerClientFactory = btcPayServerClientFactory;
@@ -45,25 +51,26 @@ public class Smartifier
public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new(); public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new();
private readonly Task<RootedKeyPath> _accountKeyPath; 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(); coins = coins.Where(data => data is not null).ToList();
if (current > 3) if (current > 3)
{ {
return; return;
} }
var txs = coins.Select(data => data.Outpoint.Hash).Distinct(); var txs = coins.Select(data => data.OutPoint.Hash).Distinct();
foreach (uint256 tx in txs) foreach (uint256 tx in txs)
{ {
cached.TryAdd(tx, _explorerClient.GetTransactionAsync(DerivationScheme, tx)); 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 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) if (unsmartTx is null)
{ {
return null; return null;
@@ -71,9 +78,6 @@ public class Smartifier
var smartTx = new SmartTransaction(unsmartTx.Transaction, var smartTx = new SmartTransaction(unsmartTx.Transaction,
unsmartTx.Height is null ? Height.Mempool : new Height((uint)unsmartTx.Height.Value), unsmartTx.Height is null ? Height.Mempool : new Height((uint)unsmartTx.Height.Value),
unsmartTx.BlockHash, firstSeen: unsmartTx.Timestamp); 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>(); var ourSpentUtxos = new Dictionary<MatchedOutput, IndexedTxIn>();
@@ -106,42 +110,28 @@ public class Smartifier
} }
} }
} }
var utxoObjects = await client.GetOnChainWalletObjects(_storeId, "BTC", var inputsToLoad = unsmartTx.Inputs.Select(output =>
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 =>
{ {
if (!ourSpentUtxos.TryGetValue(output, out var outputtxin)) if (!ourSpentUtxos.TryGetValue(output, out var outputtxin))
{ {
return null; return null;
} }
var outpoint = outputtxin.PrevOut; var outpoint = outputtxin.PrevOut;
var labels = labelsOfOurSpentUtxos return new ReceivedCoin()
.GetValueOrDefault(outpoint.ToString(),
new List<OnChainWalletObjectData.OnChainWalletObjectLink>())
.ToDictionary(link => link.Id, link => new LabelData());
return new OnChainWalletUTXOData()
{ {
Timestamp = DateTimeOffset.Now, Timestamp = DateTimeOffset.Now,
Address = Address =
output.Address?.ToString() ?? _explorerClient.Network output.Address ?? _explorerClient.Network
.CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey) .CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey),
.ToString(),
KeyPath = output.KeyPath, KeyPath = output.KeyPath,
Amount = ((Money)output.Value).ToDecimal(MoneyUnit.BTC), Value = output.Value,
Outpoint = outpoint, OutPoint = outpoint,
Labels = labels,
Confirmations = unsmartTx.Confirmations 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) foreach (MatchedOutput input in unsmartTx.Inputs)
{ {
if (!ourSpentUtxos.TryGetValue(input, out var outputtxin)) if (!ourSpentUtxos.TryGetValue(input, out var outputtxin))
@@ -159,19 +149,18 @@ public class Smartifier
return smartTx; return smartTx;
}); });
var smartCoin = await Coins.GetOrAdd(coin.Outpoint, async point => var smartCoin = await Coins.GetOrAdd(coin.OutPoint, async point =>
{ {
utxoLabels.TryGetValue(coin.OutPoint, out var labels);
var unsmartTx = await cached[coin.Outpoint.Hash]; var unsmartTx = await cached[coin.OutPoint.Hash];
var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey; var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey;
var kp = (await _accountKeyPath).Derive(coin.KeyPath).KeyPath; 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); current == 1 ? KeyState.Clean : KeyState.Used);
var anonsetLabel = coin.Labels.Keys.FirstOrDefault(s => s.StartsWith("anonset-")) var anonsetLabel = labels?.FirstOrDefault(s => s.Id.StartsWith("anonset-"))?.Id.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1";
?.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1";
hdPubKey.SetAnonymitySet(double.Parse(anonsetLabel)); 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; c.PropertyChanged += CoinPropertyChanged;
return c; return c;
}); });

View File

@@ -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>

View File

@@ -39,42 +39,10 @@
} }
@if (available) @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 settings = await WabisabiService.GetWabisabiForStore(storeId);
var enabledSettings = settings.Settings.Where(coordinatorSettings => coordinatorSettings.Enabled); var enabledSettings = settings.Settings.Where(coordinatorSettings => coordinatorSettings.Enabled);
var cjHistory = await Client.GetOnChainWalletObjects(storeId, "BTC", new GetWalletObjectsRequest() var cjHistory = (await WabisabiService.GetCoinjoinHistory(storeId)).Take(10);
{
Type = "coinjoin"
}).ContinueWith(task => task.Result.Select(data => (data, data.Data.ToObject<BTCPayWallet.CoinjoinData>())).OrderByDescending(tuple => tuple.Item2.Timestamp));
@if (!enabledSettings.Any()) @if (!enabledSettings.Any())
{ {
@@ -97,7 +65,11 @@
{ {
<div class="widget store-wallet-balance"> <div class="widget store-wallet-balance">
<header> <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> </header>
@if (!cjHistory.Any()) @if (!cjHistory.Any())
{ {
@@ -109,93 +81,8 @@
{ {
<div class="table-responsive-sm my-0"> <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> </div>
} }
@@ -404,8 +291,7 @@
@{ @{
foreach (var setting in enabledSettings) foreach (var setting in enabledSettings)
{ {
if (!WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator))
if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator))
{ {
continue; continue;
} }

View File

@@ -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>

View File

@@ -118,7 +118,7 @@
<div id="advanced" class="@(Model.PlebMode ? "d-none" : "")"> <div id="advanced" class="@(Model.PlebMode ? "d-none" : "")">
<div class="form-group"> <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"> <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> <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" /> <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> <p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
</div> </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 "> <div class="form-group ">
<label asp-for="MixToOtherWallet" class="form-check-label">Send to other wallet</label> <label asp-for="MixToOtherWallet" class="form-check-label">Send to other wallet</label>
<select asp-for="MixToOtherWallet" asp-items="selectStores" class="form-select"></select> <select asp-for="MixToOtherWallet" asp-items="selectStores" class="form-select"></select>

View File

@@ -158,8 +158,7 @@ public class WabisabiCoordinatorClientInstance
var roundStateUpdaterHttpClient = var roundStateUpdaterHttpClient =
WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit); WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit);
sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient); sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient);
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
} }
WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger); WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger);
@@ -168,14 +167,12 @@ public class WabisabiCoordinatorClientInstance
if (coordinatorName == "local") if (coordinatorName == "local")
{ {
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater, CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
sharedWabisabiClient, sharedWabisabiClient, null,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier); WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
} }
else else
{ {
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater,null, WasabiHttpClientFactory,
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
WasabiHttpClientFactory,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier); WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
} }

View File

@@ -70,11 +70,13 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin
provider.GetRequiredService<WabisabiCoordinatorClientInstanceManager>()); provider.GetRequiredService<WabisabiCoordinatorClientInstanceManager>());
applicationBuilder.AddSingleton<WabisabiService>(); applicationBuilder.AddSingleton<WabisabiService>();
applicationBuilder.AddSingleton<WalletProvider>(provider => new( applicationBuilder.AddSingleton<WalletProvider>(provider => new(
provider,
provider.GetRequiredService<IStoreRepository>(), provider.GetRequiredService<IStoreRepository>(),
provider.GetRequiredService<IBTCPayServerClientFactory>(), provider.GetRequiredService<IBTCPayServerClientFactory>(),
provider.GetRequiredService<IExplorerClientProvider>(), provider.GetRequiredService<IExplorerClientProvider>(),
provider.GetRequiredService<ILoggerFactory>(), provider.GetRequiredService<ILoggerFactory>(),
utxoLocker utxoLocker,
provider.GetRequiredService<EventAggregator>()
)); ));
applicationBuilder.AddWabisabiCoordinator(); applicationBuilder.AddWabisabiCoordinator();
applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>()); applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>());

View File

@@ -4,7 +4,9 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Services;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json.Linq;
using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client;
namespace BTCPayServer.Plugins.Wabisabi namespace BTCPayServer.Plugins.Wabisabi
@@ -14,15 +16,18 @@ namespace BTCPayServer.Plugins.Wabisabi
private readonly IStoreRepository _storeRepository; private readonly IStoreRepository _storeRepository;
private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager; private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager;
private readonly WalletProvider _walletProvider; private readonly WalletProvider _walletProvider;
private readonly WalletRepository _walletRepository;
private string[] _ids => _coordinatorClientInstanceManager.HostedServices.Keys.ToArray(); private string[] _ids => _coordinatorClientInstanceManager.HostedServices.Keys.ToArray();
public WabisabiService( IStoreRepository storeRepository, public WabisabiService( IStoreRepository storeRepository,
WabisabiCoordinatorClientInstanceManager coordinatorClientInstanceManager, WabisabiCoordinatorClientInstanceManager coordinatorClientInstanceManager,
WalletProvider walletProvider) WalletProvider walletProvider,
WalletRepository walletRepository)
{ {
_storeRepository = storeRepository; _storeRepository = storeRepository;
_coordinatorClientInstanceManager = coordinatorClientInstanceManager; _coordinatorClientInstanceManager = coordinatorClientInstanceManager;
_walletProvider = walletProvider; _walletProvider = walletProvider;
_walletRepository = walletRepository;
} }
public async Task<WabisabiStoreSettings> GetWabisabiForStore(string storeId) public async Task<WabisabiStoreSettings> GetWabisabiForStore(string storeId)
@@ -68,6 +73,18 @@ namespace BTCPayServer.Plugins.Wabisabi
await _walletProvider.SettingsUpdated(storeId, wabisabiSettings); 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();
}
} }
} }

View File

@@ -11,6 +11,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Common; using BTCPayServer.Common;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -176,6 +177,18 @@ namespace BTCPayServer.Plugins.Wabisabi
return View(vm); 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")] [HttpGet("spend")]
public async Task<IActionResult> Spend(string storeId) public async Task<IActionResult> Spend(string storeId)

View File

@@ -19,7 +19,8 @@ public class WabisabiStoreSettings
public int AnonymitySetTarget { get; set; } = 5; public int AnonymitySetTarget { get; set; } = 5;
public bool BatchPayments { get; set; } = true; public bool BatchPayments { get; set; } = true;
public int ExtraJoinProbability { get; set; } = 0;
public bool CrossMixBetweenCoordinators { get; set; } = false;
} }

View File

@@ -8,7 +8,12 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Common; using BTCPayServer.Common;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
@@ -16,29 +21,39 @@ using WalletWasabi.Bases;
using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client;
using WalletWasabi.Wallets; using WalletWasabi.Wallets;
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
using PayoutData = BTCPayServer.Data.PayoutData;
namespace BTCPayServer.Plugins.Wabisabi; namespace BTCPayServer.Plugins.Wabisabi;
public class WalletProvider : PeriodicRunner,IWalletProvider public class WalletProvider : PeriodicRunner,IWalletProvider
{ {
private Dictionary<string, WabisabiStoreSettings>? _cachedSettings; private Dictionary<string, WabisabiStoreSettings>? _cachedSettings;
private readonly IServiceProvider _serviceProvider;
private readonly IStoreRepository _storeRepository;
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
private readonly IExplorerClientProvider _explorerClientProvider; private readonly IExplorerClientProvider _explorerClientProvider;
public IUTXOLocker UtxoLocker { get; set; } public IUTXOLocker UtxoLocker { get; set; }
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly EventAggregator _eventAggregator;
public WalletProvider(IStoreRepository storeRepository, IBTCPayServerClientFactory btcPayServerClientFactory, public WalletProvider(
IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker ) : base(TimeSpan.FromMinutes(5)) IServiceProvider serviceProvider,
IStoreRepository storeRepository,
IBTCPayServerClientFactory btcPayServerClientFactory,
IExplorerClientProvider explorerClientProvider,
ILoggerFactory loggerFactory,
IUTXOLocker utxoLocker,
EventAggregator eventAggregator ) : base(TimeSpan.FromMinutes(5))
{ {
UtxoLocker = utxoLocker; UtxoLocker = utxoLocker;
_serviceProvider = serviceProvider;
_storeRepository = storeRepository;
_btcPayServerClientFactory = btcPayServerClientFactory; _btcPayServerClientFactory = btcPayServerClientFactory;
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
initialLoad = Task.Run(async () => _eventAggregator = eventAggregator;
{
_cachedSettings =
await storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
});
} }
public readonly ConcurrentDictionary<string, Task<IWallet?>> LoadedWallets = new(); 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 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); 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, _btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker,
_loggerFactory, smartifier, BannedCoins); _loggerFactory, smartifier, BannedCoins);
@@ -96,6 +114,8 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
} }
private Task initialLoad = null; private Task initialLoad = null;
private IEventAggregatorSubscription _subscription;
public async Task<IEnumerable<IWallet>> GetWalletsAsync() public async Task<IEnumerable<IWallet>> GetWalletsAsync()
{ {
var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var explorerClient = _explorerClientProvider.GetExplorerClient("BTC");
@@ -135,26 +155,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
public async Task ResetWabisabiStuckPayouts() public async Task ResetWabisabiStuckPayouts()
{ {
var wallets = await GetWalletsAsync(); 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); States = new PayoutState[]
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)
{ {
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 = State = PayoutState.AwaitingPayment,
payout.PaymentProof.ToObject<NBXInternalDestinationProvider.WabisabiPaymentProof>(); PayoutId = payout.Id
if (paymentproof.Candidates?.Any() is not true) });
await client.MarkPayout(wallet.StoreId, payout.Id, }
new MarkPayoutRequest() {State = PayoutState.AwaitingPayment}); catch (Exception e)
} {
catch (Exception e)
{
}
} }
} }
} }
@@ -254,4 +279,22 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
return offsets; 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);
}
} }