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" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources" />
<PackageReference Include="NNostr.Client" Version="0.0.17" />
</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>
</Project>

View File

@@ -1,15 +1,18 @@
using System;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Secp256k1;
using NNostr.Client;
using WalletWasabi.Backend.Controllers;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
namespace BTCPayServer.Plugins.Wabisabi
{
@@ -18,6 +21,17 @@ namespace BTCPayServer.Plugins.Wabisabi
[Route("plugins/wabisabi-coordinator/edit")]
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;
public WabisabiCoordinatorConfigController(WabisabiCoordinatorService wabisabiCoordinatorService)
{
@@ -41,13 +55,47 @@ namespace BTCPayServer.Plugins.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("")]
public async Task<IActionResult> UpdateWabisabiSettings(WabisabiCoordinatorSettings vm,
string command, string config)
{
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":
if (ECPrivKey.TryCreate(new ReadOnlySpan<byte>(RandomNumberGenerator.GetBytes(32)), out var key))
{
@@ -67,7 +115,7 @@ namespace BTCPayServer.Plugins.Wabisabi
}
catch (Exception e)
{
ModelState.AddModelError("config", "config json was invalid");
ModelState.AddModelError("config", $"config json was invalid ({e.Message})");
return View(vm);
}
await _wabisabiCoordinatorService.UpdateSettings( vm);

View File

@@ -23,6 +23,7 @@ using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.RPC;
using NBXplorer;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using NNostr.Client;
using WalletWasabi.Bases;
@@ -43,8 +44,6 @@ public class WabisabiCoordinatorService : PeriodicRunner
private readonly IMemoryCache _memoryCache;
private readonly WabisabiCoordinatorClientInstanceManager _instanceManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;
public readonly IdempotencyRequestCache IdempotencyRequestCache;
@@ -54,8 +53,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
public WabisabiCoordinatorService(ISettingsRepository settingsRepository,
IOptions<DataDirectories> dataDirectories, IExplorerClientProvider clientProvider, IMemoryCache memoryCache,
WabisabiCoordinatorClientInstanceManager instanceManager,
IHttpClientFactory httpClientFactory,
IConfiguration configuration, IServiceProvider serviceProvider) : base(TimeSpan.FromMinutes(15))
IHttpClientFactory httpClientFactory) : base(TimeSpan.FromMinutes(15))
{
_settingsRepository = settingsRepository;
_dataDirectories = dataDirectories;
@@ -63,8 +61,6 @@ public class WabisabiCoordinatorService : PeriodicRunner
_memoryCache = memoryCache;
_instanceManager = instanceManager;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_serviceProvider = serviceProvider;
IdempotencyRequestCache = new(memoryCache);
}
@@ -93,10 +89,14 @@ public class WabisabiCoordinatorService : PeriodicRunner
break;
}
}
else if (existing.Enabled &&
_instanceManager.HostedServices.TryGetValue("local", out var instance))
{
instance.TermsConditions = wabisabiCoordinatorSettings.TermsConditions;
}
await this.ActionAsync(CancellationToken.None);
await _settingsRepository.UpdateSetting(wabisabiCoordinatorSettings, nameof(WabisabiCoordinatorSettings));
}
public class BtcPayRpcClient : CachedRpcClient
@@ -119,6 +119,37 @@ public class WabisabiCoordinatorService : PeriodicRunner
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)
@@ -148,60 +179,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
public async Task StartCoordinator(CancellationToken cancellationToken)
{
await HostedServices.StartAllAsync(cancellationToken);
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;
});
_instanceManager.AddCoordinator("Local Coordinator", "local", _ => null, cachedSettings.TermsConditions);
}
public async Task StopAsync(CancellationToken cancellationToken)
@@ -211,52 +189,11 @@ public class WabisabiCoordinatorService : PeriodicRunner
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();
if (s.Enabled && !string.IsNullOrEmpty(s.NostrIdentity) && s.NostrRelay is not null)
{
try
{
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;
}
await Nostr.Publish(s.NostrRelay, network, s.NostrIdentity, s.UriToAdvertise, s.CoordinatorDescription,cancel);
}
}
}

View File

@@ -17,10 +17,33 @@ public class WabisabiCoordinatorSettings
[JsonIgnore] public ECPrivKey? Key => string.IsNullOrEmpty(NostrIdentity)? null: NostrExtensions.ParseKey(NostrIdentity);
[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 Uri Uri { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}

View File

@@ -17,11 +17,21 @@ public class WasabiLeechController:Controller
{
_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}")]
public async Task<IActionResult> Forward(string key, CancellationToken cancellationToken)
{
if (!_coordinatorClientInstanceManager.HostedServices.TryGetValue("zksnacks", out var coordinator))
return BadRequest();
@@ -32,5 +42,4 @@ public class WasabiLeechController:Controller
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 WalletWasabi.Backend.Controllers
@using Microsoft.AspNetCore.Mvc.ModelBinding
@using BTCPayServer.Plugins.Wabisabi
@model WalletWasabi.Backend.Controllers.WabisabiCoordinatorSettings
@inject WabisabiCoordinatorService WabisabiCoordinatorService
@@ -13,11 +14,35 @@
<form method="post">
<div class="row">
<div class="col-xxl-constrain col-xl-8">
<div class="form-group form-check">
<label asp-for="Enabled" class="form-check-label">Enable Coordinator</label>
<input asp-for="Enabled" type="checkbox" class="form-check-input" />
</div>
<div class="form-group form-check">
<div class="form-group pt-3">
<label class="form-label" for="config">Config</label>
@if (ViewData.ModelState.TryGetValue("config", out var error) && error.Errors.Any())
{
<span class="text-danger">@string.Join("\n", error.Errors)</span>
}
<textarea rows="10" cols="40" class="form-control" id="config" name="config" >
@Html.Raw(WabisabiCoordinatorService.WabiSabiCoordinator.Config.ToString())
</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 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>
@@ -28,22 +53,23 @@
<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>
<div class="row ">
<div class="col-xxl-constrain">
<div class="form-group pt-3">
<label class="form-label" for="config">Config</label>
@if (ViewData.ModelState.TryGetValue("config", out var error) && error.Errors.Any())
{
<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">
@Html.Raw(WabisabiCoordinatorService.WabiSabiCoordinator.Config.ToString())
</textarea>
</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>

View File

@@ -221,24 +221,43 @@
{
<p>
Coordinator Status: Connected
<a href="@(coordinator.Coordinator)api/v4/Wasabi/legaldocuments"
target="_blank" rel="noreferrer noopener">
T&C
</a>
</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"/>
<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>
}
@@ -246,7 +265,6 @@
{
foreach (var coordinator in discoveredCoordinators)
{
<div class="card mt-3" permission="@Policies.CanModifyServerSettings">
<div class="card-header d-flex justify-content-between">
<div>
@@ -265,7 +283,6 @@
</div>
}
}
<button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>

View File

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

View File

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

View File

@@ -67,7 +67,6 @@ namespace BTCPayServer.Plugins.Wabisabi
return View(Wabisabi);
}
public const int coordinatorEventKind = 15750;
[HttpPost("")]
public async Task<IActionResult> UpdateWabisabiStoreSettings(string storeId, WabisabiStoreSettings vm,
@@ -86,63 +85,13 @@ namespace BTCPayServer.Plugins.Wabisabi
case "discover":
coordSettings = await _wabisabiCoordinatorService.GetSettings();
var relay = commandIndex ??
(await _wabisabiCoordinatorService.GetSettings())?.NostrRelay.ToString();
coordSettings?.NostrRelay.ToString();
if (Uri.TryCreate(relay, UriKind.Absolute, out var relayUri))
{
using var nostrClient = new NostrClient(relayUri);
await nostrClient.CreateSubscription("nostr-wabisabi-coordinators",
new[]
{
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();
ViewBag.DiscoveredCoordinators =await Nostr.Discover(relayUri,
_explorerClientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork,
coordSettings.Key?.CreateXOnlyPubKey().ToHex(), CancellationToken.None);
}
else
{

View File

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

View File

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