This commit is contained in:
Kukks
2023-11-29 12:13:14 +01:00
parent 32c5ee14f5
commit 361503e6c8
23 changed files with 573 additions and 957 deletions

View File

@@ -53,8 +53,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.FileSe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DynamicReports", "Plugins\BTCPayServer.Plugins.DynamicReports\BTCPayServer.Plugins.DynamicReports.csproj", "{BCB4E68D-089F-481E-A3AE-FC9CED6AA34D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Vouchers", "Plugins\BTCPayServer.Plugins.Vouchers\BTCPayServer.Plugins.Vouchers.csproj", "{ED061A37-488F-429C-A291-6A5188A47443}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -255,14 +253,6 @@ Global
{BCB4E68D-089F-481E-A3AE-FC9CED6AA34D}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
{BCB4E68D-089F-481E-A3AE-FC9CED6AA34D}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
{BCB4E68D-089F-481E-A3AE-FC9CED6AA34D}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Release|Any CPU.Build.0 = Release|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
{ED061A37-488F-429C-A291-6A5188A47443}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}

View File

@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- -->
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>Vouchers</Product>
<Description>Allows you to give users Bitcoin vouchers.</Description>
<Version>1.0.0</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Views\Voucher\Edit.cshtml" />
<AdditionalFiles Include="Views\Shared\Nip05Nav.cshtml" />
</ItemGroup>
</Project>

View File

@@ -1,22 +0,0 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.Vouchers
{
public class VoucherPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.11.0"}
};
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("VoucherNav",
"store-integrations-nav"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -1,34 +0,0 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-controller="Voucher" asp-action="ListVouchers" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Voucher")"
permission="@Policies.CanModifyStoreSettings">
<svg xmlns="http://www.w3.org/2000/svg" style=" width: 20px;
margin-right: 6px;" viewBox="0 0 1536.000000 1536.000000" preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1536.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none">
<path d="M1434 12669 c-3 -6 -6 -49 -5 -95 2 -130 -17 -333 -40 -414 -75 -273 -301 -506 -574 -593 -85 -27 -163 -37 -349 -42 -128 -4 -182 -9 -188 -18 -4 -6 -8 -1315 -8 -2908 0 -2383 2 -2898 13 -2907 8 -7 102 -15 228 -21 233 -11 290 -22 409 -80 80 -39 93 -47 150 -90 154 -117 270 -284 320 -462 19 -67 33 -225 43 -454 l2 -60 5320 0 5320 0 6 195 c3 107 12 220 18 250 61 274 239 501 484 618 147 70 311 98 505 86 100 -6 115 -5 132 11 20 18 20 40 20 2915 0 2783 -1 2898 -18 2913 -15 14 -32 16 -98 11 -105 -8 -342 11 -429 34 -39 10 -100 34 -135 52 -36 18 -77 39 -91 47 -51 26 -187 152 -236 220 -74 99 -124 204 -158 333 -3 14 -10 120 -15 237 -5 117 -11 217 -15 223 -9 15 -10601 14 -10611 -1z m10099 -599 c40 -163 117 -339 211 -480 88 -131 295 -327 441 -417 101 -63 373 -173 425 -173 9 0 21 -5 28 -12 17 -17 17 -4759 0 -4776 -7 -7 -21 -12 -31 -12 -38 0 -230 -72 -342 -127 -194 -97 -406 -290 -541 -493 -73 -110 -172 -341 -189 -442 -4 -28 -12 -39 -28 -43 -12 -3 -2166 -4 -4787 -3 l-4764 3 -9 35 c-103 421 -388 777 -772 961 -103 49 -269 109 -303 109 -11 0 -25 6 -31 14 -8 10 -10 642 -9 2391 3 2102 5 2379 18 2385 8 5 44 16 80 26 327 89 615 296 814 586 74 109 196 383 196 442 0 8 4 26 10 40 l10 26 4782 -2 4782 -3 9 -35z"/>
<path d="M8080 11514 c-84 -7 -265 -36 -309 -50 -25 -8 -54 -14 -65 -14 -12 0 -41 -6 -66 -14 -25 -8 -72 -22 -105 -32 -252 -73 -428 -148 -680 -289 -112 -63 -269 -174 -390 -278 -535 -457 -865 -1031 -998 -1732 -64 -336 -64 -720 -1 -1060 43 -230 104 -439 179 -615 35 -84 128 -270 162 -328 60 -101 83 -137 147 -227 234 -333 555 -630 901 -833 49 -29 97 -57 105 -62 28 -17 202 -100 254 -121 154 -63 196 -79 246 -94 30 -10 64 -21 76 -25 11 -5 58 -17 105 -28 291 -71 401 -84 699 -85 205 -1 270 3 400 21 269 40 483 99 765 214 65 26 272 128 355 174 327 181 673 506 899 843 336 502 486 975 508 1606 11 325 -58 774 -164 1060 -9 22 -21 56 -28 75 -170 460 -476 899 -839 1203 -168 141 -275 217 -469 331 -211 124 -560 255 -827 310 -197 41 -276 49 -545 51 -148 2 -290 1 -315 -1z m520 -569 c389 -42 726 -162 1038 -369 303 -201 580 -491 738 -772 15 -27 38 -67 51 -89 12 -22 27 -50 33 -63 5 -13 21 -46 34 -75 83 -177 153 -419 192 -667 13 -85 15 -154 12 -360 -3 -140 -11 -282 -17 -315 -7 -33 -20 -96 -28 -140 -73 -378 -238 -701 -525 -1031 -229 -265 -429 -424 -703 -561 -165 -83 -193 -95 -323 -139 -516 -173 -1108 -169 -1587 11 -38 15 -78 29 -88 32 -9 3 -70 31 -135 63 -399 195 -732 493 -977 875 -59 91 -171 318 -218 440 -21 55 -71 225 -91 310 -65 281 -65 709 0 1010 26 117 81 293 120 380 92 206 146 306 239 445 265 392 664 708 1119 885 146 57 355 112 491 129 141 18 463 19 625 1z"/>
<path d="M7455 10436 c-12 -19 -16 -48 -14 -129 1 -58 -2 -110 -5 -116 -5 -7 -56 -11 -145 -11 -93 0 -141 -4 -149 -12 -9 -9 -12 -364 -12 -1534 0 -837 3 -1529 6 -1538 5 -14 27 -16 139 -16 73 0 140 -3 149 -6 13 -5 16 -26 16 -123 0 -78 4 -123 13 -134 11 -16 37 -17 277 -15 l265 3 3 124 c1 71 7 130 14 138 11 13 141 19 172 7 13 -5 16 -26 16 -128 0 -82 4 -126 12 -134 9 -9 84 -12 275 -12 249 0 263 1 273 19 5 11 10 72 10 135 0 133 -16 117 130 136 310 43 561 216 714 494 32 57 50 105 77 201 25 90 36 317 20 433 -20 154 -100 308 -220 427 -34 33 -61 65 -61 73 0 7 23 51 50 98 28 47 50 87 50 90 0 3 6 20 13 37 39 91 67 232 67 337 0 62 -23 207 -41 260 -68 199 -196 367 -364 475 -107 69 -271 135 -368 149 -64 9 -67 15 -67 140 0 168 22 156 -283 156 -141 0 -259 -4 -267 -10 -12 -7 -16 -38 -20 -137 l-5 -128 -92 -3 c-110 -3 -102 -15 -106 158 l-2 115 -262 3 -262 2 -16 -24z m1401 -877 c75 -28 154 -102 184 -172 14 -31 25 -81 28 -122 8 -123 -36 -222 -127 -289 -89 -65 -97 -66 -677 -66 -335 0 -525 4 -529 10 -9 14 -7 642 2 651 14 13 69 14 569 12 478 -2 492 -3 550 -24z m29 -1194 c95 -11 210 -98 260 -195 13 -25 29 -74 35 -108 31 -157 -65 -339 -218 -413 -36 -18 -72 -25 -153 -29 -154 -10 -1060 -3 -1071 8 -6 6 -12 526 -8 720 0 7 9 16 19 20 24 8 1065 6 1136 -3z"/>
<path d="M3455 11427 c-3 -7 -4 -94 -3 -192 l3 -180 275 0 275 0 0 190 0 190 -273 3 c-216 2 -274 0 -277 -11z"/>
<path d="M3455 10357 c-3 -7 -4 -94 -3 -192 l3 -180 275 0 275 0 0 190 0 190 -273 3 c-216 2 -274 0 -277 -11z"/>
<path d="M3462 9322 c-9 -7 -12 -52 -10 -193 l3 -184 275 0 275 0 3 184 c2 141 -1 186 -10 193 -7 4 -128 8 -268 8 -140 0 -261 -4 -268 -8z"/>
<path d="M3455 8247 c-3 -7 -4 -94 -3 -192 l3 -180 275 0 275 0 0 190 0 190 -273 3 c-216 2 -274 0 -277 -11z"/>
<path d="M3455 7207 c-3 -7 -4 -94 -3 -192 l3 -180 275 0 275 0 0 190 0 190 -273 3 c-216 2 -274 0 -277 -11z"/>
<path d="M3463 6173 c-10 -3 -13 -52 -13 -193 0 -186 0 -188 23 -194 37 -10 484 -7 511 4 l26 10 0 184 c0 158 -2 185 -16 190 -19 7 -513 7 -531 -1z"/>
</g>
</svg>
<span>Vouchers</span>
</a>
</li>
}

View File

@@ -1,40 +0,0 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer
@using BTCPayServer.Data
@model List<BTCPayServer.Plugins.Vouchers.VoucherController.VoucherViewModel>
@{
ViewData.SetActivePage("Voucher", "Create Voucher", "Voucher");
}
<form method="post" asp-action="CreateVoucher">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<button name="command" type="submit" value="save" class="btn btn-primary">Create</button>
</div>
</div>
<partial name="_StatusMessage"/>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div class="d-flex justify-content-between">
<div class="form-group flex-fill me-4">
<label for="amount" class="form-label">Amount</label>
<input inputmode="decimal" min="0" name="amount" id="amount" class="form-control"/>
</div>
<div class="form-group">
<label for="currency" class="form-label">Currency</label>
<input name="currency" id="currency" class="form-control w-auto" value="@Context.GetStoreData().GetStoreBlob().DefaultCurrency" currency-selection/>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,140 +0,0 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Models
@using BTCPayServer.Client
@using BTCPayServer.Services
@inject IScopeProvider ScopeProvider
@inject DisplayFormatter DisplayFormatter
@model List<BTCPayServer.Plugins.Vouchers.VoucherController.VoucherViewModel>
@{
ViewData.SetActivePage("Voucher", "Voucher", "Voucher");
var storeId = ScopeProvider.GetCurrentStoreId();
}
<form method="post">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<a asp-controller="Voucher" asp-action="CreateVoucher" asp-route-storeId="@storeId" class="btn btn-primary">Create</a>
</div>
</div>
<partial name="_StatusMessage"/>
@if (Model.Any())
{
@foreach (var pp in Model)
{
<script id="tooptip_template_@pp.Id" type="text/template">
<span>Awaiting:&nbsp;<span class="float-end">@pp.Progress.AwaitingFormatted</span></span>
<br />
<span>Completed:&nbsp;<span class="float-end">@pp.Progress.CompletedFormatted</span></span>
<br />
<span>Limit:&nbsp;<span class="float-end">@pp.Progress.LimitFormatted</span></span>
@if (pp.Progress.ResetIn != null)
{
<br />
<span>Resets in:&nbsp;<span class="float-end">@pp.Progress.ResetIn</span></span>
}
@if (pp.Progress.EndIn != null)
{
<br />
<span>Expires in:&nbsp;<span class="float-end">@pp.Progress.EndIn</span></span>
}
</script>
}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Amount</th>
<th scope="col">Progress</th>
<th scope="col" class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var pp in Model)
{
<tr>
<td>
<a asp-action="EditPullPayment"
asp-controller="UIPullPayment"
asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id">
@pp.Name
</a>
</td>
<td>@DisplayFormatter.Currency(pp.Amount, pp.Currency)</td>
<td>@string.Join(", ", pp.PaymentMethods.Select(id => id.ToPrettyString()))</td>
<td class="align-middle">
<div class="progress ppProgress" data-pp="@pp.Id" data-bs-toggle="tooltip" data-bs-html="true">
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.CompletedPercent"
aria-valuemin="0" aria-valuemax="100" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.CompletedPercent)%;">
</div>
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.AwaitingPercent"
aria-valuemin="0" aria-valuemax="100" style="background-color:orange; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.AwaitingPercent)%;">
</div>
</div>
</td>
<td class="text-end">
<a asp-action="ArchivePullPayment"
asp-controller="UIStorePullPayments"
permission="@Policies.CanArchivePullPayments"
asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-description="Do you really want to archive the pull payment <strong>@Html.Encode(pp.Name)</strong>?">
Archive
</a>
}
<span> - </span>
<a asp-action="ViewPullPayment"
asp-controller="UIPullPayment"
asp-route-pullPaymentId="@pp.Id">
Full view
</a>
<span> - </span>
<a asp-action="View"
asp-controller="Voucher"
asp-route-id="@pp.Id">
Print view
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"))"/>
@section PageFootContent {
<script>
const ppProgresses = document.getElementsByClassName("ppProgress");
for (var i = 0; i < ppProgresses.length; i++) {
var pp = ppProgresses[i];
var ppId = pp.getAttribute("data-pp");
var template = document.getElementById("tooptip_template_" + ppId);
pp.setAttribute("title", template.innerHTML);
}
</script>
}
}
else
{
<p class="text-secondary mt-4">
There are no active vouchers.
</p>
}
</form>

View File

@@ -1,87 +0,0 @@
@model BTCPayServer.Plugins.Vouchers.VoucherController.VoucherViewModel
@using BTCPayServer.Components.QRCode
@using BTCPayServer.Services
@inject BTCPayServerEnvironment Env
@inject DisplayFormatter DisplayFormatter
@{
Layout = null;
ViewData["Title"] = Model.Name;
string lnurl = null;
if (Model.SupportsLNURL)
{
lnurl = LNURL.LNURL.EncodeBech32(new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new {cryptoCode = "BTC", pullPaymentId = Model.Id}, Context.Request.Scheme, Context.Request.Host.ToString())));
}
var fullView = Url.Action("ViewPullPayment", "UIPullPayment", new {pullPaymentId = Model.Id}, Context.Request.Scheme, Context.Request.Host.ToString());
}
<!DOCTYPE html>
<html lang="en" @(Env.IsDeveloping ? " data-devenv" : "")>
<head>
<partial name="LayoutHead"/>
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, "", "")"/>
<meta name="robots" content="noindex,nofollow">
<style>
#InvoiceSummary { gap: var(--btcpay-space-l); }
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
#posData td > table:last-child { margin-bottom: 0 !important; }
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
</style>
</head>
<body class="min-vh-100">
<div class="public-page-wrap">
<main class="flex-grow-1">
<div class="container" style="max-width:720px;">
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) {{"Margin", "mb-4"}})"/>
<div class="d-flex flex-column justify-content-center gap-4">
<partial name="_StoreHeader" model="(Model.StoreName, Model.LogoFileId)"/>
<h2 class="w-100 text-center">Voucher</h2>
<div id="InvoiceSummary" class="bg-tile p-3 p-sm-4 rounded d-flex flex-wrap align-items-center justify-content-center">
@if (lnurl != null)
{
<vc:qr-code data="@lnurl"></vc:qr-code>
<p>Scan with a Bitcoin Lightning wallet to redeem your voucher </p>
}
<a href="@fullView">
<vc:qr-code data="@fullView"></vc:qr-code>
</a>
<p>Scan or open this link in your browser for the full option list for redemption</p>
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
<div class="d-flex flex-column">
<div class="d-flex align-items-center justify-content-between">
<dd class="text-muted mb-0 fw-semibold">Amount</dd>
</div>
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</dt>
</div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="d-flex flex-column">
@Model.Description
</div>
}
</dl>
</div>
</div>
</div>
</main>
<footer class="store-footer">
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
Powered by <partial name="_StoreFooterLogo"/>
</a>
</footer>
</div>
<partial name="LayoutFoot"/>
</body>
<script>
window.print();
</script>
</html>

View File

@@ -1,218 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Plugins.Vouchers;
public class VoucherController : Controller
{
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly UIStorePullPaymentsController _uiStorePullPaymentsController;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly StoreRepository _storeRepository;
private readonly RateFetcher _rateFetcher;
private readonly BTCPayNetworkProvider _networkProvider;
public VoucherController(PullPaymentHostedService pullPaymentHostedService,
UIStorePullPaymentsController uiStorePullPaymentsController,
ApplicationDbContextFactory dbContextFactory,
IEnumerable< IPayoutHandler> payoutHandlers, StoreRepository storeRepository, RateFetcher rateFetcher, BTCPayNetworkProvider networkProvider )
{
_pullPaymentHostedService = pullPaymentHostedService;
_uiStorePullPaymentsController = uiStorePullPaymentsController;
_dbContextFactory = dbContextFactory;
_payoutHandlers = payoutHandlers;
_storeRepository = storeRepository;
_rateFetcher = rateFetcher;
_networkProvider = networkProvider;
}
[HttpGet("~/plugins/{storeId}/vouchers")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> ListVouchers(string storeId)
{
var now = DateTimeOffset.UtcNow;
await using var ctx = _dbContextFactory.CreateContext();
var ppsQuery = await ctx.PullPayments
.Include(data => data.Payouts)
.Where(p => p.StoreId == storeId && p.Archived == false)
.OrderByDescending(data => data.Id).ToListAsync();
var vouchers = ppsQuery.Select(pp => (PullPayment: pp, Blob: pp.GetBlob())).Where(blob => blob.Blob.Name.StartsWith("Voucher")).ToList();
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a voucher.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new {storeId});
}
return View( vouchers.Select(tuple => new VoucherViewModel()
{
Amount = tuple.Blob.Limit,
Currency = tuple.Blob.Currency,
Id = tuple.PullPayment.Id,
Name = tuple.Blob.Name,
PaymentMethods = tuple.Blob.SupportedPaymentMethods,
Progress = _pullPaymentHostedService.CalculatePullPaymentProgress(tuple.PullPayment, now)
}).ToList());
}
[HttpGet("~/plugins/{storeId}/vouchers/create")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreateVoucher(string storeId)
{
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a voucher.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new {storeId});
}
return View();
}
[HttpPost("~/plugins/{storeId}/vouchers/create")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreateVoucher(string storeId, decimal amount, string currency)
{
ModelState.Clear();
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "You must enable at least one payment method before creating a voucher.";
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new {storeId});
}
if (amount <= 0)
{
TempData[WellKnownTempData.ErrorMessage] = "Amount must be greater than 0";
return View();
}
var storeBLob = HttpContext.GetStoreData().GetStoreBlob();
currency??=storeBLob.DefaultCurrency;
var rate = await _rateFetcher.FetchRate(new CurrencyPair("BTC", currency),
storeBLob.GetRateRules(_networkProvider), CancellationToken.None);
if (rate.BidAsk == null)
{
TempData[WellKnownTempData.ErrorMessage] = "Currency is not supported";
return View();
}
string description = string.Empty;
if (currency!= "BTC")
{
description = $"{amount} {currency} voucher redeemable for {amount * rate.BidAsk.Bid} BTC";
}
var pp = await _pullPaymentHostedService.CreatePullPayment(new CreatePullPayment()
{
Amount = amount* rate.BidAsk.Bid,
Currency = "BTC",
Name = "Voucher " + Encoders.Base58.EncodeData(RandomUtils.GetBytes(6)),
Description = description,
StoreId = storeId,
PaymentMethodIds = paymentMethods.ToArray(),
AutoApproveClaims = true
});
return RedirectToAction(nameof(View), new {id = pp});
}
[HttpGet("~/plugins/vouchers/{id}")]
[AllowAnonymous]
public async Task<IActionResult> View(string id)
{
await using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments
.Include(data => data.Payouts)
.SingleOrDefaultAsync(p => p.Id == id && p.Archived == false);
if (pp == null)
{
return NotFound();
}
var blob = pp.GetBlob();
if (!blob.Name.StartsWith("Voucher"))
{
return NotFound();
}
var now = DateTimeOffset.UtcNow;
var store = await _storeRepository.FindStore(pp.StoreId);
var storeBlob = store.GetStoreBlob();
var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, now);
return View(new VoucherViewModel()
{
Amount = blob.Limit,
Currency = blob.Currency,
Id = pp.Id,
Name = blob.Name,
PaymentMethods = blob.SupportedPaymentMethods,
Progress = progress,
StoreName = store.StoreName,
BrandColor = storeBlob.BrandColor,
CssFileId = storeBlob.CssFileId,
LogoFileId = storeBlob.LogoFileId,
SupportsLNURL = _pullPaymentHostedService.SupportsLNURL(blob),
Description = blob.Description
});
}
public class VoucherViewModel
{
public string Id { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public PaymentMethodId[] PaymentMethods { get; set; }
public PullPaymentsModel.PullPaymentModel.ProgressModel Progress { get; set; }
public string StoreName { get; set; }
public string LogoFileId { get; set; }
public string BrandColor { get; set; }
public string CssFileId { get; set; }
public bool SupportsLNURL { get; set; }
public string Description { get; set; }
}
}

View File

@@ -1,5 +0,0 @@
@using BTCPayServer.Abstractions.Services
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer
@addTagHelper *, BTCPayServer.Abstractions

View File

@@ -89,11 +89,22 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
maxPerType.TryAdd(AnonsetType.Red, 1);
}
var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments,
var isLowFee = utxoSelectionParameters.MiningFeeRate.SatoshiPerByte <= _wallet.LowFeeTarget;
var consolidationMode = _wallet.ConsolidationMode switch
{
ConsolidationModeType.Always => true,
ConsolidationModeType.Never => false,
ConsolidationModeType.WhenLowFee => isLowFee,
ConsolidationModeType.WhenLowFeeAndManyUTXO => isLowFee && coinCandidates.Count() > BTCPayWallet.HighAmountOfCoins,
_ => throw new ArgumentOutOfRangeException()
};
var solution = await SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments,
Random.Shared.Next(10, 31),
maxPerType,
new Dictionary<AnonsetType, int>() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}},
_wallet.ConsolidationMode, liquidityClue, secureRandom);
consolidationMode, liquidityClue, secureRandom);
if (attemptingTobeParanoid && !solution.HandledPayments.Any())
{
@@ -112,7 +123,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
return solution.Coins.ToImmutableList();
}
private SubsetSolution SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters,
private async Task<SubsetSolution> SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters,
IEnumerable<SmartCoin> coins, IEnumerable<PendingPayment> pendingPayments,
int maxCoins,
Dictionary<AnonsetType, int> maxPerType, Dictionary<AnonsetType, int> idealMinimumPerType,
@@ -128,11 +139,11 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
var remainingPendingPayments = new List<PendingPayment>(pendingPayments);
var solution = new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget,
utxoSelectionParameters);
var fullyPrivate = remainingCoins.All(coin => coin.CoinColor(_wallet.AnonScoreTarget) == AnonsetType.Green);
var fullyPrivate = await _wallet.IsWalletPrivateAsync(new CoinsView(remainingCoins));
var coinjoiningOnlyForPayments = fullyPrivate && remainingPendingPayments.Any();
var isMixingToOther = !_wallet.WabisabiStoreSettings.PlebMode &&
!string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet);
if (fullyPrivate && !coinjoiningOnlyForPayments && !isMixingToOther)
if (fullyPrivate && !coinjoiningOnlyForPayments )
{
var rand = Random.Shared.Next(1, 1001);
if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability)

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
@@ -31,9 +28,11 @@ using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Blockchain.Transactions;
using WalletWasabi.Extensions;
using WalletWasabi.Models;
using WalletWasabi.WabiSabi;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Client;
using WalletWasabi.Wallets;
using LogLevel = WalletWasabi.Logging.LogLevel;
namespace BTCPayServer.Plugins.Wabisabi;
@@ -91,13 +90,34 @@ public class BTCPayWallet : IWallet, IDestinationProvider
}
public string StoreId { get; set; }
public List<(Microsoft.Extensions.Logging.LogLevel, string)> LastLogs { get; private set; } = new();
public void Log(LogLevel logLevel, string logMessage, string callerFilePath = "", string callerMemberName = "",
int callerLineNumber = -1)
{
var ll = logLevel switch
{
LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
LogLevel.Info => Microsoft.Extensions.Logging.LogLevel.Information,
LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
_ => throw new ArgumentOutOfRangeException(nameof(logLevel))
};
if(LastLogs.FirstOrDefault().Item2 != logMessage)
LastLogs.Insert(0, (ll, logMessage) );
if (LastLogs.Count >= 100)
LastLogs.RemoveLast();
Logger.Log(ll, logMessage, callerFilePath, callerMemberName, callerLineNumber);
}
public string WalletName => StoreId;
public bool IsUnderPlebStop => !WabisabiStoreSettings.Active;
bool IWallet.IsMixable(string coordinator)
{
return KeyChain is BTCPayKeyChain {KeysAvailable: true} && WabisabiStoreSettings.Settings.SingleOrDefault(
return WabisabiStoreSettings.Active && KeyChain is BTCPayKeyChain {KeysAvailable: true} && WabisabiStoreSettings.Settings.SingleOrDefault(
settings =>
settings.Coordinator.Equals(coordinator))?.Enabled is true;
}
@@ -106,18 +126,32 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public IDestinationProvider DestinationProvider => this;
public int AnonScoreTarget => WabisabiStoreSettings.PlebMode? 5: WabisabiStoreSettings.AnonymitySetTarget;
public bool ConsolidationMode => !WabisabiStoreSettings.PlebMode && WabisabiStoreSettings.ConsolidationMode;
public ConsolidationModeType ConsolidationMode =>
WabisabiStoreSettings.PlebMode? ConsolidationModeType.WhenLowFeeAndManyUTXO: WabisabiStoreSettings.ConsolidationMode;
public TimeSpan FeeRateMedianTimeFrame => TimeSpan.FromHours(WabisabiStoreSettings.PlebMode?
KeyManager.DefaultFeeRateMedianTimeFrameHours: WabisabiStoreSettings.FeeRateMedianTimeFrameHours);
public const int DefaultExplicitHighestFeeTarget = 75;
public const int DefaultLowFeeTarget = 10;
public const int HighAmountOfCoins = 30;
public int ExplicitHighestFeeTarget => WabisabiStoreSettings.PlebMode? DefaultExplicitHighestFeeTarget: WabisabiStoreSettings.ExplicitHighestFeeTarget;
public int LowFeeTarget => WabisabiStoreSettings.PlebMode? DefaultLowFeeTarget: WabisabiStoreSettings.LowFeeTarget;
public bool RedCoinIsolation => !WabisabiStoreSettings.PlebMode &&WabisabiStoreSettings.RedCoinIsolation;
public bool BatchPayments => WabisabiStoreSettings.PlebMode || WabisabiStoreSettings.BatchPayments;
public long? MinimumDenominationAmount => WabisabiStoreSettings.PlebMode? 10000 : WabisabiStoreSettings.MinimumDenominationAmount;
public async Task<bool> IsWalletPrivateAsync()
{
return !BatchPayments && await GetPrivacyPercentageAsync() >= 1 && (WabisabiStoreSettings.PlebMode ||
string.IsNullOrEmpty(WabisabiStoreSettings
.MixToOtherWallet));
return await IsWalletPrivateAsync(await GetAllCoins());
}
public async Task<bool> IsWalletPrivateAsync(CoinsView coins)
{
var privacy= GetPrivacyPercentage(coins, AnonScoreTarget);
var mixToOtherWallet = !WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings
.MixToOtherWallet);
var forceConsolidate = ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && coins.Available().Confirmed().Count() > HighAmountOfCoins;
return !BatchPayments && privacy >= 1 && !mixToOtherWallet && !forceConsolidate;
}
public async Task<double> GetPrivacyPercentageAsync()
@@ -270,7 +304,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
}
catch (Exception e)
{
Logger.LogError(e, "Could not compute coin candidate");
this.LogError($"Could not compute coin candidate: {e.Message}");
return Array.Empty<SmartCoin>();
}
}
@@ -356,10 +390,8 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public async Task<IEnumerable<SmartTransaction>> GetTransactionsAsync()
{
return Array.Empty<SmartTransaction>();
}
public class CoinjoinData
{
public class CoinjoinDataCoin

View File

@@ -1,9 +1,10 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Configuration;
using BTCPayServer.Plugins.Wabisabi.Coordinator;
using BTCPayServer.Services;
using Microsoft.Extensions.DependencyInjection;
using WalletWasabi.Affiliation;
using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Models.Serialization;
namespace WalletWasabi.Backend.Controllers;
@@ -14,6 +15,7 @@ public static class CoordinatorExtensions
{
services.AddSingleton<WabisabiCoordinatorService>();
services.AddSingleton<WabiSabiConfig.CoordinatorScriptResolver, WabisabiScriptResolver>();
services.AddTransient(provider =>
{
var s = provider.GetRequiredService<WabisabiCoordinatorService>();

View File

@@ -27,6 +27,7 @@ using WalletWasabi.BitcoinCore.Rpc;
using WalletWasabi.Cache;
using WalletWasabi.Services;
using WalletWasabi.WabiSabi;
using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
using WalletWasabi.WabiSabi.Backend.Statistics;
@@ -53,7 +54,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
WabisabiCoordinatorClientInstanceManager instanceManager,
IHttpClientFactory httpClientFactory,
IServiceProvider serviceProvider,
ILogger<WabisabiCoordinatorService> logger ) : base(TimeSpan.FromMinutes(15))
ILogger<WabisabiCoordinatorService> logger, WabiSabiConfig.CoordinatorScriptResolver coordinatorScriptResolver) : base(TimeSpan.FromMinutes(15))
{
_settingsRepository = settingsRepository;
_dataDirectories = dataDirectories;
@@ -62,6 +63,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
_instanceManager = instanceManager;
_httpClientFactory = httpClientFactory;
_logger = logger;
_coordinatorScriptResolver = coordinatorScriptResolver;
_socks5HttpClientHandler = serviceProvider.GetRequiredService<Socks5HttpClientHandler>();
IdempotencyRequestCache = new(memoryCache);
}
@@ -69,6 +71,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
private WabisabiCoordinatorSettings cachedSettings;
private readonly Socks5HttpClientHandler _socks5HttpClientHandler;
private readonly WabiSabiConfig.CoordinatorScriptResolver _coordinatorScriptResolver;
public async Task<WabisabiCoordinatorSettings> GetSettings()
{
@@ -211,7 +214,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
var rpc = new BtcPayRpcClient(explorerClient.RPCClient, _memoryCache, explorerClient);
WabiSabiCoordinator = new WabiSabiCoordinator(coordinatorParameters, rpc, coinJoinIdStore, coinJoinScriptStore,
_httpClientFactory);
_httpClientFactory, _coordinatorScriptResolver);
HostedServices.Register<WabiSabiCoordinator>(() => WabiSabiCoordinator, "WabiSabi Coordinator");
var settings = await GetSettings();

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using NBitcoin;
using Newtonsoft.Json.Linq;
using WalletWasabi.WabiSabi.Backend;
namespace BTCPayServer.Plugins.Wabisabi.Coordinator;
public class WabisabiScriptResolver: WabiSabiConfig.CoordinatorScriptResolver
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly BTCPayWalletProvider _walletProvider;
public WabisabiScriptResolver(IHttpClientFactory httpClientFactory,
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider)
{
_httpClientFactory = httpClientFactory;
_storeRepository = storeRepository;
_networkProvider = networkProvider;
_walletProvider = walletProvider;
}
private static async Task<string> GetRedirectedUrl(HttpClient client, string url,
CancellationToken cancellationToken)
{
var redirectedUrl = url;
using var response = await client.PostAsync(url, new FormUrlEncodedContent(Array.Empty<KeyValuePair<string, string>>()), cancellationToken).ConfigureAwait(false);
using var content = response.Content;
// ... Read the response to see if we have the redirected url
if (response.StatusCode == System.Net.HttpStatusCode.Found)
{
var headers = response.Headers;
if (headers.Location != null)
{
redirectedUrl = new Uri(new Uri(url), headers.Location.ToString()).ToString();
}
}
return redirectedUrl;
}
public override async Task<Script> ResolveScript(string type, string value, Network network, CancellationToken cancellationToken)
{
using var httpClient = _httpClientFactory.CreateClient("wabisabi-coordinator-scripts-no-redirect.onion");
string? invoiceUrl = null;
switch (type)
{
case "store":
var store = await _storeRepository.FindStore(value);
var cryptoCode = _networkProvider.GetAll().OfType<BTCPayNetwork>()
.First(payNetwork => payNetwork.NBitcoinNetwork == network);
var dss = store.GetDerivationSchemeSettings(_networkProvider, cryptoCode.CryptoCode);
var w = _walletProvider.GetWallet(cryptoCode.CryptoCode);
var kpi = await w.ReserveAddressAsync(store.Id, dss.AccountDerivation, "wabisabi coordinator");
return kpi.ScriptPubKey;
case "hrf":
return await ResolveScript("btcpaybutton", "https://btcpay.hrf.org/api/v1/invoices?storeId=BgQWsm5WmU9qDPbZVgxVYZu3hWJsbnAtJ3f7wc56b1fC&currency=BTC&jsonResponse=true", network, cancellationToken).ConfigureAwait(false);
case "btcpaybutton":
var buttonResult = await httpClient.GetAsync(value, cancellationToken).ConfigureAwait(false);
var c = await buttonResult.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
invoiceUrl = JObject.Parse(c).Value<string>("invoiceUrl");
break;
case "dev":
return await ResolveScript("btcpaypos", "https://btcpay.kukks.org/apps/4NmbS9jCAEHyPqtaynSXeqNm1hgC/pos", network, cancellationToken).ConfigureAwait(false);
case "btcpaypos":
invoiceUrl = await GetRedirectedUrl(httpClient, value, cancellationToken).ConfigureAwait(false);
break;
case "opensats":
{
if (string.IsNullOrEmpty(value))
{
value = "btcpayserver";
}
var content = new StringContent(JObject.FromObject(new
{
project_name = value,
project_slug = value,
name = "kukks <3 you"
}).ToString(), Encoding.UTF8, "application/json");
var result = await httpClient.PostAsync("https://opensats.org/api/btcpay",content, cancellationToken).ConfigureAwait(false);
var rawInvoice = JObject.Parse(await result.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
invoiceUrl = rawInvoice.Value<string>("checkoutLink");
break;
}
}
invoiceUrl = invoiceUrl.TrimEnd('/');
invoiceUrl += "/BTC/status";
var invoiceBtcpayModel = JObject.Parse(await httpClient.GetStringAsync(invoiceUrl, cancellationToken).ConfigureAwait(false));
var btcAddress = invoiceBtcpayModel.Value<string>("btcAddress");
foreach (var n in Network.GetNetworks())
{
try
{
return BitcoinAddress.Create(btcAddress, n).ScriptPubKey;
}
catch (Exception e)
{
}
}
return null;
}
}

View File

@@ -1,9 +1,8 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Common
@using BTCPayServer.Plugins.Wabisabi
@using Microsoft.Extensions.Logging
@using NBitcoin
@using Org.BouncyCastle.Asn1.Ocsp
@using WalletWasabi.Extensions
@using WalletWasabi.WabiSabi.Backend.Rounds
@using WalletWasabi.WabiSabi.Client
@using WalletWasabi.WabiSabi.Models
@@ -23,7 +22,7 @@
return;
}
var storeId = ScopeProvider.GetCurrentStoreId();
Context.Items["cjlite"] = true;
}
@if (available)
{
@@ -35,18 +34,7 @@
@if (!enabledSettings.Any())
{
<div class="widget">
<header>
<h3>Coinjoin</h3>
<a asp-controller="WabisabiStore" asp-action="UpdateWabisabiStoreSettings" asp-route-storeId="@storeId" class="fw-semibold">
Manage
</a>
</header>
<p class="text-secondary my-3">
There are no configured coinjoin coordinators
</p>
<a asp-controller="WabisabiStore" asp-action="UpdateWabisabiStoreSettings" asp-route-storeId="@storeId" class="fw-semibold">
Configure coinjoin settings
</a>
<partial name="../WabisabiStore/UpdateWabisabiStoreSettings" model="@settings"/>
</div>
}
else
@@ -304,82 +292,7 @@ updateInProgressAnimation(myChart);
<div class="d-flex justify-content-center mb-4" style="max-height: 400px; "> <canvas id="cjchart"></canvas></div>
}
@* <div> *@
@* <h6 class="mb-2">Privacy progress</h6> *@
@* <div class="progress mb-2 position-relative" style="height: 2rem;"> *@
@* <div class="w-100 text-center position-absolute bg-transparent progress-bar h-100"> @privacyPercentage%</div> *@
@* <div class="progress-bar bg-success" role="progressbar" style="width: @privacyPercentage%"></div> *@
@* </div> *@
@* </div> *@
@* <div> *@
@* <h6 class="mb-2">Coins per privacy</h6> *@
@* <div class="progress mb-2" style="height: 2rem;"> *@
@* @foreach (var cc in colorCoins) *@
@* { *@
@* var cssClass = cc.Key == AnonsetType.Green ? "bg-success" : cc.Key == AnonsetType.Orange ? "bg-warning" : *@
@* "bg-danger"; *@
@* var text = cc.Key == AnonsetType.Green ? "private" : cc.Key == AnonsetType.Orange ? "semi-private" : *@
@* "non-private"; *@
@* *@
@* var tooltiptext = $"{cc.Value.Count()} {text} coins"; *@
@* text = cc.Value.Count().ToString(); *@
@* var percentage = decimal.Divide(cc.Value.Count(), coins.Count()) * 100; *@
@* <div class="progress-bar @cssClass" role="progressbar" style="width: @percentage%" data-bs-toggle="tooltip" title="@tooltiptext">@text</div> *@
@* } *@
@* </div> *@
@* </div> *@
@* <div> *@
@* <h6 class="mb-2">Value per privacy</h6> *@
@* <div class="progress mb-2" style="height: 2rem;"> *@
@* @foreach (var cc in colorCoins) *@
@* { *@
@* var cssClass = cc.Key == AnonsetType.Green ? "bg-success" : cc.Key == AnonsetType.Orange ? "bg-warning" : *@
@* "bg-danger"; *@
@* var text = cc.Key == AnonsetType.Green ? "private" : cc.Key == AnonsetType.Orange ? "semi-private" : *@
@* "non-private"; *@
@* var percentage = decimal.Divide(cc.Value.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)), coins.TotalAmount().ToDecimal(MoneyUnit.BTC)) * 100; *@
@* var tooltiptext = $"{cc.Value.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC))} {text} BTC"; *@
@* *@
@* text = cc.Value.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)).ToString(); *@
@* <div class="progress-bar @cssClass" role="progressbar" style="width: @percentage%" data-bs-toggle="tooltip" title="@tooltiptext">@text</div> *@
@* } *@
@* </div> *@
@* </div> *@
@* @{ *@
@* var coinjoined = @coins.CoinJoinInProcess(); *@
@* } *@
@* @if (coinjoined.Any()) *@
@* { *@
@* var count = @coins.CoinJoinInProcess().Count(); *@
@* var totalCount = @coins.Count(); *@
@* var sum = @coinjoined.TotalAmount().ToDecimal(MoneyUnit.BTC); *@
@* var totalSum = @coins.TotalAmount().ToDecimal(MoneyUnit.BTC); *@
@* var sumPercentage = decimal.Divide(sum, totalSum) * 100; *@
@* var countPercentage = decimal.Divide(count, totalCount) * 100; *@
@* *@
@* <div> *@
@* <h6 class="mb-2">Coins currently joining</h6> *@
@* <div class="progress mb-2 position-relative" style="height: 2rem;"> *@
@* <div class="w-100 text-center position-absolute bg-transparent progress-bar h-100">@count </div> *@
@* <div class="progress-bar bg-info progress-bar-striped progress-bar-animated w-100" role="progressbar"></div> *@
@* </div> *@
@* </div> *@
@* <div> *@
@* <h6 class="mb-2">Value currently joining</h6> *@
@* <div class="progress mb-2 position-relative" style="height: 2rem;"> *@
@* <div class="w-100 text-center position-absolute bg-transparent progress-bar h-100">@sum BTC</div> *@
@* <div class="progress-bar bg-info progress-bar-striped progress-bar-animated w-100" role="progressbar"></div> *@
@* *@
@* *@
@* </div> *@
@* </div> *@
@* } *@
<!-- Modal -->
<div class="modal modal-lg fade" id="coins" data-bs-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
@@ -477,9 +390,16 @@ updateInProgressAnimation(myChart);
</div>
}
else if (coordinator.WasabiCoordinatorStatusFetcher.Connected)
{
if (coordinator.CoinJoinManager.TrackedWallets.TryGetValue(wallet.WalletName, out _))
{
<span class="h6">Idle</span>
}
else
{
<span class="h6">Inactive</span>
}
}
</div>
@{
if (coordinator.CoinPrison is not null)
@@ -551,78 +471,7 @@ updateInProgressAnimation(myChart);
</div>
@* <div class="collapse table-responsive" id="cj-@currentRound.Id"> *@
@* *@
@* <dl> *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px"> *@
@* Status *@
@* </dt> *@
@* <dd> *@
@* @currentRound.Phase.ToString().ToSentenceCase() *@
@* </dd> *@
@* </div> *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px"> *@
@* Round ID *@
@* </dt> *@
@* <dd> *@
@* @currentRound.Id.ToString() *@
@* </dd> *@
@* </div> *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px"> *@
@* Mining feerate *@
@* </dt> *@
@* <dd> *@
@* @currentRound.CoinjoinState.Parameters.MiningFeeRate.ToString() *@
@* </dd> *@
@* </div> *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px"> *@
@* Coinjoin total inputs *@
@* </dt> *@
@* <dd> *@
@* @currentRound.CoinjoinState.Inputs.Count() inputs (@currentRound.CoinjoinState.Inputs.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) *@
@* </dd> *@
@* </div> *@
@* *@
@* *@
@* @if (!tracker.CoinJoinClient.CoinsToRegister.IsEmpty) *@
@* { *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px">Your inputs</dt> *@
@* <dd> *@
@* <span class="w-100">Registered @tracker.CoinJoinClient.CoinsInCriticalPhase.Count() inputs (@tracker.CoinJoinClient.CoinsInCriticalPhase.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) / @tracker.CoinJoinClient.CoinsToRegister.Count() inputs (@tracker.CoinJoinClient.CoinsToRegister.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) </span> *@
@* @if (tracker.BannedCoins.Any()) *@
@* { *@
@* <span class="w-100 text-danger">but got @tracker.BannedCoins.Count() inputs (@tracker.BannedCoins.Sum(coin => coin.Coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) banned</span> *@
@* } *@
@* </dd> *@
@* </div> *@
@* } *@
@* *@
@* @if (currentRound.Phase >= Phase.OutputRegistration) *@
@* { *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px">Coinjoin total outputs</dt> *@
@* <dd> *@
@* @currentRound.CoinjoinState.Outputs.Count() outputs (@currentRound.CoinjoinState.Outputs.Sum(coin => coin.Value.ToDecimal(MoneyUnit.BTC)) BTC) *@
@* </dd> *@
@* </div> *@
@* if (tracker.CoinJoinClient.OutputTxOuts is { } outputs) *@
@* { *@
@* <div class="d-flex flex-wrap align-items-center gap-2"> *@
@* <dt class="w-100px">>Your outputs</dt> *@
@* <dd> *@
@* @outputs.outputTxOuts.Count() outputs (@outputs.outputTxOuts.Sum(coin => coin.Value.ToDecimal(MoneyUnit.BTC)) BTC, @outputs.batchedPayments.Count() batched payments) *@
@* </dd> *@
@* </div> *@
@* } *@
@* } *@
@* </dl> *@
@* *@
@* </div> *@
}
}
</div>
@@ -637,6 +486,45 @@ updateInProgressAnimation(myChart);
View coins
</button>
}
@if (wallet.LastLogs.Any())
{
<button type="button" class="btn btn-text p-1" data-bs-toggle="modal" data-bs-target="#logs">
Recent logs
</button>
<div class="modal modal-lg fade" id="logs" data-bs-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="mb-0">Coinjoin logs</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body table-responsive mt-0">
<table class="table">
<thead>
<tr>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach (var evt in wallet.LastLogs)
{
string cssClass = evt.Item1 <= (LogLevel) 2 ? "info" : evt.Item1 == (LogLevel) 4 ? "warning" : "danger";
<tr class="text-@cssClass">
<td>@evt.Item2</td>
</tr>
}
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
}

View File

@@ -48,6 +48,7 @@
document.getElementById("privatesend").addEventListener("click", async ev => {
document.getElementById("InputSelection").value = "True";
const amountElements = document.querySelectorAll("[name^='Outputs'][name$='Amount']");
const labelElements = document.querySelectorAll("[name^='Outputs'][name$='Labels'] option:checked");
let amount = 0;
amountElements.forEach(value => {
try {
@@ -58,8 +59,14 @@
if (!amount){
return;
}
const url =@Safe.Json(@Url.Action("ComputeCoinSelection", "WabisabiStore", new { storeId }));
const response = await fetch(`${url}?amount=${amount}`);
let labels = [];
labelElements.forEach(value => {
labels.push(value.value);
});
let url =@Safe.Json(@Url.Action("ComputeCoinSelection", "WabisabiStore", new { storeId }));
url += "?amount=${amount}";
url += labels.length >0? "&" + labels.map(x => "labels=" + encodeURIComponent(x)).join("&"): "";
const response = await fetch(url);
const coins = await response.json();
let selectedInputsElement = document.getElementById("SelectedInputs");
if (!selectedInputsElement){

View File

@@ -8,6 +8,7 @@
@using BTCPayServer.Services.Stores
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using WalletWasabi.Backend.Controllers
@using WalletWasabi.Wallets
@model BTCPayServer.Plugins.Wabisabi.WabisabiStoreSettings
@inject WabisabiCoordinatorClientInstanceManager WabisabiCoordinatorClientInstanceManager
@inject IScopeProvider _scopeProvider
@@ -16,6 +17,8 @@
@inject BTCPayNetworkProvider BtcPayNetworkProvider
@inject BTCPayServerOptions BtcPayServerOptions
@{
var liteMode = Context.Items.TryGetValue("cjlite" , out _);
var storeId = _scopeProvider.GetCurrentStoreId();
ViewData.SetActivePage("CoinjoinSettings", "Coinjoin", "Coinjoin settings", storeId);
var userid = Context.User.Claims.Single(claim => claim.Type == ClaimTypes.NameIdentifier).Value;
@@ -33,7 +36,7 @@
}
<form method="post">
<form method="post" asp-action="UpdateWabisabiStoreSettings" asp-controller="WabisabiStore" asp-route-storeId="@storeId">
<partial name="_StatusMessage"/>
@@ -45,19 +48,19 @@
</a>
</h3>
<div class="d-flex align-items-center gap-1 ">
<div class="d-flex align-items-center">
<div class="d-flex align-items-center @(liteMode? "d-none": "")">
<input asp-for="Active" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="Active" class="form-label mb-0 me-1"></label>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary mt-3 mt-sm-0">Save</button>
<a asp-action="ListCoinjoins" asp-route-storeId="@storeId" class="btn btn-secondary mt-3 mt-sm-0" role="button">
<button name="command" type="submit" value="save" class="btn btn-primary mt-3 mt-sm-0 @(liteMode? "btn-lin": "btn-secondary")">Save</button>
<a asp-action="ListCoinjoins" asp-route-storeId="@storeId" class="btn btn-secondary mt-3 mt-sm-0 @(liteMode? "d-none": "")" role="button">
Coinjoin History
</a>
<button type="button" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings"
<button type="button" class="btn btn-secondary mt-3 mt-sm-0 @(liteMode? "d-none": "")" permission="@Policies.CanModifyServerSettings"
data-bs-toggle="modal" data-bs-target="#discover-prompt">
Add Coordinator
</button>
<a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings">Coordinator</a>
<a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn @(liteMode? "btn-lin": "btn-secondary") mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings">Coordinator</a>
@* <a class="btn btn-secondary mt-3 mt-sm-0" href="https://gist.github.com/nopara73/bb17e89d7dc9af536ca41f50f705d329" rel="noreferrer noopener" target="_blank">Enable Discreet payments - Coming soon</a> *@
</div>
@@ -72,16 +75,14 @@
}
var wallet = await WalletProvider.GetWalletAsync(storeId);
if (wallet is BTCPayWallet)
{
@if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable)
if (wallet is BTCPayWallet && !((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 either not a hot wallet, or enabled in your store settings and will not be able to participate in coinjoins.</span>
</div>
}
}
}
<style>
#blocker:hover{
@@ -125,6 +126,7 @@
</div>
</div>
<div id="advanced" class="@(Model.PlebMode ? "d-none" : "")">
<button type="submit" name="command" value="reset" class="btn btn-link">Reset to defaults </button>
<div class="form-group">
<label asp-for="AnonymitySetTarget" class="form-label">Anon score target</label>
@@ -132,12 +134,58 @@
<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="row">
<div class="col-sm-12 col-md-6">
<div class="form-group ">
<label asp-for="FeeRateMedianTimeFrameHours" class="form-label">Mining fee limits in hours</label>
<div class="input-group">
<input type="number" class="form-control" asp-for="FeeRateMedianTimeFrameHours" placeholder="hours" min="0">
<span class="input-group-text">hours</span>
</div>
<p class="text-muted">Only coinjoin if the mining fee is below the median of the specified past number of hours. Set to 0 to ignore</p>
</div>
</div>
<p class="text-muted">Only coinjoin if the mining fee is below the median of the specified number of hours</p>
<div class="col-sm-12 col-md-6">
<div class="form-group ">
<label asp-for="ExplicitHighestFeeTarget" class="form-label">Highest feerate allowed</label>
<div class="input-group">
<input type="number" class="form-control" asp-for="ExplicitHighestFeeTarget" placeholder="sat/b" min="1">
<span class="input-group-text">sat/b</span>
</div>
<p class="text-muted">Only coinjoin if the mining fee is below this feerate</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-6">
<div class="form-group ">
<label asp-for="ConsolidationMode" class="form-check-label">Coinsolidation mode</label>
<select asp-for="ConsolidationMode" class="form-select">
<option value="@ConsolidationModeType.Never">Never</option>
<option value="@ConsolidationModeType.Always">Always</option>
<option value="@ConsolidationModeType.WhenLowFee">When the mining fee is low</option>
<option value="@ConsolidationModeType.WhenLowFeeAndManyUTXO">When the mining fee is low and you have too many UTXOs</option>
</select>
<p class="text-muted">Feed as many coins to the coinjoin as possible.</p>
<p class="text-muted">NOTE: When choosing WhenLowFeeAndManyUTXO, if you have over @BTCPayWallet.HighAmountOfCoins coins, coinjoins will happen regardless if your wallet is full private, to consolidate your wallet to a smaller utxo set.</p>
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-group ">
<label asp-for="LowFeeTarget" class="form-label">Mining low fee threshold</label>
<div class="input-group">
<input type="number" class="form-control" asp-for="LowFeeTarget" placeholder="sat/b" min="1">
<span class="input-group-text">sat/b</span>
</div>
<p class="text-muted">Consider it low fees when it is below this threshold</p>
</div>
</div>
</div>
<div class="form-group">
@@ -145,26 +193,29 @@
<input type="number" class="form-control" asp-for="MinimumDenominationAmount" placeholder="sats" min="0">
<p class="text-muted">Do no use any of the standard denominations below this amount (creates change (which will get remixed) but prevent tiny utxos)</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"/>
<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"/>
<p class="text-muted">Only allow a single non-private coin into a coinjoin.</p>
</div>
<div class="row">
<div class="col-sm-12 col-md-6">
<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"/>
<p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-group form-check">
<label asp-for="ParanoidPayments" class="form-check-label">Paranoid payments</label>
<input asp-for="ParanoidPayments" type="checkbox" class="form-check-input"/>
<p class="text-muted">Only batch payments with fully private coins.</p>
</div>
</div>
</div>
<div class="form-group">
<label asp-for="CrossMixBetweenCoordinatorsMode" class="form-label">Mix funds between different coordinators</label>
<select asp-for="CrossMixBetweenCoordinatorsMode" class="form-select">
@@ -176,7 +227,11 @@
</div>
<div class="form-group">
<label asp-for="ExtraJoinProbability" class="form-label">Continuous Coinjoin</label>
<div class="input-group">
<input asp-for="ExtraJoinProbability" type="number" min="0" max="100" step="any" class="form-control"/>
<span class="input-group-text">% * 0.01 </span>
</div>
<p class="text-muted">Percentage (100 = 1% reality) probability of joining a round even if you have no payments to batch and all coins are private, prevents timing analysis. (Warning: a high probability will quickly eat up your balance in mining fees) </p>
</div>
<div class="form-group ">
@@ -184,7 +239,9 @@
<select asp-for="MixToOtherWallet" asp-items="selectStores" class="form-select"></select>
<p class="text-muted">Send coins that have been created in a coinjoin in a standard denomination to another wallet</p>
</div>
<div class="row">
<div class="col-sm-12 col-md-6">
<div class="list-group form-group">
<div class="list-group-item font-weight-bold">Only mix coins with these labels</div>
@if (Model.InputLabelsAllowed?.Any() is not true)
@@ -207,6 +264,8 @@
<button name="command" value="include-label-add" type="submit" class="btn btn-secondary btn-sm">Add</button>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="list-group form-group">
<div class="list-group-item font-weight-bold">Only mix coins without these labels</div>
@if (Model.InputLabelsExcluded?.Any() is not true)
@@ -230,6 +289,9 @@
<button name="command" value="exclude-label-add" type="submit" class="btn btn-secondary btn-sm">Add</button>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -243,6 +305,7 @@
{
continue;
}
<div class="card mt-3">
<div class="card-header d-flex justify-content-between">
<div>
@@ -387,9 +450,12 @@
</form>
@if (!liteMode)
{
<partial name="Wabisabi/AddCoordinatorPrompt" model="@(new DiscoveredCoordinator())"/>
}
@section PageFootContent {
@@ -429,6 +495,29 @@
}
document.addEventListener("DOMContentLoaded", function () {
const batchPaymentsEl = document.getElementById("BatchPayments");
const paranoidPaymentsEl = document.getElementById("ParanoidPayments");
const consolidationModeEl = document.getElementById("ConsolidationMode");
const lowFeeTargetEl = document.getElementById("LowFeeTarget");
function handle(){
if (consolidationModeEl.value.startsWith("WhenLowFee")){
lowFeeTargetEl.parentElement.parentElement.classList.remove("d-none");
} else{
lowFeeTargetEl.parentElement.parentElement.classList.add("d-none");
}
if (!batchPaymentsEl.checked){
paranoidPaymentsEl.parentElement.parentElement.classList.add("d-none");
} else{
paranoidPaymentsEl.parentElement.parentElement.classList.remove("d-none");
}
}
handle();
batchPaymentsEl.addEventListener("change", handle);
consolidationModeEl.addEventListener("change", handle);
document.querySelectorAll("input.toggle-settings").forEach(value => value.addEventListener("change", handleCoordinatorEnabled));
document.querySelectorAll("input.plebModeRadio").forEach(value => value.addEventListener("change", handlePlebModeChange));
});

View File

@@ -39,14 +39,19 @@ public class WabisabiCoordinatorClientInstanceManager:IHostedService
{
_provider = provider;
_walletProvider = walletProvider;
_walletProvider.WalletUnloaded += WalletProviderOnWalletUnloaded;
// _walletProvider.WalletUnloaded += WalletProviderOnWalletUnloaded;
// _walletProvider.Walleloaded += WalletProviderOnWalletloaded;
//
}
private void WalletProviderOnWalletUnloaded(object sender, WalletProvider.WalletUnloadEventArgs e)
{
_ =StopWallet(e.Wallet);
}
// private void WalletProviderOnWalletUnloaded(object sender, WalletProvider.WalletUnloadEventArgs e)
// {
// _ =StopWallet(e.Wallet);
// }
// private void WalletProviderOnWalletloaded(object sender, WalletProvider.WalletUnloadEventArgs e)
// {
// _ =StartWallet(e.Wallet as BTCPayWallet);
// }
private bool started = false;
public LocalisedUTXOLocker UTXOLocker;
@@ -62,26 +67,40 @@ public class WabisabiCoordinatorClientInstanceManager:IHostedService
public async Task StopAsync(CancellationToken cancellationToken)
{
foreach (KeyValuePair<string,WabisabiCoordinatorClientInstance> coordinatorManager in HostedServices)
foreach (var coordinatorManager in HostedServices)
{
await coordinatorManager.Value.StopAsync(cancellationToken);
}
}
public async Task StopWallet(IWallet wallet, string coordinator = null)
{
if (coordinator is not null && HostedServices.TryGetValue(coordinator, out var instance))
{
await instance.StopWallet(wallet);
}
else if (coordinator is null)
{
foreach (var servicesValue in HostedServices.Values)
{
await servicesValue.StopWallet(wallet);
}
}
}
// public async Task StopWallet(IWallet wallet, string coordinator = null)
// {
// if (coordinator is not null && HostedServices.TryGetValue(coordinator, out var instance))
// {
// await instance.StopWallet(wallet);
// }
// else if (coordinator is null)
// {
// foreach (var servicesValue in HostedServices.Values)
// {
// await servicesValue.StopWallet(wallet);
// }
// }
// }
// public async Task StartWallet(BTCPayWallet wallet, string coordinator = null)
// {
// if (coordinator is not null && HostedServices.TryGetValue(coordinator, out var instance))
// {
// await instance.StartWallet(wallet);
// }
// else if (coordinator is null)
// {
// foreach (var servicesValue in HostedServices.Values)
// {
// await servicesValue.StartWallet(wallet);
// }
// }
// }
public void AddCoordinator(string displayName, string name,
@@ -309,12 +328,6 @@ public class WabisabiCoordinatorClientInstance:IHostedService
_hostedServices.Register<WasabiCoordinatorStatusFetcher>(() => WasabiCoordinatorStatusFetcher, "WasabiCoordinatorStatusFetcher");
_hostedServices.Register<CoinJoinManager>(() => CoinJoinManager, "WasabiCoordinatorStatusFetcher");
}
public async Task StopWallet(IWallet wallet)
{
await CoinJoinManager.StopAsync(wallet, CancellationToken.None);
}
private void OnStatusChanged(object sender, StatusChangedEventArgs e)
{
bool stopWhenAllMixed;
@@ -324,10 +337,10 @@ public class WabisabiCoordinatorClientInstance:IHostedService
_logger.LogTrace(coinJoinStatusEventArgs.CoinJoinProgressEventArgs.GetType() + " :" +
e.Wallet.WalletName);
break;
case LoadedEventArgs loadedEventArgs:
stopWhenAllMixed = !((BTCPayWallet)loadedEventArgs.Wallet).BatchPayments;
_ = CoinJoinManager.StartAsync(loadedEventArgs.Wallet, stopWhenAllMixed, false, CancellationToken.None);
break;
// case LoadedEventArgs loadedEventArgs:
// stopWhenAllMixed = !((BTCPayWallet)loadedEventArgs.Wallet).BatchPayments;
// _ = CoinJoinManager.StartAsync(loadedEventArgs.Wallet, stopWhenAllMixed, false, CancellationToken.None);
// break;
case StartErrorEventArgs errorArgs:
_logger.LogTrace("Could not start wallet for coinjoin:" + errorArgs.Error.ToString() + " :" + e.Wallet.WalletName);
break;

View File

@@ -88,6 +88,10 @@ namespace BTCPayServer.Plugins.Wabisabi
Processors = new[] {"Wabisabi"},
})).FirstOrDefault();
if (!paybatching && existingProcessor is null)
{
return;
}
_eventAggregator.Publish(new PayoutProcessorUpdated()
{
@@ -105,16 +109,6 @@ namespace BTCPayServer.Plugins.Wabisabi
public async Task SetWabisabiForStore(string storeId, WabisabiStoreSettings wabisabiSettings,
string termsCoord = null)
{
foreach (var setting in wabisabiSettings.Settings.Where(setting => !setting.Enabled))
{
_walletProvider.LoadedWallets.TryGetValue(storeId, out var walletTask);
if (walletTask != null)
{
var wallet = await walletTask.Value;
await _coordinatorClientInstanceManager.StopWallet(wallet, setting.Coordinator);
}
}
var res = await GetWabisabiForStore(storeId);
foreach (var wabisabiStoreCoordinatorSettings in wabisabiSettings.Settings)
{

View File

@@ -10,6 +10,8 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Common;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
@@ -20,12 +22,15 @@ using NBitcoin.Payment;
using NBitcoin.Secp256k1;
using Newtonsoft.Json;
using NNostr.Client;
using WabiSabi.Crypto.Randomness;
using WalletWasabi.Backend.Controllers;
using WalletWasabi.Blockchain.TransactionBuilding;
using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Extensions;
namespace BTCPayServer.Plugins.Wabisabi
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/Wabisabi")]
@@ -106,6 +111,12 @@ namespace BTCPayServer.Plugins.Wabisabi
WabisabiCoordinatorSettings coordSettings;
switch (actualCommand)
{
case "reset":
var newS = new WabisabiStoreSettings();
newS.Settings = vm.Settings;
await _WabisabiService.SetWabisabiForStore(storeId, vm, commandIndex);
TempData["SuccessMessage"] = $"Advanced settings reset to default";
return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId});
case "accept-terms":
var coord = vm.Settings.SingleOrDefault(settings => settings.Coordinator == commandIndex);
@@ -395,7 +406,7 @@ namespace BTCPayServer.Plugins.Wabisabi
}
[HttpGet("select-coins")]
public async Task<IActionResult> ComputeCoinSelection(string storeId, decimal amount)
public async Task<IActionResult> ComputeCoinSelection(string storeId, decimal amount, string[] labels)
{
if ((await _walletProvider.GetWalletAsync(storeId)) is not BTCPayWallet wallet)
{
@@ -404,15 +415,50 @@ namespace BTCPayServer.Plugins.Wabisabi
var coins = await wallet.GetAllCoins();
var selectedCoins = new List<ICoin>();
if (labels?.Any() is true)
{
var directLinkCoins = coins.FilterBy(coin => coin.HdPubKey.Labels.Any(labels.Contains)).Available();
selectedCoins.AddRange(directLinkCoins.Select(coin => coin.Coin));
if (directLinkCoins.TotalAmount().ToDecimal(MoneyUnit.BTC) > amount)
{
//select enough to be able to spend the amount requested
var result = directLinkCoins.ToShuffled(new InsecureRandom()).Aggregate(
(Money.Zero, new List<SmartCoin>()), (acc, coin) =>
{
var (sum, list) = acc;
if (sum.ToDecimal(MoneyUnit.BTC) <= amount)
{
list.Add(coin);
return (sum + coin.Amount, list);
}
return acc;
});
return Ok(result.Item2.Select(coin => coin.Outpoint.ToString()).ToArray());
}
}
var selectedCoinSum = selectedCoins.Sum(coin => ((Money)coin.Amount).ToDecimal(MoneyUnit.BTC));
var remaining = amount - selectedCoinSum;
var remainingCoins = coins.FilterBy(coin => !selectedCoins.Contains(coin.Coin)).Available().ToList();
var defaultCoinSelector = new DefaultCoinSelector();
var defaultSelection =
(defaultCoinSelector.Select(coins.Select(coin => coin.Coin).ToArray(),
new Money(amount, MoneyUnit.BTC)) ?? Array.Empty<ICoin>())
.ToArray();
var selector = new SmartCoinSelector(coins.ToList());
var smartSelection = selector.Select(defaultSelection,
new Money((decimal) amount, MoneyUnit.BTC));
return Ok(smartSelection.Select(coin => coin.Outpoint.ToString()).ToArray());
var selector = new SmartCoinSelector(remainingCoins);
selectedCoins.AddRange(selector.Select(defaultSelection, new Money(remaining, MoneyUnit.BTC)).ToList());
return Ok(selectedCoins.Select(coin => coin.Outpoint.ToString()).ToArray());
}
public class SpendViewModel

View File

@@ -1,6 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using WalletWasabi.Wallets;
namespace BTCPayServer.Plugins.Wabisabi;
@@ -15,7 +19,9 @@ public class WabisabiStoreSettings
public List<string> InputLabelsAllowed { get; set; } = new();
public List<string> InputLabelsExcluded { get; set; } = new();
public bool ConsolidationMode { get; set; } = false;
[JsonConverter(typeof(ConsolidationModeTypeJsonConverter))]
public ConsolidationModeType ConsolidationMode { get; set; } = ConsolidationModeType.WhenLowFeeAndManyUTXO;
public bool RedCoinIsolation { get; set; } = false;
public int AnonymitySetTarget { get; set; } = 5;
@@ -25,6 +31,8 @@ public class WabisabiStoreSettings
public CrossMixMode CrossMixBetweenCoordinatorsMode { get; set; } = CrossMixMode.WhenFree;
public int FeeRateMedianTimeFrameHours { get; set; }
public long MinimumDenominationAmount { get; set; } = 10000;
public int ExplicitHighestFeeTarget { get; set; } = BTCPayWallet.DefaultExplicitHighestFeeTarget;
public int LowFeeTarget { get; set; } = BTCPayWallet.DefaultLowFeeTarget;
public enum CrossMixMode
{
@@ -34,6 +42,33 @@ public class WabisabiStoreSettings
}
}
public class ConsolidationModeTypeJsonConverter: JsonConverter<ConsolidationModeType>
{
private readonly StringEnumConverter _converter;
public ConsolidationModeTypeJsonConverter()
{
_converter = new StringEnumConverter();
}
public override void WriteJson(JsonWriter writer, ConsolidationModeType value, JsonSerializer serializer)
{
_converter.WriteJson(writer, value, serializer);
}
public override ConsolidationModeType ReadJson(JsonReader reader, Type objectType, ConsolidationModeType existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
return reader switch
{
{TokenType: JsonToken.Boolean, Value: true} => ConsolidationModeType.Always,
{TokenType: JsonToken.Boolean} => ConsolidationModeType.Never,
_ => (ConsolidationModeType) _converter.ReadJson(reader, objectType, existingValue, serializer)!
};
}
}
public class WabisabiStoreCoordinatorSettings
{

View File

@@ -39,6 +39,11 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IMemoryCache _memoryCache;
public readonly ConcurrentDictionary<string, Lazy<Task<BTCPayWallet>>> LoadedWallets = new();
private readonly TaskCompletionSource _initialLoad = new();
private readonly CompositeDisposable _disposables = new();
public WalletProvider(
IServiceProvider serviceProvider,
StoreRepository storeRepository,
@@ -60,23 +65,9 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
_networkProvider = networkProvider;
_memoryCache = memoryCache;
}
public readonly ConcurrentDictionary<string, Lazy<Task<IWallet>>> LoadedWallets = new();
public class WalletUnloadEventArgs : EventArgs
public async Task<BTCPayWallet?> GetWalletAsync(string name)
{
public IWallet Wallet { get; }
public WalletUnloadEventArgs(IWallet wallet)
{
Wallet = wallet;
}
}
public event EventHandler<WalletUnloadEventArgs>? WalletUnloaded;
public async Task<IWallet?> GetWalletAsync(string name)
{
await initialLoad.Task;
await _initialLoad.Task;
return await Smartifier.GetOrCreate(LoadedWallets, name, async () =>
{
if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings))
@@ -122,7 +113,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
}
return (IWallet)new BTCPayWallet(
return new BTCPayWallet(
_serviceProvider.GetRequiredService<WalletRepository>(),
_serviceProvider.GetRequiredService<BTCPayNetworkProvider>(),
_serviceProvider.GetRequiredService<BitcoinLikePayoutHandler>(),
@@ -138,10 +129,6 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
}, _logger);
}
private TaskCompletionSource initialLoad = new();
private CompositeDisposable _disposables = new();
public async Task<IEnumerable<IWallet>> GetWalletsAsync()
{
var explorerClient = _explorerClientProvider.GetExplorerClient("BTC");
@@ -151,7 +138,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
return Array.Empty<IWallet>();
}
await initialLoad.Task;
await _initialLoad.Task;
return (await Task.WhenAll(_cachedSettings
.Select(pair => GetWalletAsync(pair.Key))))
.Where(wallet => wallet is not null);
@@ -162,7 +149,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
public async Task ResetWabisabiStuckPayouts(string[] storeIds)
{
await initialLoad.Task;
await _initialLoad.Task;
storeIds??= _cachedSettings?.Keys.ToArray() ?? Array.Empty<string>();
@@ -233,60 +220,48 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
private async Task UnloadWallet(string name)
{
LoadedWallets.TryRemove(name, out var walletTask);
if (walletTask != null)
{
var wallet = await walletTask.Value;
WalletUnloaded?.Invoke(this, new WalletUnloadEventArgs(wallet));
}
}
public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings)
{
if (wabisabiSettings.Settings.All(settings => !settings.Enabled) || wabisabiSettings.Active == false)
{
_cachedSettings.AddOrReplace(storeId, wabisabiSettings);
if (!wabisabiSettings.Active || wabisabiSettings.Settings.All(settings => !settings.Enabled) )
{
await UnloadWallet(storeId);
}else if (LoadedWallets.TryGetValue(storeId, out var existingWallet))
}
if (LoadedWallets.TryGetValue(storeId, out var existingWallet))
{
_cachedSettings.AddOrReplace(storeId, wabisabiSettings);
var btcpayWalet = (BTCPayWallet) await existingWallet.Value;
if (btcpayWalet is null)
var btcpayWallet = await existingWallet.Value;
if (btcpayWallet is null)
{
LoadedWallets.TryRemove(storeId, out _);
}
else
{
btcpayWallet.WabisabiStoreSettings = wabisabiSettings;
}
}
var w = await GetWalletAsync(storeId);
if (w is null)
{
await UnloadWallet(storeId);
}
}
else
{
btcpayWalet.WabisabiStoreSettings = wabisabiSettings;
}
}
else
{
_cachedSettings.AddOrReplace(storeId, wabisabiSettings);
await GetWalletAsync(storeId);
}
}
public override Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(async () =>
{
_cachedSettings =
await _storeRepository.GetSettingsAsync<WabisabiStoreSettings>(nameof(WabisabiStoreSettings));
initialLoad.SetResult();
_initialLoad.SetResult();
}, cancellationToken);
_disposables.Add(_eventAggregator.SubscribeAsync<StoreRemovedEvent>(async @event =>
{
await initialLoad.Task;
await _initialLoad.Task;
await UnloadWallet(@event.StoreId);
}));
@@ -294,7 +269,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
{
if (@event.WalletId.CryptoCode == "BTC")
{
await initialLoad.Task;
await _initialLoad.Task;
await Check(@event.WalletId.StoreId, cancellationToken);
}