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 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;
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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(
|
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);
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>());
|
||||||
|
|||||||
@@ -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,
|
||||||
|
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,
|
if (masterKey is null || accountKey is null || accountKeyPath is null)
|
||||||
WellknownMetadataKeys.MasterHDKey);
|
{
|
||||||
var accountKey = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivationStrategy,
|
|
||||||
WellknownMetadataKeys.AccountHDKey);
|
|
||||||
|
|
||||||
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(
|
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;
|
await UnloadWallet(storeId);
|
||||||
var kc = (BTCPayKeyChain)wallet.KeyChain;
|
if(_cachedSettings.TryGetValue(storeId , out var settings) && settings.Settings.Any(coordinatorSettings => coordinatorSettings.Enabled))
|
||||||
var pm = await client.GetStoreOnChainPaymentMethod(storeId, "BTC", cancellationToken);
|
await GetWalletAsync(storeId);
|
||||||
if (pm.DerivationScheme != wallet.OnChainPaymentMethodData.DerivationScheme)
|
await GetWalletAsync(storeId);
|
||||||
{
|
|
||||||
await UnloadWallet(storeId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule submodules/walletwasabi updated: 3a51a87542...fdfddd8583
Reference in New Issue
Block a user