mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
imrpovs
This commit is contained in:
@@ -13,6 +13,7 @@ namespace BTCPayServer.Plugins.Wabisabi;
|
||||
|
||||
public class BTCPayKeyChain : IKeyChain
|
||||
{
|
||||
public Smartifier Smartifier { get; }
|
||||
private readonly ExplorerClient _explorerClient;
|
||||
private readonly DerivationStrategyBase _derivationStrategy;
|
||||
private readonly ExtKey _masterKey;
|
||||
@@ -21,8 +22,9 @@ public class BTCPayKeyChain : IKeyChain
|
||||
public bool KeysAvailable => _masterKey is not null && _accountKey is not null;
|
||||
|
||||
public BTCPayKeyChain(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategy, ExtKey masterKey,
|
||||
ExtKey accountKey)
|
||||
ExtKey accountKey, Smartifier smartifier)
|
||||
{
|
||||
Smartifier = smartifier;
|
||||
_explorerClient = explorerClient;
|
||||
_derivationStrategy = derivationStrategy;
|
||||
_masterKey = masterKey;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.23" />
|
||||
<PackageReference Include="NNostr.Client" Version="0.0.24" />
|
||||
</ItemGroup>
|
||||
<Target Name="DeleteExampleFile" AfterTargets="Publish">
|
||||
<RemoveDir Directories="$(PublishDir)\Microservices" />
|
||||
|
||||
@@ -44,10 +44,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly Services.Wallets.BTCPayWallet _btcPayWallet;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
public OnChainPaymentMethodData OnChainPaymentMethodData;
|
||||
// public OnChainPaymentMethodData OnChainPaymentMethodData;
|
||||
public readonly DerivationStrategyBase DerivationScheme;
|
||||
public readonly ExplorerClient ExplorerClient;
|
||||
public readonly IBTCPayServerClientFactory BtcPayServerClientFactory;
|
||||
// public readonly IBTCPayServerClientFactory BtcPayServerClientFactory;
|
||||
public WabisabiStoreSettings WabisabiStoreSettings;
|
||||
public readonly IUTXOLocker UtxoLocker;
|
||||
public readonly ILogger Logger;
|
||||
@@ -59,16 +59,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
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,
|
||||
StoreRepository storeRepository,
|
||||
ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins, EventAggregator eventAggregator)
|
||||
{
|
||||
@@ -79,14 +76,11 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_btcPayWallet = btcPayWallet;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
OnChainPaymentMethodData = onChainPaymentMethodData;
|
||||
DerivationScheme = derivationScheme;
|
||||
ExplorerClient = explorerClient;
|
||||
BtcPayServerClientFactory = btcPayServerClientFactory;
|
||||
StoreId = storeId;
|
||||
WabisabiStoreSettings = wabisabiStoreSettings;
|
||||
UtxoLocker = utxoLocker;
|
||||
_smartifier = smartifier;
|
||||
_storeRepository = storeRepository;
|
||||
_bannedCoins = bannedCoins;
|
||||
_eventAggregator = eventAggregator;
|
||||
@@ -101,8 +95,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
|
||||
bool IWallet.IsMixable(string coordinator)
|
||||
{
|
||||
return OnChainPaymentMethodData?.Enabled is true && WabisabiStoreSettings.Settings.SingleOrDefault(settings =>
|
||||
settings.Coordinator.Equals(coordinator))?.Enabled is true && ((BTCPayKeyChain)KeyChain).KeysAvailable;
|
||||
return KeyChain is BTCPayKeyChain {KeysAvailable: true} && WabisabiStoreSettings.Settings.SingleOrDefault(
|
||||
settings =>
|
||||
settings.Coordinator.Equals(coordinator))?.Enabled is true;
|
||||
}
|
||||
|
||||
public IKeyChain KeyChain { get; }
|
||||
@@ -152,7 +147,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
}
|
||||
|
||||
private IRoundCoinSelector _coinSelector;
|
||||
public readonly Smartifier _smartifier;
|
||||
public Smartifier _smartifier => (KeyChain as BTCPayKeyChain)?.Smartifier;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> _bannedCoins;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
@@ -352,7 +347,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName)
|
||||
{
|
||||
await _savingProgress;
|
||||
|
||||
|
||||
|
||||
|
||||
_savingProgress = RegisterCoinjoinTransactionInternal(result, coordinatorName);
|
||||
|
||||
|
||||
await _savingProgress;
|
||||
}
|
||||
private async Task RegisterCoinjoinTransactionInternal(SuccessfulCoinJoinResult result, string coordinatorName)
|
||||
@@ -378,10 +379,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
|
||||
|
||||
Dictionary<IndexedTxOut, PendingPayment> indexToPayment = new();
|
||||
foreach (var script in result.OutputScripts)
|
||||
foreach (var script in result.Outputs)
|
||||
{
|
||||
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
|
||||
@@ -393,7 +394,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
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));
|
||||
@@ -405,9 +406,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
coin.SpenderTransaction = smartTx;
|
||||
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()
|
||||
.PubKey;
|
||||
@@ -582,22 +583,22 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
||||
}
|
||||
|
||||
|
||||
public async Task UnlockUTXOs()
|
||||
{
|
||||
var client = await BtcPayServerClientFactory.Create(null, StoreId);
|
||||
var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC");
|
||||
var unlocked = new List<string>();
|
||||
foreach (OnChainWalletUTXOData utxo in utxos)
|
||||
{
|
||||
|
||||
if (await UtxoLocker.TryUnlock(utxo.Outpoint))
|
||||
{
|
||||
unlocked.Add(utxo.Outpoint.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}");
|
||||
}
|
||||
// public async Task UnlockUTXOs()
|
||||
// {
|
||||
// var client = await BtcPayServerClientFactory.Create(null, StoreId);
|
||||
// var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC");
|
||||
// var unlocked = new List<string>();
|
||||
// foreach (OnChainWalletUTXOData utxo in utxos)
|
||||
// {
|
||||
//
|
||||
// if (await UtxoLocker.TryUnlock(utxo.Outpoint))
|
||||
// {
|
||||
// unlocked.Add(utxo.Outpoint.ToString());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}");
|
||||
// }
|
||||
|
||||
public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count, bool mixedOutputs)
|
||||
{
|
||||
@@ -605,15 +606,14 @@ public async Task<IEnumerable<IDestination>> GetNextDestinationsAsync(int count,
|
||||
{
|
||||
try
|
||||
{
|
||||
var mixClient = await BtcPayServerClientFactory.Create(null, WabisabiStoreSettings.MixToOtherWallet);
|
||||
var pm = await mixClient.GetStoreOnChainPaymentMethod(WabisabiStoreSettings.MixToOtherWallet,
|
||||
"BTC");
|
||||
var mixStore = await _storeRepository.FindStore(WabisabiStoreSettings.MixToOtherWallet);
|
||||
var pm = mixStore.GetDerivationSchemeSettings(_btcPayNetworkProvider, "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(_ =>
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
240
Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs
Normal file
240
Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs
Normal 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
|
||||
}
|
||||
}
|
||||
228
Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs
Normal file
228
Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -31,21 +31,20 @@ public class Smartifier
|
||||
public Smartifier(
|
||||
WalletRepository walletRepository,
|
||||
ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId,
|
||||
IUTXOLocker utxoLocker)
|
||||
IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath)
|
||||
{
|
||||
_walletRepository = walletRepository;
|
||||
_explorerClient = explorerClient;
|
||||
DerivationScheme = derivationStrategyBase;
|
||||
_storeId = storeId;
|
||||
_utxoLocker = utxoLocker;
|
||||
_accountKeyPath = _explorerClient.GetMetadataAsync<RootedKeyPath>(DerivationScheme,
|
||||
WellknownMetadataKeys.AccountKeyPath);
|
||||
_accountKeyPath = accountKeyPath;
|
||||
}
|
||||
|
||||
public readonly ConcurrentDictionary<uint256, Task<TransactionInformation>> CachedTransactions = new();
|
||||
public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> Transactions = 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 ,
|
||||
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);
|
||||
var unsmartTx = await CachedTransactions[coin.OutPoint.Hash];
|
||||
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>()),
|
||||
current == 1 ? KeyState.Clean : KeyState.Used);
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
return;
|
||||
}
|
||||
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)
|
||||
@@ -87,7 +82,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
var wallet = (BTCPayWallet) await WalletProvider.GetWalletAsync(storeId);
|
||||
var wallet = (BTCPayWallet?) await WalletProvider.GetWalletAsync(storeId);
|
||||
|
||||
var coins = await wallet.GetAllCoins();
|
||||
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);
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,24 +53,14 @@
|
||||
}
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +62,12 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin
|
||||
applicationBuilder.AddSingleton<WalletProvider>(provider => new(
|
||||
provider,
|
||||
provider.GetRequiredService<StoreRepository>(),
|
||||
provider.GetRequiredService<IBTCPayServerClientFactory>(),
|
||||
provider.GetRequiredService<IExplorerClientProvider>(),
|
||||
provider.GetRequiredService<ILoggerFactory>(),
|
||||
utxoLocker,
|
||||
provider.GetRequiredService<EventAggregator>(),
|
||||
provider.GetRequiredService<ILogger<WalletProvider>>()
|
||||
provider.GetRequiredService<ILogger<WalletProvider>>(),
|
||||
provider.GetRequiredService<BTCPayNetworkProvider>()
|
||||
));
|
||||
applicationBuilder.AddWabisabiCoordinator();
|
||||
applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>());
|
||||
|
||||
@@ -18,13 +18,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using WalletWasabi.Bases;
|
||||
using WalletWasabi.Blockchain.Analysis;
|
||||
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;
|
||||
@@ -34,31 +30,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
private Dictionary<string, WabisabiStoreSettings>? _cachedSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
||||
private readonly IExplorerClientProvider _explorerClientProvider;
|
||||
public IUTXOLocker UtxoLocker { get; set; }
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly ILogger<WalletProvider> _logger;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
|
||||
public WalletProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
StoreRepository storeRepository,
|
||||
IBTCPayServerClientFactory btcPayServerClientFactory,
|
||||
IExplorerClientProvider explorerClientProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IUTXOLocker utxoLocker,
|
||||
EventAggregator eventAggregator,
|
||||
ILogger<WalletProvider> logger) : base(TimeSpan.FromMinutes(5))
|
||||
ILogger<WalletProvider> logger,
|
||||
BTCPayNetworkProvider networkProvider) : base(TimeSpan.FromMinutes(5))
|
||||
{
|
||||
UtxoLocker = utxoLocker;
|
||||
_serviceProvider = serviceProvider;
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayServerClientFactory = btcPayServerClientFactory;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_loggerFactory = loggerFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
public readonly ConcurrentDictionary<string, Task<IWallet?>> LoadedWallets = new();
|
||||
@@ -76,32 +72,49 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
}
|
||||
|
||||
public event EventHandler<WalletUnloadEventArgs>? WalletUnloaded;
|
||||
public async Task<IWallet> GetWalletAsync(string name)
|
||||
public async Task<IWallet?> GetWalletAsync(string name)
|
||||
{
|
||||
await initialLoad.Task;
|
||||
return await LoadedWallets.GetOrAddAsync(name, async s =>
|
||||
{
|
||||
|
||||
if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings))
|
||||
{
|
||||
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 derivationStrategy =
|
||||
explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme);
|
||||
var isHotWallet = paymentMethod.IsHotWallet;
|
||||
var enabled = store.GetEnabledPaymentIds(_networkProvider).Contains(paymentMethod.PaymentId);
|
||||
var derivationStrategy = paymentMethod.AccountDerivation;
|
||||
BTCPayKeyChain keychain;
|
||||
if (isHotWallet && enabled)
|
||||
{
|
||||
var masterKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
var accountKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
|
||||
WellknownMetadataKeys.AccountHDKey);
|
||||
var accountKeyPath = await explorerClient.GetMetadataAsync<RootedKeyPath>(derivationStrategy,
|
||||
WellknownMetadataKeys.AccountKeyPath);
|
||||
|
||||
var masterKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
var accountKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
|
||||
WellknownMetadataKeys.AccountHDKey);
|
||||
if (masterKey is null || accountKey is null || accountKeyPath is null)
|
||||
{
|
||||
|
||||
var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey);
|
||||
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(
|
||||
_serviceProvider.GetRequiredService<WalletRepository>(),
|
||||
@@ -109,10 +122,9 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
_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,
|
||||
_serviceProvider.GetRequiredService<PullPaymentHostedService>(),derivationStrategy, explorerClient, keychain,
|
||||
name, wabisabiStoreSettings, UtxoLocker,
|
||||
_loggerFactory,
|
||||
_serviceProvider.GetRequiredService<StoreRepository>(), BannedCoins,
|
||||
_eventAggregator);
|
||||
|
||||
@@ -121,8 +133,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
}
|
||||
|
||||
private TaskCompletionSource initialLoad = new();
|
||||
private IEventAggregatorSubscription _subscription;
|
||||
private IEventAggregatorSubscription _subscription2;
|
||||
private CompositeDisposable _disposables = new();
|
||||
|
||||
public async Task<IEnumerable<IWallet>> GetWalletsAsync()
|
||||
{
|
||||
@@ -186,38 +197,25 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
protected override async Task ActionAsync(CancellationToken cancel)
|
||||
{
|
||||
|
||||
var toCheck = LoadedWallets.Keys.ToList();
|
||||
while (toCheck.Any())
|
||||
{
|
||||
var storeid = toCheck.First();
|
||||
await Check(storeid, cancel);
|
||||
toCheck.Remove(storeid);
|
||||
}
|
||||
// var toCheck = LoadedWallets.Keys.ToList();
|
||||
// while (toCheck.Any())
|
||||
// {
|
||||
// var storeid = toCheck.First();
|
||||
// await Check(storeid, cancel);
|
||||
// toCheck.Remove(storeid);
|
||||
// }
|
||||
}
|
||||
|
||||
public async Task Check(string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = await _btcPayServerClientFactory.Create(null, storeId);
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
wallet.OnChainPaymentMethodData = pm;
|
||||
}
|
||||
|
||||
if (!kc.KeysAvailable)
|
||||
{
|
||||
await UnloadWallet(storeId);
|
||||
}
|
||||
await UnloadWallet(storeId);
|
||||
if(_cachedSettings.TryGetValue(storeId , out var settings) && settings.Settings.Any(coordinatorSettings => coordinatorSettings.Enabled))
|
||||
await GetWalletAsync(storeId);
|
||||
await GetWalletAsync(storeId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -291,15 +289,28 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
|
||||
await _storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
|
||||
initialLoad.SetResult();
|
||||
}, cancellationToken);
|
||||
_subscription = _eventAggregator.SubscribeAsync<WalletChangedEvent>(@event =>
|
||||
Check(@event.WalletId.StoreId, cancellationToken));
|
||||
_disposables.Add(_eventAggregator.SubscribeAsync<StoreRemovedEvent>(async @event =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
_subscription2?.Dispose();
|
||||
_disposables?.Dispose();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Submodule submodules/walletwasabi updated: 3a51a87542...fdfddd8583
Reference in New Issue
Block a user