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

View File

@@ -13,6 +13,7 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class BTCPayKeyChain : IKeyChain
{
public 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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,21 +31,20 @@ public class Smartifier
public Smartifier(
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);

View File

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

View File

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

View File

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

View File

@@ -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 keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey);
if (masterKey is null || accountKey is null || accountKeyPath is null)
{
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null);
}else
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey, new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath));
}
else
{
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null);
}
var smartifier = new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, name, UtxoLocker);
return (IWallet)new BTCPayWallet(
_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);
}
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);
}
}