Update to v1.13.5 submodule and 0.1.2 plugin

This commit is contained in:
Adil El Farissi
2024-10-23 19:13:33 +00:00
parent 5b95640110
commit cc6bcb24dc
749 changed files with 20751 additions and 10731 deletions
@@ -1,14 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>OpenSimulator Plugin For BTCPay</Product>
<Description>The OpenSimulator Plugin implement a permissioned public interface to allow safe interactions with the OpenSimulator's virtual worlds LSL scripted objects without exposing sensive data like the Greenfield API keys.</Description>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
</PropertyGroup>
<!-- Plugin development properties -->
@@ -6,6 +6,8 @@ using BTCPayServer.Plugins.OpenSimulator.Models;
using BTCPayServer.Plugins.OpenSimulator.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.OpenSimulator;
@@ -25,8 +27,9 @@ public class UIOpenSimulatorController : Controller
[HttpGet("{storeId}/plugins/opensim")]
public async Task<IActionResult> Index()
{
var data = await _OpenSimulatorService.GetAuthorizations(HttpContext.GetCurrentStoreId());
return View(new OpenSimulatorPageViewModel {
Data = await _OpenSimulatorService.GetAuthorizations(HttpContext.GetCurrentStoreId()),
Data = data,
StoreID = HttpContext.GetCurrentStoreId(),
StoreDefaultPaymetMethod = HttpContext.GetStoreData().GetDefaultPaymentId().ToString(),
StoreDefaultCurrency = HttpContext.GetStoreData().GetStoreBlob().DefaultCurrency,
@@ -14,23 +14,35 @@ using Microsoft.AspNetCore.Routing;
using NicolasDorier.RateLimits;
using BTCPayServer.Plugins.OpenSimulator.Models;
using BTCPayServer.Plugins.OpenSimulator.Services;
using BTCPayServer.HostedServices;
using BTCPayServer.Services.Rates;
using System.Collections.Generic;
using BTCPayServer.Data;
using System.Linq;
using BTCPayServer.Payments;
namespace BTCPayServer.Plugins.OpenSimulator.Controllers
{
public class UIOpenSimulatorPublicController : Controller
{
public UIOpenSimulatorPublicController(UIInvoiceController invoiceController,
StoreRepository storeRepository, OpenSimulatorService openSimService, LinkGenerator linkGenerator)
StoreRepository storeRepository, OpenSimulatorService openSimService, PullPaymentHostedService pullPaymentService, CurrencyNameTable currencyNameTable, IEnumerable<IPayoutHandler> payoutHandlers, LinkGenerator linkGenerator)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
_OpenSimulatorService = openSimService;
_pullPaymentService = pullPaymentService;
_currencyNameTable = currencyNameTable;
_payoutHandlers = payoutHandlers;
_linkGenerator = linkGenerator;
}
private readonly UIInvoiceController _InvoiceController;
private readonly StoreRepository _StoreRepository;
private readonly OpenSimulatorService _OpenSimulatorService;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly CurrencyNameTable _currencyNameTable;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly LinkGenerator _linkGenerator;
@@ -238,5 +250,141 @@ namespace BTCPayServer.Plugins.OpenSimulator.Controllers
InvoiceUrl = url
});
}
[HttpPost("opensim/withdrawals")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> OpenSimWithdrawalHandle([FromHeader] OpenSimAuthorizationData obj, OpenSimulatorWithdrawalData request)
{
var store = await _StoreRepository.FindStore(obj.StoreId);
if (store == null)
return Json(new {
ospError = "Invalid store"
});
var a = await _OpenSimulatorService.isAuthorized(obj.StoreId, obj.AvatarId, obj.ObjectId, obj.AvatarHomeURL, obj.ObjectURL);
if(a == null)
return Json(new {
ospError = "This avatar or object is not authorized to create invoices in this store."
});
if (request is null)
{
return Json(new {
ospError = "Missing body"
});
}
if (request.Amount <= 0.0m)
{
return Json(new {
ospError = "The amount should more than 0."
});
}
if (request.Name is String name && name.Length > 50)
{
return Json(new {
ospError = "The name should be maximum 50 characters."
});
}
if (request.Currency is String currency)
{
request.Currency = currency.ToUpperInvariant().Trim();
if (_currencyNameTable.GetCurrencyData(request.Currency, false) is null)
{
return Json(new {
ospError = "Invalid currency"
});
}
}
else
{
return Json(new {
ospError = "Currency field is required"
});
}
PaymentMethodId[] paymentMethods = null;
if (request.PaymentMethodId is { } paymentMethodsStr)
{
paymentMethods = paymentMethodsStr.Select(s =>
{
PaymentMethodId.TryParse(s.ToString(), out var pmi);
return pmi;
}).ToArray();
var supported = (await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData())).ToArray();
for (int i = 0; i < paymentMethods.Length; i++)
{
if (!supported.Contains(paymentMethods[i]))
{
return Json(new {
ospError = "Invalid or unsupported payment method"
});
}
}
}
else
{
return Json(new {
ospError = "payment method field is required"
});
}
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
{
Name = request.Name,
Description = request.Description,
Amount = request.Amount,
Currency = request.Currency,
StoreId = obj.StoreId,
PaymentMethodIds = paymentMethods,
AutoApproveClaims = false
});
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
if (pp is null)
{
return Json(new {
ospError = "Failed to create a pull payment"
});
}
var ppBlob = pp.GetBlob();
var paymentMethodId = ppBlob.SupportedPaymentMethods[0];
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
if (payoutHandler is null)
{
return Json(new {
ospError = "Invalid payment method"
});
}
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob, CancellationToken.None);
if (destination.destination is null)
{
return Json(new {
ospError = "The destination is invalid for the specified payment"
});
}
var result = await _pullPaymentService.Claim(new ClaimRequest()
{
Destination = destination.destination,
PullPaymentId = pp.Id,
Value = request.Amount,
PaymentMethodId = paymentMethodId
});
if (result is null)
{
return Json(new {
ospError = "Failed to create a payout"
});
}
return Json(new {
PullPaymentId = pp.Id,
ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment),
"UIPullPayment",
new { pullPaymentId = pp.Id },
Request.Scheme,
Request.Host,
Request.PathBase)
});
}
}
}
@@ -23,4 +23,6 @@ public class OpenSimulatorData
public DateTimeOffset Timestamp { get; set; }
public string Secret { get; set; }
}
@@ -29,7 +29,8 @@ namespace BTCPayServer.Plugins.OpenSimulator.Migrations
ObjectLocation = table.Column<string>(nullable: false),
ObjectURL = table.Column<string>(nullable: false),
ObjectAuthorization = table.Column<bool>(nullable: false),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
Timestamp = table.Column<DateTimeOffset>(nullable: false),
Secret = table.Column<string>(nullable: false)
},
constraints: table =>
{
@@ -39,9 +40,9 @@ namespace BTCPayServer.Plugins.OpenSimulator.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
/* migrationBuilder.DropTable(
migrationBuilder.DropTable(
name: "Authorizations",
schema: "BTCPayServer.Plugins.OpenSimulator");*/
schema: "BTCPayServer.Plugins.OpenSimulator");
}
}
}
@@ -0,0 +1,50 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Plugins.OpenSimulator.Migrations
{
[DbContext(typeof(OpenSimulatorDbContext))]
[Migration("20241117073744_AddSecret")]
public partial class AddSecret : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "BTCPayServer.Plugins.OpenSimulator");
migrationBuilder.CreateTable(
name: "Authorizations",
schema: "BTCPayServer.Plugins.OpenSimulator",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreId = table.Column<string>(nullable: false),
AvatarName = table.Column<string>(nullable: false),
AvatarId = table.Column<string>(nullable: false),
AvatarHomeURL = table.Column<string>(nullable: false),
ObjectName = table.Column<string>(nullable: false),
ObjectId = table.Column<string>(nullable: false),
ObjectRegion = table.Column<string>(nullable: false),
ObjectLocation = table.Column<string>(nullable: false),
ObjectURL = table.Column<string>(nullable: false),
ObjectAuthorization = table.Column<bool>(nullable: false),
Timestamp = table.Column<DateTimeOffset>(nullable: false),
Secret = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authorizations", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Authorizations",
schema: "BTCPayServer.Plugins.OpenSimulator");
}
}
}
@@ -15,7 +15,7 @@ namespace BTCPayServer.Plugins.OpenSimulator.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("BTCPayServer.Plugins.OpenSimulator")
.HasAnnotation("ProductVersion", "0.1.1");
.HasAnnotation("ProductVersion", "0.1.2");
modelBuilder.Entity("BTCPayServer.Plugins.OpenSimulator.Data.OpenSimulatorData", b =>
{
@@ -58,6 +58,9 @@ namespace BTCPayServer.Plugins.OpenSimulator.Migrations
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
@@ -20,6 +20,8 @@ public class OpenSimAuthorizationFormData
public DateTimeOffset Timestamp { get; set; }
public string Secret { get; set; }
public string Task { get; set; }
}
@@ -0,0 +1,14 @@
using System;
using BTCPayServer.Payments;
namespace BTCPayServer.Plugins.OpenSimulator.Models;
public class OpenSimulatorWithdrawalData
{
public string StoreId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public PaymentMethodId[] PaymentMethodId { get; set; }
public string Destination { get; set; }
}
@@ -10,7 +10,7 @@ public class OpenSimulatorPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.11.7" }
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.13.5" }
};
public override void Execute(IServiceCollection services)
@@ -24,5 +24,6 @@ public class OpenSimulatorPlugin : BaseBTCPayServerPlugin
OpenSimulatorDbContextFactory factory = provider.GetRequiredService<OpenSimulatorDbContextFactory>();
factory.ConfigureBuilder(o);
});
services.AddControllers().AddXmlSerializerFormatters();
}
}
@@ -1,7 +1,7 @@
$("#SectionNav .nav .nav-link").on("click", function(e){
if($(this).attr("id") != "SectionNav-Index"){
if($(this).attr("id") != "SectionNav-Index" && $(this).attr("id") != "osDocumentation"){
e.preventDefault();
$("#SectionNav .nav .nav-link").removeClass("active");
$(".osPage").hide();
@@ -43,7 +43,7 @@ function getItemHtml(item, action){
}
function LoadAutorizations(){
if(osModel.length == 0){
if(osModel == null || osModel.length == 0){
$("#authorizedObjects tbody, #pendingAutorizations tbody").html('<tr><td>No data to display.</td></tr>');
}else{
var pendingAutorizationsHtml = '';
@@ -120,7 +120,7 @@ $("#authorizationModal").on("hide.bs.modal", () =>{
});
$("#scriptsNav li").on("click", function(e){
if($(this).attr("id") != "SectionNav-Index"){
if($(this).attr("id") != "SectionNav-Index" && $(this).attr("id") != "osDocumentation"){
e.preventDefault();
$("#scriptsNav li").removeClass("bg-yellow");
$(".osScriptBox").hide();
@@ -131,7 +131,7 @@ $("#scriptsNav li").on("click", function(e){
function updateTipjarScript (){
var tipjarHtml = '';
tipjarHtml += '/* BTCPay Server Crypto Tip-jar Script for OpenSimulator Plugin v0.1.1.\n\n';
tipjarHtml += '/* BTCPay Server Crypto Tip-jar Script for OpenSimulator Plugin v0.1.2.\n\n';
tipjarHtml += 'THIS SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n';
tipjarHtml += 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n';
@@ -623,7 +623,7 @@ $(".vDisplayCurrency, .rbDisplayCurrency").html('<option value="'+ defaultCurren
function updateVendorScript(){
var vendorHtml = '';
vendorHtml += '/* BTCPay Server Single Product Vendor Script for OpenSimulator Plugin v0.1.1.\n\n';
vendorHtml += '/* BTCPay Server Single Product Vendor Script for OpenSimulator Plugin v0.1.2.\n\n';
vendorHtml += 'THIS SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n';
vendorHtml += 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n';
@@ -1154,7 +1154,7 @@ $("#vDisplayCurrency, #vItemPrice, #vNotificationEmail, #vRedirectURL, #vCheckou
function updateRentalBoxScript(){
var rentalBoxHtml = '';
rentalBoxHtml += '/* BTCPay Server Parcels Rental Script for OpenSimulator Plugin v0.1.1.\n\n';
rentalBoxHtml += '/* BTCPay Server Parcels Rental Script for OpenSimulator Plugin v0.1.2.\n\n';
rentalBoxHtml += 'THIS SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n';
rentalBoxHtml += 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n';
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Plugins.OpenSimulator.Data;
using BTCPayServer.Plugins.OpenSimulator.Models;
@@ -31,7 +32,8 @@ public class OpenSimulatorService
ObjectLocation = model.ObjectLocation,
ObjectURL = model.ObjectURL,
ObjectAuthorization = false,
Timestamp = DateTimeOffset.UtcNow });
Timestamp = DateTimeOffset.UtcNow,
Secret = NBitcoin.RandomUtils.GetUInt256().ToString().Substring(0, 9) });
var res = await context.SaveChangesAsync();
if(res > 0){
return true;
@@ -65,6 +67,22 @@ public class OpenSimulatorService
context.Update(osdModel);
return await context.SaveChangesAsync();
}
public async Task <int> UpdateSecret(OpenSimAuthorizationFormData model)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
OpenSimulatorData osdModel = await context.Authorizations.Where(i => i.Id == model.Id && i.StoreId == model.StoreId && i.AvatarId == model.AvatarId && i.ObjectId == model.ObjectId).FirstOrDefaultAsync();
if (string.IsNullOrEmpty(osdModel.Id))
{
return 0;
}
osdModel.Secret = NBitcoin.RandomUtils.GetUInt256().ToString().Substring(0, 9);
context.Update(osdModel);
return await context.SaveChangesAsync();
}
public async Task<List<OpenSimulatorData>> GetDestinationsGuide()
{
await using var context = _OpenSimDbContextFactory.CreateContext();
@@ -20,14 +20,14 @@
@section PageFootContent {
<script src="~/Resources/js/opensimulator.js" asp-append-version="true"></script>
}
<img id="opensimLogo" src="~/Resources/img/opensimLogo.png" style="position: relative;top:-10px;width:64px" asp-append-version="true">&nbsp;<h2 style="display: inline;">@ViewData["Title"]</h2>&nbsp;&nbsp;<span class="ospVersion">v0.1.1</span>
<img id="opensimLogo" src="~/Resources/img/opensimLogo.png" style="position: relative;top:-10px;width:64px" asp-append-version="true">&nbsp;<h2 style="display: inline;">@ViewData["Title"]</h2>&nbsp;&nbsp;<span class="ospVersion">v0.1.2</span>
<div class="sticky-header mb-l">
<nav id="SectionNav">
<div class="nav">
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(OpenSimulatorNavPages.Index))" class="nav-link @ViewData.IsActivePage(OpenSimulatorNavPages.Index)" asp-controller="UIOpenSimulator" asp-action="Index" asp-route-storeId="@Model.StoreID">Authorizations Manager</a>
<a id="osScripts" href="" class="nav-link">LSL Scripts</a>
<a id="osDocumentation" href="" class="nav-link">Documentation</a>
<a id="osCredits" href="" class="nav-link">Credits</a>
<a id="osDocumentation" href="https://github.com/AdilElFarissi/btcpay-opensimulator-plugin/wiki" target="_blank" rel="noreferrer" class="nav-link">Documentation</a>
<vc:ui-extension-point location="store-nav" model="@Model"/>
</div>
</nav>
@@ -88,7 +88,7 @@
</div>
</div>
<script type="text/javascript" nonce="@nonce">
var osModel = @Html.Raw(JsonConvert.SerializeObject(Model.Data.OrderByDescending(t => t.Timestamp)));
var osModel = @Html.Raw(JsonConvert.SerializeObject(Model.Data.OrderByDescending(t => t.Timestamp)));
var StoreId = "@Model.StoreID";
var defaultPaymentMethod = "@Model.StoreDefaultPaymetMethod";
var defaultCurrency = "@Model.StoreDefaultCurrency";
@@ -337,552 +337,4 @@
</div>
</div>
</div>
<div id="osDocumentationPage" class="osPage" style="display: none;">
<h5>Opensimulator Plugin <span class="ospVersion">v0.1.1</span> Documentation</h5>
<hr>
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse1" aria-expanded="true">About Opensimulator Plugin:</a>
</h6>
</div>
<div id="collapse1" class="panel-collapse collapse show">
<div class="panel-body">
<p>
This plugin targets an audiance that already have a bit of experience not only with the technical part of Opensimulator but also with the HyperGrid economy and the ways to conduct an inWorld business... In majority standalones or grids operators, 3D hosting providers, contents creators and merchants, many educative services providers and all kind of non-profit entities and organizations.
</p>
<p>
If this is not your case, please, give to yourself a bit of time to learn about the <a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator software</a> and the 3D Web of Virtual Worlds known as the <a href="https://www.hypergridbusiness.com/category/metaverse/" target="_blank">HyperGrid</a> (HG). You can also setup a local sandbox to practice the multiple edition arts including the scripting or <a href="https://opensimworld.com/dir" target="_blank">explore the HyperGrid</a> by creating an account in an HG enabled grid like <a href="https://osgrid.org/" target="_blank">osGrid</a> where you can meet <a href="http://opensimulator.org/wiki/Office_hours">the Opensimulator DEV team</a> and practice using their public sandbox.
</p>
<span>That said...</span>
<p>
The Opensimulator plugin add to BTCPay a permissioned public interface similar to the Pay Button but adapted for the Opensimulator usages and allow:
<br>- Only 3D objects from a virtual world to interact with the plugin.
<br>- Only the 3D objects that the store owner authorize in the "Authorizations Manager" section to create invoices in her/his store.
<br>- Only a maximum of 3 pending authorizations per store.
</p>
<p>
The scripts samples in the "LSL Scripts" section was made to interact with this plugin and handle the inWorld part like the objects registration and the invoices requests. Also handles the delivery of the purchased product / service after a successful payment (paid and confirmed) and the exceptions / failures.
<br>The following video is a proof of work and concept that show the scripts and the Opensimulator plugin in action:
<br><a href="https://www.youtube.com/watch?v=NTb8PPp0qco&vq=hd720" target="_blank"><i class="fa fa-youtube-play" style="color: #f00;font-size:16px;margin-right:5px;"></i><b>Youtube video Introduction - Proof of work and concept.</b></a><br>
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse2" aria-expanded="false">Setting Your Opensimulator Sandbox:</a>
</h6>
</div>
<div id="collapse2" class="panel-collapse collapse">
<div class="panel-body">
<div class="alert alert-info" style="margin-left: 0;"><i>This part of the docu was add for the BTCPay Dev team audit and may be removed in the release version because the Opensimulator users already know all this...</i></div>
<p>
The best way to try and audit the plugin and the included scripts is setting up your own virtual world where you can have the full permissions and the Estate Owner rights. To do this, you need for a minimalist DEV and testing environement to:
</p>
<ul>
<li>Install "mono" if you have a linux OS or Mac</li>
<li>Download and unpack the last version of <a href="http://opensimulator.org/wiki/Main_Page" target="_blank">the Opensimulator binaries</a>.</li>
<li>Download and install the last version of Firestorm Viewer for Opensimulator ( <a href="https://www.firestormviewer.org/windows-for-open-simulator/" target="_blank">Windows</a> - <a href="https://www.firestormviewer.org/linux-for-open-simulator/" target="_blank">Linux</a> - <a href="https://www.firestormviewer.org/mac-for-open-simulator/" target="_blank">Mac</a> ).</li>
</ul>
<p>
<div class="alert alert-warning" style="margin-left: 0;"><i>NOTE: The following instructions are for testing only and is not the best way to setup Opensimulator!
<br>If you want to start a production virtual world, use the Diva distro version (Documentation is included as .txt files).
</i></div>
</p>
<p>
When done, navigate to ..\opensim-x.x.x.x\bin folder...<br>
If your BTCPay Server instance is not under a local or LAN IP (DEV mode), you have just to double click the OpenSim.exe in windows or open a console in the bin folder and type "mono OpenSim.exe" and hit Enter in linux.
</p>
<p>
If your BTCPay Server instance is under 127.0.0.1:14142 or a LAN IP, you have to add an exception to allow the LSL scripts to interact with the local/LAN IP and ports... To do, open the OpenSim.ini file in the bin folder, scroll down to the [Network] section and add the following line to the end of the section (Read the section comments for more infos). Change "127.0.0.1:14142"&nbsp;&nbsp;by&nbsp;&nbsp;"Your-LAN-IP:14142" if needed.
<pre><code>OutboundDisallowForUserScriptsExcept = 127.0.0.1:9000|127.0.0.1:14142</code></pre>
Save & close OpenSim.ini and double click the OpenSim.exe in windows or open a console in the bin folder and type "mono OpenSim.exe" and hit Enter in linux.
</p>
<p>
In the first Opensimulator startup, the OpenSim console will ask you to set an Estate and Region names, the first / last names and password of the admin user (important to set)... you can just hit enter for the optional settings and wait... the console will show you when your virtual world is ready to use.
<br>At the very end, when you see (Region root #) in the console, type the following command and hit Enter:
<pre><code>terrain fill 21</code></pre>
</p>
<p>
Now is time to run Firestorm and login into your local sandbox. So double click the Firestorm icon to start it, expand the "Grid" list in the bottom bar and select "the lost continent of hippo". If you don't have it in the grids list, click CTRL+P to open the settings window and click "Opensim" at left. In the top of the tab type in the "Add new grid" field:
<pre><code>127.0.0.1:9000</code></pre>
click "Apply" & "Ok". Now you will see "the lost continent of hippo" in the grids list, select it and type the first / last names that you have set in the OpenSim console separated by a space, type your password and click the Log in button. If you did it right, you are now driving the default avatar in your region and ready to follow the video guides and try the sample scripts in the "LSL Scripts" section.
</p>
<p>
<div class="alert alert-info" style="margin-left: 0;"><i>NOTE: If you want to set the menus like the video guides, click CTRL+P to open the settings window and type "pie menu" in the top search field and click "User Interface" at left. Unselect "Use Pie Menu" and click "Ok".</i>
</div>
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse3" aria-expanded="false">Opensimulator Plugin And Scripts Workflow:</a>
</h6>
</div>
<div id="collapse3" class="panel-collapse collapse">
<div class="panel-body">
<p>
In Opensimulator, a <a href="https://wiki.secondlife.com/wiki/Getting_started_with_LSL" target="_blank">Linden Scripting Language (LSL) script</a> is the the source code of a .DLL that when compiled (on save), acts and works like a plugin attached to the object where the script is, extending it's capabilities
to allow all kind of interactions, animations and control.
</p>
<p>
Between the LSL features, there is the <a href="https://wiki.secondlife.com/wiki/Category:LSL_HTTP" target="_blank">HTTP I/O</a> that we use to interconnect an inWorld object with a BTCPay instance and the main role of the Opensimulator plugin is to secure a bit
this interactions with a simple permissions layer that only allow the invoices creation to the authorized 3D Objects / Avatars without exposing sensive data like the Greenfield API keys and protect your BTCPay instance / store in same time.
So, let's see how this works...
</p>
<p>
Because the Opensimulator plugin may have to handle 100s if not 1000s objects, is not a bad idea to divide the load by 2 public endpoints:
</p>
<span>One for the authorizations process (heavy load):</span>
<pre><code>BTCPayServerURL/opensim/authorization</code></pre>
<span>And the other for the invoicing part (limited load in the script level):</span>
<pre><code>BTCPayServerURL/opensim/invoices</code></pre>
<p>
The first thing that the script do on starting or when the object is clicked is asking the plugin about the object's status using the authorization() function with the "check" parameter.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
*/</span>
<span class="commented">/* Request the object status when the script start...*/</span>
<a href="https://wiki.secondlife.com/wiki/State_entry" target="_blank" >state_entry</a>(){
<span style="color: #f00;">authorization("check")</span>;
}
<span class="commented">/* Request the object status when the object is clicked by the object's owner...*/</span>
<a href="https://wiki.secondlife.com/wiki/Touch_start" target="_blank" >touch_start</a>(integer a){
if(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a> == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
avatar = <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>;
<span style="color: #f00;">authorization("check")</span>;
}
else{
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>,"You are not the owner of this item!");
}
}
</code></pre>
<p>
The plugin returns a Json that drive what the object will show, allow and do in the next steps. The plugin use the <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a> headers to get/set the object's data and verify if is authorized or no. The authorization() function looks like this:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string storeID
key authorizationRequest_id
*/</span>
<span style="color: #f00;">authorization(string action)</span>{
authorizationRequest_id = <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a>(
BTCPayServerURL +"/opensim/authorization",
[
HTTP_METHOD,"POST",
HTTP_MIMETYPE,"application/x-www-form-urlencoded",
HTTP_BODY_MAXLENGTH,16384,
HTTP_VERIFY_CERT,FALSE, <span class="commented">/* FALSE for 127.0.0.1 else TRUE */</span>
HTTP_CUSTOM_HEADER,"x-opensim-store-id",storeID,
HTTP_CUSTOM_HEADER,"x-opensim-owner-home-url", <a href="http://opensimulator.org/wiki/OsGetAvatarHomeURI" target="_blank" >osGetAvatarHomeURI</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner</a>()),
HTTP_CUSTOM_HEADER,"x-opensim-object-host-url", <a href="http://opensimulator.org/wiki/OsGetGridHomeURI" target="_blank" >osGetGridHomeURI</a>()
],
"action=" + action );
}</code></pre>
<p>
The result of the authorization() function is captured by <a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a> event handler.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
key authorizationRequest_id
*/</span>
<span class="commented">/* Capture the plugin response to authorization("check") and perform actions...*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == authorizationRequest_id){
if(<a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(Response,"ospError",0) == -1){ <span class="commented">/* if not an "ospError" return */</span>
string authStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["status"]);
string authorized = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["authorized"]);
if(authStatus == "registred" && authorized == "True"){
<span class="commented">/* Here our object is authorized and you can start the invoicing process.
In the included scripts, we switch to the authorized state that handles the invoicing. */</span>
<a href="https://wiki.secondlife.com/wiki/State" target="_blank" >state</a> authorized;
}
else if(authStatus == "registred" && authorized == "False"){
<span class="commented">/* Here the object is already registred but not authorized yet...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nMissing Authorization",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is registred and waiting your authorization.\n\nPlease open this page and authorize this object. When done, click this object again to enable it...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
else if(authStatus == "success" && authorized == "False"){
<span class="commented">/* Here the object was successfully registred in the plugin side but not authorized yet...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nMissing Authorization",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is registred and waiting your authorization.\n\nPlease open this page and authorize this object. When done, click this object again to enable it...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
else if(authStatus == "unknown"){
<span class="commented">/* Here the object is new and unknown to the plugin...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nUnknown Object",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is not linked to your BtcPay store yet!\n\nPlease, click [Register] to start the linking and authorization process...",["Register","Cancel"],channel);
}
}
else if(authStatus == "fail" && authorized == "False"){
<span class="commented">/* Here the registration in the plugin side failed...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object failed to register!\n\nPlease open this page and investigate the problem and try again...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
}
else{
<span class="commented">/* Show the plugin errors if there is...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nError: \n" + <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["ospError"]));
}
}
}
else{
<span class="commented">/* Show errors in case of problems with your BTCPay server...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nError: \nDetected a problem with your BTCPay instance!\nStatus: " + status);
}
}
</code></pre>
<p>
For the new objects, the plugin returns the following Json that set the object's hover text to <span style="color: #f00;">"Out Of Service - Unknown Object"</span> and makes the object only useable by it's owner.
Also, clicking the object opens a blue box ( <a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a> ) that invite the owner to register this object...
</p>
<pre><code>{"status":"unknown"}</code></pre>
<p>
When the object's owner click the [Register] button in the blue box ( <a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a> ), the script execute the authorization("register") function witn the "register" parameter under the <a href="https://wiki.secondlife.com/wiki/Listen" target="_blank" >listen</a> event handler:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
*/</span>
<a href="https://wiki.secondlife.com/wiki/Listen" target="_blank" >listen</a>(integer channel, string name, key id, string Box){
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a> && Box == "Register"){
<span style="color: #f00;">authorization("register")</span>;
}
}
</code></pre>
<p>
and the plugin return one of the 2 possible status:
</p>
<pre><code>{"status":"success","authorized":"False"}</code></pre>
Or
<pre><code>{"status":"fail","authorized":"False"}</code></pre>
<ul>
<li>
<b>"success":</b> means that the object was successfully saved in the plugin database and appear in the "Pending Authorizations" box in the "Authorizations Manager" section. Now the object's hover text show <span style="color: #f00;">"Out Of Service - Missing Authorization"</span> and the click open a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the owner to open her/his BTCPay store > plugin to authorize the object...
</li>
<li>
<b>"fail":</b> mean something gose wrong in the registration part. The object' hover text will show <span style="color: #f00;">"Out Of Service - Fatal Error"</span> and the click open a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the owner to open her/his BTCPay store > plugin to investigate and fix...
</li>
</ul>
<p>
After a successful registration, the click on the object fire the authorization("check") but the returned Json will be:
</p>
<pre><code>{"status":"registred","authorized":"False"}</code></pre>
<p>
Now, the object's owner have to login to her/his store > plugin, verify the object data and click the corresponding [Authorize] button. After that the object will appear in the "Authorized Objects" box and is allowed to request invoices from the owner's store.
<br>This manual intervention proof that the user is a human and she/he is the owner of both: the used store and the inWorld object.
<br><br>
After a successful authorization, the script restart or the object click fires the authorization("check") and the return will be:
</p>
<pre><code>{"status":"registred","authorized":"True"}</code></pre>
That it for the authorization part... <br><br>
<div class="alert alert-warning" style="margin-left: 0;"><i>Note: For security reasons, the plugin do not allow more than 3 pending authorizations requests per store and the owner have to remove or authorize the pending objects to be able to add more objects registrations.</i>
</div>
<p>For the invoicing part, the scripts do the majority of the job in term of verifications and the invoice request building and have a dedicated state (authorized) and a function. But before all, is important to know how the script protect itself and your store.
</p>
For each use, the script:
<ul>
<li>Allow only one avatar at time (current user) to request an invoice and interact with the object while processing.</li>
<li>Allow only one invoice per avatar at time. If the current user click the object while processing, a blue box will appear inviting her/him to open the current invoice page.</li>
<li>Request from Opensimulator a single time use and destructible endpoint URL using <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a> to capture the BTCPay notifications.
The life and the validity time of the generated endpoint depends on the invoice status and your store settings. The current endpoint is destroyed by <a href="https://wiki.secondlife.com/wiki/LlReleaseURL" target="_blank" >llReleaseURL()</a> in the reset() function when the invoice status is "expired" or "confirmed" or when the invoice was marked ("invalid" or "complete"). </li>
<li>Generate a single time use random UUID and set it as "orderId" in the invoice parameters. The orderID play the role of a session ID and is destroyed by the reset() function.</li>
<li>Check if the IP of the incoming notifications sender is the same as the "allowedHttpInIP" in the global variables.</li>
<li>Check if the notification "url" start with the "BTCPayServerURL" that you have set in the global variables.</li>
<li>Check if the notification "invoiceId" and the "orderId" are the same as the inMemory invoiceID and orderID.</li>
<li>Reset the script / object and purge all the current invoice data at the end of the processing.</li>
<li>Handles errors and make the object "Out Of Service" if needed.</li>
</ul>
<p>
So, let's see this as LSL code by extending a bit the previous code...
<br> In this example we will chain (on click) the authorization() and the invoice request using the plugin return in case of an authorized object to call the requestInvoice() function.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key authorizationRequest_id
key IPNEndpointRequest_id
key avatar
string invoiceID
string invoiceURL
string orderID
string txStatus
integer active
*/</span>
<a href="https://wiki.secondlife.com/wiki/Touch_start" target="_blank" >touch_start</a>(integer n){
if (avatar == NULL_KEY){
avatar = <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>;
active = TRUE;
<span style="color:#f00;">authorization("check")</span>;
}
else if (avatar != <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>){
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>, "This device is in use!/nPlease wait...");
}
else if (active && avatar == <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a> && txStatus != ""){
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nTransaction in progress!\nYour invoice ID is:\n"+ invoiceID +"\\nInvoice page at:\\n"+ invoiceURL +"\nPlease wait...");
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(avatar, "Click to open the payment page.\nYour invoice ID is : "+ invoiceID +"\\nThank you for your payment!", invoiceURL);
}
}
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == authorizationRequest_id){
if(<a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(Response,"ospError",0) == -1){ <span class="commented">/* if not an Error */</span>
string authStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["status"]);
string authorized = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["authorized"]);
if(authStatus == "registred" && authorized == "True"){
<span class="commented">/* We request our endpoint to capture the BTCPay notifications and is set later under http_request event */</span>
IPNEndpointRequest_id = <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a>;
<span class="commented">/* We cast a random key to string and set the invoice parameter "orderId". */</span>
orderID = (string)<a href="https://wiki.secondlife.com/wiki/LlGenerateKey" target="_blank" >llGenerateKey()</a>;
<span class="commented">/* If all the other invoice parameters are set, we fire the requestInvoice() function. */</span>
<span style="color: #f00;">requestInvoice()</span>;
txStatus = "new";
}
}
}
}
}
</code></pre>
<p>
The endpoint URL and the BTCPay notifications are captured by the <a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a> event handler.
For example we get the endpoint URL requested by the <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a> and set the invoice parameter "serverIpn" like this:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key IPNEndpointRequest_id
string IPNEndpointURL
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a>(key id, string method, string body){
if (id == IPNEndpointRequest_id && method == URL_REQUEST_GRANTED){
IPNEndpointURL = body;
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
}
</code></pre>
<p>
The requestInvoice() function build and POST the invoice request to your BTCPay instance and must be called only after setting or populating the invoice parameters from the the script variables:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string storeID
string currency
string price
string defaultPaymentMethod
string orderID
string IPNEndpointURL
string checkoutDesc
string notificationEmail
string redirectURL
string checkoutQueryString
key requestInvoice_id
*/</span>
<span style="color: #f00;">requestInvoice()</span>{
string formData = "";
formData += "storeId="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(storeID);
formData += "&price="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(price);
formData += "&<span>currency</span>="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(currency);
formData += "&defaultPaymentMethod="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(defaultPaymentMethod);
formData += "&orderId="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(orderID);
formData += "&serverIpn="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(IPNEndpointURL);
if (checkoutDesc != ""){
formData += "&checkoutDesc="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(checkoutDesc);
}
if (redirectURL != ""){
formData += "&browserRedirect="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(redirectURL);
}
if (notificationEmail != ""){
formData += "&<span>notifyEmail</span>="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(notificationEmail);
}
if (checkoutQueryString != ""){
formData += "&checkoutQueryString="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(checkoutQueryString);
}
string posData = "{
'AppType':'AppNameOrReference',
'PayerName':'"+ <a href="http://opensimulator.org/wiki/OsKey2Name" target="_blank" >osKey2Name</a>(avatar) +"',
'PayerUUID':'"+ (string)avatar +"',
'ObjectName':'"+ <a href="https://wiki.secondlife.com/wiki/LlGetObjectName" target="_blank" >llGetObjectName()</a> +"',
'ObjectUUID':'"+ <a href="https://wiki.secondlife.com/wiki/LlGetKey" target="_blank" >llGetKey()</a> +"'
}";
formData += "&posData=" + <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(posData);
requestInvoice_id = <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a>(
BTCPayServerURL +"/opensim/invoices",
[
HTTP_METHOD,"POST",
HTTP_MIMETYPE,"application/x-www-form-urlencoded",
HTTP_BODY_MAXLENGTH,16384,
HTTP_VERIFY_CERT,FALSE, <span class="commented">/* FALSE for 127.0.0.1 else TRUE */</span>
HTTP_CUSTOM_HEADER,"x-opensim-store-id",storeID,
HTTP_CUSTOM_HEADER,"x-opensim-owner-home-url", <a href="http://opensimulator.org/wiki/OsGetAvatarHomeURI" target="_blank" >osGetAvatarHomeURI</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>),
HTTP_CUSTOM_HEADER,"x-opensim-object-host-url", <a href="http://opensimulator.org/wiki/OsGetGridHomeURI" target="_blank" >osGetGridHomeURI</a>()
],
formData );
}</code></pre>
<p>
In the plugin side, for each incoming invoice request, the plugin verify the following points:
</p>
<ul>
<li>If the store ID is valid...</li>
<li>If the price is not zero and the script's endpoint and the order ID are not empty...</li>
<li>If the request headers contain some <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a> specific headers needed for the authorization verification...</li>
<li>If the the avatar and the object from their respective origins are authorized to create invoices in the store...</li>
<li>If the posData is under 500 characters...</li>
</ul>
<p>
If all is OK, the plugin use the form data to create the invoice and return the invoice ID and URL that are handled under <a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>
and open a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the user to open the invoice page.</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
key requestInvoice_id
string invoiceID
string invoiceURL
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == requestInvoice_id){
invoiceID = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response, ["invoiceId"]);
invoiceURL = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response, ["invoiceUrl"]);
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(avatar, "Click to open the payment page.\nYour invoice ID is : "+ invoiceID, invoiceURL);
}
}
else {
llOwnerSay("\nStatus :"+ status +"\nError : \n"+ Response);
reset();
}
}
</code></pre>
<p>
At this point of the processing, the script is in wait of the user payment and the BTCPay notifications to perform some verifications and actions based on the notification data.
The notifications are captured by the <a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a> event handler:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string allowedHttpInIP
key avatar
string invoiceID
string orderID
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a>(key id, string method, string body){
if (<a href="https://wiki.secondlife.com/wiki/LlGetHTTPHeader" target="_blank" >llGetHTTPHeader</a>(id, "x-remote-ip") == allowedHttpInIP && <a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(<a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body,["url"]), BTCPayServerURL,0) > -1){
string invoiceOrderId = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["orderId"]);
string invoiceId = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["id"]);
string invoiceStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["status"]);
if (method == "POST" && invoiceOrderId == orderID && invoiceId == invoiceID){
if(invoiceStatus == "expired" || invoiceStatus == "invalid"){
<span class="commented">/* The invoice expired or marked invalid. We inform the owner and the user... */</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "Operation Fail!\nInvoice ID: "+ invoiceID +" status is "+ invoiceStatus +".\nPlease try again or contact "+ llKey2Name(llGetOwner()) +" and provide your invoice ID: \n"+ invoiceID +"\nif you did a payment.");
if (notifications){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nWarning: Invoice ID:\n"+ invoiceID +" Fail!\nStatus is "+ invoiceStatus);
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
<span class="commented">/* And the script reset the object and make it available for the next user. */</span>
<span style="color: #f00;">reset()</span>;
}
else if (invoiceStatus == "paid"){
<span class="commented">/* The invoice was paid but not fully confirmed yet... We inform the user and the owner...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nThank you for your...\nInvoice ID: "+ invoiceID +" is paid and in wait of the usual confirmations.");
if (notifications){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\n"+ <a href="http://opensimulator.org/wiki/OsKey2Name" target="_blank" >osKey2Name</a>(avatar) +" donated "+ price +" "+ currency +"!\nYou have recived: "+ llJsonGetValue(body, ["btcPrice"]) +" "+ defaultPaymentMethod +".\nInvoice ID: "+ invoiceID +" Paid (waiting confirmations).");
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
else if (invoiceStatus == "confirmed" || invoiceStatus == "complete"){
<span class="commented">/* Now is the delivery time! The invoice was fully paid and confirmed. We inform the user and the owner and the object must do what is supposed to do after a successful payment. */</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nThank you for your...(purchase, donation...)\nInvoice ID: "+ invoiceID +" was fully paid and confirmed.");
if (notifications == TRUE){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nInvoice ID: " + invoiceID +" Confirmed.");
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
<span class="commented">/* The End :) The invoice was successfully handled and done. Now is time to reset the object and make it available for the next user. */</span>
<span style="color: #f00;">reset()</span>;
}
}
}
<span class="commented">/* Just return status 200 to any */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
</code></pre>
<p>And that it!<br>
The scripts in the "LSL Scripts" section was made with 2 states and each handle a part of the process. So, you have just to read the <b><i>default</i></b> state to get the authorization part and the <b><i>authorized</i></b> state to get the invoicing part.<br>You can also merge both states in one and adapt them to your own cases of use.
</p>
<p>Thanks to BTCPay, the adoption of cryptocurrencies in the HyperGrid become a question of choice and not a technical matter.
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse4" aria-expanded="false">Version Notes & Changelog:</a>
</h6>
</div>
<div id="collapse4" class="panel-collapse collapse">
<div class="panel-body">
<span class="commented">v0.1.1 Pre-Release:</span>
<p>This is the very first version made as proof of concept and work of BTCPay as a solution to accept cryptocurrencies payments in the Opensimulator virtual Worlds without any need to an opensim region module or exposing sensive data and using just LSL scripts.</p>
<p>The Opensimulator plugin do not set or get any sensive data from / in the BTCPay database and have it's own context and SQLite database making it safe and easy to install and remove if needed. Also the plugin do not expose the BTCPay instance more than the "Pay Button" and the permissions / limitations layer makes it a bit safer. </p>
<p>This version needs an audit and the green light from BTCPay DEV team. Also need a stress test in real conditions using multiple Altcoins and 100s objects...</p>
</div>
</div>
</div>
</div>
</div>
<div id="osCreditsPage" class="osPage" style="display: none;">
<h6>Thanks Where Due:</h6>
<ul>
<li>Special thanks to the Opensimulator Dev team and project for creating and maintaining, for more than 15 years now, an amazing free and open source alternative to the Linden Lab's virtual world. I started to learn C# in 2011-12 to understand how Opensimulator works and how is possible to generate and serve something like a virtual world with just some data.</li>
<li>Special thanks to the BTCPay Server team for making easy the adoption of cryptocurrencies everywhere with this ingenious payments processing method and the multiple integrations solutions. Also, for forcing me to update my C# knowladge. I learned many new things about ASP.NET thanks to their works.</li>
<li>Special thanks to <a href="/holy-lol" target="_blank">Mr.Dorier</a> and the BTCPay team members who want to turn those scammers in borico and i hope by adding this case of use to BTCPay will make them more obsolete than they are.
<br>As is said: The karma... good or bad... soon or later... always payback.</li>
</ul>
<hr>
<h6>Used Libraries and SDKs:</h6>
<ul>
<li>Multiple Microsoft .NET products. </li>
<li><a href="https://github.com/btcpayserver/btcpayserver" target="_blank">BTCPay Server</a>.</li>
<li><a href="https://github.com/btcpayserver/btcpayserver-plugin-template" target="_blank">BTCPay Server Plugin Template</a>.</li>
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap v5.2.3</a>.</li>
<li><a href="https://jquery.com/" target="_blank">jQuery JavaScript Library v3.6.0</a>.</li>
<li><a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator v0.9.2.2</a>.</li>
<li><a href="https://wiki.secondlife.com/wiki/Getting_started_with_LSL" target="_blank">Linden Lab Scripting Language (LSL)</a> and <a href="https://wiki.secondlife.com/wiki/Category:LSL_Functions">functions</a>.</li>
<li><a href="http://opensimulator.org/wiki/OSSL" target="_blank">Opensimulator Scripting Language (OSSL)</a> and <a href="http://opensimulator.org/wiki/OSSL_Implemented">functions</a>.</li>
</ul>
<hr>
<h6>Edited using:</h6>
<ul>
<li><a href="https://code.visualstudio.com/">Visual Studio Code</a> (Plugin side).</li>
<li><a href="https://www.firestormviewer.org/windows-for-open-simulator/" target="_blank">Firestorm for Opensimulator Win-x64 v6-6-14-69596</a> (Internal scripts editor).</li>
<li><a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator v0.9.2.2</a> (YEngine compiler and scripts engine).</li>
</ul>
<hr>
<h6>About the author:</h6>
<p>
Adil El Farissi, 47 years, i am just a modest social entrepreneur and a polyvalent freelancer R&D from Tangier in Morocco.
<br>3D and virtual worlds edition and codding are ones of my multiple hobbies... So, i do some fun where i can as i can.
<br>Thank you & Enjoy <i class="fa fa-heart" style="color: #0f0;margin-left:5px;"></i>
</p>
</div>
+10 -88
View File
@@ -31,79 +31,23 @@ jobs:
- run:
command: |
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
amd64:
machine:
image: ubuntu-2004:202111-02
docker:
docker:
- image: cimg/base:stable
steps:
- setup_remote_docker
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64
arm32v7:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7
arm64v8:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
multiarch:
machine:
image: ubuntu-2004:202201-02
steps:
- run:
command: |
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG-altcoins -p
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
docker buildx create --use
DOCKER_BUILDX_OPTS="--platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg GIT_COMMIT=${GIT_COMMIT} --push"
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG .
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins --build-arg CONFIGURATION_NAME=Altcoins-Release .
workflows:
version: 2
build_and_test:
@@ -120,7 +64,7 @@ workflows:
# only act on version tags
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- amd64:
- docker:
filters:
# ignore any commit on any branch by default
branches:
@@ -130,25 +74,3 @@ workflows:
# OR features on specific versions like v1.0.0.88-lndseedbackup-1
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
+3 -1
View File
@@ -298,4 +298,6 @@ Packed Plugins
Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json
BTCPayServer/appsettings.dev.json
BTCPayServer/appsettings.dev.json
BTCPayServer.Tests/monero_wallet
/BTCPayServer.Tests/NewBlocks.bat
+1 -1
View File
@@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net6.0/BTCPayServer.dll",
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net8.0/BTCPayServer.dll",
"args": [],
"cwd": "${workspaceFolder}/BTCPayServer",
"stopAtEntry": false,
@@ -31,11 +31,11 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Options;
using Npgsql;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
@@ -84,10 +85,10 @@ namespace BTCPayServer.Abstractions.Contracts
.UseNpgsql(_options.Value.ConnectionString, o =>
{
o.EnableRetryOnFailure(10);
if (!string.IsNullOrEmpty(_schemaPrefix))
{
o.MigrationsHistoryTable(_schemaPrefix);
}
o.SetPostgresVersion(12, 0);
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
})
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
break;
@@ -107,5 +108,11 @@ namespace BTCPayServer.Abstractions.Contracts
}
}
private string GetSearchPath(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
var searchPaths = connectionStringBuilder.SearchPath?.Split(',');
return searchPaths is not { Length: > 0 } ? null : searchPaths[0];
}
}
}
@@ -36,6 +36,17 @@ public static class HttpRequestExtensions
request.Path.ToUriComponent());
}
public static string GetCurrentUrlWithQueryString(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent(),
request.Path.ToUriComponent(),
request.QueryString.ToUriComponent());
}
public static string GetCurrentPath(this HttpRequest request)
{
return string.Concat(
@@ -20,6 +20,15 @@ namespace BTCPayServer.Abstractions.Extensions
Relative
}
public static void SetBlazorAllowed(this ViewDataDictionary viewData, bool allowed)
{
viewData["BlazorAllowed"] = allowed;
}
public static bool IsBlazorAllowed(this ViewDataDictionary viewData)
{
return viewData["BlazorAllowed"] is not false;
}
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
where T : IConvertible
{
@@ -92,6 +101,14 @@ namespace BTCPayServer.Abstractions.Extensions
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
{
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
var displayDate = date.ToString(netFormat, CultureInfo.InvariantCulture);
var tooltip = dateTime.Replace("T", " ");
return new HtmlString($"<time datetime=\"{dateTime}\" data-date-style=\"{jsDateFormat}\" data-time-style=\"{jsTimeFormat}\" data-initial=\"localized\" data-bs-toggle=\"tooltip\" data-bs-title=\"{tooltip}\">{displayDate}</time>");
}
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
{
var relative = date.ToTimeAgo();
@@ -5,7 +5,6 @@ using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
using Npgsql.Internal.TypeHandlers.GeometricHandlers;
namespace BTCPayServer.Abstractions.Form;
@@ -2,12 +2,13 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement(Attributes = "[permission]")]
[HtmlTargetElement(Attributes = "[not-permission]" )]
[HtmlTargetElement(Attributes = "[not-permission]")]
public class PermissionTagHelper : TagHelper
{
private readonly IAuthorizationService _authorizationService;
@@ -22,29 +23,72 @@ public class PermissionTagHelper : TagHelper
public string Permission { get; set; }
public string NotPermission { get; set; }
public string PermissionResource { get; set; }
public bool AndMode { get; set; } = false;
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
var permissions = Permission?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
var notPermissions = NotPermission?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
if (!permissions.Any() && !notPermissions.Any())
return;
if (_httpContextAccessor.HttpContext is null)
return;
var expectedResult = !string.IsNullOrEmpty(Permission);
var key = $"{Permission??NotPermission}_{PermissionResource}";
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
o is not AuthorizationResult res)
bool shouldRender = true; // Assume tag should be rendered unless a check fails
// Process 'Permission' - User must have these permissions
if (permissions.Any())
{
res = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
PermissionResource,
Permission);
_httpContextAccessor.HttpContext.Items.Add(key, res);
bool finalResult = AndMode;
foreach (var perm in permissions)
{
var key = $"{perm}_{PermissionResource}";
AuthorizationResult res = await GetOrAddAuthorizationResult(key, perm);
if (AndMode)
finalResult &= res.Succeeded;
else
finalResult |= res.Succeeded;
if (!AndMode && finalResult) break;
}
shouldRender = finalResult;
}
if (expectedResult != res.Succeeded)
// Process 'NotPermission' - User must not have these permissions
if (shouldRender && notPermissions.Any())
{
foreach (var notPerm in notPermissions)
{
var key = $"{notPerm}_{PermissionResource}";
AuthorizationResult res = await GetOrAddAuthorizationResult(key, notPerm);
if (res.Succeeded) // If the user has a 'NotPermission', they should not see the tag
{
shouldRender = false;
break;
}
}
}
if (!shouldRender)
{
output.SuppressOutput();
}
}
private async Task<AuthorizationResult> GetOrAddAuthorizationResult(string key, string permission)
{
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var cachedResult))
{
var res = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
PermissionResource, permission);
_httpContextAccessor.HttpContext.Items[key] = res;
return res;
}
return cachedResult as AuthorizationResult;
}
}
@@ -0,0 +1,35 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement("form", Attributes = "[permissioned]")]
public partial class PermissionedFormTagHelper(
IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor)
: TagHelper
{
public string Permissioned { get; set; }
public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (httpContextAccessor.HttpContext is null || string.IsNullOrEmpty(Permissioned))
return;
var res = await authorizationService.AuthorizeAsync(httpContextAccessor.HttpContext.User,
PermissionResource, Permissioned);
if (!res.Succeeded)
{
var content = await output.GetChildContentAsync();
var html = SubmitButtonRegex().Replace(content.GetContent(), "");
output.Content.SetHtmlContent($"<fieldset disabled>{html}</fieldset>");
}
}
[GeneratedRegex("<(button|input).*?type=\"submit\".*?>.*?</\\1>")]
private static partial Regex SubmitButtonRegex();
}
@@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -30,8 +30,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
<PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
@@ -20,6 +20,12 @@ namespace BTCPayServer.Client
return await HandleResponse<PullPaymentData>(response);
}
public virtual async Task<RegisterBoltcardResponse> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/boltcards", bodyPayload: request, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<RegisterBoltcardResponse>(response);
}
public virtual async Task<PullPaymentData[]> GetPullPayments(string storeId, bool includeArchived = false, CancellationToken cancellationToken = default)
{
Dictionary<string, object> query = new Dictionary<string, object>();
@@ -103,7 +109,7 @@ namespace BTCPayServer.Client
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"/api/v1/pull-payments/{pullPaymentId}/lnurl",
$"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/lnurl",
method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentLNURL>(response);
}
@@ -41,6 +41,14 @@ namespace BTCPayServer.Client
return response.IsSuccessStatusCode;
}
public virtual async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/approve", null,
new ApproveUserRequest { Approved = approved }, HttpMethod.Post), token);
await HandleResponse(response);
return response.IsSuccessStatusCode;
}
public virtual async Task<ApplicationUserData[]> GetUsers(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@@ -58,6 +59,8 @@ namespace BTCPayServer.Client.JsonConverters
return null;
return TimeSpan.Zero;
}
if (reader.TokenType == JsonToken.String && TimeSpan.TryParse(reader.Value?.ToString(), CultureInfo.InvariantCulture, out var res))
return res;
if (reader.TokenType != JsonToken.Integer)
throw new JsonObjectException("Invalid timespan, expected integer", reader);
return ToTimespan((long)reader.Value);
@@ -25,6 +25,16 @@ namespace BTCPayServer.Client.Models
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
/// <summary>
/// Whether the user was approved by an admin
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// whether the user needed approval on account creation
/// </summary>
public bool RequiresApproval { get; set; }
/// <summary>
/// the roles of the user
/// </summary>
@@ -0,0 +1,6 @@
namespace BTCPayServer.Client;
public class ApproveUserRequest
{
public bool Approved { get; set; }
}
@@ -26,9 +26,12 @@ namespace BTCPayServer.Client.Models
public string Template { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public PosViewType DefaultView { get; set; }
public bool ShowItems { get; set; } = false;
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = true;
public bool EnableTips { get; set; } = true;
public bool ShowDiscount { get; set; } = false;
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; } = false;
public string CustomAmountPayButtonText { get; set; } = null;
public string FixedAmountPayButtonText { get; set; } = null;
public string TipText { get; set; } = null;
@@ -40,7 +43,6 @@ namespace BTCPayServer.Client.Models
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null;
}
public enum CrowdfundResetEvery
@@ -1,3 +1,5 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class EmailSettingsData
@@ -26,4 +28,11 @@ public class EmailSettingsData
get; set;
}
public bool DisableCertificateCheck { get; set; }
[JsonIgnore]
public bool EnabledCertificateCheck
{
get => !DisableCertificateCheck;
set { DisableCertificateCheck = !value; }
}
}
@@ -1,8 +1,12 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
[JsonProperty("lud12Enabled")]
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()
@@ -16,11 +16,12 @@ namespace BTCPayServer.Client.Models
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
LUD12Enabled = lud12Enabled;
}
}
}
@@ -1,3 +1,5 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class LightningAddressData
@@ -6,5 +8,5 @@ public class LightningAddressData
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; }
}
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@@ -13,14 +16,19 @@ namespace BTCPayServer.Client.Models
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; }
}
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
public class PointOfSaleAppData : AppDataBase
{
public string Title { get; set; }
public string DefaultView { get; set; }
public bool ShowItems { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool ShowSearch { get; set; }
public bool ShowCategories { get; set; }
public bool EnableTips { get; set; }
public string Currency { get; set; }
public object Items { get; set; }
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public enum OnExistingBehavior
{
KeepVersion,
UpdateVersion
}
public class RegisterBoltcardRequest
{
[JsonProperty("LNURLW")]
public string LNURLW { get; set; }
[JsonConverter(typeof(HexJsonConverter))]
[JsonProperty("UID")]
public byte[] UID { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public OnExistingBehavior? OnExisting { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
public class RegisterBoltcardResponse
{
[JsonProperty("LNURLW")]
public string LNURLW { get; set; }
public int Version { get; set; }
[JsonProperty("K0")]
public string K0 { get; set; }
[JsonProperty("K1")]
public string K1 { get; set; }
[JsonProperty("K2")]
public string K2 { get; set; }
[JsonProperty("K3")]
public string K3 { get; set; }
[JsonProperty("K4")]
public string K4 { get; set; }
}
}
@@ -37,8 +37,11 @@ namespace BTCPayServer.Client.Models
public bool AnyoneCanCreateInvoice { get; set; }
public string DefaultCurrency { get; set; }
public bool RequiresRefundEmail { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public CheckoutType CheckoutType { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public CheckoutType? CheckoutType { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
@@ -11,8 +11,7 @@ namespace BTCPayServer.Client.Models
{
public bool Everything { get; set; } = true;
[JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty<WebhookEventType>();
public string[] SpecificEvents { get; set; } = Array.Empty<string>();
}
public bool Enabled { get; set; } = true;
@@ -9,7 +9,7 @@ namespace BTCPayServer.Client.Models
{
public class WebhookEvent
{
public readonly static JsonSerializerSettings DefaultSerializerSettings;
public static readonly JsonSerializerSettings DefaultSerializerSettings;
static WebhookEvent()
{
DefaultSerializerSettings = new JsonSerializerSettings();
@@ -45,8 +45,7 @@ namespace BTCPayServer.Client.Models
}
}
public bool IsRedelivery { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public WebhookEventType Type { get; set; }
public string Type { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonExtensionData]
@@ -1,13 +1,20 @@
namespace BTCPayServer.Client.Models
namespace BTCPayServer.Client.Models;
public static class WebhookEventType
{
public enum WebhookEventType
{
InvoiceCreated,
InvoiceReceivedPayment,
InvoiceProcessing,
InvoiceExpired,
InvoiceSettled,
InvoiceInvalid,
InvoicePaymentSettled,
}
public const string InvoiceCreated = nameof(InvoiceCreated);
public const string InvoiceReceivedPayment = nameof(InvoiceReceivedPayment);
public const string InvoiceProcessing = nameof(InvoiceProcessing);
public const string InvoiceExpired = nameof(InvoiceExpired);
public const string InvoiceSettled = nameof(InvoiceSettled);
public const string InvoiceInvalid = nameof(InvoiceInvalid);
public const string InvoicePaymentSettled = nameof(InvoicePaymentSettled);
public const string PayoutCreated = nameof(PayoutCreated);
public const string PayoutApproved = nameof(PayoutApproved);
public const string PayoutUpdated = nameof(PayoutUpdated);
public const string PaymentRequestUpdated = nameof(PaymentRequestUpdated);
public const string PaymentRequestCreated = nameof(PaymentRequestCreated);
public const string PaymentRequestArchived = nameof(PaymentRequestArchived);
public const string PaymentRequestStatusChanged = nameof(PaymentRequestStatusChanged);
}
@@ -1,44 +1,74 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class WebhookInvoiceEvent : WebhookEvent
public class WebhookPayoutEvent : StoreWebhookEvent
{
public WebhookPayoutEvent(string type, string storeId)
{
if (!type.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}
[JsonProperty(Order = 2)] public string PayoutId { get; set; }
[JsonProperty(Order = 3)] public string PullPaymentId { get; set; }
[JsonProperty(Order = 4)] [JsonConverter(typeof(StringEnumConverter))]public PayoutState PayoutState { get; set; }
}
public class WebhookPaymentRequestEvent : StoreWebhookEvent
{
public WebhookPaymentRequestEvent(string type, string storeId)
{
if (!type.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(type));
Type = type;
StoreId = storeId;
}
[JsonProperty(Order = 2)] public string PaymentRequestId { get; set; }
[JsonProperty(Order = 3)] [JsonConverter(typeof(StringEnumConverter))]public PaymentRequestData.PaymentRequestStatus Status { get; set; }
}
public abstract class StoreWebhookEvent : WebhookEvent
{
[JsonProperty(Order = 1)] public string StoreId { get; set; }
}
public class WebhookInvoiceEvent : StoreWebhookEvent
{
public WebhookInvoiceEvent()
{
}
public WebhookInvoiceEvent(WebhookEventType evtType)
{
this.Type = evtType;
public WebhookInvoiceEvent(string evtType, string storeId)
{
if (!evtType.StartsWith("invoice", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType));
Type = evtType;
StoreId = storeId;
}
[JsonProperty(Order = 1)] public string StoreId { get; set; }
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
}
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
{
public WebhookInvoiceSettledEvent()
{
}
public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoiceSettledEvent(string storeId) : base(WebhookEventType.InvoiceSettled, storeId)
{
}
public bool ManuallyMarked { get; set; }
public bool OverPaid { get; set; }
}
public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent
{
public WebhookInvoiceInvalidEvent()
{
}
public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoiceInvalidEvent(string storeId) : base(WebhookEventType.InvoiceInvalid, storeId)
{
}
@@ -47,11 +77,7 @@ namespace BTCPayServer.Client.Models
public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent
{
public WebhookInvoiceProcessingEvent()
{
}
public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoiceProcessingEvent(string storeId) : base(WebhookEventType.InvoiceProcessing, storeId)
{
}
@@ -60,38 +86,25 @@ namespace BTCPayServer.Client.Models
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
{
public WebhookInvoiceReceivedPaymentEvent()
{
}
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoiceReceivedPaymentEvent(string type, string storeId) : base(type, storeId)
{
}
public bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
public bool OverPaid { get; set; }
}
public class WebhookInvoicePaymentSettledEvent : WebhookInvoiceReceivedPaymentEvent
{
public WebhookInvoicePaymentSettledEvent()
{
}
public WebhookInvoicePaymentSettledEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoicePaymentSettledEvent(string storeId) : base(WebhookEventType.InvoicePaymentSettled, storeId)
{
}
}
public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
{
public WebhookInvoiceExpiredEvent()
{
}
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
public WebhookInvoiceExpiredEvent(string storeId) : base(WebhookEventType.InvoiceExpired, storeId)
{
}
@@ -19,6 +19,7 @@ namespace BTCPayServer.Client
public const string CanModifyStoreWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanViewReports = "btcpay.store.canviewreports";
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanModifyInvoices = "btcpay.store.canmodifyinvoices";
@@ -34,7 +35,10 @@ namespace BTCPayServer.Client
public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
public const string CanManagePayouts = "btcpay.store.canmanagepayouts";
public const string CanViewPayouts = "btcpay.store.canviewpayouts";
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanViewPullPayments = "btcpay.store.canviewpullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts";
@@ -53,6 +57,7 @@ namespace BTCPayServer.Client
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
yield return CanViewReports;
yield return CanViewPaymentRequests;
yield return CanModifyPaymentRequests;
yield return CanModifyProfile;
@@ -72,6 +77,7 @@ namespace BTCPayServer.Client
yield return CanManagePullPayments;
yield return CanArchivePullPayments;
yield return CanCreatePullPayments;
yield return CanViewPullPayments;
yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts;
yield return CanManageCustodianAccounts;
@@ -79,6 +85,8 @@ namespace BTCPayServer.Client
yield return CanWithdrawFromCustodianAccounts;
yield return CanTradeCustodianAccount;
yield return CanManageUsers;
yield return CanManagePayouts;
yield return CanViewPayouts;
}
}
public static bool IsValidPolicy(string policy)
@@ -252,11 +260,13 @@ namespace BTCPayServer.Client
Policies.CanViewStoreSettings,
Policies.CanModifyStoreWebhooks,
Policies.CanModifyPaymentRequests,
Policies.CanManagePayouts,
Policies.CanUseLightningNodeInStore);
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(policyMap, Policies.CanCreateNonApprovedPullPayments, Policies.CanViewPullPayments);
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
@@ -267,7 +277,8 @@ namespace BTCPayServer.Client
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts);
PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts);
var missingPolicies = Policies.AllPolicies.ToHashSet();
//recurse through the tree to see which policies are not included in the tree
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitAlthash()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("HTML");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Htmlcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"HTML_X = HTML_USD",
"HTML_USD = hitbtc(HTML_USD)"
},
CryptoImagePath = "imlegacy/althash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("172'") : new KeyPath("1'")
});
}
}
}
@@ -1,30 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitArgoneum()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("AGM");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Argoneum",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://chainz.cryptoid.info/agm/tx.dws?{0}"
: "https://chainz.cryptoid.info/agm-test/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"AGM_X = AGM_BTC * BTC_X",
"AGM_BTC = argoneum(AGM_BTC)"
},
CryptoImagePath = "imlegacy/argoneum.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("421'")
: new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBGold()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BGold",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://btgexplorer.com/tx/{0}" : "https://testnet.btgexplorer.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = gate(BTG_BTC)",
},
CryptoImagePath = "imlegacy/btg.svg",
LightningImagePath = "imlegacy/btg-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBPlus()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BPlus",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",
"XBC_BTC = cryptopia(XBC_BTC)"
},
CryptoImagePath = "imlegacy/xbc.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}
}
@@ -1,29 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcore()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTX");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BitCore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.bitcore.cc/tx/{0}" : "https://explorer.bitcore.cc/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = graviex(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
});
}
}
}
@@ -1,32 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDash()
{
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dash",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bitfinex(DSH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("5'")
: new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDogecoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dogecoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"DOGE_X = DOGE_BTC * BTC_X",
"DOGE_BTC = bittrex(DOGE_BTC)"
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitFeathercoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",
"FTC_BTC = bittrex(FTC_BTC)"
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}
}
@@ -1,33 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitGroestlcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Groestlcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("17'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
VaultSupported = true
});
}
}
}
@@ -1,46 +0,0 @@
using System.Collections.Generic;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitLitecoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Litecoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"LTC_X = LTC_BTC * BTC_X",
"LTC_BTC = coingecko(LTC_BTC)"
},
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
ElectrumMapping = NetworkType == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy },
{0x049d7cb2U, DerivationType.SegwitP2SH },
{0x04b24746U, DerivationType.Segwit },
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy },
{0x044a5262U, DerivationType.SegwitP2SH },
{0x045f1cf6U, DerivationType.Segwit }
}
});
}
}
}
@@ -1,29 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitMonacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = bittrex(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitPolis()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = polispay(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitUfo()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"UFO_X = UFO_BTC * BTC_X",
"UFO_BTC = coinexchange(UFO_BTC)"
},
CryptoImagePath = "imlegacy/ufo.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
});
}
}
}
@@ -1,28 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitViacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"VIA_X = VIA_BTC * BTC_X",
"VIA_BTC = bittrex(VIA_BTC)"
},
CryptoImagePath = "imlegacy/viacoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
});
}
}
}
@@ -1,37 +0,0 @@
#if ALTCOINS
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.Altcoins.Elements;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitLiquid()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LBTC");
Add(new ElementsBTCPayNetwork()
{
AssetId = NetworkType == ChainName.Mainnet ? ElementsParams<Liquid>.PeggedAssetId : ElementsParams<Liquid.LiquidRegtest>.PeggedAssetId,
CryptoCode = "LBTC",
NetworkCryptoCode = "LBTC",
DisplayName = "Liquid Bitcoin",
DefaultRateRules = new[]
{
"LBTC_X = LBTC_BTC * BTC_X",
"LBTC_BTC = 1",
},
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
}
}
}
#endif
@@ -1,83 +0,0 @@
#if ALTCOINS
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitLiquidAssets()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LBTC");
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "USDt",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"USDT_UST = 1",
"USDT_X = USDT_BTC * BTC_X",
"USDT_BTC = bitfinex(UST_BTC)",
},
AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "ETB",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"ETB_X = ETB_BTC * BTC_X",
"ETB_BTC = bitpay(ETB_BTC)"
},
Divisibility = 2,
AssetId = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "LCAD",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"LCAD_CAD = 1",
"LCAD_X = CAD_BTC * BTC_X",
"LCAD_BTC = bylls(CAD_BTC)",
"CAD_BTC = LCAD_BTC"
},
AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
});
}
}
}
#endif
@@ -1,51 +0,0 @@
#if ALTCOINS
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Common;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
namespace BTCPayServer
{
public class ElementsBTCPayNetwork : BTCPayNetwork
{
public string NetworkCryptoCode { get; set; }
public uint256 AssetId { get; set; }
public override bool ReadonlyWallet { get; set; } = true;
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(
NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Where(output =>
(output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint);
});
}
public override List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
{
return transactionInformationSet.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
}
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
{
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
builder.QueryParams.Add("assetid", AssetId.ToString());
return builder;
}
}
}
#endif
@@ -1,18 +0,0 @@
#if ALTCOINS
using System.Collections.Generic;
using System.Linq;
namespace BTCPayServer
{
public static class LiquidExtensions
{
public static IEnumerable<string> GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfiltered)
{
var elementsBased = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>();
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
return unfiltered.GetAll().OfType<ElementsBTCPayNetwork>()
.Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant());
}
}
}
#endif
@@ -1,28 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitMonero()
{
Add(new MoneroLikeSpecificBtcPayNetwork()
{
CryptoCode = "XMR",
DisplayName = "Monero",
Divisibility = 12,
BlockExplorerLink =
NetworkType == ChainName.Mainnet
? "https://www.exploremonero.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]
{
"XMR_X = XMR_BTC * BTC_X",
"XMR_BTC = kraken(XMR_BTC)"
},
CryptoImagePath = "/imlegacy/monero.svg",
UriScheme = "monero"
});
}
}
}
@@ -1,8 +0,0 @@
namespace BTCPayServer
{
public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase
{
public int MaxTrackedConfirmation = 10;
public string UriScheme { get; set; }
}
}
@@ -1,29 +0,0 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
// Change this if you want another zcash coin
public void InitZcash()
{
Add(new ZcashLikeSpecificBtcPayNetwork()
{
CryptoCode = "ZEC",
DisplayName = "Zcash",
Divisibility = 8,
BlockExplorerLink =
NetworkType == ChainName.Mainnet
? "https://www.exploreZcash.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]
{
"ZEC_X = ZEC_BTC * BTC_X",
"ZEC_BTC = kraken(ZEC_BTC)"
},
CryptoImagePath = "/imlegacy/zcash.png",
UriScheme = "zcash"
});
}
}
}
@@ -1,8 +0,0 @@
namespace BTCPayServer
{
public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase
{
public int MaxTrackedConfirmation = 10;
public string UriScheme { get; set; }
}
}
@@ -4,6 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
@@ -62,6 +64,31 @@ namespace BTCPayServer
public KeyPath CoinType { get; set; }
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
public BTCPayNetwork SetDefaultElectrumMapping(ChainName chainName)
{
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = chainName == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x04b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy}, // tpub
{0x044a5262U, DerivationType.SegwitP2SH}, // upub
{0x045f1cf6U, DerivationType.Segwit} // vpub
};
if (!NBitcoinNetwork.Consensus.SupportSegwit)
{
ElectrumMapping =
ElectrumMapping
.Where(kv => kv.Value == DerivationType.Legacy)
.ToDictionary(k => k.Key, k => k.Value);
}
return this;
}
public virtual bool WalletSupported { get; set; } = true;
public virtual bool ReadonlyWallet { get; set; } = false;
@@ -107,25 +134,8 @@ namespace BTCPayServer
public abstract class BTCPayNetworkBase
{
private string _blockExplorerLink;
public bool ShowSyncSummary { get; set; } = true;
public string CryptoCode { get; set; }
public string BlockExplorerLink
{
get => _blockExplorerLink;
set
{
if (string.IsNullOrEmpty(BlockExplorerLinkDefault))
{
BlockExplorerLinkDefault = value;
}
_blockExplorerLink = value;
}
}
public string BlockExplorerLinkDefault { get; set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
public bool IsBTC
@@ -153,5 +163,8 @@ namespace BTCPayServer
{
return NBitcoin.JsonConverters.Serializer.ToString(obj, null);
}
[Obsolete("Use TransactionLinkProviders service instead")]
public string BlockExplorerLink { get; set; }
}
}
@@ -1,44 +0,0 @@
using System.Collections.Generic;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mempool.space/tx/{0}" :
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://mempool.space/signet/tx/{0}"
: "https://mempool.space/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
VaultSupported = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x04b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy}, // tpub
{0x044a5262U, DerivationType.SegwitP2SH}, // upub
{0x045f1cf6U, DerivationType.Segwit} // vpub
}
});
}
}
}
@@ -1,8 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using StandardConfiguration;
namespace BTCPayServer
{
@@ -19,92 +26,37 @@ namespace BTCPayServer
}
}
BTCPayNetworkProvider(BTCPayNetworkProvider unfiltered, string[] cryptoCodes)
{
NetworkType = unfiltered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(unfiltered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetworkBase>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in unfiltered._Networks)
{
if (cryptoCodes.Contains(network.Key))
{
_Networks.Add(network.Key, network.Value);
}
}
}
public ChainName NetworkType { get; private set; }
public BTCPayNetworkProvider(ChainName networkType)
public BTCPayNetworkProvider(
IEnumerable<BTCPayNetworkBase> networks,
SelectedChains selectedChains,
NBXplorerNetworkProvider nbxplorerNetworkProvider,
Logs logs)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
#if ALTCOINS
InitLiquid();
InitLiquidAssets();
InitLitecoin();
InitBitcore();
InitDogecoin();
InitBGold();
InitMonacoin();
InitDash();
InitFeathercoin();
InitAlthash();
InitGroestlcoin();
InitViacoin();
InitMonero();
InitZcash();
// InitArgoneum();//their rate source is down 9/15/20.
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
var networksList = networks.ToList();
#if !ALTCOINS
var onlyBTC = networksList.Count == 1 && networksList.First().IsBTC;
if (!onlyBTC)
throw new ConfigException($"This build of BTCPay Server does not support altcoins. Configured networks: {string.Join(',', networksList.Select(n => n.CryptoCode).ToArray())}");
#endif
_NBXplorerNetworkProvider = nbxplorerNetworkProvider;
NetworkType = nbxplorerNetworkProvider.NetworkType;
foreach (var network in networksList)
{
if (network.ElectrumMapping.Count == 0)
{
network.ElectrumMapping = GetNetwork<BTCPayNetwork>("BTC").ElectrumMapping;
if (!network.NBitcoinNetwork.Consensus.SupportSegwit)
{
network.ElectrumMapping =
network.ElectrumMapping
.Where(kv => kv.Value == DerivationType.Legacy)
.ToDictionary(k => k.Key, k => k.Value);
}
}
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitBPlus();
//InitUfo();
#endif
}
foreach (var chain in selectedChains.ExplicitlySelected)
{
if (GetNetwork<BTCPayNetworkBase>(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
/// <summary>
/// Keep only the specified crypto
/// </summary>
/// <param name="cryptoCodes">Crypto to support</param>
/// <returns></returns>
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
{
return new BTCPayNetworkProvider(this, cryptoCodes);
logs.Configuration.LogInformation("Supported chains: {Chains}", string.Join(',', _Networks.Select(n => n.Key).ToArray()));
}
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
public BTCPayNetworkBase DefaultNetwork => BTC ?? GetAll().First();
public void Add(BTCPayNetwork network)
{
if (network.NBitcoinNetwork == null)
return;
Add(network as BTCPayNetworkBase);
}
public void Add(BTCPayNetworkBase network)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetworkBase> GetAll()
{
return _Networks.Values.ToArray();
@@ -4,10 +4,13 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Altcoins\" />
</ItemGroup>
</Project>
@@ -174,7 +174,6 @@ namespace BTCPayServer.Logging
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Bson;
namespace BTCPayServer
{
public class SelectedChains
{
HashSet<string> chains = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool all = false;
public SelectedChains(IConfiguration configuration, Logs logs)
{
foreach (var chain in (configuration["chains"] ?? "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant()))
{
if (new[] { "ETH", "USDT20", "FAU" }.Contains(chain, StringComparer.OrdinalIgnoreCase))
{
logs.Configuration.LogWarning($"'{chain}' is not anymore supported, please remove it from 'chains'");
continue;
}
if (chain == "*")
{
all = true;
continue;
}
chains.Add(chain);
}
if (chains.Count == 0)
chains.Add("BTC");
if (all)
chains.Clear();
}
public bool Contains(string cryptoCode)
{
return all || chains.Contains(cryptoCode);
}
public void Add(string cryptoCode)
{
chains.Add(cryptoCode);
}
public IEnumerable<string> ExplicitlySelected => chains;
}
}
@@ -93,7 +93,7 @@ namespace BTCPayServer.Data
ApplicationUser.OnModelCreating(builder, Database);
AddressInvoiceData.OnModelCreating(builder);
APIKeyData.OnModelCreating(builder, Database);
AppData.OnModelCreating(builder);
AppData.OnModelCreating(builder, Database);
CustodianAccountData.OnModelCreating(builder, Database);
//StoredFile.OnModelCreating(builder);
InvoiceEventData.OnModelCreating(builder);
@@ -107,10 +107,10 @@ namespace BTCPayServer.Data
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PayoutData.OnModelCreating(builder);
PayoutData.OnModelCreating(builder, Database);
PendingInvoiceData.OnModelCreating(builder);
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder, Database);
RefundData.OnModelCreating(builder);
SettingData.OnModelCreating(builder, Database);
StoreSettingData.OnModelCreating(builder, Database);
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
@@ -1,5 +1,6 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace BTCPayServer.Data
{
@@ -1,5 +1,6 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace BTCPayServer.Data
@@ -16,13 +17,20 @@ namespace BTCPayServer.Data
public string Settings { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<AppData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
if (databaseFacade.IsNpgsql())
{
builder.Entity<AppData>()
.Property(o => o.Settings)
.HasColumnType("JSONB");
}
}
// utility methods
@@ -11,6 +11,8 @@ namespace BTCPayServer.Data
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
{
public bool RequiresEmailConfirmation { get; set; }
public bool RequiresApproval { get; set; }
public bool Approved { get; set; }
public List<StoredFile> StoredFiles { get; set; }
[Obsolete("U2F support has been replace with FIDO2")]
public List<U2FDevice> U2FDevices { get; set; }
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -30,11 +31,10 @@ namespace BTCPayServer.Data
public List<PendingInvoiceData> PendingInvoices { get; set; }
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
public List<RefundData> Refunds { get; set; }
public string CurrentRefundId { get; set; }
[ForeignKey("Id,CurrentRefundId")]
public RefundData CurrentRefund { get; set; }
[Timestamp]
// With this, update of InvoiceData will fail if the row was modified by another process
public uint XMin { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<InvoiceData>()
@@ -42,8 +42,6 @@ namespace BTCPayServer.Data
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>()
.HasOne(o => o.CurrentRefund);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql())
@@ -40,7 +40,6 @@ namespace BTCPayServer.Data
{
return Severity switch
{
EventSeverity.Info => "info",
EventSeverity.Error => "danger",
EventSeverity.Success => "success",
EventSeverity.Warning => "warning",
@@ -1,8 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
namespace BTCPayServer.Data
@@ -21,14 +24,14 @@ namespace BTCPayServer.Data
[MaxLength(20)]
[Required]
public string PaymentMethodId { get; set; }
public byte[] Blob { get; set; }
public byte[] Proof { get; set; }
public string Blob { get; set; }
public string Proof { get; set; }
#nullable enable
public string? Destination { get; set; }
#nullable restore
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<PayoutData>()
.HasOne(o => o.PullPaymentData)
@@ -43,6 +46,33 @@ namespace BTCPayServer.Data
.HasIndex(o => o.State);
builder.Entity<PayoutData>()
.HasIndex(x => new { DestinationId = x.Destination, x.State });
if (databaseFacade.IsNpgsql())
{
builder.Entity<PayoutData>()
.Property(o => o.Blob)
.HasColumnType("JSONB");
builder.Entity<PayoutData>()
.Property(o => o.Proof)
.HasColumnType("JSONB");
}
else if (databaseFacade.IsMySql())
{
builder.Entity<PayoutData>()
.Property(o => o.Blob)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
builder.Entity<PayoutData>()
.Property(o => o.Proof)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
}
}
// utility methods
@@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NBitcoin;
namespace BTCPayServer.Data
@@ -24,16 +27,33 @@ namespace BTCPayServer.Data
public DateTimeOffset? EndDate { get; set; }
public bool Archived { get; set; }
public List<PayoutData> Payouts { get; set; }
public byte[] Blob { get; set; }
public string Blob { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<PullPaymentData>()
.HasIndex(o => o.StoreId);
builder.Entity<PullPaymentData>()
.HasOne(o => o.StoreData)
.HasOne(o => o.StoreData)
.WithMany(o => o.PullPayments).OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())
{
builder.Entity<PullPaymentData>()
.Property(o => o.Blob)
.HasColumnType("JSONB");
}
else if (databaseFacade.IsMySql())
{
builder.Entity<PullPaymentData>()
.Property(o => o.Blob)
.HasConversion(new ValueConverter<string, byte[]>
(
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
));
}
}
public (DateTimeOffset Start, DateTimeOffset? End)? GetPeriod(DateTimeOffset now)
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace BTCPayServer.Data
{
@@ -13,7 +14,6 @@ namespace BTCPayServer.Data
public PullPaymentData PullPaymentData { get; set; }
public InvoiceData InvoiceData { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<RefundData>()
@@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WalletObjectData
public class WalletObjectData : IEqualityComparer<WalletObjectData>
{
public class Types
{
@@ -88,9 +86,30 @@ namespace BTCPayServer.Data
if (databaseFacade.IsNpgsql())
{
builder.Entity<WalletObjectData>()
.Property(o => o.Data)
.HasColumnType("JSONB");
.Property(o => o.Data)
.HasColumnType("JSONB");
}
}
public bool Equals(WalletObjectData x, WalletObjectData y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return string.Equals(x.WalletId, y.WalletId, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(x.Type, y.Type, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(x.Id, y.Id, StringComparison.InvariantCultureIgnoreCase);
}
public int GetHashCode(WalletObjectData obj)
{
HashCode hashCode = new HashCode();
hashCode.Add(obj.WalletId, StringComparer.InvariantCultureIgnoreCase);
hashCode.Add(obj.Type, StringComparer.InvariantCultureIgnoreCase);
hashCode.Add(obj.Id, StringComparer.InvariantCultureIgnoreCase);
return hashCode.ToHashCode();
}
}
}
@@ -1,4 +1,4 @@
using BTCPayServer.Data;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -0,0 +1,38 @@
using System.Security.Permissions;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20231020135844_AddBoltcardsTable")]
public partial class AddBoltcardsTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "boltcards",
columns: table => new
{
id = table.Column<string>(maxLength: 32, nullable: false),
counter = table.Column<int>(type: "INT", nullable: false, defaultValue: 0),
ppid = table.Column<string>(maxLength: 30, nullable: true),
version = table.Column<int>(nullable: false, defaultValue: 0)
},
constraints: table =>
{
table.PrimaryKey("PK_id", x => x.id);
table.ForeignKey("FK_boltcards_PullPayments", x => x.ppid, "PullPayments", "Id", onDelete: ReferentialAction.SetNull);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable("boltcards");
}
}
}
@@ -0,0 +1,36 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20231121031609_removecurrentrefund")]
public partial class removecurrentrefund : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.DropForeignKey(
name: "FK_Invoices_Refunds_Id_CurrentRefundId",
table: "Invoices");
migrationBuilder.DropIndex(
name: "IX_Invoices_Id_CurrentRefundId",
table: "Invoices");
migrationBuilder.DropColumn(
name: "CurrentRefundId",
table: "Invoices");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,26 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20231219031609_appssettingstojson")]
public partial class appssettingstojson : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"Apps\" ALTER COLUMN \"Settings\" TYPE JSONB USING \"Settings\"::JSONB");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240104155620_AddApprovalToApplicationUser")]
public partial class AddApprovalToApplicationUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Approved",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "RequiresApproval",
table: "AspNetUsers",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Approved",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "RequiresApproval",
table: "AspNetUsers");
}
}
}
@@ -0,0 +1,26 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240220000000_FixWalletObjectsWithEmptyWalletId")]
public partial class FixWalletObjectsWithEmptyWalletId : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("DELETE FROM \"WalletObjects\" WHERE \"WalletId\"='';");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,28 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240229000000_PayoutAndPullPaymentToJsonBlob")]
public partial class PayoutAndPullPaymentToJsonBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Proof\" TYPE JSONB USING regexp_replace(convert_from(\"Proof\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
migrationBuilder.Sql("ALTER TABLE \"PullPayments\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,98 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Newtonsoft.Json;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240229092905_AddManagerAndEmployeeToStoreRoles")]
public partial class AddManagerAndEmployeeToStoreRoles : Migration
{
object GetPermissionsData(MigrationBuilder migrationBuilder, string[] permissions)
{
return migrationBuilder.IsNpgsql()
? permissions
: JsonConvert.SerializeObject(permissions);
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
migrationBuilder.InsertData(
"StoreRoles",
columns: new[] { "Id", "Role", "Permissions" },
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
values: new object[,]
{
{
"Manager", "Manager", GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canviewstoresettings",
"btcpay.store.canmodifyinvoices",
"btcpay.store.webhooks.canmodifywebhooks",
"btcpay.store.canmodifypaymentrequests",
"btcpay.store.canmanagepullpayments",
"btcpay.store.canmanagepayouts"
})
},
{
"Employee", "Employee", GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canmodifyinvoices",
"btcpay.store.canmodifypaymentrequests",
"btcpay.store.cancreatenonapprovedpullpayments",
"btcpay.store.canviewpayouts",
"btcpay.store.canviewpullpayments"
})
}
});
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Guest" },
columns: new[] { "Permissions" },
columnTypes: new[] { permissionsType },
values: new object[]
{
GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canmodifyinvoices",
"btcpay.store.canviewpaymentrequests",
"btcpay.store.canviewpullpayments",
"btcpay.store.canviewpayouts"
})
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData("StoreRoles", "Id", "Manager");
migrationBuilder.DeleteData("StoreRoles", "Id", "Employee");
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Guest" },
columns: new[] { "Permissions" },
columnTypes: new[] { permissionsType },
values: new object[]
{
GetPermissionsData(migrationBuilder, new[]
{
"btcpay.store.canviewstoresettings",
"btcpay.store.canmodifyinvoices",
"btcpay.store.canviewcustodianaccounts",
"btcpay.store.candeposittocustodianaccount"
})
});
}
}
}
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@@ -16,25 +16,7 @@ namespace BTCPayServer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("CreatedTime")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
@@ -71,6 +53,24 @@ namespace BTCPayServer.Migrations
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("CreatedTime")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
@@ -112,6 +112,9 @@ namespace BTCPayServer.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<bool>("Approved")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@@ -158,6 +161,9 @@ namespace BTCPayServer.Migrations
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresApproval")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresEmailConfirmation")
.HasColumnType("INTEGER");
@@ -287,9 +293,6 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("CurrentRefundId")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail")
.HasColumnType("TEXT");
@@ -308,6 +311,11 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<uint>("XMin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Created");
@@ -316,8 +324,6 @@ namespace BTCPayServer.Migrations
b.HasIndex("StoreDataId");
b.HasIndex("Id", "CurrentRefundId");
b.ToTable("Invoices");
});
@@ -593,7 +599,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Date")
.HasColumnType("TEXT");
@@ -607,7 +613,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Proof")
.HasColumnType("BLOB");
.HasColumnType("TEXT");
b.Property<string>("PullPaymentDataId")
.HasColumnType("TEXT");
@@ -698,7 +704,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("EndDate")
.HasColumnType("TEXT");
@@ -786,31 +792,6 @@ namespace BTCPayServer.Migrations
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("StorageFileName")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.Property<string>("Id")
@@ -868,6 +849,31 @@ namespace BTCPayServer.Migrations
b.ToTable("StoreWebhooks");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("StorageFileName")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.Property<string>("Id")
@@ -1176,16 +1182,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@@ -1203,6 +1199,16 @@ namespace BTCPayServer.Migrations
b.Navigation("User");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@@ -1251,12 +1257,6 @@ namespace BTCPayServer.Migrations
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.RefundData", "CurrentRefund")
.WithMany()
.HasForeignKey("Id", "CurrentRefundId");
b.Navigation("CurrentRefund");
b.Navigation("StoreData");
});
@@ -1419,15 +1419,6 @@ namespace BTCPayServer.Migrations
b.Navigation("PullPaymentData");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@@ -1468,6 +1459,15 @@ namespace BTCPayServer.Migrations
b.Navigation("Webhook");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Version>1.0.0.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>btcpay-plugin</ToolCommandName>
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@@ -109,9 +110,22 @@ namespace BTCPayServer.PluginPacker
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
{
return assembly.GetTypes().Where(type =>
return GetLoadableTypes(assembly).Where(type =>
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
!type.IsAbstract).ToArray();
}
static Type[] GetLoadableTypes(Assembly assembly)
{
if (assembly == null)
throw new ArgumentNullException(nameof(assembly));
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null).ToArray();
}
}
}
}
@@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup>
@@ -1,4 +1,4 @@
[
[
{
"name":"Afghan Afghani",
"code":"AFN",
@@ -58,7 +58,7 @@
{
"name":"Argentine Peso",
"code":"ARS",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},
@@ -289,7 +289,7 @@
{
"name":"Colombian Peso",
"code":"COP",
"divisibility":2,
"divisibility":0,
"symbol":null,
"crypto":false
},
@@ -64,11 +64,28 @@ namespace BTCPayServer.Services.Rates
{
if (_CurrencyProviders.Count == 0)
{
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
// This avoid storms of exception throwing slowing up
// startup and debugging sessions
if (culture switch
{
{ LCID: 0x007F or 0x0000 or 0x0c00 or 0x1000 } => true,
{ IsNeutralCulture : true } => true,
_ => false
})
continue;
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
var symbol = new RegionInfo(culture.LCID).ISOCurrencySymbol;
var c = symbol switch
{
// ARS and COP are officially 2 digits, but due to depreciation,
// nobody really use those anymore. (See https://github.com/btcpayserver/btcpayserver/issues/5708)
"ARS" or "COP" => ModifyCurrencyDecimalDigit(culture, 0),
_ => culture
};
_CurrencyProviders.TryAdd(symbol, c);
}
catch { }
}
@@ -82,6 +99,15 @@ namespace BTCPayServer.Services.Rates
}
}
private CultureInfo ModifyCurrencyDecimalDigit(CultureInfo culture, int decimals)
{
var modifiedCulture = new CultureInfo(culture.Name);
NumberFormatInfo modifiedNumberFormat = (NumberFormatInfo)modifiedCulture.NumberFormat.Clone();
modifiedNumberFormat.CurrencyDecimalDigits = decimals;
modifiedCulture.NumberFormat = modifiedNumberFormat;
return modifiedCulture;
}
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
{
var culture = new CultureInfo("en-US");
@@ -29,7 +29,7 @@ namespace BTCPayServer.Rating.Providers
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.btcturk.com/api/v2/ticker", cancellationToken);
using var response = await _httpClient.GetAsync("https://api.btcturk.com/api/v2/ticker", cancellationToken);
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
var tickers = jarray.ToObject<Ticker[]>();
return tickers
@@ -110,7 +110,7 @@ namespace BTCPayServer.Services.Rates
public void LoadState(BackgroundFetcherState state)
{
if (state.LastRequested is DateTimeOffset lastRequested)
if (state.LastRequested is DateTimeOffset)
this.LastRequested = state.LastRequested;
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
{
@@ -21,7 +21,7 @@ namespace BTCPayServer.Services.Rates
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://public.bitbank.cc/tickers", cancellationToken);
using var response = await _httpClient.GetAsync("https://public.bitbank.cc/tickers", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var data = jobj.ContainsKey("data") ? jobj["data"] : null;
if (jobj["success"]?.Value<int>() != 1)
@@ -19,7 +19,7 @@ namespace BTCPayServer.Services.Rates
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://api.bitflyer.jp/v1/ticker", cancellationToken);
using var response = await _httpClient.GetAsync("https://api.bitflyer.jp/v1/ticker", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
if (jobj.Property("error_message")?.Value?.Value<string>() is string err)
{

Some files were not shown because too many files have changed in this diff Show More