Files
BTCPayServerPlugins/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml
2024-09-03 11:42:12 +02:00

558 lines
28 KiB
Plaintext

@using BTCPayServer.Plugins.Wabisabi
@using BTCPayServer.Abstractions.Contracts
@using NBitcoin
@using System.Security.Claims
@using BTCPayServer
@using BTCPayServer.Client
@using BTCPayServer.Configuration
@using BTCPayServer.Services.Stores
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using WalletWasabi.Backend.Controllers
@using WalletWasabi.Blockchain.Analysis
@using WalletWasabi.Wallets
@model BTCPayServer.Plugins.Wabisabi.WabisabiStoreSettings
@inject WabisabiCoordinatorClientInstanceManager WabisabiCoordinatorClientInstanceManager
@inject IScopeProvider _scopeProvider
@inject StoreRepository StoreRepository
@inject WalletProvider WalletProvider
@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;
var anyEnabled = Model.Settings.Any(settings => settings.Enabled);
ScriptPubKeyType? scriptType;
var stores = (await StoreRepository.GetStoresByUserId(userid))
.ToDictionary(s => s.Id, s => (s, s.GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC")));
stores.TryGetValue(storeId, out var thisStore);
scriptType = thisStore.Item2?.AccountDerivation.ScriptPubKeyType();
var selectStores =
stores.Where(pair => pair.Key != storeId && pair.Value.Item2 is not null && pair.Value.Item2?.AccountDerivation.ScriptPubKeyType() == scriptType)
.Select(pair => new SelectListItem(pair.Value.s.StoreName, pair.Key, Model.MixToOtherWallet == pair.Key)).Prepend(new SelectListItem("None", ""));
}
<form method="post" asp-action="UpdateWabisabiStoreSettings" asp-controller="WabisabiStore" asp-route-storeId="@storeId">
<partial name="_StatusMessage"/>
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">
<span>@ViewData["Title"]</span>
<a href="https://docs.btcpayserver.org/Wabisabi" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info"/>
</a>
</h3>
<div class="d-flex align-items-center gap-1 ">
<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 @(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 @(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 @(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>
</div>
@{
if (BtcPayServerOptions.SocksEndpoint is null)
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<vc:icon symbol="warning"/>
<span class="ms-3">TOR is not configured on this BTCPay Server instance. All communication will be over clearnet and therefore not private!</span>
</div>
}
var wallet = await WalletProvider.GetWalletAsync(storeId);
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{
background-color: rgba(128,128,128, 0.5);
}
#blocker:hover h4{
display: block !important;
left: 0;
position: fixed;
width: 100%
}
</style>
<div class="@(anyEnabled ? "" : "d-none") card card-body coordinator-settings">
@if (Model.Active && anyEnabled)
{
<div class="position-absolute w-100 h-100 text-center rounded" id="blocker" style=" left: 0; top: 0; z-index: 1">
<h4 class="d-none pt-4">Settings cannot be changed while active</h4>
</div>
}
<div class="row">
<div class="col-sm-12 col-md-6">
<div class="form-check">
<input class="form-check-input plebModeRadio"
type="radio" asp-for="PlebMode" value="true">
<label class="form-check-label" asp-for="PlebMode">
Pleb mode
</label>
<p class="text-muted">I just want to coinjoin.</p>
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-check">
<input class="form-check-input plebModeRadio" asp-for="PlebMode" type="radio"value="false">
<label class="form-check-label" asp-for="PlebMode">
Scientist mode
</label>
<p class="text-muted">The world is broken and I need to be vigilant about my bitcoin practices.</p>
</div>
</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="row">
<div class="col-sm-12 col-md-6">
<div class="form-group">
<label asp-for="AnonymitySetTarget" class="form-label">Anon score target</label>
<input type="number" class="form-control" asp-for="AnonymitySetTarget" placeholder="target anon score" min="0">
<p class="text-muted">Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value.<br/> Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.</p>
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-group form-check">
<label asp-for="ConsiderEntryProximity" class="form-check-label">Consider entry proximity</label>
<input asp-for="ConsiderEntryProximity" type="checkbox" class="form-check-input"/>
<p class="text-muted">Coins which have been mixed but are only one hop away from the original coinjoin transaction won't be considered private. This helps break linking assumptions when coins are spent.</p>
</div>
</div>
</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>
<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="row">
<div class="col-sm-12 col-md-6">
<div class="form-group">
<label asp-for="MinimumDenominationAmount" class="form-label">Minimum denomination (in sats)</label>
<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>
<div class="col-sm-12 col-md-6">
<div class="form-group">
<label asp-for="AllowedDenominations" class="form-label">Allowed denominations (in sats)</label>
<select asp-for="AllowedDenominations" class="form-select" multiple="multiple">
@foreach (var denomination in BlockchainAnalyzer.StdDenoms)
{
<option value="@denomination">@denomination sats</option>
}
</select>
<p>DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING</p>
<p class="text-muted">Only generate outputs of these denoms. Leave blank to ignore. Generates change. You must match other user's settings to gain any anonymity.</p>
</div>
</div>
</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">
<option value="@WabisabiStoreSettings.CrossMixMode.WhenFree">Cross mix when free</option>
<option value="@WabisabiStoreSettings.CrossMixMode.Always">Always cross mix</option>
<option value="@WabisabiStoreSettings.CrossMixMode.Never">Never cross mix</option>
</select>
<p class="text-muted">Whether to allow mixed coins to be mixed within different coordinators for greater privacy (Warning: This will make your coins to lose the free remix within the same coordinator)</p>
</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 ">
<label asp-for="MixToOtherWallet" class="form-check-label">Send to other wallet</label>
<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)
{
<div class="list-group-item">No label filter applied</div>
}
else
{
@for (var xIndex = 0; xIndex < Model.InputLabelsAllowed.Count; xIndex++)
{
<div class="list-group-item">
<div class="input-group input-group-sm">
<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>
}
}
<div class="list-group-item">
<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)
{
<div class="list-group-item">No label filter applied</div>
}
else
{
@for (var xIndex = 0; xIndex < Model.InputLabelsExcluded.Count; xIndex++)
{
<div class="list-group-item">
<div class="input-group input-group-sm">
<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>
}
}
<div class="list-group-item">
<button name="command" value="exclude-label-add" type="submit" class="btn btn-secondary btn-sm">Add</button>
</div>
</div>
</div>
</div>
</div>
</div>
@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">
<div class="card-header d-flex justify-content-between">
<div>
<div class="d-flex">
<h3>@coordinator.CoordinatorDisplayName</h3>
@if (coordinator.CoordinatorName != "local" && coordinator.CoordinatorName != "zksnacks")
{
<button name="command" type="submit" value="remove-coordinator:@(coordinator.CoordinatorName)" class="btn btn-link txt-danger" permission="@Policies.CanModifyServerSettings">Remove</button>
}
</div>
<span class="text-muted">@coordinator.Coordinator</span>
<div>
<div>@(!coordinator.WasabiCoordinatorStatusFetcher.Connected ? "Coordinator Status: Not connected" : "Coordinator Status: Connected")</div>
@if (!string.IsNullOrEmpty(coordinator.Description))
{
<p class="text-muted">@coordinator.Description</p>
}
@if (coordinator.RoundStateUpdater.AnyRound && coordinator.RoundStateUpdater.RoundStates.Any(pair => pair.Value.BlameOf == uint256.Zero))
{
var round = coordinator.RoundStateUpdater.RoundStates.Last(pair => pair.Value.BlameOf == uint256.Zero).Value;
var roundParameters = round.CoinjoinState.Parameters;
<div class="modal modal-lg fade" id="config-@s.Coordinator">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">@coordinator.CoordinatorDisplayName Last round parameters </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">
<table class="table table-responsive my-0">
<tr>
<th scope="row">Fee charged</th>
@{
var fee = $"{roundParameters.CoordinationFeeRate.Rate * 100}%";
}
<td>
@(fee)
</td>
</tr>
<tr>
<th scope="row">Allowed input amounts</th>
<td>@roundParameters.AllowedInputAmounts.Min.ToDecimal(MoneyUnit.BTC) BTC - @roundParameters.AllowedInputAmounts.Max.ToDecimal(MoneyUnit.BTC) BTC</td>
</tr>
<tr>
<th scope="row">Allowed input types</th>
<td>@string.Join(", ", roundParameters.AllowedInputTypes)</td>
</tr>
<tr>
<th scope="row">Allowed output amounts</th>
<td>@roundParameters.AllowedOutputAmounts.Min.ToDecimal(MoneyUnit.BTC) BTC - @roundParameters.AllowedOutputAmounts.Max.ToDecimal(MoneyUnit.BTC) BTC</td>
</tr>
<tr>
<th scope="row">Allowed output types</th>
<td>@string.Join(", ", roundParameters.AllowedOutputTypes)</td>
</tr>
<tr>
<th scope="row">Minimum inputs</th><td>@roundParameters.MinInputCountByRound</td>
</tr>
<tr>
<th scope="row">Maximum inputs</th><td>@roundParameters.MaxInputCountByRound</td>
</tr>
<tr>
<th scope="row">Maximum round registration time</th><td>@roundParameters.StandardInputRegistrationTimeout.ToString()</td>
</tr>
</table>
<div class="alert alert-info">Please note that a coordinator can change its configuration at will. This is only a display of the last round received from them.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<a class="px-2 cursor-pointer"
data-bs-toggle="modal" data-bs-target="#config-@s.Coordinator">
Coordinator Config
</a>
@if (Model.Settings[index].RoundWhenEnabled is not null && !BTCPayWallet.IsRoundOk(roundParameters, Model.Settings[index]))
{
<div class="alert alert-danger w-100 mb-0 p-1">Round fees/parameters changed. Coinjoins will not occur unless you accept the new parameters.<button class="btn btn-link alert-link p-0" name="command" type="submit" value="accept-terms:@s.Coordinator"> Accept new terms</button></div>
}
}
<a class="px-2 w-100 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>
</div>
@{
var canEnable = coordinator.WasabiCoordinatorStatusFetcher.Connected && coordinator.RoundStateUpdater.AnyRound;
}
<div class="form-group form-check form" data-bs-toggle="tooltip" title="@(!canEnable ? "You cannot enable this coordinator until it is connected and a round has been seen" : string.Empty)">
@if (Model.Settings[index].RoundWhenEnabled is not null)
{
<input type="hidden" asp-for="Settings[index].RoundWhenEnabled.CoordinationFeeRate"/>
<input type="hidden" asp-for="Settings[index].RoundWhenEnabled.MinInputCountByRound"/>
}
<input asp-for="Settings[index].Enabled"
type="checkbox" class="form-check-input form-control-lg toggle-settings"
data-coordinator="@s.Coordinator"
disabled="@(!Model.Settings[index].Enabled && !canEnable)"/>
</div>
</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>
}
</form>
@if (!liteMode)
{
<partial name="Wabisabi/AddCoordinatorPrompt" model="@(new DiscoveredCoordinator())"/>
}
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
}
<script type="text/javascript">
function handlePlebModeChange(evt){
const isPlebMode = evt.target.value === "true";
const el = document.querySelector(`#advanced`);
if (isPlebMode){
el.classList.add("d-none");
}else{
el.classList.remove("d-none");
}
}
function handleCoordinatorEnabled(evt){
let enabled = evt.target.checked;
if (!enabled){
for (const settings of document.querySelectorAll("input.toggle-settings")) {
if (settings.checked){
enabled = true;
break;
}
}}
const el = document.querySelector(`.coordinator-settings`);
if (!enabled){
el.classList.add("d-none");
}else{
el.classList.remove("d-none");
}
}
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));
});
</script>