mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 19:37:37 +01:00
Easier payment testing (#2672)
* Easier payment testing * WIP, more TODOs and some cleanup. Help is appreciated. * Added dummy button to expire monitoring (doesn't work yet) * Added TODO * Make fake tab default if present * Split controller and change wording from fake to testing * Extract and simplify checkout testing UI * Restrict testing access to regtest Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
78a8c8c1be
commit
e842a00402
9 changed files with 289 additions and 4 deletions
10
BTCPayServer.Client/Models/ExpireInvoiceResponse.cs
Normal file
10
BTCPayServer.Client/Models/ExpireInvoiceResponse.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class ExpireInvoiceResponse
|
||||||
|
{
|
||||||
|
public String SuccessMessage { get; set; }
|
||||||
|
public String ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
BTCPayServer.Client/Models/FakePaymentRequest.cs
Normal file
9
BTCPayServer.Client/Models/FakePaymentRequest.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class FakePaymentRequest
|
||||||
|
{
|
||||||
|
public Decimal Amount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
12
BTCPayServer.Client/Models/FakePaymentResponse.cs
Normal file
12
BTCPayServer.Client/Models/FakePaymentResponse.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class FakePaymentResponse
|
||||||
|
{
|
||||||
|
public Decimal AmountRemaining { get; set; }
|
||||||
|
public String Txid { get; set; }
|
||||||
|
public String SuccessMessage { get; set; }
|
||||||
|
public String ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
124
BTCPayServer/Controllers/InvoiceController.Testing.cs
Normal file
124
BTCPayServer/Controllers/InvoiceController.Testing.cs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
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.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Filters;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.InvoicingModels;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Plugins.CoinSwitch;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Invoices.Export;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.RPC;
|
||||||
|
using NBitpayClient;
|
||||||
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class InvoiceController
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
[Route("i/{invoiceId}/test-payment")]
|
||||||
|
public async Task<IActionResult> TestPayment(string invoiceId, FakePaymentRequest request)
|
||||||
|
{
|
||||||
|
if (_NetworkProvider.NetworkType != ChainName.Regtest) return Conflict();
|
||||||
|
|
||||||
|
var credentialString = "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3";
|
||||||
|
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||||
|
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||||
|
|
||||||
|
// TODO support altcoins, not just bitcoin
|
||||||
|
//var network = invoice.Networks.GetNetwork(invoice.Currency);
|
||||||
|
var cryptoCode = "BTC";
|
||||||
|
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
var ExplorerNode = new RPCClient(RPCCredentialString.Parse(credentialString), network.NBitcoinNetwork);
|
||||||
|
var paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||||
|
|
||||||
|
//var network = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||||
|
var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination();
|
||||||
|
|
||||||
|
var bitcoinAddressObj = BitcoinAddress.Create(bitcoinAddressString, network.NBitcoinNetwork);
|
||||||
|
var BtcAmount = request.Amount;
|
||||||
|
|
||||||
|
var FakePaymentResponse = new FakePaymentResponse();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
|
||||||
|
var rate = paymentMethod.Rate;
|
||||||
|
|
||||||
|
FakePaymentResponse.Txid = ExplorerNode.SendToAddress(bitcoinAddressObj, new Money(BtcAmount, MoneyUnit.BTC)).ToString();
|
||||||
|
|
||||||
|
// TODO The value of totalDue is wrong. How can we get the real total due? invoice.Price is only correct if this is the 2nd payment, not for a 3rd or 4th payment.
|
||||||
|
var totalDue = invoice.Price;
|
||||||
|
|
||||||
|
FakePaymentResponse.AmountRemaining = (totalDue - (BtcAmount * rate)) / rate;
|
||||||
|
FakePaymentResponse.SuccessMessage = "Created transaction " + FakePaymentResponse.Txid;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
FakePaymentResponse.ErrorMessage = e.Message;
|
||||||
|
FakePaymentResponse.AmountRemaining = invoice.Price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FakePaymentResponse.Txid != null)
|
||||||
|
{
|
||||||
|
return Ok(FakePaymentResponse);
|
||||||
|
}
|
||||||
|
return BadRequest(FakePaymentResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("i/{invoiceId}/expire")]
|
||||||
|
public async Task<IActionResult> TestExpireNow(string invoiceId)
|
||||||
|
{
|
||||||
|
if (_NetworkProvider.NetworkType != ChainName.Regtest) return Conflict();
|
||||||
|
|
||||||
|
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||||
|
ExpireInvoiceResponse expireInvoiceResponse = new ExpireInvoiceResponse();
|
||||||
|
|
||||||
|
// TODO complete this
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _InvoiceRepository.UpdateInvoiceExpiry(invoiceId, DateTimeOffset.Now);
|
||||||
|
expireInvoiceResponse.SuccessMessage = "Invoice is now expired.";
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
expireInvoiceResponse.ErrorMessage = e.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expireInvoiceResponse.ErrorMessage == null)
|
||||||
|
{
|
||||||
|
return Ok(expireInvoiceResponse);
|
||||||
|
}
|
||||||
|
return BadRequest(expireInvoiceResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBitcoin.RPC;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
@ -448,7 +449,7 @@ namespace BTCPayServer.Controllers
|
||||||
model.IsModal = true;
|
model.IsModal = true;
|
||||||
return View(nameof(Checkout), model);
|
return View(nameof(Checkout), model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("invoice-noscript")]
|
[Route("invoice-noscript")]
|
||||||
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
|
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
|
||||||
|
|
|
@ -133,6 +133,18 @@ namespace BTCPayServer.Services.Invoices
|
||||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateInvoiceExpiry(string invoiceId, DateTimeOffset dateTimeOffset)
|
||||||
|
{
|
||||||
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
|
||||||
|
if (invoiceData == null)
|
||||||
|
return;
|
||||||
|
// TODO change the expiry time. But how?
|
||||||
|
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ExtendInvoiceMonitor(string invoiceId)
|
public async Task ExtendInvoiceMonitor(string invoiceId)
|
||||||
{
|
{
|
||||||
|
|
109
BTCPayServer/Views/Invoice/Checkout-Testing.cshtml
Normal file
109
BTCPayServer/Views/Invoice/Checkout-Testing.cshtml
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
@model PaymentModel
|
||||||
|
|
||||||
|
<div id="testing">
|
||||||
|
<hr class="my-3" />
|
||||||
|
<form id="test-payment" action="/i/@Model.InvoiceId/test-payment" method="post" class="form-inline my-2">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label for="test-payment-crypto-code" class="control-label">{{$t("Fake a @Model.CryptoCode payment for testing")}}</label>
|
||||||
|
<p class="alert alert-danger" style="display: none; word-break: break-all;"></p>
|
||||||
|
<p class="alert alert-success" style="display: none; word-break: break-all;"></p>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="test-payment-amount" name="amount" type="number" step="any" min="0" class="form-control" placeholder="Amount" value="@Model.BtcDue" aria-label="Amount" aria-describedby="test-payment-crypto-code">
|
||||||
|
<div class="input-group-addon" id="test-payment-crypto-code">@Model.CryptoCode</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">{{$t("Fake Payment")}}</button>
|
||||||
|
<p class="text-muted mt-1">{{$t("This is the same as running bitcoin-cli.sh sendtoaddress xxx")}}</p>
|
||||||
|
</form>
|
||||||
|
<form id="expire-invoice" action="/i/@Model.InvoiceId/expire" method="post" class="mb-1">
|
||||||
|
<button class="btn btn-secondary" type="submit">{{$t("Expire Invoice Now")}} (TODO)</button>
|
||||||
|
</form>
|
||||||
|
<form id="expire-monitoring" action="/i/@Model.InvoiceId/expire-monitoring" method="post" class="mb-1">
|
||||||
|
<!-- TODO only show when expired -->
|
||||||
|
<button class="btn btn-secondary" type="submit">{{$t("Expire Monitoring Now")}} (TODO)</button>
|
||||||
|
</form>
|
||||||
|
<form id="mine-block" action="/i/@Model.InvoiceId/mine-block" method="post">
|
||||||
|
<!-- TODO only show when BTC On-chain -->
|
||||||
|
<!-- TODO to make it work use Bitcoin RPC calls getnewaddress + generatetoaddress -->
|
||||||
|
<button class="btn btn-secondary" type="submit">{{$t("Mine a block now")}} (TODO)</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
let payForm = $('form#test-payment');
|
||||||
|
let loader = $('form#test-payment-loading');
|
||||||
|
let inputField = $('#test-payment-amount');
|
||||||
|
let submitButton = payForm.find('button[type=submit]');
|
||||||
|
let paySuccess = payForm.find('p.alert-success');
|
||||||
|
let payAlert = payForm.find('p.alert-danger');
|
||||||
|
|
||||||
|
payForm.submit(function (e){
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let form = $(this);
|
||||||
|
let data = form.serialize();
|
||||||
|
|
||||||
|
paySuccess.hide();
|
||||||
|
payAlert.hide();
|
||||||
|
loader.show();
|
||||||
|
inputField.prop('disabled', true);
|
||||||
|
submitButton.prop('disabled', true);
|
||||||
|
|
||||||
|
$.post({
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: data,
|
||||||
|
success: function (data,status,xhr){
|
||||||
|
paySuccess.html(data.successMessage);
|
||||||
|
paySuccess.show();
|
||||||
|
inputField.val(data.amountRemaining);
|
||||||
|
if (data.amountRemaining <= 0){
|
||||||
|
// No need to fake any more payments.
|
||||||
|
form.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: function (xhr,status){
|
||||||
|
loader.hide();
|
||||||
|
inputField.prop('disabled', false);
|
||||||
|
submitButton.prop('disabled', false);
|
||||||
|
},
|
||||||
|
error: function (xhr,status,error){
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
payAlert.html(data.errorMessage);
|
||||||
|
payAlert.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expire invoice form
|
||||||
|
let expireForm = $('form#expire-invoice');
|
||||||
|
let expireButton = expireForm.find('[type=submit]');
|
||||||
|
let expireSuccess = expireForm.find('p.alert-success');
|
||||||
|
let expireAlert = expireForm.find('p.alert-danger');
|
||||||
|
|
||||||
|
expireForm.submit(function (e){
|
||||||
|
e.preventDefault();
|
||||||
|
expireSuccess.hide();
|
||||||
|
expireAlert.hide();
|
||||||
|
|
||||||
|
$.post({
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: data,
|
||||||
|
success: function (data,status,xhr){
|
||||||
|
expireSuccess.html(data.successMessage);
|
||||||
|
expireSuccess.show();
|
||||||
|
expireButton.hide();
|
||||||
|
},
|
||||||
|
complete: function (xhr,status){
|
||||||
|
loader.hide();
|
||||||
|
submitButton.prop('disabled', false);
|
||||||
|
},
|
||||||
|
error: function (xhr,status,error){
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
expireAlert.html(data.errorMessage);
|
||||||
|
expireAlert.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,10 +1,13 @@
|
||||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
@inject BTCPayServer.Services.LanguageService langService
|
@inject BTCPayServer.Services.LanguageService langService
|
||||||
|
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||||
|
@using NBitcoin
|
||||||
@model PaymentModel
|
@model PaymentModel
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -116,6 +119,10 @@
|
||||||
<div class="powered__by__btcpayserver">
|
<div class="powered__by__btcpayserver">
|
||||||
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver" rel="noreferrer noopener">BTCPay Server</a>
|
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver" rel="noreferrer noopener">BTCPay Server</a>
|
||||||
</div>
|
</div>
|
||||||
|
@if (BTCPayNetworkProvider.NetworkType == ChainName.Regtest)
|
||||||
|
{
|
||||||
|
<partial name="Checkout-Testing" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
@using BTCPayServer.BIP78.Sender
|
@using BTCPayServer.BIP78.Sender
|
||||||
|
@using NBitcoin
|
||||||
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
||||||
|
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||||
|
|
||||||
<script type="text/x-template" id="bitcoin-method-checkout-template">
|
<script type="text/x-template" id="bitcoin-method-checkout-template">
|
||||||
<div>
|
<div>
|
||||||
|
@ -63,10 +65,10 @@
|
||||||
|
|
||||||
<script type="text/x-template" id="bitcoin-method-checkout-header-template">
|
<script type="text/x-template" id="bitcoin-method-checkout-header-template">
|
||||||
<div class="payment-tabs">
|
<div class="payment-tabs">
|
||||||
<div class="payment-tabs__tab " id="scan-tab" v-on:click="switchTab('scan')" v-bind:class="{ 'active': currentTab == 'scan'}" >
|
<div class="payment-tabs__tab " id="scan-tab" v-on:click="switchTab('scan')" v-bind:class="{ 'active': currentTab == 'scan'}" >
|
||||||
<span>{{$t("Scan")}}</span>
|
<span>{{$t("Scan")}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="payment-tabs__tab" id="copy-tab" v-on:click="switchTab('copy')" v-bind:class="{ 'active': currentTab == 'copy'}" >
|
<div class="payment-tabs__tab" id="copy-tab" v-on:click="switchTab('copy')" v-bind:class="{ 'active': currentTab == 'copy'}" >
|
||||||
<span>{{$t("Copy")}}</span>
|
<span>{{$t("Copy")}}</span>
|
||||||
</div>
|
</div>
|
||||||
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-bitcoin-post-tabs", model = Model})
|
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-bitcoin-post-tabs", model = Model})
|
||||||
|
@ -118,7 +120,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Clipboard Copy
|
// Clipboard Copy
|
||||||
var copySpan = new Clipboard('._copySpan', {
|
var copySpan = new Clipboard('._copySpan', {
|
||||||
|
|
Loading…
Add table
Reference in a new issue