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

View File

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

View File

@@ -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);
@@ -141,30 +169,39 @@ 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; }
}
}

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

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 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>();
@@ -106,42 +110,28 @@ 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;
});

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

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

View File

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

View File

@@ -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>());

View File

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

View File

@@ -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,6 +177,18 @@ 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)

View File

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

View File

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