This commit is contained in:
Kukks
2023-03-13 16:37:47 +01:00
parent d8318b7eb3
commit 4052aaefc9
11 changed files with 592 additions and 134 deletions

View File

@@ -13,6 +13,7 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class BTCPayKeyChain : IKeyChain public class BTCPayKeyChain : IKeyChain
{ {
public Smartifier Smartifier { get; }
private readonly ExplorerClient _explorerClient; private readonly ExplorerClient _explorerClient;
private readonly DerivationStrategyBase _derivationStrategy; private readonly DerivationStrategyBase _derivationStrategy;
private readonly ExtKey _masterKey; private readonly ExtKey _masterKey;
@@ -21,8 +22,9 @@ public class BTCPayKeyChain : IKeyChain
public bool KeysAvailable => _masterKey is not null && _accountKey is not null; public bool KeysAvailable => _masterKey is not null && _accountKey is not null;
public BTCPayKeyChain(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategy, ExtKey masterKey, public BTCPayKeyChain(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategy, ExtKey masterKey,
ExtKey accountKey) ExtKey accountKey, Smartifier smartifier)
{ {
Smartifier = smartifier;
_explorerClient = explorerClient; _explorerClient = explorerClient;
_derivationStrategy = derivationStrategy; _derivationStrategy = derivationStrategy;
_masterKey = masterKey; _masterKey = masterKey;

View File

@@ -43,7 +43,7 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NNostr.Client" Version="0.0.23" /> <PackageReference Include="NNostr.Client" Version="0.0.24" />
</ItemGroup> </ItemGroup>
<Target Name="DeleteExampleFile" AfterTargets="Publish"> <Target Name="DeleteExampleFile" AfterTargets="Publish">
<RemoveDir Directories="$(PublishDir)\Microservices" /> <RemoveDir Directories="$(PublishDir)\Microservices" />

View File

@@ -44,10 +44,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly Services.Wallets.BTCPayWallet _btcPayWallet; private readonly Services.Wallets.BTCPayWallet _btcPayWallet;
private readonly PullPaymentHostedService _pullPaymentHostedService; 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;
public readonly IBTCPayServerClientFactory BtcPayServerClientFactory; // public readonly IBTCPayServerClientFactory BtcPayServerClientFactory;
public WabisabiStoreSettings WabisabiStoreSettings; public WabisabiStoreSettings WabisabiStoreSettings;
public readonly IUTXOLocker UtxoLocker; public readonly IUTXOLocker UtxoLocker;
public readonly ILogger Logger; public readonly ILogger Logger;
@@ -59,16 +59,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
Services.Wallets.BTCPayWallet btcPayWallet, Services.Wallets.BTCPayWallet btcPayWallet,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
OnChainPaymentMethodData onChainPaymentMethodData,
DerivationStrategyBase derivationScheme, DerivationStrategyBase derivationScheme,
ExplorerClient explorerClient, ExplorerClient explorerClient,
BTCPayKeyChain keyChain, BTCPayKeyChain keyChain,
IBTCPayServerClientFactory btcPayServerClientFactory,
string storeId, string storeId,
WabisabiStoreSettings wabisabiStoreSettings, WabisabiStoreSettings wabisabiStoreSettings,
IUTXOLocker utxoLocker, IUTXOLocker utxoLocker,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
Smartifier smartifier,
StoreRepository storeRepository, StoreRepository storeRepository,
ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins, EventAggregator eventAggregator) ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins, EventAggregator eventAggregator)
{ {
@@ -79,14 +76,11 @@ public class BTCPayWallet : IWallet, IDestinationProvider
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_btcPayWallet = btcPayWallet; _btcPayWallet = btcPayWallet;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
OnChainPaymentMethodData = onChainPaymentMethodData;
DerivationScheme = derivationScheme; DerivationScheme = derivationScheme;
ExplorerClient = explorerClient; ExplorerClient = explorerClient;
BtcPayServerClientFactory = btcPayServerClientFactory;
StoreId = storeId; StoreId = storeId;
WabisabiStoreSettings = wabisabiStoreSettings; WabisabiStoreSettings = wabisabiStoreSettings;
UtxoLocker = utxoLocker; UtxoLocker = utxoLocker;
_smartifier = smartifier;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_bannedCoins = bannedCoins; _bannedCoins = bannedCoins;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@@ -101,8 +95,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider
bool IWallet.IsMixable(string coordinator) bool IWallet.IsMixable(string coordinator)
{ {
return OnChainPaymentMethodData?.Enabled is true && WabisabiStoreSettings.Settings.SingleOrDefault(settings => return KeyChain is BTCPayKeyChain {KeysAvailable: true} && WabisabiStoreSettings.Settings.SingleOrDefault(
settings.Coordinator.Equals(coordinator))?.Enabled is true && ((BTCPayKeyChain)KeyChain).KeysAvailable; settings =>
settings.Coordinator.Equals(coordinator))?.Enabled is true;
} }
public IKeyChain KeyChain { get; } public IKeyChain KeyChain { get; }
@@ -152,7 +147,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
} }
private IRoundCoinSelector _coinSelector; private IRoundCoinSelector _coinSelector;
public readonly Smartifier _smartifier; public Smartifier _smartifier => (KeyChain as BTCPayKeyChain)?.Smartifier;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> _bannedCoins; private readonly ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> _bannedCoins;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
@@ -352,7 +347,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName) public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName)
{ {
await _savingProgress; await _savingProgress;
_savingProgress = RegisterCoinjoinTransactionInternal(result, coordinatorName); _savingProgress = RegisterCoinjoinTransactionInternal(result, coordinatorName);
await _savingProgress; await _savingProgress;
} }
private async Task RegisterCoinjoinTransactionInternal(SuccessfulCoinJoinResult result, string coordinatorName) private async Task RegisterCoinjoinTransactionInternal(SuccessfulCoinJoinResult result, string coordinatorName)
@@ -378,10 +379,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider
Dictionary<IndexedTxOut, PendingPayment> indexToPayment = new(); Dictionary<IndexedTxOut, PendingPayment> indexToPayment = new();
foreach (var script in result.OutputScripts) foreach (var script in result.Outputs)
{ {
var txout = result.UnsignedCoinJoin.Outputs.AsIndexedOutputs() var txout = result.UnsignedCoinJoin.Outputs.AsIndexedOutputs()
.Single(@out => @out.TxOut.ScriptPubKey == script); .Single(@out => @out.TxOut.ScriptPubKey == script.ScriptPubKey && @out.TxOut.Value == script.Value);
//this was not a mix to self, but rather a payment //this was not a mix to self, but rather a payment
@@ -393,7 +394,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
continue; continue;
} }
scriptInfos.Add((txout, ExplorerClient.GetKeyInformationAsync(BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value)?utxoDerivationScheme:DerivationScheme, script))); scriptInfos.Add((txout, ExplorerClient.GetKeyInformationAsync(BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value)?utxoDerivationScheme:DerivationScheme, script.ScriptPubKey)));
} }
await Task.WhenAll(scriptInfos.Select(t => t.Item2)); await Task.WhenAll(scriptInfos.Select(t => t.Item2));
@@ -405,9 +406,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider
coin.SpenderTransaction = smartTx; coin.SpenderTransaction = smartTx;
smartTx.TryAddWalletInput(coin); smartTx.TryAddWalletInput(coin);
}); });
result.OutputScripts.ForEach(s => result.Outputs.ForEach(s =>
{ {
if (scriptInfos2.TryGetValue(s, out var si)) if (scriptInfos2.TryGetValue(s.ScriptPubKey, out var si))
{ {
var derivation = DerivationScheme.GetChild(si.Item2.Result.KeyPath).GetExtPubKeys().First() var derivation = DerivationScheme.GetChild(si.Item2.Result.KeyPath).GetExtPubKeys().First()
.PubKey; .PubKey;
@@ -582,22 +583,22 @@ public class BTCPayWallet : IWallet, IDestinationProvider
} }
public async Task UnlockUTXOs() // public async Task UnlockUTXOs()
{ // {
var client = await BtcPayServerClientFactory.Create(null, StoreId); // var client = await BtcPayServerClientFactory.Create(null, StoreId);
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); // var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC");
var unlocked = new List<string>(); // var unlocked = new List<string>();
foreach (OnChainWalletUTXOData utxo in utxos) // foreach (OnChainWalletUTXOData utxo in utxos)
{ // {
//
if (await UtxoLocker.TryUnlock(utxo.Outpoint)) // if (await UtxoLocker.TryUnlock(utxo.Outpoint))
{ // {
unlocked.Add(utxo.Outpoint.ToString()); // unlocked.Add(utxo.Outpoint.ToString());
} // }
} // }
//
Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}"); // Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}");
} // }
public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count, bool mixedOutputs) public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count, bool mixedOutputs)
{ {
@@ -605,15 +606,14 @@ public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count,
{ {
try try
{ {
var mixClient = await BtcPayServerClientFactory.Create(null, WabisabiStoreSettings.MixToOtherWallet); var mixStore = await _storeRepository.FindStore(WabisabiStoreSettings.MixToOtherWallet);
var pm = await mixClient.GetStoreOnChainPaymentMethod(WabisabiStoreSettings.MixToOtherWallet, var pm = mixStore.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC");
"BTC");
var deriv = ExplorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme);
if (deriv.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType()) if (pm?.AccountDerivation?.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType())
{ {
return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ =>
_btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, deriv, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address)); _btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, pm.AccountDerivation, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address));
} }
} }

View File

@@ -0,0 +1,240 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitcoin.Secp256k1;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NNostr.Client;
using WalletWasabi.Logging;
using WalletWasabi.WabiSabi;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Models;
using WalletWasabi.WabiSabi.Models.Serialization;
namespace BTCPayServer.Plugins.Wabisabi;
public class NostrWabiSabiApiClient : IWabiSabiApiRequestHandler, IHostedService
{
public static int RoundStateKind = 15750;
public static int CommunicationKind = 25750;
private readonly NostrClient _client;
private readonly ECXOnlyPubKey _coordinatorKey;
private string _coordinatorKeyHex => _coordinatorKey.ToHex();
private readonly string _coordinatorFilterId;
public NostrWabiSabiApiClient(NostrClient client, ECXOnlyPubKey coordinatorKey)
{
_client = client;
_coordinatorKey = coordinatorKey;
_coordinatorFilterId = new Guid().ToString();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = _client.ListenForMessages();
var filter = new NostrSubscriptionFilter()
{
Authors = new[] {_coordinatorKey.ToHex()},
Kinds = new[] {RoundStateKind, CommunicationKind},
Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1))
};
await _client.CreateSubscription(_coordinatorFilterId, new[] {filter}, cancellationToken);
_client.EventsReceived += EventsReceived;
await _client.ConnectAndWaitUntilConnected(cancellationToken);
}
private RoundStateResponse _lastRoundState { get; set; }
private TaskCompletionSource _lastRoundStateTask = new();
private void EventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e)
{
if (e.subscriptionId == _coordinatorFilterId)
{
var roundState = e.events.Where(evt => evt.Kind == RoundStateKind).MaxBy(@event => @event.CreatedAt);
if (roundState != null)
{
_lastRoundState = Deserialize<RoundStateResponse>(roundState.Content);
_lastRoundStateTask.TrySetResult();
}
}
}
private async Task SendAndWaitForReply<TRequest>(RemoteAction action, TRequest request,
CancellationToken cancellationToken)
{
await SendAndWaitForReply<TRequest, JObject>(action, request, cancellationToken);
}
private async Task<TResponse> SendAndWaitForReply<TRequest, TResponse>(RemoteAction action, TRequest request,
CancellationToken cancellationToken)
{
var newKey = ECPrivKey.Create(RandomUtils.GetBytes(32));
var pubkey = newKey.CreateXOnlyPubKey();
var evt = new NostrEvent()
{
Content = Serialize(new
{
Action = action,
Request = request
}),
PublicKey = pubkey.ToHex(),
Kind = 4,
CreatedAt = DateTimeOffset.Now
};
evt.SetTag("p", _coordinatorKeyHex);
await evt.EncryptNip04EventAsync(newKey);
evt.Kind = CommunicationKind;
await evt.ComputeIdAndSignAsync(newKey);
var tcs = new TaskCompletionSource<NostrEvent>(cancellationToken);
void OnClientEventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e)
{
foreach (var nostrEvent in e.events)
{
if (nostrEvent.PublicKey != _coordinatorKeyHex) continue;
var replyToEvent = evt.GetTaggedData("e");
var replyToUser = evt.GetTaggedData("p");
if (replyToEvent.All(s => s != evt.Id) || replyToUser.All(s => s != evt.PublicKey)) continue;
if (!nostrEvent.Verify()) continue;
_client.EventsReceived -= OnClientEventsReceived;
tcs.TrySetResult(nostrEvent);
break;
}
}
_client.EventsReceived += OnClientEventsReceived;
try
{
var replyEvent = await tcs.Task;
replyEvent.Kind = 4;
var response = await replyEvent.DecryptNip04EventAsync(newKey);
var jobj = JObject.Parse(response);
if (jobj.TryGetValue("error", out var errorJson))
{
var contentString = errorJson.Value<string>();
var error = JsonConvert.DeserializeObject<Error>(contentString, new JsonSerializerSettings()
{
Converters = JsonSerializationOptions.Default.Settings.Converters,
Error = (_, e) => e.ErrorContext.Handled = true // Try to deserialize an Error object
});
var innerException = error switch
{
{Type: ProtocolConstants.ProtocolViolationType} => Enum.TryParse<WabiSabiProtocolErrorCode>(
error.ErrorCode, out var code)
? new WabiSabiProtocolException(code, error.Description, exceptionData: error.ExceptionData)
: new NotSupportedException(
$"Received WabiSabi protocol exception with unknown '{error.ErrorCode}' error code.\n\tDescription: '{error.Description}'."),
{Type: "unknown"} => new Exception(error.Description),
_ => null
};
if (innerException is not null)
{
throw new HttpRequestException("Remote coordinator responded with an error.", innerException);
}
// Remove " from beginning and end to ensure backwards compatibility and it's kind of trash, too.
if (contentString.Count(f => f == '"') <= 2)
{
contentString = contentString.Trim('"');
}
var errorMessage = string.Empty;
if (!string.IsNullOrWhiteSpace(contentString))
{
errorMessage = $"\n{contentString}";
}
throw new HttpRequestException($"ERROR:{errorMessage}");
}
return jobj.ToObject<TResponse>(JsonSerializer.Create(JsonSerializationOptions.Default.Settings));
}
catch (OperationCanceledException e)
{
_client.EventsReceived -= OnClientEventsReceived;
throw;
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _client.CloseSubscription(_coordinatorFilterId, cancellationToken);
_client.EventsReceived -= EventsReceived;
}
public async Task<RoundStateResponse> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
await _lastRoundStateTask.Task;
return _lastRoundState;
}
public Task<InputRegistrationResponse> RegisterInputAsync(InputRegistrationRequest request,
CancellationToken cancellationToken) =>
SendAndWaitForReply<InputRegistrationRequest, InputRegistrationResponse>(RemoteAction.RegisterInput, request,
cancellationToken);
public Task<ConnectionConfirmationResponse> ConfirmConnectionAsync(ConnectionConfirmationRequest request,
CancellationToken cancellationToken) =>
SendAndWaitForReply<ConnectionConfirmationRequest, ConnectionConfirmationResponse>(
RemoteAction.ConfirmConnection, request, cancellationToken);
public Task RegisterOutputAsync(OutputRegistrationRequest request, CancellationToken cancellationToken) =>
SendAndWaitForReply(RemoteAction.RegisterOutput, request, cancellationToken);
public Task<ReissueCredentialResponse> ReissuanceAsync(ReissueCredentialRequest request,
CancellationToken cancellationToken) =>
SendAndWaitForReply<ReissueCredentialRequest, ReissueCredentialResponse>(RemoteAction.ReissueCredential,
request, cancellationToken);
public Task RemoveInputAsync(InputsRemovalRequest request, CancellationToken cancellationToken) =>
SendAndWaitForReply(RemoteAction.RemoveInput, request, cancellationToken);
public virtual Task SignTransactionAsync(TransactionSignaturesRequest request,
CancellationToken cancellationToken) =>
SendAndWaitForReply(RemoteAction.SignTransaction, request, cancellationToken);
public Task ReadyToSignAsync(ReadyToSignRequestRequest request, CancellationToken cancellationToken) =>
SendAndWaitForReply(RemoteAction.ReadyToSign, request, cancellationToken);
private static string Serialize<T>(T obj)
=> JsonConvert.SerializeObject(obj, JsonSerializationOptions.Default.Settings);
private static TResponse Deserialize<TResponse>(string jsonString)
{
try
{
return JsonConvert.DeserializeObject<TResponse>(jsonString, JsonSerializationOptions.Default.Settings)
?? throw new InvalidOperationException("Deserialization error");
}
catch
{
Logger.LogDebug($"Failed to deserialize {typeof(TResponse)} from JSON '{jsonString}'");
throw;
}
}
private enum RemoteAction
{
RegisterInput,
RemoveInput,
ConfirmConnection,
RegisterOutput,
ReissueCredential,
SignTransaction,
GetStatus,
ReadyToSign
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitcoin.Secp256k1;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NNostr.Client;
using WalletWasabi.Logging;
using WalletWasabi.WabiSabi;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Crypto;
using WalletWasabi.WabiSabi.Models;
using WalletWasabi.WabiSabi.Models.Serialization;
namespace BTCPayServer.Plugins.Wabisabi;
public class NostrWabisabiApiServer: IHostedService
{
public static int RoundStateKind = 15750;
public static int CommunicationKind = 25750;
private readonly Arena _arena;
private readonly NostrClient _client;
private readonly ECPrivKey _coordinatorKey;
private string _coordinatorKeyHex => _coordinatorKey.CreateXOnlyPubKey().ToHex();
private readonly string _coordinatorFilterId;
private Channel<NostrEvent> PendingEvents { get; } = Channel.CreateUnbounded<NostrEvent>();
public NostrWabisabiApiServer(Arena arena,NostrClient client, ECPrivKey coordinatorKey)
{
_arena = arena;
_client = client;
_coordinatorKey = coordinatorKey;
_coordinatorFilterId = new Guid().ToString();
_serializer = JsonSerializer.Create(JsonSerializationOptions.Default.Settings);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = _client.ListenForMessages();
var filter = new NostrSubscriptionFilter()
{
PublicKey = new[] {_coordinatorKey.ToHex()},
Kinds = new[] { CommunicationKind},
Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1))
};
await _client.CreateSubscription(_coordinatorFilterId, new[] {filter}, cancellationToken);
_client.EventsReceived += EventsReceived;
await _client.ConnectAndWaitUntilConnected(cancellationToken);
_ = RoutinelyUpdateRoundEvent(cancellationToken);
_ = ProcessRequests(cancellationToken);
}
private void EventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e)
{
if (e.subscriptionId != _coordinatorFilterId) return;
var requests = e.events.Where(evt =>
evt.Kind == CommunicationKind &&
evt.GetTaggedData("p").Any(s => s == _coordinatorKeyHex) && evt.Verify());
foreach (var request in requests)
PendingEvents.Writer.TryWrite(request);
}
private async Task RoutinelyUpdateRoundEvent(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var response = await _arena.GetStatusAsync(RoundStateRequest.Empty, cancellationToken);
var nostrEvent = new NostrEvent()
{
Kind = RoundStateKind,
PublicKey = _coordinatorKeyHex,
CreatedAt = DateTimeOffset.Now,
Content = Serialize(response)
};
await _client.PublishEvent(nostrEvent, cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
private async Task ProcessRequests(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested &&
await PendingEvents.Reader.WaitToReadAsync(cancellationToken) &&
PendingEvents.Reader.TryRead(out var evt))
{
evt.Kind = 4;
var content = JObject.Parse(await evt.DecryptNip04EventAsync(_coordinatorKey));
if (content.TryGetValue("action", out var actionJson) &&
actionJson.Value<string>(actionJson) is { } actionString &&
Enum.TryParse<RemoteAction>(actionString, out var action) &&
content.ContainsKey("request"))
{
try
{
switch (action)
{
case RemoteAction.GetStatus:
// ignored as we use a dedicated public event for this to not spam
break;
case RemoteAction.RegisterInput:
var registerInputRequest =
content["request"].ToObject<InputRegistrationRequest>(_serializer);
var registerInputResponse =
await _arena.RegisterInputAsync(registerInputRequest, CancellationToken.None);
await Reply(evt, registerInputResponse, CancellationToken.None);
break;
case RemoteAction.RegisterOutput:
var registerOutputRequest =
content["request"].ToObject<OutputRegistrationRequest>(_serializer);
await _arena.RegisterOutputAsync(registerOutputRequest, CancellationToken.None);
break;
case RemoteAction.RemoveInput:
var removeInputRequest = content["request"].ToObject<InputsRemovalRequest>(_serializer);
await _arena.RemoveInputAsync(removeInputRequest, CancellationToken.None);
break;
case RemoteAction.ConfirmConnection:
var connectionConfirmationRequest =
content["request"].ToObject<ConnectionConfirmationRequest>(_serializer);
var connectionConfirmationResponse =
await _arena.ConfirmConnectionAsync(connectionConfirmationRequest,
CancellationToken.None);
await Reply(evt, connectionConfirmationResponse, CancellationToken.None);
break;
case RemoteAction.ReissueCredential:
var reissueCredentialRequest =
content["request"].ToObject<ReissueCredentialRequest>(_serializer);
var reissueCredentialResponse =
await _arena.ReissuanceAsync(reissueCredentialRequest, CancellationToken.None);
await Reply(evt, reissueCredentialResponse, CancellationToken.None);
break;
case RemoteAction.SignTransaction:
var transactionSignaturesRequest =
content["request"].ToObject<TransactionSignaturesRequest>(_serializer);
await _arena.SignTransactionAsync(transactionSignaturesRequest, CancellationToken.None);
break;
case RemoteAction.ReadyToSign:
var readyToSignRequest =
content["request"].ToObject<ReadyToSignRequestRequest>(_serializer);
await _arena.ReadyToSignAsync(readyToSignRequest, CancellationToken.None);
break;
}
}
catch (Exception ex)
{
object response = ex switch
{
WabiSabiProtocolException wabiSabiProtocolException => new
{
Error = new Error(Type: ProtocolConstants.ProtocolViolationType,
ErrorCode: wabiSabiProtocolException.ErrorCode.ToString(),
Description: wabiSabiProtocolException.Message,
ExceptionData: wabiSabiProtocolException.ExceptionData ??
EmptyExceptionData.Instance)
},
WabiSabiCryptoException wabiSabiCryptoException => new
{
Error = new Error(Type: ProtocolConstants.ProtocolViolationType,
ErrorCode: WabiSabiProtocolErrorCode.CryptoException.ToString(),
Description: wabiSabiCryptoException.Message,
ExceptionData: EmptyExceptionData.Instance)
},
_ => new
{
Error = new Error(Type: "unknown", ErrorCode: ex.GetType().Name,
Description: ex.Message, ExceptionData: EmptyExceptionData.Instance)
}
};
await Reply(evt, response, CancellationToken.None);
}
}
}
}
private readonly JsonSerializer _serializer;
private async Task Reply<TResponse>(NostrEvent originaltEvent,TResponse response,
CancellationToken cancellationToken)
{
var evt = new NostrEvent()
{
Content = Serialize(response),
PublicKey = _coordinatorKeyHex,
Kind = 4,
CreatedAt = DateTimeOffset.Now
};
evt.SetTag("p", originaltEvent.PublicKey);
evt.SetTag("e", originaltEvent.Id);
await evt.EncryptNip04EventAsync(_coordinatorKey);
evt.Kind = CommunicationKind;
await evt.ComputeIdAndSignAsync(_coordinatorKey);
await _client.PublishEvent(evt, cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _client.CloseSubscription(_coordinatorFilterId, cancellationToken);
_client.EventsReceived -= EventsReceived;
}
private static string Serialize<T>(T obj)
=> JsonConvert.SerializeObject(obj, JsonSerializationOptions.Default.Settings);
private enum RemoteAction
{
RegisterInput,
RemoveInput,
ConfirmConnection,
RegisterOutput,
ReissueCredential,
SignTransaction,
GetStatus,
ReadyToSign
}
}

View File

@@ -31,21 +31,20 @@ public class Smartifier
public Smartifier( public Smartifier(
WalletRepository walletRepository, WalletRepository walletRepository,
ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId, ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId,
IUTXOLocker utxoLocker) IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath)
{ {
_walletRepository = walletRepository; _walletRepository = walletRepository;
_explorerClient = explorerClient; _explorerClient = explorerClient;
DerivationScheme = derivationStrategyBase; DerivationScheme = derivationStrategyBase;
_storeId = storeId; _storeId = storeId;
_utxoLocker = utxoLocker; _utxoLocker = utxoLocker;
_accountKeyPath = _explorerClient.GetMetadataAsync<RootedKeyPath>(DerivationScheme, _accountKeyPath = accountKeyPath;
WellknownMetadataKeys.AccountKeyPath);
} }
public readonly ConcurrentDictionary<uint256, Task<TransactionInformation>> CachedTransactions = new(); public readonly ConcurrentDictionary<uint256, Task<TransactionInformation>> CachedTransactions = new();
public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> Transactions = new(); public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> Transactions = new();
public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new(); public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new();
private readonly Task<RootedKeyPath> _accountKeyPath; private readonly RootedKeyPath _accountKeyPath;
public async Task LoadCoins(List<ReceivedCoin> coins, int current , public async Task LoadCoins(List<ReceivedCoin> coins, int current ,
Dictionary<OutPoint, (HashSet<string> labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels) Dictionary<OutPoint, (HashSet<string> labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels)
@@ -150,7 +149,7 @@ public class Smartifier
utxoLabels.TryGetValue(coin.OutPoint, out var labels); utxoLabels.TryGetValue(coin.OutPoint, out var labels);
var unsmartTx = await CachedTransactions[coin.OutPoint.Hash]; var unsmartTx = await CachedTransactions[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 = _accountKeyPath.Derive(coin.KeyPath).KeyPath;
var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(labels.labels ?? new HashSet<string>()), var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(labels.labels ?? new HashSet<string>()),
current == 1 ? KeyState.Clean : KeyState.Used); current == 1 ? KeyState.Clean : KeyState.Used);

View File

@@ -29,11 +29,6 @@
return; return;
} }
var storeId = ScopeProvider.GetCurrentStoreId(); var storeId = ScopeProvider.GetCurrentStoreId();
// var methods = await Client.GetStoreOnChainPaymentMethods(storeId, true);
// var method = methods.FirstOrDefault(data => data.CryptoCode == "BTC");
// var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
// contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
// contentSecurityPolicies.AllowUnsafeHashes();
} }
@if (available) @if (available)
@@ -87,7 +82,7 @@
} }
</div> </div>
var wallet = (BTCPayWallet) await WalletProvider.GetWalletAsync(storeId); var wallet = (BTCPayWallet?) await WalletProvider.GetWalletAsync(storeId);
var coins = await wallet.GetAllCoins(); var coins = await wallet.GetAllCoins();
var privacy = wallet.GetPrivacyPercentage(coins, wallet.AnonScoreTarget); var privacy = wallet.GetPrivacyPercentage(coins, wallet.AnonScoreTarget);
@@ -96,20 +91,13 @@
var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonScoreTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping); var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonScoreTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping);
<div class="widget store-numbers" > <div class="widget store-numbers" >
@if (wallet is BTCPayWallet btcPayWallet) @if (wallet is { })
{ {
@if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true) @if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
{ {
<div class="alert alert-danger d-flex align-items-center" role="alert"> <div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning"/> <vc:icon symbol="warning"/>
<span class="ms-3">This wallet is not enabled in your store settings and will not be able to participate in coinjoins..</span> <span class="ms-3">This wallet is either not a hot wallet, or enabled in yout store settings and will not be able to participate in coinjoins.</span>
</div>
}
else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning"/>
<span class="ms-3">This wallet is not a hot wallet and will not be able to participate in coinjoins.</span>
</div> </div>
} }
} }

View File

@@ -53,24 +53,14 @@
} }
var wallet = await WalletProvider.GetWalletAsync(storeId); var wallet = await WalletProvider.GetWalletAsync(storeId);
if (wallet is BTCPayWallet btcPayWallet) if (wallet is BTCPayWallet)
{ {
@if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true)
@if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
{ {
<div class="alert alert-danger d-flex align-items-center" role="alert"> <div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning"/> <vc:icon symbol="warning"/>
<span class="ms-3">This wallet is not enabled in your store settings and will not be able to participate in coinjoins..</span> <span class="ms-3">This wallet is either not a hot wallet, or enabled in yout store settings and will not be able to participate in coinjoins.</span>
<button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div>
}
else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning"/>
<span class="ms-3">This wallet is not a hot wallet and will not be able to participate in coinjoins.</span>
<button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div> </div>
} }
} }

View File

@@ -62,12 +62,12 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin
applicationBuilder.AddSingleton<WalletProvider>(provider => new( applicationBuilder.AddSingleton<WalletProvider>(provider => new(
provider, provider,
provider.GetRequiredService<StoreRepository>(), provider.GetRequiredService<StoreRepository>(),
provider.GetRequiredService<IBTCPayServerClientFactory>(),
provider.GetRequiredService<IExplorerClientProvider>(), provider.GetRequiredService<IExplorerClientProvider>(),
provider.GetRequiredService<ILoggerFactory>(), provider.GetRequiredService<ILoggerFactory>(),
utxoLocker, utxoLocker,
provider.GetRequiredService<EventAggregator>(), provider.GetRequiredService<EventAggregator>(),
provider.GetRequiredService<ILogger<WalletProvider>>() provider.GetRequiredService<ILogger<WalletProvider>>(),
provider.GetRequiredService<BTCPayNetworkProvider>()
)); ));
applicationBuilder.AddWabisabiCoordinator(); applicationBuilder.AddWabisabiCoordinator();
applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>()); applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>());

View File

@@ -18,13 +18,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
using NBXplorer.Models;
using WalletWasabi.Bases; using WalletWasabi.Bases;
using WalletWasabi.Blockchain.Analysis;
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; using PayoutData = BTCPayServer.Data.PayoutData;
namespace BTCPayServer.Plugins.Wabisabi; namespace BTCPayServer.Plugins.Wabisabi;
@@ -34,31 +30,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
private Dictionary<string, WabisabiStoreSettings>? _cachedSettings; private Dictionary<string, WabisabiStoreSettings>? _cachedSettings;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
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; private readonly EventAggregator _eventAggregator;
private readonly ILogger<WalletProvider> _logger; private readonly ILogger<WalletProvider> _logger;
private readonly BTCPayNetworkProvider _networkProvider;
public WalletProvider( public WalletProvider(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
StoreRepository storeRepository, StoreRepository storeRepository,
IBTCPayServerClientFactory btcPayServerClientFactory,
IExplorerClientProvider explorerClientProvider, IExplorerClientProvider explorerClientProvider,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IUTXOLocker utxoLocker, IUTXOLocker utxoLocker,
EventAggregator eventAggregator, EventAggregator eventAggregator,
ILogger<WalletProvider> logger) : base(TimeSpan.FromMinutes(5)) ILogger<WalletProvider> logger,
BTCPayNetworkProvider networkProvider) : base(TimeSpan.FromMinutes(5))
{ {
UtxoLocker = utxoLocker; UtxoLocker = utxoLocker;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_btcPayServerClientFactory = btcPayServerClientFactory;
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
_networkProvider = networkProvider;
} }
public readonly ConcurrentDictionary<string, Task<IWallet?>> LoadedWallets = new(); public readonly ConcurrentDictionary<string, Task<IWallet?>> LoadedWallets = new();
@@ -76,32 +72,49 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
} }
public event EventHandler<WalletUnloadEventArgs>? WalletUnloaded; public event EventHandler<WalletUnloadEventArgs>? WalletUnloaded;
public async Task<IWallet> GetWalletAsync(string name) public async Task<IWallet?> GetWalletAsync(string name)
{ {
await initialLoad.Task; await initialLoad.Task;
return await LoadedWallets.GetOrAddAsync(name, async s => return await LoadedWallets.GetOrAddAsync(name, async s =>
{ {
if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings)) if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings))
{ {
return null; return null;
} }
var store = await _storeRepository.FindStore(name);
var paymentMethod = store?.GetDerivationSchemeSettings(_networkProvider, "BTC");
if (paymentMethod is null)
{
return null;
}
var client = await _btcPayServerClientFactory.Create(null, name);
var pm = await client.GetStoreOnChainPaymentMethod(name, "BTC");
var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var explorerClient = _explorerClientProvider.GetExplorerClient("BTC");
var derivationStrategy = var isHotWallet = paymentMethod.IsHotWallet;
explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); var enabled = store.GetEnabledPaymentIds(_networkProvider).Contains(paymentMethod.PaymentId);
var derivationStrategy = paymentMethod.AccountDerivation;
BTCPayKeyChain keychain;
if (isHotWallet && enabled)
{
var masterKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy, var masterKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
WellknownMetadataKeys.MasterHDKey); WellknownMetadataKeys.MasterHDKey);
var accountKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy, var accountKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
WellknownMetadataKeys.AccountHDKey); WellknownMetadataKeys.AccountHDKey);
var accountKeyPath = await explorerClient.GetMetadataAsync<RootedKeyPath>(derivationStrategy,
WellknownMetadataKeys.AccountKeyPath);
var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey); if (masterKey is null || accountKey is null || accountKeyPath is null)
{
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null);
}else
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey, new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath));
}
else
{
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null);
}
var smartifier = new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, name, UtxoLocker);
return (IWallet)new BTCPayWallet( return (IWallet)new BTCPayWallet(
_serviceProvider.GetRequiredService<WalletRepository>(), _serviceProvider.GetRequiredService<WalletRepository>(),
@@ -109,10 +122,9 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
_serviceProvider.GetRequiredService<BitcoinLikePayoutHandler>(), _serviceProvider.GetRequiredService<BitcoinLikePayoutHandler>(),
_serviceProvider.GetRequiredService<BTCPayNetworkJsonSerializerSettings>(), _serviceProvider.GetRequiredService<BTCPayNetworkJsonSerializerSettings>(),
_serviceProvider.GetRequiredService<Services.Wallets.BTCPayWalletProvider>().GetWallet("BTC"), _serviceProvider.GetRequiredService<Services.Wallets.BTCPayWalletProvider>().GetWallet("BTC"),
_serviceProvider.GetRequiredService<PullPaymentHostedService>(), _serviceProvider.GetRequiredService<PullPaymentHostedService>(),derivationStrategy, explorerClient, keychain,
pm, derivationStrategy, explorerClient, keychain, name, wabisabiStoreSettings, UtxoLocker,
_btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker, _loggerFactory,
_loggerFactory, smartifier,
_serviceProvider.GetRequiredService<StoreRepository>(), BannedCoins, _serviceProvider.GetRequiredService<StoreRepository>(), BannedCoins,
_eventAggregator); _eventAggregator);
@@ -121,8 +133,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
} }
private TaskCompletionSource initialLoad = new(); private TaskCompletionSource initialLoad = new();
private IEventAggregatorSubscription _subscription; private CompositeDisposable _disposables = new();
private IEventAggregatorSubscription _subscription2;
public async Task<IEnumerable<IWallet>> GetWalletsAsync() public async Task<IEnumerable<IWallet>> GetWalletsAsync()
{ {
@@ -186,38 +197,25 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
protected override async Task ActionAsync(CancellationToken cancel) protected override async Task ActionAsync(CancellationToken cancel)
{ {
var toCheck = LoadedWallets.Keys.ToList(); // var toCheck = LoadedWallets.Keys.ToList();
while (toCheck.Any()) // while (toCheck.Any())
{ // {
var storeid = toCheck.First(); // var storeid = toCheck.First();
await Check(storeid, cancel); // await Check(storeid, cancel);
toCheck.Remove(storeid); // toCheck.Remove(storeid);
} // }
} }
public async Task Check(string storeId, CancellationToken cancellationToken) public async Task Check(string storeId, CancellationToken cancellationToken)
{ {
var client = await _btcPayServerClientFactory.Create(null, storeId);
try try
{ {
if (LoadedWallets.TryGetValue(storeId, out var currentWallet)) if (LoadedWallets.TryGetValue(storeId, out var currentWallet))
{
var wallet = (BTCPayWallet)await currentWallet;
var kc = (BTCPayKeyChain)wallet.KeyChain;
var pm = await client.GetStoreOnChainPaymentMethod(storeId, "BTC", cancellationToken);
if (pm.DerivationScheme != wallet.OnChainPaymentMethodData.DerivationScheme)
{ {
await UnloadWallet(storeId); await UnloadWallet(storeId);
} if(_cachedSettings.TryGetValue(storeId , out var settings) && settings.Settings.Any(coordinatorSettings => coordinatorSettings.Enabled))
else await GetWalletAsync(storeId);
{ await GetWalletAsync(storeId);
wallet.OnChainPaymentMethodData = pm;
}
if (!kc.KeysAvailable)
{
await UnloadWallet(storeId);
}
} }
} }
catch (Exception e) catch (Exception e)
@@ -291,15 +289,28 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
await _storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings)); await _storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
initialLoad.SetResult(); initialLoad.SetResult();
}, cancellationToken); }, cancellationToken);
_subscription = _eventAggregator.SubscribeAsync<WalletChangedEvent>(@event => _disposables.Add(_eventAggregator.SubscribeAsync<StoreRemovedEvent>(async @event =>
Check(@event.WalletId.StoreId, cancellationToken)); {
await initialLoad.Task;
await UnloadWallet(@event.StoreId);
}));
_disposables.Add(_eventAggregator.SubscribeAsync<WalletChangedEvent>(async @event =>
{
if (@event.WalletId.CryptoCode == "BTC")
{
await initialLoad.Task;
await Check(@event.WalletId.StoreId, cancellationToken);
}
}));
return base.StartAsync(cancellationToken); return base.StartAsync(cancellationToken);
} }
public override Task StopAsync(CancellationToken cancellationToken) public override Task StopAsync(CancellationToken cancellationToken)
{ {
_subscription?.Dispose(); _disposables?.Dispose();
_subscription2?.Dispose();
return base.StopAsync(cancellationToken); return base.StopAsync(cancellationToken);
} }
} }