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,13 +61,11 @@ public class WabisabiCoordinatorService : PeriodicRunner
_memoryCache = memoryCache;
_instanceManager = instanceManager;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_serviceProvider = serviceProvider;
IdempotencyRequestCache = new(memoryCache);
}
private WabisabiCoordinatorSettings cachedSettings;
private WabisabiCoordinatorSettings cachedSettings;
public async Task<WabisabiCoordinatorSettings> GetSettings()
{
return cachedSettings ??= (await _settingsRepository.GetSettingAsync<WabisabiCoordinatorSettings>(
@@ -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

@@ -9,7 +9,7 @@ namespace WalletWasabi.Backend.Controllers;
[Route("plugins/wabisabi-coordinator")]
[AllowAnonymous]
public class WasabiLeechController:Controller
public class WasabiLeechController : Controller
{
private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager;
@@ -17,20 +17,29 @@ 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))
if (!_coordinatorClientInstanceManager.HostedServices.TryGetValue("zksnacks", out var coordinator))
return BadRequest();
var b = new UriBuilder(coordinator.Coordinator);
b.Path = key;
b.Query = Request.QueryString.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 WalletWasabi.Backend.Controllers
@using Microsoft.AspNetCore.Mvc.ModelBinding
@using BTCPayServer.Plugins.Wabisabi
@model WalletWasabi.Backend.Controllers.WabisabiCoordinatorSettings
@inject WabisabiCoordinatorService WabisabiCoordinatorService
@@ -13,24 +14,12 @@
<form method="post">
<div class="row">
<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">
<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 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>
</div>
<div class="row ">
<div class="col-xxl-constrain">
<div class="form-group pt-3">
<label class="form-label" for="config">Config</label>
@@ -38,17 +27,54 @@
{
<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>
<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>
<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>
</form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
<partial name="_ValidationScriptsPartial" />
}

View File

@@ -67,30 +67,30 @@
<h2 class="mb-4">Coinjoin configuration</h2>
<form method="post">
@{
var wallet = await WalletProvider.GetWalletAsync(storeId);
if (wallet is BTCPayWallet btcPayWallet)
@{
var wallet = await WalletProvider.GetWalletAsync(storeId);
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"/>
<span class="ms-3">This wallet is not enabled in your store settings and will not be able to participate in coinjoins..</span>
<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>
</div>
}
else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning" />
<span class="ms-3">This wallet is not a hot wallet and will not be able to participate in coinjoins.</span>
<button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div>
}
<button name="command" type="submit" value="check" class="btn btn-text">Refresh</button>
</div>
}
}
}
<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>
<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 class="form-group form-check">
<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>
</div>
<div class="form-group form-check">
<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>
</div>
<div class="form-group form-check">
<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>
</div>
<div class="form-group ">
@@ -156,7 +156,7 @@
{
<div class="list-group-item">
<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>
</div>
</div>
@@ -179,7 +179,7 @@
<div class="list-group-item">
<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>
</div>
</div>
@@ -193,92 +193,109 @@
</div>
</div>
@for (var index = 0; index < Model.Settings.Count; index++)
{
<input asp-for="Settings[index].Coordinator" type="hidden"/>
var s = Model.Settings[index];
@for (var index = 0; index < Model.Settings.Count; index++)
{
<input asp-for="Settings[index].Coordinator" type="hidden" />
var s = Model.Settings[index];
if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(s.Coordinator, out var coordinator))
{
continue;
}
<div class="card mt-3">
if (!WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(s.Coordinator, out var coordinator))
{
continue;
}
<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>
<div class="d-flex">
<h3>@coordinator.CoordinatorDisplayName</h3>
<h3>@coordinator.Name</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
<a href="@(coordinator.Coordinator)api/v4/Wasabi/legaldocuments"
target="_blank" rel="noreferrer noopener">
T&C
</a>
</p>
}
</div>
<span class="text-muted">@coordinator.Uri</span>
</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"/>
<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>
@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>
}
@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>
<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>
}
<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>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
<partial name="_ValidationScriptsPartial" />
}
<script type="text/javascript" nonce="@nonce">

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;
@@ -67,11 +70,16 @@ 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,19 +115,21 @@ 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; }
public CoinJoinManager CoinJoinManager { get; set; }
public WabisabiCoordinatorClientInstance(string coordinatorDisplayName,
string coordinatorName,
Uri coordinator,
ILoggerFactory loggerFactory,
IServiceProvider serviceProvider,
string coordinatorName,
Uri coordinator,
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);
WasabiHttpClientFactory = new HttpClientFactory(torEndpoint, () => Coordinator);
var roundStateUpdaterCircuit = new PersonCircuit();
var roundStateUpdaterHttpClient =
WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit);
var sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient);
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);
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;