wabi updates

This commit is contained in:
Kukks
2023-01-17 14:23:08 +01:00
parent d86c5d40c7
commit ac9e07429e
14 changed files with 465 additions and 300 deletions

View File

@@ -41,10 +41,20 @@
<ProjectReference Include="..\..\submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj" /> <ProjectReference Include="..\..\submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources" /> <PackageReference Include="NNostr.Client" Version="0.0.17" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NNostr.Client" Version="0.0.17" /> <_ContentIncludedByDefault Remove="Views\Shared\Wabisabi\StoreIntegrationWabisabiOption.cshtml" />
<_ContentIncludedByDefault Remove="Views\Shared\Wabisabi\WabisabiDashboard.cshtml" />
<_ContentIncludedByDefault Remove="Views\Shared\Wabisabi\WabisabiNav.cshtml" />
<_ContentIncludedByDefault Remove="Views\Shared\Wabisabi\WabisabiServerNavvExtension.cshtml" />
<_ContentIncludedByDefault Remove="Views\WabisabiCoordinatorConfig\UpdateWabisabiSettings.cshtml" />
<_ContentIncludedByDefault Remove="Views\WabisabiStore\Spend.cshtml" />
<_ContentIncludedByDefault Remove="Views\WabisabiStore\UpdateWabisabiStoreSettings.cshtml" />
<_ContentIncludedByDefault Remove="Views\_ViewImports.cshtml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,15 +1,18 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client; using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Secp256k1; using NBitcoin.Secp256k1;
using NNostr.Client; using NNostr.Client;
using WalletWasabi.Backend.Controllers; using WalletWasabi.Backend.Controllers;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
namespace BTCPayServer.Plugins.Wabisabi namespace BTCPayServer.Plugins.Wabisabi
{ {
@@ -18,6 +21,17 @@ namespace BTCPayServer.Plugins.Wabisabi
[Route("plugins/wabisabi-coordinator/edit")] [Route("plugins/wabisabi-coordinator/edit")]
public class WabisabiCoordinatorConfigController : Controller public class WabisabiCoordinatorConfigController : Controller
{ {
public static string OurDisclaimer = @"By using this plugin, the user agrees that they are solely responsible for determining the legality of its operation in their jurisdiction and for the handling of any revenue generated through its use. The user also agrees that any coordinator fees generated through the use of the plugin may be configured to be split up and forwarded to multiple destinations, which by default are non-profit organizations. However, there is also a hardcoded destination intended to fund the further development and maintenance of the plugin. The user understands that the forwarding of these fees to the hardcoded destination should be considered a donation and not an obligation of the developer or the plugin. The user also acknowledges that the plugin is open-source and licensed under the MIT license and therefore has the right to adapt the code and remove this feature if they do not accept the hardcoded donation. The user agrees to use the plugin at their own risk and acknowledges that being a coinjoin coordinator carries certain risks, including but not limited to legal risks, technical risks, privacy risks and reputational risks. It is their responsibility to carefully consider these risks before using the plugin.
This disclaimer serves as a binding agreement between the user and the developers of this plugin and supersedes any previous agreements or understandings, whether written or oral. The user agrees to fully waive and release the developers of this plugin and BTCPay Server contributors, from any and all liabilities, claims, demands, damages, or causes of action arising out of or related to the use of this plugin. In the event of any legal issues arising from the use of this plugin, the user also agrees to indemnify and hold harmless the developers of this plugin and BTCPay Server contributors from any claims, costs, losses, damages, liabilities, judgments and expenses (including reasonable fees of attorneys and other professionals) arising from or in any way related to the user's use of the plugin or violation of these terms. Any failure or delay by the developer to exercise or enforce any right or remedy provided under this disclaimer will not constitute a waiver of that or any other right or remedy, and no single or partial exercise of any right or remedy will preclude or restrict the further exercise of that or any other right or remedy.
Legal risks: as the coordinator, the user may be considered to be operating a money transmitting business and may be subject to regulatory requirements and oversight.
Technical risks: the plugin uses complex cryptography and code, and there may be bugs or vulnerabilities that could result in the loss of funds.
Privacy risks: as the coordinator, the user may have access to sensitive transaction data, and it is their responsibility to protect this data and comply with any applicable privacy laws.
Reputation risks: as the coordinator, the user may be associated with illegal activities and may face reputational damage.";
private readonly WabisabiCoordinatorService _wabisabiCoordinatorService; private readonly WabisabiCoordinatorService _wabisabiCoordinatorService;
public WabisabiCoordinatorConfigController(WabisabiCoordinatorService wabisabiCoordinatorService) public WabisabiCoordinatorConfigController(WabisabiCoordinatorService wabisabiCoordinatorService)
{ {
@@ -41,13 +55,47 @@ namespace BTCPayServer.Plugins.Wabisabi
return View(Wabisabi); return View(Wabisabi);
} }
private static bool IsLocalNetwork(string server)
{
ArgumentNullException.ThrowIfNull(server);
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
}
if (IPAddress.TryParse(server, out var ip))
{
return ip.IsLocal() || ip.IsRFC1918();
}
if (Uri.TryCreate(server, UriKind.Absolute, out var res) && res.IsLoopback || res.Host == "localhost")
{
return true;
}
return false;
}
[HttpPost("")] [HttpPost("")]
public async Task<IActionResult> UpdateWabisabiSettings(WabisabiCoordinatorSettings vm, public async Task<IActionResult> UpdateWabisabiSettings(WabisabiCoordinatorSettings vm,
string command, string config) string command, string config)
{ {
switch (command) switch (command)
{ {
case "nostr-current-url":
if (IsLocalNetwork(Request.GetAbsoluteRoot()))
{
TempData["ErrorMessage"] = "the current url is only reachable from your local network. You need a public domain or use Tor.";
return View(vm);
}
else
{
vm.UriToAdvertise = Request.GetAbsoluteRootUri();
TempData["SuccessMessage"] = $"Will create nostr events that point to ${ vm.UriToAdvertise }";
await _wabisabiCoordinatorService.UpdateSettings( vm);
return RedirectToAction(nameof(UpdateWabisabiSettings));
}
case "generate-nostr-key": case "generate-nostr-key":
if (ECPrivKey.TryCreate(new ReadOnlySpan<byte>(RandomNumberGenerator.GetBytes(32)), out var key)) if (ECPrivKey.TryCreate(new ReadOnlySpan<byte>(RandomNumberGenerator.GetBytes(32)), out var key))
{ {
@@ -67,7 +115,7 @@ namespace BTCPayServer.Plugins.Wabisabi
} }
catch (Exception e) catch (Exception e)
{ {
ModelState.AddModelError("config", "config json was invalid"); ModelState.AddModelError("config", $"config json was invalid ({e.Message})");
return View(vm); return View(vm);
} }
await _wabisabiCoordinatorService.UpdateSettings( vm); await _wabisabiCoordinatorService.UpdateSettings( vm);

View File

@@ -23,6 +23,7 @@ using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBitcoin.RPC; using NBitcoin.RPC;
using NBXplorer; using NBXplorer;
using NBXplorer.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NNostr.Client; using NNostr.Client;
using WalletWasabi.Bases; using WalletWasabi.Bases;
@@ -43,8 +44,6 @@ public class WabisabiCoordinatorService : PeriodicRunner
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly WabisabiCoordinatorClientInstanceManager _instanceManager; private readonly WabisabiCoordinatorClientInstanceManager _instanceManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;
public readonly IdempotencyRequestCache IdempotencyRequestCache; public readonly IdempotencyRequestCache IdempotencyRequestCache;
@@ -54,8 +53,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
public WabisabiCoordinatorService(ISettingsRepository settingsRepository, public WabisabiCoordinatorService(ISettingsRepository settingsRepository,
IOptions<DataDirectories> dataDirectories, IExplorerClientProvider clientProvider, IMemoryCache memoryCache, IOptions<DataDirectories> dataDirectories, IExplorerClientProvider clientProvider, IMemoryCache memoryCache,
WabisabiCoordinatorClientInstanceManager instanceManager, WabisabiCoordinatorClientInstanceManager instanceManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory) : base(TimeSpan.FromMinutes(15))
IConfiguration configuration, IServiceProvider serviceProvider) : base(TimeSpan.FromMinutes(15))
{ {
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
_dataDirectories = dataDirectories; _dataDirectories = dataDirectories;
@@ -63,13 +61,11 @@ public class WabisabiCoordinatorService : PeriodicRunner
_memoryCache = memoryCache; _memoryCache = memoryCache;
_instanceManager = instanceManager; _instanceManager = instanceManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_configuration = configuration;
_serviceProvider = serviceProvider;
IdempotencyRequestCache = new(memoryCache); IdempotencyRequestCache = new(memoryCache);
} }
private WabisabiCoordinatorSettings cachedSettings; private WabisabiCoordinatorSettings cachedSettings;
public async Task<WabisabiCoordinatorSettings> GetSettings() public async Task<WabisabiCoordinatorSettings> GetSettings()
{ {
return cachedSettings ??= (await _settingsRepository.GetSettingAsync<WabisabiCoordinatorSettings>( return cachedSettings ??= (await _settingsRepository.GetSettingAsync<WabisabiCoordinatorSettings>(
@@ -93,10 +89,14 @@ public class WabisabiCoordinatorService : PeriodicRunner
break; break;
} }
} }
else if (existing.Enabled &&
_instanceManager.HostedServices.TryGetValue("local", out var instance))
{
instance.TermsConditions = wabisabiCoordinatorSettings.TermsConditions;
}
await this.ActionAsync(CancellationToken.None); await this.ActionAsync(CancellationToken.None);
await _settingsRepository.UpdateSetting(wabisabiCoordinatorSettings, nameof(WabisabiCoordinatorSettings)); await _settingsRepository.UpdateSetting(wabisabiCoordinatorSettings, nameof(WabisabiCoordinatorSettings));
} }
public class BtcPayRpcClient : CachedRpcClient public class BtcPayRpcClient : CachedRpcClient
@@ -119,6 +119,37 @@ public class WabisabiCoordinatorService : PeriodicRunner
return result; return result;
} }
public override async Task<uint256> SendRawTransactionAsync(Transaction transaction,
CancellationToken cancellationToken = default)
{
var result = await _explorerClient.BroadcastAsync(transaction, cancellationToken);
if (!result.Success)
{
throw new RPCException((RPCErrorCode)result.RPCCode, result.RPCMessage, null);
}
return transaction.GetHash();
}
public override async Task<EstimateSmartFeeResponse> EstimateSmartFeeAsync(int confirmationTarget,
EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative,
CancellationToken cancellationToken = default)
{
string cacheKey = $"{nameof(EstimateSmartFeeAsync)}:{confirmationTarget}:{estimateMode}";
return await IdempotencyRequestCache.GetCachedResponseAsync(
cacheKey,
action: async (_, cancellationToken) =>
{
var result = await _explorerClient.GetFeeRateAsync(confirmationTarget, cancellationToken);
return new EstimateSmartFeeResponse() {FeeRate = result.FeeRate, Blocks = result.BlockCount};
},
options: CacheOptionsWithExpirationToken(size: 1, expireInSeconds: 60),
cancellationToken).ConfigureAwait(false);
}
} }
public override async Task StartAsync(CancellationToken cancellationToken) public override async Task StartAsync(CancellationToken cancellationToken)
@@ -148,60 +179,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
public async Task StartCoordinator(CancellationToken cancellationToken) public async Task StartCoordinator(CancellationToken cancellationToken)
{ {
await HostedServices.StartAllAsync(cancellationToken); await HostedServices.StartAllAsync(cancellationToken);
_instanceManager.AddCoordinator("Local Coordinator", "local", _ => null, cachedSettings.TermsConditions);
var host = await _serviceProvider.GetService<Task<IWebHost>>();
Console.Error.WriteLine("ADDRESSES:" +
host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.FirstOrDefault());
string rootPath = _configuration.GetValue<string>("rootpath", "/");
var serverAddress = host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.FirstOrDefault();
_instanceManager.AddCoordinator("Local Coordinator", "local", provider =>
{
if (!string.IsNullOrEmpty(serverAddress))
{
var serverAddressUri = new Uri(serverAddress);
if (new[] {UriHostNameType.IPv4, UriHostNameType.IPv6}.Contains(serverAddressUri.HostNameType))
{
var ipEndpoint = IPEndPoint.Parse(serverAddressUri.Host);
if (Equals(ipEndpoint.Address, IPAddress.Any))
{
ipEndpoint.Address = IPAddress.Loopback;
}
if (Equals(ipEndpoint.Address, IPAddress.IPv6Any))
{
ipEndpoint.Address = IPAddress.Loopback;
}
UriBuilder builder = new(serverAddressUri);
builder.Host = ipEndpoint.Address.ToString();
builder.Path = $"{rootPath}plugins/wabisabi-coordinator/";
Console.Error.WriteLine($"COORD URL-1: {builder.Uri}");
return builder.Uri;
}
}
Uri result;
var rawBind = _configuration.GetValue("bind", IPAddress.Loopback.ToString())
.Split(":", StringSplitOptions.RemoveEmptyEntries);
var bindAddress = IPAddress.Parse(rawBind.First());
if (Equals(bindAddress, IPAddress.Any))
{
bindAddress = IPAddress.Loopback;
}
if (Equals(bindAddress, IPAddress.IPv6Any))
{
bindAddress = IPAddress.IPv6Loopback;
}
int bindPort = rawBind.Length > 2 ? int.Parse(rawBind[1]) : _configuration.GetValue("port", 443);
result = new Uri($"https://{bindAddress}:{bindPort}{rootPath}plugins/wabisabi-coordinator/");
Console.Error.WriteLine($"COORD URL: {result}");
return result;
});
} }
public async Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
@@ -211,52 +189,11 @@ public class WabisabiCoordinatorService : PeriodicRunner
protected override async Task ActionAsync(CancellationToken cancel) protected override async Task ActionAsync(CancellationToken cancel)
{ {
var network = _clientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork.Name.ToLower(); var network = _clientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork;
var s = await GetSettings(); var s = await GetSettings();
if (s.Enabled && !string.IsNullOrEmpty(s.NostrIdentity) && s.NostrRelay is not null) if (s.Enabled && !string.IsNullOrEmpty(s.NostrIdentity) && s.NostrRelay is not null)
{ {
try await Nostr.Publish(s.NostrRelay, network, s.NostrIdentity, s.UriToAdvertise, s.CoordinatorDescription,cancel);
{
var key = NostrExtensions.ParseKey(s.NostrIdentity);
var client = new NostrClient(s.NostrRelay);
await client.ConnectAndWaitUntilConnected(cancel);
_= client.ListenForMessages();
var evt = new NostrEvent()
{
Kind = WabisabiStoreController.coordinatorEventKind,
PublicKey = key.CreatePubKey().ToXOnlyPubKey().ToHex(),
CreatedAt = DateTimeOffset.UtcNow,
Tags = new List<NostrEventTag>()
{
new()
{
TagIdentifier = "uri",
Data = new List<string>()
{
"https://somewhere.com"
}
},
new()
{
TagIdentifier = "network",
Data = new List<string>()
{
network
}
}
}
};
await evt.ComputeIdAndSign(key);
await client.PublishEvent(evt, cancel);
client.Dispose();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
} }
} }
} }

View File

@@ -17,10 +17,33 @@ public class WabisabiCoordinatorSettings
[JsonIgnore] public ECPrivKey? Key => string.IsNullOrEmpty(NostrIdentity)? null: NostrExtensions.ParseKey(NostrIdentity); [JsonIgnore] public ECPrivKey? Key => string.IsNullOrEmpty(NostrIdentity)? null: NostrExtensions.ParseKey(NostrIdentity);
[JsonIgnore] public ECXOnlyPubKey PubKey => Key?.CreatePubKey().ToXOnlyPubKey(); [JsonIgnore] public ECXOnlyPubKey PubKey => Key?.CreatePubKey().ToXOnlyPubKey();
public Uri UriToAdvertise { get; set; }
public string TermsConditions { get; set; } = @"
These terms and conditions govern your use of the Coinjoin Coordinator service. By using the service, you agree to be bound by these terms and conditions. If you do not agree to these terms and conditions, you should not use the service.
Coinjoin Coordinator Service: The Coinjoin Coordinator service is a tool that allows users to anonymize their cryptocurrency transactions by pooling them with other users' funds and sending them within a common transaction. The service does not store, transmit, or otherwise handle users' cryptocurrency funds.
Legal Compliance: You are responsible for complying with all applicable laws and regulations in your jurisdiction in relation to your use of the Coinjoin Coordinator service. The service is intended to be used for lawful purposes only.
No Warranty: The Coinjoin Coordinator service is provided on an ""as is"" and ""as available"" basis, without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose.
Limitation of Liability: In no event shall the Coinjoin Coordinator be liable for any direct, indirect, incidental, special, or consequential damages, or loss of profits, arising out of or in connection with your use of the service.
Indemnification: You agree to indemnify and hold the Coinjoin Coordinator, its affiliates, officers, agents, and employees harmless from any claim or demand, including reasonable attorneys' fees, made by any third party due to or arising out of your use of the service, your violation of these terms and conditions, or your violation of any rights of another.
Severability: If any provision of these terms and conditions is found to be invalid or unenforceable, the remaining provisions shall remain in full force and effect.
Governing Law: These terms and conditions shall be governed by and construed in accordance with the laws of the jurisdiction in which the Coinjoin Coordinator is based.
Changes to Terms and Conditions: Coinjoin Coordinator reserves the right, at its sole discretion, to modify or replace these terms and conditions at any time.
";
public string CoordinatorDescription { get; set; }
} }
public class DiscoveredCoordinator public class DiscoveredCoordinator
{ {
public Uri Uri { get; set; } public Uri Uri { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Description { get; set; }
} }

View File

@@ -9,7 +9,7 @@ namespace WalletWasabi.Backend.Controllers;
[Route("plugins/wabisabi-coordinator")] [Route("plugins/wabisabi-coordinator")]
[AllowAnonymous] [AllowAnonymous]
public class WasabiLeechController:Controller public class WasabiLeechController : Controller
{ {
private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager; private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager;
@@ -17,20 +17,29 @@ public class WasabiLeechController:Controller
{ {
_coordinatorClientInstanceManager = coordinatorClientInstanceManager; _coordinatorClientInstanceManager = coordinatorClientInstanceManager;
} }
[HttpGet("api/v4/Wasabi/legaldocuments")]
public async Task<IActionResult> GetLegalDocuments()
{
if (_coordinatorClientInstanceManager.HostedServices.TryGetValue("local", out var instance))
{
return Ok(instance.TermsConditions);
}
return NotFound();
}
[Route("{*key}")] [Route("{*key}")]
public async Task<IActionResult> Forward(string key, CancellationToken cancellationToken) public async Task<IActionResult> Forward(string key, CancellationToken cancellationToken)
{ {
if (!_coordinatorClientInstanceManager.HostedServices.TryGetValue("zksnacks", out var coordinator))
if(!_coordinatorClientInstanceManager.HostedServices.TryGetValue("zksnacks", out var coordinator))
return BadRequest(); return BadRequest();
var b = new UriBuilder(coordinator.Coordinator); var b = new UriBuilder(coordinator.Coordinator);
b.Path = key; b.Path = key;
b.Query = Request.QueryString.ToString(); b.Query = Request.QueryString.ToString();
return RedirectPreserveMethod(b.ToString()); return RedirectPreserveMethod(b.ToString());
} }
} }

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json.Linq;
using NNostr.Client;
using WalletWasabi.Backend.Controllers;
namespace BTCPayServer.Plugins.Wabisabi;
public class Nostr
{
public static int Kind = 15750;
public static async Task Publish(
Uri relayUri,
Network currentNetwork,
string key,
Uri coordinatorUri,
string description,
CancellationToken cancellationToken)
{
var privateKey = NostrExtensions.ParseKey(key);
var client = new NostrClient(relayUri);
await client.ConnectAndWaitUntilConnected(cancellationToken);
_ = client.ListenForMessages();
var evt = new NostrEvent()
{
Kind = Kind,
Content = description,
PublicKey = privateKey.CreatePubKey().ToXOnlyPubKey().ToHex(),
CreatedAt = DateTimeOffset.UtcNow,
Tags = new List<NostrEventTag>()
{
new() {TagIdentifier = "uri", Data = new List<string>() {new Uri(coordinatorUri, "plugins/wabisabi-coordinator").ToString()}},
new() {TagIdentifier = "network", Data = new List<string>() {currentNetwork.Name}}
}
};
await evt.ComputeIdAndSign(privateKey);
await client.PublishEvent(evt, cancellationToken);
client.Dispose();
}
public static async Task<List<DiscoveredCoordinator>> Discover(
Uri relayUri,
Network currentNetwork,
string ourPubKey,
CancellationToken cancellationToken)
{
using var nostrClient = new NostrClient(relayUri);
await nostrClient.CreateSubscription("nostr-wabisabi-coordinators",
new[]
{
new NostrSubscriptionFilter()
{
Kinds = new[] {Kind}, Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1)),
}
}, cancellationToken);
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
await nostrClient.ConnectAndWaitUntilConnected(cts.Token);
_ = nostrClient.ListenForMessages();
var result = new List<NostrEvent>();
var tcs = new TaskCompletionSource();
Stopwatch stopwatch = new();
stopwatch.Start();
nostrClient.MessageReceived += (sender, s) =>
{
if (JArray.Parse(s).FirstOrDefault()?.Value<string>() == "EOSE")
{
tcs.SetResult();
}
};
nostrClient.EventsReceived += (sender, tuple) =>
{
stopwatch.Restart();
result.AddRange(tuple.events);
};
while (!tcs.Task.IsCompleted && !cts.IsCancellationRequested &&
stopwatch.ElapsedMilliseconds < 10000)
{
await Task.Delay(1000, cts.Token);
}
nostrClient.Dispose();
var network = currentNetwork.Name
.ToLower();
return result.Where(@event =>
@event.PublicKey != ourPubKey &&
@event.CreatedAt < DateTimeOffset.UtcNow.AddMinutes(15) &&
@event.Verify() &&
@event.Tags.Any(tag =>
tag.TagIdentifier == "uri" &&
tag.Data.Any(s => Uri.IsWellFormedUriString(s, UriKind.Absolute))) &&
@event.Tags.Any(tag =>
tag.TagIdentifier == "network" && tag.Data.FirstOrDefault() == network)
).Select(@event => new DiscoveredCoordinator()
{
Description = @event.Content,
Name = @event.PublicKey,
Uri = new Uri(@event.GetTaggedData("uri")
.First(s => Uri.IsWellFormedUriString(s, UriKind.Absolute)))
}).ToList();
}
}

View File

@@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using WalletWasabi.Backend.Controllers @using WalletWasabi.Backend.Controllers
@using Microsoft.AspNetCore.Mvc.ModelBinding @using Microsoft.AspNetCore.Mvc.ModelBinding
@using BTCPayServer.Plugins.Wabisabi
@model WalletWasabi.Backend.Controllers.WabisabiCoordinatorSettings @model WalletWasabi.Backend.Controllers.WabisabiCoordinatorSettings
@inject WabisabiCoordinatorService WabisabiCoordinatorService @inject WabisabiCoordinatorService WabisabiCoordinatorService
@@ -13,24 +14,12 @@
<form method="post"> <form method="post">
<div class="row"> <div class="row">
<div class="form-group form-check"> <div class="col-xxl-constrain col-xl-8">
<label asp-for="Enabled" class="form-check-label">Enable Coordinator</label> <div class="form-group form-check">
<input asp-for="Enabled" type="checkbox" class="form-check-input"/> <label asp-for="Enabled" class="form-check-label">Enable Coordinator</label>
</div> <input asp-for="Enabled" type="checkbox" class="form-check-input" />
<div class="form-group form-check">
<label asp-for="NostrRelay" class="form-label">Nostr Relay</label>
<input asp-for="NostrRelay" type="text" class="form-control"/>
</div>
<div class="form-group">
<label asp-for="NostrIdentity" class="form-label">nostr privkey</label>
<div class="input-group input-group-sm">
<input asp-for="NostrIdentity" type="text" class="form-control" />
<button name="command" value="generate-nostr-key" type="submit" class="btn btn-secondary btn-sm">Generate</button>
</div> </div>
</div>
</div>
<div class="row ">
<div class="col-xxl-constrain">
<div class="form-group pt-3"> <div class="form-group pt-3">
<label class="form-label" for="config">Config</label> <label class="form-label" for="config">Config</label>
@@ -38,17 +27,54 @@
{ {
<span class="text-danger">@string.Join("\n", error.Errors)</span> <span class="text-danger">@string.Join("\n", error.Errors)</span>
} }
<textarea rows="10" cols="40" class="form-control valid" id="config" name="config" spellcheck="false" aria-invalid="false"> <textarea rows="10" cols="40" class="form-control" id="config" name="config" >
@Html.Raw(WabisabiCoordinatorService.WabiSabiCoordinator.Config.ToString()) @Html.Raw(WabisabiCoordinatorService.WabiSabiCoordinator.Config.ToString())
</textarea> </textarea>
</div>
<div class="form-group pt-3">
<label class="form-label" for="config">Terms & Conditions </label>
<textarea rows="10" cols="40" class="form-control " asp-for="TermsConditions" >
</textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="row ">
<div class="col-xxl-constrain col-xl-8">
<h3 class="mb-3">Publish to Nostr </h3>
<div class="form-group ">
<label asp-for="NostrRelay" class="form-label">Nostr Relay</label>
<input asp-for="NostrRelay" type="text" class="form-control" />
</div>
<div class="form-group">
<label asp-for="NostrIdentity" class="form-label">nostr privkey</label>
<div class="input-group input-group-sm">
<input asp-for="NostrIdentity" type="text" class="form-control" />
<button name="command" value="generate-nostr-key" type="submit" class="btn btn-secondary btn-sm">Generate</button>
</div>
</div>
<div class="form-group">
<label asp-for="CoordinatorDescription" class="form-label">Description</label>
<textarea asp-for="CoordinatorDescription" class="form-control"></textarea>
</div>
<div class="form-group">
<label asp-for="UriToAdvertise" class="form-label">What url to advertise? </label>
<div class="input-group input-group-sm">
<input asp-for="UriToAdvertise" type="text" class="form-control" />
<button name="command" value="nostr-current-url" type="submit" class="btn btn-secondary btn-sm">Use current url</button>
</div>
</div>
</div>
</div>
<p class=" alert alert-warning" style="white-space: pre-line">
@WabisabiCoordinatorConfigController.OurDisclaimer
</p>
<button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button> <button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>
</form> </form>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial"/> <partial name="_ValidationScriptsPartial" />
} }

View File

@@ -67,30 +67,30 @@
<h2 class="mb-4">Coinjoin configuration</h2> <h2 class="mb-4">Coinjoin configuration</h2>
<form method="post"> <form method="post">
@{ @{
var wallet = await WalletProvider.GetWalletAsync(storeId); var wallet = await WalletProvider.GetWalletAsync(storeId);
if (wallet is BTCPayWallet btcPayWallet) if (wallet is BTCPayWallet btcPayWallet)
{
@if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true)
{ {
@if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true) <div class="alert alert-danger d-flex align-items-center" role="alert">
{ <vc:icon symbol="warning" />
<div class="alert alert-danger d-flex align-items-center" role="alert"> <span class="ms-3">This wallet is not enabled in your store settings and will not be able to participate in coinjoins..</span>
<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> <button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div> </div>
} }
else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) else 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 a hot wallet and will not be able to participate in coinjoins.</span> <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> <button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div> </div>
}
} }
} }
}
<div class="@(anyEnabled ? "" : "d-none") card card-body coordinator-settings"> <div class="@(anyEnabled ? "" : "d-none") card card-body coordinator-settings">
@@ -121,21 +121,21 @@
<label asp-for="AnonymitySetTarget" class="form-check-label">Use Anon score model</label> <label asp-for="AnonymitySetTarget" class="form-check-label">Use Anon score model</label>
<input type="number" class="form-control" asp-for="AnonymitySetTarget" placeholder="target anon score"> <input type="number" class="form-control" asp-for="AnonymitySetTarget" placeholder="target anon score">
<p class="text-muted">Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value.<br/> Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.</p> <p class="text-muted">Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value.<br /> Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.</p>
</div> </div>
<div class="form-group form-check"> <div class="form-group form-check">
<label asp-for="ConsolidationMode" class="form-check-label">Coinsolidation mode</label> <label asp-for="ConsolidationMode" class="form-check-label">Coinsolidation mode</label>
<input asp-for="ConsolidationMode" type="checkbox" class="form-check-input"/> <input asp-for="ConsolidationMode" type="checkbox" class="form-check-input" />
<p class="text-muted">Feed as many coins to the coinjoin as possible.</p> <p class="text-muted">Feed as many coins to the coinjoin as possible.</p>
</div> </div>
<div class="form-group form-check"> <div class="form-group form-check">
<label asp-for="RedCoinIsolation" class="form-check-label">Cautious coinjoin entry mode </label> <label asp-for="RedCoinIsolation" class="form-check-label">Cautious coinjoin entry mode </label>
<input asp-for="RedCoinIsolation" type="checkbox" class="form-check-input"/> <input asp-for="RedCoinIsolation" type="checkbox" class="form-check-input" />
<p class="text-muted">Only allow a single non-private coin into a coinjoin.</p> <p class="text-muted">Only allow a single non-private coin into a coinjoin.</p>
</div> </div>
<div class="form-group form-check"> <div class="form-group form-check">
<label asp-for="BatchPayments" class="form-check-label">Batch payments</label> <label asp-for="BatchPayments" class="form-check-label">Batch payments</label>
<input asp-for="BatchPayments" type="checkbox" class="form-check-input"/> <input asp-for="BatchPayments" type="checkbox" class="form-check-input" />
<p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p> <p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
</div> </div>
<div class="form-group "> <div class="form-group ">
@@ -156,7 +156,7 @@
{ {
<div class="list-group-item"> <div class="list-group-item">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input asp-for="InputLabelsAllowed[xIndex]" type="text" class="form-control"/> <input asp-for="InputLabelsAllowed[xIndex]" type="text" class="form-control" />
<button name="command" value="include-label-remove:@Model.InputLabelsAllowed[xIndex]" type="submit" class="btn btn-secondary btn-sm">Remove</button> <button name="command" value="include-label-remove:@Model.InputLabelsAllowed[xIndex]" type="submit" class="btn btn-secondary btn-sm">Remove</button>
</div> </div>
</div> </div>
@@ -179,7 +179,7 @@
<div class="list-group-item"> <div class="list-group-item">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input asp-for="InputLabelsExcluded[xIndex]" type="text" class="form-control"/> <input asp-for="InputLabelsExcluded[xIndex]" type="text" class="form-control" />
<button name="command" value="exclude-label-remove:@Model.InputLabelsExcluded[xIndex]" type="submit" class="btn btn-secondary btn-sm">Remove</button> <button name="command" value="exclude-label-remove:@Model.InputLabelsExcluded[xIndex]" type="submit" class="btn btn-secondary btn-sm">Remove</button>
</div> </div>
</div> </div>
@@ -193,92 +193,109 @@
</div> </div>
</div> </div>
@for (var index = 0; index < Model.Settings.Count; index++) @for (var index = 0; index < Model.Settings.Count; index++)
{ {
<input asp-for="Settings[index].Coordinator" type="hidden"/> <input asp-for="Settings[index].Coordinator" type="hidden" />
var s = Model.Settings[index]; var s = Model.Settings[index];
if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(s.Coordinator, out var coordinator)) if (!WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(s.Coordinator, out var coordinator))
{ {
continue; continue;
} }
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header d-flex justify-content-between">
<div>
<div class="d-flex">
<h3>@coordinator.CoordinatorDisplayName</h3>
</div>
<span class="text-muted">@coordinator.Coordinator</span>
<div>
@if (!coordinator.WasabiCoordinatorStatusFetcher.Connected)
{
<p>Coordinator Status: Not connected</p>
}
else
{
<p>
Coordinator Status: Connected
</p>
}
</div>
</div>
<div class="form-group form-check form">
<input asp-for="Settings[index].Enabled" type="checkbox" class="form-check-input form-control-lg toggle-settings" data-coordinator="@s.Coordinator" disabled="@(!coordinator.WasabiCoordinatorStatusFetcher.Connected)" />
<a class="w-100 px-2 position-absolute bottom-0 cursor-pointer"
data-bs-toggle="modal" data-bs-target="#terms-@s.Coordinator"
style="
right: 0;
text-align: right;
">
By enabling this coordinator, you agree to their terms and conditions.
</a>
</div>
@if (coordinator.CoordinatorName != "local" && coordinator.CoordinatorName != "zksnacks")
{
<button name="command" type="submit" value="remove-coordinator:@(coordinator.CoordinatorName)" class="btn btn-link btn-danger" permission="@Policies.CanModifyServerSettings">Remove</button>
}
</div>
<div class="modal modal-lg fade" id="terms-@s.Coordinator">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">@coordinator.CoordinatorName Terms & Conditions </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="white-space: pre-line">
@Safe.Raw(coordinator.TermsConditions)
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
}
@if (ViewBag.DiscoveredCoordinators is List<DiscoveredCoordinator> discoveredCoordinators)
{
foreach (var coordinator in discoveredCoordinators)
{
<div class="card mt-3" permission="@Policies.CanModifyServerSettings">
<div class="card-header d-flex justify-content-between"> <div class="card-header d-flex justify-content-between">
<div> <div>
<div class="d-flex"> <div class="d-flex">
<h3>@coordinator.CoordinatorDisplayName</h3> <h3>@coordinator.Name</h3>
</div> </div>
<span class="text-muted">@coordinator.Coordinator</span> <span class="text-muted">@coordinator.Uri</span>
<div>
@if (!coordinator.WasabiCoordinatorStatusFetcher.Connected)
{
<p>Coordinator Status: Not connected</p>
}
else
{
<p>
Coordinator Status: Connected
<a href="@(coordinator.Coordinator)api/v4/Wasabi/legaldocuments"
target="_blank" rel="noreferrer noopener">
T&C
</a>
</p>
}
</div>
</div> </div>
<div class="form-group form-check form"> <div class="form-group form-check">
<input asp-for="Settings[index].Enabled" type="checkbox" class="form-check-input form-control-lg toggle-settings" data-coordinator="@s.Coordinator"/> <button name="command" type="submit" value="add-coordinator:@coordinator.Name:@coordinator.Uri" class="btn btn-primary btn-lg">Add</button>
</div> </div>
@if (coordinator.CoordinatorName != "local" && coordinator.CoordinatorName != "zksnacks")
{
<button name="command" type="submit" value="remove-coordinator:@(coordinator.CoordinatorName)" class="btn btn-link btn-danger" permission="@Policies.CanModifyServerSettings">Remove</button>
}
</div> </div>
</div> </div>
} }
@if (ViewBag.DiscoveredCoordinators is List<DiscoveredCoordinator> discoveredCoordinators) }
{ <button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>
foreach (var coordinator in discoveredCoordinators) <a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn btn-secondary mt-2" permission="@Policies.CanModifyServerSettings">Coordinator runner</a>
{ <button name="command" type="submit" value="discover" class="btn btn-secondary mt-2" permission="@Policies.CanModifyServerSettings">Discover coordinators over Nostr</button>
<a class="btn btn-secondary mt-2" href="https://gist.github.com/nopara73/bb17e89d7dc9af536ca41f50f705d329" rel="noreferrer noopener" target="_blank">Enable Discrete payments - Coming soon</a>
<div class="card mt-3" permission="@Policies.CanModifyServerSettings">
<div class="card-header d-flex justify-content-between">
<div>
<div class="d-flex">
<h3>@coordinator.Name</h3>
</div>
<span class="text-muted">@coordinator.Uri</span>
</div>
<div class="form-group form-check">
<button name="command" type="submit" value="add-coordinator:@coordinator.Name:@coordinator.Uri" class="btn btn-primary btn-lg">Add</button>
</div>
</div>
</div>
}
}
<button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>
<a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn btn-secondary mt-2" permission="@Policies.CanModifyServerSettings" >Coordinator runner</a>
<button name="command" type="submit" value="discover" class="btn btn-secondary mt-2" permission="@Policies.CanModifyServerSettings">Discover coordinators over Nostr</button>
<a class="btn btn-secondary mt-2" href="https://gist.github.com/nopara73/bb17e89d7dc9af536ca41f50f705d329" rel="noreferrer noopener" target="_blank">Enable Discrete payments - Coming soon</a>
</form> </form>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial"/> <partial name="_ValidationScriptsPartial" />
} }
<script type="text/javascript" nonce="@nonce"> <script type="text/javascript" nonce="@nonce">

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payments.PayJoin;
@@ -10,9 +11,11 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using WalletWasabi.Backend.Controllers;
using WalletWasabi.Services; using WalletWasabi.Services;
using WalletWasabi.Tor.Socks5.Pool.Circuits; using WalletWasabi.Tor.Socks5.Pool.Circuits;
using WalletWasabi.Userfacing; using WalletWasabi.Userfacing;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client;
using WalletWasabi.WabiSabi.Client.RoundStateAwaiters; using WalletWasabi.WabiSabi.Client.RoundStateAwaiters;
using WalletWasabi.WabiSabi.Client.StatusChangedEvents; using WalletWasabi.WabiSabi.Client.StatusChangedEvents;
@@ -67,11 +70,16 @@ public class WabisabiCoordinatorClientInstanceManager:IHostedService
} }
} }
public void AddCoordinator(string displayName, string name, public void AddCoordinator(string displayName, string name,
Func<IServiceProvider, Uri> fetcher, string termsConditions = null) Func<IServiceProvider, Uri> fetcher, string termsConditions = null)
{ {
if (termsConditions is null && name == "zksnacks")
{
termsConditions = new HttpClient().GetStringAsync("https://wasabiwallet.io/api/v4/Wasabi/legaldocuments")
.Result;
}
if (HostedServices.ContainsKey(name)) if (HostedServices.ContainsKey(name))
{ {
return; return;
@@ -79,7 +87,7 @@ public class WabisabiCoordinatorClientInstanceManager:IHostedService
var instance = new WabisabiCoordinatorClientInstance( var instance = new WabisabiCoordinatorClientInstance(
displayName, displayName,
name, fetcher.Invoke(_provider), _provider.GetService<ILoggerFactory>(), _provider, UTXOLocker, name, fetcher.Invoke(_provider), _provider.GetService<ILoggerFactory>(), _provider, UTXOLocker,
_provider.GetService<WalletProvider>()); _provider.GetService<WalletProvider>(), termsConditions);
if (HostedServices.TryAdd(instance.CoordinatorName, instance)) if (HostedServices.TryAdd(instance.CoordinatorName, instance))
{ {
if(started) if(started)
@@ -107,19 +115,21 @@ public class WabisabiCoordinatorClientInstance
public string CoordinatorName { get; set; } public string CoordinatorName { get; set; }
public Uri Coordinator { get; set; } public Uri Coordinator { get; set; }
public WalletProvider WalletProvider { get; } public WalletProvider WalletProvider { get; }
public string TermsConditions { get; set; }
public HttpClientFactory WasabiHttpClientFactory { get; set; } public HttpClientFactory WasabiHttpClientFactory { get; set; }
public RoundStateUpdater RoundStateUpdater { get; set; } public RoundStateUpdater RoundStateUpdater { get; set; }
public WasabiCoordinatorStatusFetcher WasabiCoordinatorStatusFetcher { get; set; } public WasabiCoordinatorStatusFetcher WasabiCoordinatorStatusFetcher { get; set; }
public CoinJoinManager CoinJoinManager { get; set; } public CoinJoinManager CoinJoinManager { get; set; }
public WabisabiCoordinatorClientInstance(string coordinatorDisplayName, public WabisabiCoordinatorClientInstance(string coordinatorDisplayName,
string coordinatorName, string coordinatorName,
Uri coordinator, Uri coordinator,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IUTXOLocker utxoLocker, IUTXOLocker utxoLocker,
WalletProvider walletProvider) WalletProvider walletProvider, string termsConditions, string coordinatorIdentifier = "CoinJoinCoordinatorIdentifier")
{ {
_utxoLocker = utxoLocker; _utxoLocker = utxoLocker;
var config = serviceProvider.GetService<IConfiguration>(); var config = serviceProvider.GetService<IConfiguration>();
var socksEndpoint = config.GetValue<string>("socksendpoint"); var socksEndpoint = config.GetValue<string>("socksendpoint");
@@ -132,17 +142,42 @@ public class WabisabiCoordinatorClientInstance
CoordinatorName = coordinatorName; CoordinatorName = coordinatorName;
Coordinator = coordinator; Coordinator = coordinator;
WalletProvider = walletProvider; WalletProvider = walletProvider;
TermsConditions = termsConditions;
_logger = loggerFactory.CreateLogger(coordinatorName); _logger = loggerFactory.CreateLogger(coordinatorName);
WasabiHttpClientFactory = new HttpClientFactory(torEndpoint, () => Coordinator); IWabiSabiApiRequestHandler sharedWabisabiClient;
var roundStateUpdaterCircuit = new PersonCircuit(); if (coordinatorName == "local")
var roundStateUpdaterHttpClient = {
WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit); sharedWabisabiClient = serviceProvider.GetRequiredService<WabiSabiController>();
var sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient);
}
else
{
WasabiHttpClientFactory = new HttpClientFactory(torEndpoint, () => Coordinator);
var roundStateUpdaterCircuit = new PersonCircuit();
var roundStateUpdaterHttpClient =
WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit);
sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient);
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
}
WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger); WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger);
RoundStateUpdater = new RoundStateUpdater(TimeSpan.FromSeconds(5),sharedWabisabiClient, WasabiCoordinatorStatusFetcher); RoundStateUpdater = new RoundStateUpdater(TimeSpan.FromSeconds(5),sharedWabisabiClient, WasabiCoordinatorStatusFetcher);
CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory, if (coordinatorName == "local")
WasabiCoordinatorStatusFetcher, "CoinJoinCoordinatorIdentifier"); {
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
sharedWabisabiClient,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
}
else
{
CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater,
WasabiHttpClientFactory,
WasabiCoordinatorStatusFetcher, coordinatorIdentifier);
}
CoinJoinManager.StatusChanged += OnStatusChanged; CoinJoinManager.StatusChanged += OnStatusChanged;
CoinJoinManager.OnBan += (sender, args) => CoinJoinManager.OnBan += (sender, args) =>
{ {

View File

@@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@@ -67,7 +67,6 @@ namespace BTCPayServer.Plugins.Wabisabi
return View(Wabisabi); return View(Wabisabi);
} }
public const int coordinatorEventKind = 15750;
[HttpPost("")] [HttpPost("")]
public async Task<IActionResult> UpdateWabisabiStoreSettings(string storeId, WabisabiStoreSettings vm, public async Task<IActionResult> UpdateWabisabiStoreSettings(string storeId, WabisabiStoreSettings vm,
@@ -86,63 +85,13 @@ namespace BTCPayServer.Plugins.Wabisabi
case "discover": case "discover":
coordSettings = await _wabisabiCoordinatorService.GetSettings(); coordSettings = await _wabisabiCoordinatorService.GetSettings();
var relay = commandIndex ?? var relay = commandIndex ??
(await _wabisabiCoordinatorService.GetSettings())?.NostrRelay.ToString(); coordSettings?.NostrRelay.ToString();
if (Uri.TryCreate(relay, UriKind.Absolute, out var relayUri)) if (Uri.TryCreate(relay, UriKind.Absolute, out var relayUri))
{ {
using var nostrClient = new NostrClient(relayUri); ViewBag.DiscoveredCoordinators =await Nostr.Discover(relayUri,
await nostrClient.CreateSubscription("nostr-wabisabi-coordinators", _explorerClientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork,
new[] coordSettings.Key?.CreateXOnlyPubKey().ToHex(), CancellationToken.None);
{
new NostrSubscriptionFilter()
{
Kinds = new[] {coordinatorEventKind},
Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1)),
}
});
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
await nostrClient.ConnectAndWaitUntilConnected(cts.Token);
_ = nostrClient.ListenForMessages();
var result = new List<NostrEvent>();
var tcs = new TaskCompletionSource();
Stopwatch stopwatch = new();
stopwatch.Start();
nostrClient.MessageReceived += (sender, s) =>
{
if (JArray.Parse(s).FirstOrDefault()?.Value<string>() == "EOSE")
{
tcs.SetResult();
}
};
nostrClient.EventsReceived += (sender, tuple) =>
{
stopwatch.Restart();
result.AddRange(tuple.events);
};
while (!tcs.Task.IsCompleted && !cts.IsCancellationRequested &&
stopwatch.ElapsedMilliseconds < 10000)
{
await Task.Delay(1000, cts.Token);
}
nostrClient.Dispose();
var network = _explorerClientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork.Name
.ToLower();
ViewBag.DiscoveredCoordinators = result.Where(@event =>
@event.CreatedAt < DateTimeOffset.UtcNow.AddMinutes(15) &&
@event.Verify() &&
@event.Tags.Any(tag =>
tag.TagIdentifier == "uri" &&
tag.Data.Any(s => Uri.IsWellFormedUriString(s, UriKind.Absolute))) &&
@event.Tags.Any(tag =>
tag.TagIdentifier == "network" && tag.Data.FirstOrDefault() == network)
).Select(@event => new DiscoveredCoordinator()
{
Name = @event.PublicKey,
Uri = new Uri(@event.GetTaggedData("uri")
.First(s => Uri.IsWellFormedUriString(s, UriKind.Absolute)))
}).Where(discoveredCoordinator => string.IsNullOrEmpty(coordSettings.NostrIdentity) || discoveredCoordinator.Name != coordSettings.PubKey?.ToHex()).ToList();
} }
else else
{ {

View File

@@ -17,6 +17,7 @@ public class WabisabiStoreSettings
public bool ConsolidationMode { get; set; } = false; public bool ConsolidationMode { get; set; } = false;
public bool RedCoinIsolation { get; set; } = false; public bool RedCoinIsolation { get; set; } = false;
public int AnonymitySetTarget { get; set; } = 5; public int AnonymitySetTarget { get; set; } = 5;
public double MaxFee { get; set; } = 5;
public bool BatchPayments { get; set; } = true; public bool BatchPayments { get; set; } = true;

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using WalletWasabi.Backend.Models.Responses; using WalletWasabi.Backend.Models.Responses;
using WalletWasabi.Bases; using WalletWasabi.Bases;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client;
using WalletWasabi.WabiSabi.Models; using WalletWasabi.WabiSabi.Models;
using WalletWasabi.WebClients.Wasabi; using WalletWasabi.WebClients.Wasabi;
@@ -14,10 +15,10 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class WasabiCoordinatorStatusFetcher : PeriodicRunner, IWasabiBackendStatusProvider public class WasabiCoordinatorStatusFetcher : PeriodicRunner, IWasabiBackendStatusProvider
{ {
private readonly WabiSabiHttpApiClient _wasabiClient; private readonly IWabiSabiApiRequestHandler _wasabiClient;
private readonly ILogger _logger; private readonly ILogger _logger;
public bool Connected { get; set; } = false; public bool Connected { get; set; } = false;
public WasabiCoordinatorStatusFetcher(WabiSabiHttpApiClient wasabiClient, ILogger logger) : public WasabiCoordinatorStatusFetcher(IWabiSabiApiRequestHandler wasabiClient, ILogger logger) :
base(TimeSpan.FromSeconds(30)) base(TimeSpan.FromSeconds(30))
{ {
_wasabiClient = wasabiClient; _wasabiClient = wasabiClient;