initial commit

This commit is contained in:
Kukks
2023-01-16 10:31:48 +01:00
parent 136273406c
commit 25ccd99558
171 changed files with 10592 additions and 0 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
**/bin/**/*
**/obj
.idea

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "submodules/walletwasabi"]
path = submodules/walletwasabi
url = https://github.com/kukks/walletwasabi
[submodule "submodules/btcpayserver"]
path = submodules/btcpayserver
url = https://github.com/btcpayserver/btcpayserver

View File

@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BTCPayServer: Altcoins-HTTPS" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net6.0" />
<option name="LAUNCH_PROFILE_NAME" value="Altcoins-HTTPS" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build Solution" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="ConfigBuilder" run_configuration_type="DotNetProject" />
<option name="Build" />
</method>
</configuration>
</component>

117
BTCPayServerPlugins.sln Normal file
View File

@@ -0,0 +1,117 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletWasabi", "submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj", "{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer", "submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj", "{B19C9F52-DC47-466D-8B5C-2D202B7B003F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.BitcoinWhitepaper", "Plugins\BTCPayServer.Plugins.BitcoinWhitepaper\BTCPayServer.Plugins.BitcoinWhitepaper.csproj", "{AD9635BB-C70E-4676-BB04-900D51B01666}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{8F158B88-0FEE-44FF-8552-7C0F17D5C508}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj", "{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Data", "submodules\btcpayserver\BTCPayServer.Data\BTCPayServer.Data.csproj", "{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Rating", "submodules\btcpayserver\BTCPayServer.Rating\BTCPayServer.Rating.csproj", "{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Common", "submodules\btcpayserver\BTCPayServer.Common\BTCPayServer.Common.csproj", "{3F2E0BA0-9EA7-490F-894D-F9703F35B174}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Custodians.FakeCustodian", "submodules\btcpayserver\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj", "{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BTCPay", "BTCPay", "{9E04ECE9-E304-4FF2-9CBC-83256E6C6962}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigBuilder", "ConfigBuilder\ConfigBuilder.csproj", "{6295533A-F941-40CA-B889-FE6C0432ED53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.FixedFloat", "Plugins\BTCPayServer.Plugins.FixedFloat\BTCPayServer.Plugins.FixedFloat.csproj", "{58863D86-3C78-4BEC-ACB6-2F82CC141210}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.LiquidPlus", "Plugins\BTCPayServer.Plugins.LiquidPlus\BTCPayServer.Plugins.LiquidPlus.csproj", "{B4E2ED08-4AD3-4648-8BDB-3107200460B9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.NFC", "Plugins\BTCPayServer.Plugins.NFC\BTCPayServer.Plugins.NFC.csproj", "{71885A5E-1B00-4676-9566-D81AAE37406C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.SideShift", "Plugins\BTCPayServer.Plugins.SideShift\BTCPayServer.Plugins.SideShift.csproj", "{5E1BAA06-7828-47BC-89D6-19C2A78EA427}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.TicketTailor", "Plugins\BTCPayServer.Plugins.TicketTailor\BTCPayServer.Plugins.TicketTailor.csproj", "{7AFC20EB-1696-47D7-8E57-822B05DD18F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Wabisabi", "Plugins\BTCPayServer.Plugins.Wabisabi\BTCPayServer.Plugins.Wabisabi.csproj", "{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Release|Any CPU.Build.0 = Release|Any CPU
{B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Release|Any CPU.Build.0 = Release|Any CPU
{B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU
{B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU
{AD9635BB-C70E-4676-BB04-900D51B01666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD9635BB-C70E-4676-BB04-900D51B01666}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD9635BB-C70E-4676-BB04-900D51B01666}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD9635BB-C70E-4676-BB04-900D51B01666}.Release|Any CPU.Build.0 = Release|Any CPU
{8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Release|Any CPU.Build.0 = Release|Any CPU
{8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU
{8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU
{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Release|Any CPU.Build.0 = Release|Any CPU
{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Release|Any CPU.Build.0 = Release|Any CPU
{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU
{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU
{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Release|Any CPU.Build.0 = Release|Any CPU
{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Debug|Any CPU.ActiveCfg = Altcoins-Release|Any CPU
{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Debug|Any CPU.Build.0 = Altcoins-Release|Any CPU
{3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU
{3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU
{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Release|Any CPU.Build.0 = Release|Any CPU
{6295533A-F941-40CA-B889-FE6C0432ED53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6295533A-F941-40CA-B889-FE6C0432ED53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6295533A-F941-40CA-B889-FE6C0432ED53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6295533A-F941-40CA-B889-FE6C0432ED53}.Release|Any CPU.Build.0 = Release|Any CPU
{58863D86-3C78-4BEC-ACB6-2F82CC141210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58863D86-3C78-4BEC-ACB6-2F82CC141210}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58863D86-3C78-4BEC-ACB6-2F82CC141210}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58863D86-3C78-4BEC-ACB6-2F82CC141210}.Release|Any CPU.Build.0 = Release|Any CPU
{B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Release|Any CPU.Build.0 = Release|Any CPU
{71885A5E-1B00-4676-9566-D81AAE37406C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71885A5E-1B00-4676-9566-D81AAE37406C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71885A5E-1B00-4676-9566-D81AAE37406C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71885A5E-1B00-4676-9566-D81AAE37406C}.Release|Any CPU.Build.0 = Release|Any CPU
{5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Release|Any CPU.Build.0 = Release|Any CPU
{7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Release|Any CPU.Build.0 = Release|Any CPU
{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{8F158B88-0FEE-44FF-8552-7C0F17D5C508} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{3F2E0BA0-9EA7-490F-894D-F9703F35B174} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>

18
ConfigBuilder/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["ConfigBuilder/ConfigBuilder.csproj", "ConfigBuilder/"]
RUN dotnet restore "ConfigBuilder/ConfigBuilder.csproj"
COPY . .
WORKDIR "/src/ConfigBuilder"
RUN dotnet build "ConfigBuilder.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ConfigBuilder.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConfigBuilder.dll"]

12
ConfigBuilder/Program.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Text.Json;
var plugins = Directory.GetDirectories("../../../../Plugins");
Console.WriteLine(string.Join(',',plugins));
var p = string.Join(';', plugins.Select(s => $"{Path.GetFullPath(s)}/bin/Debug/net6.0/{Path.GetFileName(s)}.dll" ));;
var fileContents = $"{{ \"DEBUG_PLUGINS\": \"{p}\"}}";
var content = JsonSerializer.Serialize(new
{
DEBUG_PLUGINS = p
});
await File.WriteAllTextAsync("../../../../submodules/BTCPayServer/BTCPayServer/appsettings.dev.json", content);

View File

@@ -0,0 +1,222 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.Protocol;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.AOPP
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/AOPP")]
public class AOPPController : Controller
{
private readonly BTCPayServerClient _btcPayServerClient;
private readonly AOPPService _AOPPService;
public AOPPController(BTCPayServerClient btcPayServerClient, AOPPService AOPPService)
{
_btcPayServerClient = btcPayServerClient;
_AOPPService = AOPPService;
}
[HttpGet("")]
public async Task<IActionResult> UpdateAOPPSettings(string storeId)
{
var store = await _btcPayServerClient.GetStore(storeId);
UpdateAOPPSettingsViewModel vm = new UpdateAOPPSettingsViewModel();
vm.StoreName = store.Name;
AOPPSettings AOPP = null;
try
{
AOPP = await _AOPPService.GetAOPPForStore(storeId);
}
catch (Exception)
{
// ignored
}
SetExistingValues(AOPP, vm);
return View(vm);
}
private void SetExistingValues(AOPPSettings existing, UpdateAOPPSettingsViewModel vm)
{
if (existing == null)
return;
vm.Enabled = existing.Enabled;
}
[HttpPost("")]
public async Task<IActionResult> UpdateAOPPSettings(string storeId, UpdateAOPPSettingsViewModel vm,
string command)
{
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var AOPPSettings = new AOPPSettings()
{
Enabled = vm.Enabled,
};
switch (command)
{
case "save":
await _AOPPService.SetAOPPForStore(storeId, AOPPSettings);
TempData["SuccessMessage"] = "AOPP settings modified";
return RedirectToAction(nameof(UpdateAOPPSettings), new {storeId});
default:
return View(vm);
}
}
internal static String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n";
internal static byte[] BITCOIN_SIGNED_MESSAGE_HEADER_BYTES = Encoding.UTF8.GetBytes(BITCOIN_SIGNED_MESSAGE_HEADER);
//http://bitcoinj.googlecode.com/git-history/keychain/core/src/main/java/com/google/bitcoin/core/Utils.java
private static byte[] FormatMessageForSigning(byte[] messageBytes)
{
MemoryStream ms = new MemoryStream();
ms.WriteByte((byte)BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length);
ms.Write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES, 0, BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length);
VarInt size = new VarInt((ulong)messageBytes.Length);
ms.Write(size.ToBytes(), 0, size.ToBytes().Length);
ms.Write(messageBytes, 0, messageBytes.Length);
return ms.ToArray();
}
public class AoppRequest
{
public Uri aopp { get; set; }
}
[HttpPost]
[Route("{invoiceId}")]
[AllowAnonymous]
public async Task<IActionResult> AOPPExecute(string storeId, string invoiceId,
[FromBody] AoppRequest request ,
[FromServices] IHttpClientFactory httpClientFactory,
[FromServices] BTCPayNetworkProvider btcPayNetworkProvider,
[FromServices] IExplorerClientProvider explorerClientProvider,
[FromServices] BTCPayServerClient btcPayServerClient,
[FromServices] IBTCPayServerClientFactory btcPayServerClientFactory)
{
try
{
var client = await btcPayServerClientFactory.Create(null, new[] {storeId});
var invoice = await client.GetInvoice(storeId, invoiceId);
if (invoice.Status is not InvoiceStatus.New)
{
return NotFound();
}
var qs = HttpUtility.ParseQueryString(request.aopp.Query);
var asset = qs.Get("asset");
var network = btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(asset);
var invoicePaymentMethods = await client.GetInvoicePaymentMethods(storeId, invoiceId);
var pm = invoicePaymentMethods.FirstOrDefault(model =>
model.PaymentMethod.Equals(asset, StringComparison.InvariantCultureIgnoreCase));
if (pm is null)
{
return NotFound();
}
var supported = (await client.GetStoreOnChainPaymentMethods(storeId))
.FirstOrDefault(settings => settings.CryptoCode.Equals(asset, StringComparison.InvariantCultureIgnoreCase));
;
var msg = qs.Get("msg");
var format = qs.Get("format");
var callback = new Uri(qs.Get("callback")!, UriKind.Absolute);
ScriptType? expectedType = null;
switch (format)
{
case "p2pkh":
expectedType = ScriptType.P2PKH;
break;
case "p2wpkh":
expectedType = ScriptType.P2WPKH;
break;
case "p2sh":
expectedType = ScriptType.P2SH;
break;
case "p2tr":
expectedType = ScriptType.Taproot;
break;
case "any":
break;
}
var address = BitcoinAddress.Create(pm.Destination, network.NBitcoinNetwork);
if (expectedType is not null && !address.ScriptPubKey
.IsScriptType(expectedType.Value))
{
return BadRequest();
}
var derivatonScheme =
network.NBXplorerNetwork.DerivationStrategyFactory.Parse(supported.DerivationScheme);
var explorerClient = explorerClientProvider.GetExplorerClient(network);
var extKeyStr = await explorerClient.GetMetadataAsync<string>(
derivatonScheme,
WellknownMetadataKeys.AccountHDKey);
if (extKeyStr == null)
{
return BadRequest();
}
var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
var keyInfo = await explorerClient.GetKeyInformationAsync(derivatonScheme, address.ScriptPubKey);
var privateKey = accountKey.Derive(keyInfo.KeyPath).PrivateKey;
var messageBytes = Encoding.UTF8.GetBytes(msg);
byte[] data = FormatMessageForSigning(messageBytes);
var hash = Hashes.DoubleSHA256(data);
var sig = Convert.ToBase64String(privateKey.SignCompact(hash, true).Signature);
var response = new
{
version = 0,
address = pm.Destination,
signature = sig
};
using var httpClient = httpClientFactory.CreateClient();
await httpClient.PostAsync(callback,
new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"));
return Ok();
}
catch (Exception e)
{
return BadRequest(new {ErrorMessage = e.Message});
}
}
}
}

View File

@@ -0,0 +1,42 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.AOPP
{
public class AOPPPlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.AOPP";
public override string Name => "AOPP";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0" }
};
public override string Description =>
"Allows you to support the AOPP protocol in invoices to allow customers to bypass stupid KYC rules.";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<AOPPService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/StoreIntegrationAOPPOption",
"store-integrations-list"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/CheckoutContentExtension",
"checkout-bitcoin-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/CheckoutContentExtension",
"checkout-lightning-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/CheckoutTabExtension",
"checkout-bitcoin-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/CheckoutTabExtension",
"checkout-lightning-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/CheckoutEnd",
"checkout-end"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("AOPP/AOPPNav",
"store-integrations-nav"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Plugins.AOPP
{
public class AOPPService
{
private readonly ISettingsRepository _settingsRepository;
private readonly IMemoryCache _memoryCache;
private readonly IStoreRepository _storeRepository;
public AOPPService(ISettingsRepository settingsRepository, IMemoryCache memoryCache,
IStoreRepository storeRepository)
{
_settingsRepository = settingsRepository;
_memoryCache = memoryCache;
_storeRepository = storeRepository;
}
public async Task<AOPPSettings> GetAOPPForStore(string storeId)
{
var k = $"{nameof(AOPPSettings)}_{storeId}";
return await _memoryCache.GetOrCreateAsync(k, async _ =>
{
var res = await _storeRepository.GetSettingAsync<AOPPSettings>(storeId,
nameof(AOPPSettings));
if (res is not null) return res;
res = await _settingsRepository.GetSettingAsync<AOPPSettings>(k);
if (res is not null)
{
await SetAOPPForStore(storeId, res);
}
await _settingsRepository.UpdateSetting<AOPPSettings>(null, k);
return res;
});
}
public async Task SetAOPPForStore(string storeId, AOPPSettings AOPPSettings)
{
var k = $"{nameof(AOPPSettings)}_{storeId}";
await _storeRepository.UpdateSetting(storeId, nameof(AOPPSettings), AOPPSettings);
_memoryCache.Set(k, AOPPSettings);
}
}
}

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Plugins.AOPP
{
public class AOPPSettings
{
public bool Enabled { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<AssemblyVersion>1.0.1</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<ProjectReference Include="..\..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Shared" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.AOPP
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.AOPP BTCPayServer.Plugins.AOPP ../packed

View File

@@ -0,0 +1,36 @@
Vue.component("AOPP", {
props: ["srvModel"],
methods: {
onaoppChange: function(){
this.aoppAddressInputDirty = true;
this.aoppAddressInputInvalid = false;
},
onSubmit : function(){
var self = this;
if (this.aoppAddressInput && this.aoppAddressInput.startsWith("aopp:?")) {
this.aoppAddressFormSubmitting = true;
// Push the email to a server, once the reception is confirmed move on
$.ajax({
url: "/plugins/"+this.srvModel.storeId+"/AOPP/" +this.srvModel.invoiceId,
type: "POST",
data: JSON.stringify({ aopp: this.aoppAddressInput }),
contentType: "application/json; charset=utf-8"
})
.done(function () {
}).always(function () {
self.aoppAddressFormSubmitting = false;
});
} else {
this.aoppAddressInputInvalid = true;
}
}
},
data: function () {
return {
aoppAddressInput: "",
aoppAddressInputDirty: false,
aoppAddressInputInvalid: false,
aoppAddressFormSubmitting: false
}
}
});

View File

@@ -0,0 +1,8 @@
namespace BTCPayServer.Plugins.AOPP
{
public class UpdateAOPPSettingsViewModel
{
public bool Enabled { get; set; }
public string StoreName { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
@model BTCPayServer.Plugins.AOPP.UpdateAOPPSettingsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIStores/_Nav";
}
<h2 class="mb-4">@ViewData["PageTitle"]</h2>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group form-check">
<label asp-for="Enabled" class="form-check-label"></label>
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View File

@@ -0,0 +1,18 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.AOPP
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(AOPPController).StartsWith(controller.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-area="" asp-controller="AOPP" asp-action="UpdateAOPPSettings" asp-route-storeId="@storeId" class="nav-link js-scroll-trigger @(isActive? "active": string.Empty)">
<svg role="img" class="icon">
</svg>
<span>AOPP</span>
</a>
</li>
}

View File

@@ -0,0 +1,40 @@
@using BTCPayServer.Plugins.AOPP
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject AOPPService AOPPService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await AOPPService.GetAOPPForStore(storeId);
if (settings?.Enabled is true)
{
<div id="AOPP" class="bp-view payment manual-flow" v-bind:class="{ 'active': currentTab == 'AOPP'}">
<AOPP inline-template v-if="currentTab == 'AOPP'" v-bind:srv-model="srvModel">
<form class="manual__step-one refund-address-form contact-email-form aopp-form" id="aopp-form" name="aopp-form" novalidate="" v-on:submit.prevent="onSubmit">
<div class="manual__step-one__header">
<span>{{$t("AOPP")}}</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" v-show="!aoppAddressInputInvalid">
<span>If you are sending funds from an exchange that requiores that you "verify" the withdrawal access, you can use this tool to bypass this madness. You even earn bonus points if they try to pass that data over to a chain surveillance service, by poisoning their clusters. </span>
</span>
<span class="submission-error-label" v-show="aoppAddressInputInvalid">{{$t("Please enter a valid aopp address")}}</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input"
v-bind:class="{ 'ng-pristine ng-submitted ng-touched': !aoppAddressInputDirty, 'ng-invalid form-input-invalid': aoppAddressInputInvalid }" id="aoppAddressFormInput"
v-bind:placeholder="$t('AOPP url')" type="url" v-model="aoppAddressInput"
v-on:change="onaoppChange">
<bp-loading-button>
<button type="submit" class="action-button" style="margin-top: 15px;" v-bind:disabled="aoppAddressFormSubmitting" v-bind:class="{ 'loading': aoppAddressFormSubmitting }">
<span class="button-text">{{$t("Submit")}}</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>
</div>
</form>
</AOPP>
</div>
}
}

View File

@@ -0,0 +1,13 @@
@using BTCPayServer.Plugins.AOPP
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject AOPPService AOPPService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await AOPPService.GetAOPPForStore(storeId);
if (settings?.Enabled is true)
{
<script src="~/Resources/js/aoppComponent.js"></script>
}
}

View File

@@ -0,0 +1,14 @@
@using BTCPayServer.Plugins.AOPP
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject AOPPService AOPPService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await AOPPService.GetAOPPForStore(storeId);
if (settings?.Enabled is true)
{
<div class="payment-tabs__tab" id="AOPP-tab" v-on:click="switchTab('AOPP')" v-bind:class="{ 'active': currentTab == 'AOPP'}" v-if="srvModel.paymentMethodId.indexOf('_') === -1">
<span>{{$t("AOPP")}}</span>
</div>
}
}

View File

@@ -0,0 +1,57 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.AOPP
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject AOPPService AOPPService
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
AOPPSettings settings = null;
if (!string.IsNullOrEmpty(storeId))
{
try
{
settings = await AOPPService.GetAOPPForStore(storeId);
}
catch (Exception)
{
}
}
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="list-group-item bg-tile ">
<div class="d-flex align-items-center">
<span class="d-flex flex-wrap flex-fill flex-column flex-sm-row">
<strong class="me-3">
AOPP
</strong>
<span title="" class="d-flex me-3">
<span class="text-secondary">Allows your customers to pay with altcoins that are not supported by BTCPay Server.</span>
</span>
</span>
<span class="d-flex align-items-center fw-semibold">
@if (settings?.Enabled is true)
{
<span class="d-flex align-items-center text-success">
<span class="me-2 btcpay-status btcpay-status--enabled"></span>
Enabled
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="AOPP" asp-action="UpdateAOPPSettings" asp-route-storeId="@storeId">
Modify
</a>
}
else
{
<span class="d-flex align-items-center text-danger">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="AOPP" asp-action="UpdateAOPPSettings" asp-route-storeId="@storeId">
Setup
</a>
}
</span>
</div>
</li>
}

View File

@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Title>Bitcoin Whitepaper</Title>
<Description>This makes the Bitcoin whitepaper available on your BTCPay Server.</Description>
<Authors>Kukks</Authors>
<Version>1.0.2</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj">
</ProjectReference>
<EmbeddedResource Include="bitcoin.pdf" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
namespace BTCPayServer.Plugins.BitcoinWhitepaper
{
public class BitcoinWhitepaperPlugin: BaseBTCPayServerPlugin
{
public override string Identifier { get; } = "BTCPayServer.Plugins.BitcoinWhitepaper";
public override string Name { get; } = "Bitcoin Whitepaper";
public override string Description { get; } = "This makes the Bitcoin whitepaper available on your BTCPay Server.";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.4.0.0" }
};
}
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Title>FixedFloat</Title>
<Description>Allows you to embed a FixedFloat conversion screen to allow customers to pay with altcoins.</Description>
<Authors>Kukks</Authors>
<Version>1.0.6</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Shared" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.FixedFloat
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/FixedFloat")]
public class FixedFloatController : Controller
{
private readonly BTCPayServerClient _btcPayServerClient;
private readonly FixedFloatService _FixedFloatService;
public FixedFloatController(BTCPayServerClient btcPayServerClient, FixedFloatService FixedFloatService)
{
_btcPayServerClient = btcPayServerClient;
_FixedFloatService = FixedFloatService;
}
[HttpGet("")]
public async Task<IActionResult> UpdateFixedFloatSettings(string storeId)
{
var store = await _btcPayServerClient.GetStore(storeId);
UpdateFixedFloatSettingsViewModel vm = new UpdateFixedFloatSettingsViewModel();
vm.StoreName = store.Name;
FixedFloatSettings FixedFloat = null;
try
{
FixedFloat = await _FixedFloatService.GetFixedFloatForStore(storeId);
}
catch (Exception)
{
// ignored
}
SetExistingValues(FixedFloat, vm);
return View(vm);
}
private void SetExistingValues(FixedFloatSettings existing, UpdateFixedFloatSettingsViewModel vm)
{
if (existing == null)
return;
vm.Enabled = existing.Enabled;
}
[HttpPost("")]
public async Task<IActionResult> UpdateFixedFloatSettings(string storeId, UpdateFixedFloatSettingsViewModel vm,
string command)
{
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var FixedFloatSettings = new FixedFloatSettings()
{
Enabled = vm.Enabled,
};
switch (command)
{
case "save":
await _FixedFloatService.SetFixedFloatForStore(storeId, FixedFloatSettings);
TempData["SuccessMessage"] = "FixedFloat settings modified";
return RedirectToAction(nameof(UpdateFixedFloatSettings), new {storeId});
default:
return View(vm);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.FixedFloat
{
public class FixedFloatPlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.FixedFloat";
public override string Name => "FixedFloat";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" }
};
public override string Description =>
"Allows you to embed a FixedFloat conversion screen to allow customers to pay with altcoins.";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<FixedFloatService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/FixedFloatNav",
"store-integrations-nav"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/StoreIntegrationFixedFloatOption",
"store-integrations-list"));
// Checkout v2
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutPaymentMethodExtension",
"checkout-payment-method"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutPaymentExtension",
"checkout-payment"));
// Checkout Classic
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutContentExtension",
"checkout-bitcoin-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutContentExtension",
"checkout-lightning-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutTabExtension",
"checkout-bitcoin-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutTabExtension",
"checkout-lightning-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FixedFloat/CheckoutEnd",
"checkout-end"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Plugins.FixedFloat
{
public class FixedFloatService
{
private readonly ISettingsRepository _settingsRepository;
private readonly IStoreRepository _storeRepository;
private readonly IMemoryCache _memoryCache;
public FixedFloatService(ISettingsRepository settingsRepository, IStoreRepository storeRepository, IMemoryCache memoryCache)
{
_settingsRepository = settingsRepository;
_storeRepository = storeRepository;
_memoryCache = memoryCache;
}
public async Task<FixedFloatSettings> GetFixedFloatForStore(string storeId)
{
var k = $"{nameof(FixedFloatSettings)}_{storeId}";
return await _memoryCache.GetOrCreateAsync(k, async _ =>
{
var res = await _storeRepository.GetSettingAsync<FixedFloatSettings>(storeId,
nameof(FixedFloatSettings));
if (res is not null) return res;
res = await _settingsRepository.GetSettingAsync<FixedFloatSettings>(k);
if (res is not null)
{
await SetFixedFloatForStore(storeId, res);
}
await _settingsRepository.UpdateSetting<FixedFloatSettings>(null, k);
return res;
});
}
public async Task SetFixedFloatForStore(string storeId, FixedFloatSettings fixedFloatSettings)
{
var k = $"{nameof(FixedFloatSettings)}_{storeId}";
await _storeRepository.UpdateSetting(storeId, nameof(FixedFloatSettings), fixedFloatSettings);
_memoryCache.Set(k, fixedFloatSettings);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace BTCPayServer.Plugins.FixedFloat
{
public class FixedFloatSettings
{
public bool Enabled { get; set; }
public decimal AmountMarkupPercentage { get; set; } = 0;
}
}

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.FixedFloat
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.FixedFloat BTCPayServer.Plugins.FixedFloat ../packed

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@@ -0,0 +1,35 @@
Vue.component("fixed-float", {
props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress"],
data() {
return {
shown: false,
};
},
computed: {
url() {
let settleMethodId = "";
if (
this.toCurrency.endsWith("LightningLike") ||
this.toCurrency.endsWith("LNURLPay")
) {
settleMethodId = "BTCLN";
} else {
settleMethodId = this.toCurrency
.replace("_BTCLike", "")
.replace("_MoneroLike", "")
.replace("_ZcashLike", "")
.toUpperCase();
}
const topup = this.$parent.srvModel.isUnsetTopUp;
return (
"https://widget.fixedfloat.com/?" +
`to=${settleMethodId}` +
"&lockReceive=true&ref=fkbyt39c" +
`&address=${this.toCurrencyAddress}` +
(topup
? ""
: `&lockType=true&hideType=true&lockAmount=true&toAmount=${this.toCurrencyDue}`)
);
},
},
});

View File

@@ -0,0 +1,8 @@
namespace BTCPayServer.Plugins.FixedFloat
{
public class UpdateFixedFloatSettingsViewModel
{
public bool Enabled { get; set; }
public string StoreName { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
@using BTCPayServer.Abstractions.Extensions
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Plugins.FixedFloat.UpdateFixedFloatSettingsViewModel
@{
ViewData.SetActivePage("FixedFloat", "FixedFloat", "FixedFloat");
}
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2>
<div class="alert alert-warning mb-4" role="alert">
If you are enabling FixedFloat support, we advise that you configure the invoice expiration to a minimum of 30 minutes as it may take longer than the default 15 minutes to convert the funds.
</div>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="Enabled" class="form-label mb-0 me-1"></label>
</div>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
</form>
</div>
</div>

View File

@@ -0,0 +1,19 @@
@using BTCPayServer.Plugins.FixedFloat
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject FixedFloatService FixedFloatService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await FixedFloatService.GetFixedFloatForStore(storeId);
if (settings?.Enabled is true)
{
<div id="FixedFloat" class="bp-view payment manual-flow" style="padding:0" :class="{ active: currentTab == 'undefined' || currentTab == 'FixedFloat' }">
<fixed-float inline-template
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.btcDue * (1 + (@settings.AmountMarkupPercentage / 100)) "
:to-currency-address="srvModel.btcAddress">
<iframe :src="url" style="min-height:600px;width:100%;border:none" allowtransparency="true"></iframe>
</fixed-float>
</div>
}
}

View File

@@ -0,0 +1,12 @@
@using BTCPayServer.Plugins.FixedFloat
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject FixedFloatService FixedFloatService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await FixedFloatService.GetFixedFloatForStore(storeId);
if (settings?.Enabled is true)
{
<script src="~/Resources/js/fixedFloatComponent.js"></script>
}
}

View File

@@ -0,0 +1,46 @@
@using BTCPayServer.Plugins.FixedFloat
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject FixedFloatService FixedFloatService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await FixedFloatService.GetFixedFloatForStore(storeId);
}
@if (settings?.Enabled is true)
{
<template id="fixed-float-checkout-template">
<iframe :src="url" style="min-height:600px;width:100%;border:none" allowtransparency="true"></iframe>
</template>
<script>
const markupPercentage = @settings.AmountMarkupPercentage;
Vue.component("FixedFloatCheckout", {
template: "#fixed-float-checkout-template",
props: ["model"],
computed: {
url () {
return "https://widget.fixedfloat.com/?" +
`to=${this.settleMethodId}` +
"&lockReceive=true&ref=fkbyt39c" +
`&address=${this.model.btcAddress}` +
this.amountQuery;
},
currency () {
return this.model.paymentMethodId;
},
settleMethodId () {
return this.currency.endsWith('LightningLike') || this.currency.endsWith('LNURLPay')
? 'BTCLN'
: this.currency.replace('_BTCLike', '').replace('_MoneroLike', '').replace('_ZcashLike', '').toUpperCase();
},
amountQuery () {
return this.model.isUnsetTopUp
? ''
: `&lockType=true&hideType=true&lockAmount=true&toAmount=${this.amountDue}`;
},
amountDue () {
return this.model.btcDue * (1 + (markupPercentage / 100));
}
}
});
</script>
}

View File

@@ -0,0 +1,16 @@
@using BTCPayServer.Plugins.FixedFloat
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject FixedFloatService FixedFloatService
@{
const string id = "FixedFloat";
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await FixedFloatService.GetFixedFloatForStore(storeId);
if (settings?.Enabled is true)
{
<a href="#@id" class="btcpay-pill m-0 payment-method" :class="{ active: pmId === '@id' }" v-on:click.prevent="changePaymentMethod('@id')">
@id
</a>
}
}

View File

@@ -0,0 +1,14 @@
@using BTCPayServer.Plugins.FixedFloat
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@inject FixedFloatService FixedFloatService
@{
var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value<string>();
var settings = await FixedFloatService.GetFixedFloatForStore(storeId);
if (settings?.Enabled is true)
{
<div class="payment-tabs__tab py-0" id="FixedFloat-tab" v-on:click="switchTab('FixedFloat')" v-bind:class="{ 'active': currentTab == 'FixedFloat'}" v-if="!srvModel.paymentMethodId.endsWith('LNURLPAY')">
<span>{{$t("Altcoins (FixedFloat)")}}</span>
</div>
}
}

View File

@@ -0,0 +1,16 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-area="" asp-controller="FixedFloat" asp-action="UpdateFixedFloatSettings" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("FixedFloat")" id="Nav-FixedFloat">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160" alt="FixedFloat" class="icon"><g transform="translate(-20,-20)"><path fill="currentColor" d="m111.6 96.1-9.6-9.6-9.5-9.5a4.7 4.7 0 0 0-3.3-1.4c-1.2 0-2.4.5-3.3 1.4L73.6 89.3l-12.3 12.3-3.9-3.9-3.9-3.9c-.7-.7-1.6-.8-2.3-.5-.7.3-1.3 1-1.3 1.9V138a2.04 2.04 0 0 0 2.1 2.1h42.6c.9 0 1.6-.6 1.9-1.3.3-.7.2-1.6-.5-2.3l-4.6-4.6-4.6-4.6L99.3 115l12.3-12.3c.9-.9 1.4-2.1 1.4-3.3 0-1.2-.5-2.4-1.4-3.3z"/><path fill="currentColor" d="M107.1 133.4c-1.2 0-2.3-.4-3.2-1.3a4.47 4.47 0 0 1 0-6.4l27.6-27.6c1.8-1.8 4.6-1.8 6.4 0l3 3V68.9h-32.3l3.1 3.1c1.8 1.8 1.8 4.6 0 6.4a4.47 4.47 0 0 1-6.4 0L94.6 67.6a4.47 4.47 0 0 1-1-4.9c.7-1.7 2.3-2.8 4.2-2.8h47.7c2.5 0 4.5 2 4.5 4.5v47.7c0 1.8-1.1 3.5-2.8 4.2-1.7.7-3.6.3-4.9-1l-7.6-7.6-24.4 24.4c-.8.9-2 1.3-3.2 1.3z"/></g></svg>
<span>FixedFloat</span>
</a>
</li>
}

View File

@@ -0,0 +1,58 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.FixedFloat
@using Microsoft.AspNetCore.Routing
@inject FixedFloatService FixedFloatService
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
FixedFloatSettings settings = null;
if (!string.IsNullOrEmpty(storeId))
{
try
{
settings = await FixedFloatService.GetFixedFloatForStore(storeId);
}
catch (Exception)
{
}
}
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="list-group-item bg-tile ">
<div class="d-flex align-items-center">
<span class="d-flex flex-wrap flex-fill flex-column flex-sm-row">
<strong class="me-3">
FixedFloat
</strong>
<span title="" class="d-flex me-3">
<span class="text-secondary">Allows your customers to pay with altcoins that are not supported by BTCPay Server.</span>
</span>
</span>
<span class="d-flex align-items-center fw-semibold">
@if (settings?.Enabled is true)
{
<span class="d-flex align-items-center text-success">
<span class="me-2 btcpay-status btcpay-status--enabled"></span>
Enabled
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="FixedFloat" asp-action="UpdateFixedFloatSettings" asp-route-storeId="@storeId">
Modify
</a>
}
else
{
<span class="d-flex align-items-center text-danger">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="FixedFloat" asp-action="UpdateFixedFloatSettings" asp-route-storeId="@storeId">
Setup
</a>
}
</span>
</div>
</li>
}

View File

@@ -0,0 +1,2 @@
@addTagHelper *, BTCPayServer.Abstractions
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<AssemblyVersion>1.0.0</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin.Secp256k1" Version="3.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.VisualBasic;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBitcoin.Secp256k1;
using Newtonsoft.Json.Linq;
using Key = NBitcoin.Key;
namespace BTCPayServer.Plugins.FujiOracle
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/FujiOracle")]
public class FujiOracleController : Controller
{
private async Task<BTCPayServerClient> CreateClient(string storeId)
{
return await _btcPayServerClientFactory.Create(null, new[] {storeId}, new DefaultHttpContext()
{
Request =
{
Scheme = "https",
Host = Request.Host,
Path = Request.Path,
PathBase = Request.PathBase
}
});
}
private readonly IHttpClientFactory _httpClientFactory;
private readonly FujiOracleService _FujiOracleService;
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
public FujiOracleController(IHttpClientFactory httpClientFactory,
FujiOracleService FujiOracleService,
IBTCPayServerClientFactory btcPayServerClientFactory)
{
_httpClientFactory = httpClientFactory;
_FujiOracleService = FujiOracleService;
_btcPayServerClientFactory = btcPayServerClientFactory;
}
[HttpGet("update")]
public async Task<IActionResult> UpdateFujiOracleSettings(string storeId)
{
var
FujiOracle = (await _FujiOracleService.GetFujiOracleForStore(storeId)) ?? new();
return View(FujiOracle);
}
[HttpPost("update")]
public async Task<IActionResult> UpdateFujiOracleSettings(string storeId,
FujiOracleSettings vm,
string command)
{
if (command == "generate")
{
ModelState.Clear();
if (ECPrivKey.TryCreate(new ReadOnlySpan<byte>(RandomNumberGenerator.GetBytes(32)), out var key))
{
vm.Key = key.ToHex();
}
return View(vm);
}
if (command == "add-pair")
{
vm.Pairs ??= new List<string>();
vm.Pairs.Add("");
return View(vm);
}
if (command.StartsWith("remove-pair"))
{
var i = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) +
1));
vm.Pairs.RemoveAt(i);
return View(vm);
}
var validPairsToQuery = "";
for (var i = 0; i < vm.Pairs.Count; i++)
{
string vmPair = vm.Pairs[i];
if (string.IsNullOrWhiteSpace(vmPair))
{
ModelState.AddModelError($"{vm.Pairs}[{i}]",
$"Remove invalid");
continue;
}
var split = vmPair.Split("_", StringSplitOptions.RemoveEmptyEntries);
if (split.Length != 2)
{
ModelState.AddModelError($"{vm.Pairs}[{i}]",
$"Invalid format, needs to be BTC_USD format");
continue;
}
validPairsToQuery += "," + vmPair;
}
validPairsToQuery = validPairsToQuery.TrimStart(',');
if (!string.IsNullOrEmpty(validPairsToQuery))
{
try
{
var url = Request.GetAbsoluteUri(Url.Action("GetRates2",
"BitpayRate", new {storeId, currencyPairs = validPairsToQuery}));
var resp = JArray.Parse(await _httpClientFactory.CreateClient().GetStringAsync(url));
for (var i = 0; i < vm.Pairs.Count; i++)
{
string vmPair = vm.Pairs[i];
if (!resp.Any(token => token["currencyPair"].Value<string>() == vmPair))
{
ModelState.AddModelError($"{vm.Pairs}[{i}]",
$"You store could not resolve pair {vmPair}");
}
}
}
catch (Exception e)
{
}
}
if (string.IsNullOrEmpty(vm.Key) && vm.Enabled)
{
ModelState.AddModelError(nameof(vm.Enabled),
$"Cannot enable without a key");
}
if (!string.IsNullOrEmpty(vm.Key))
{
try
{
if (HexEncoder.IsWellFormed(vm.Key))
{
ECPrivKey.Create(Encoders.Hex.DecodeData(vm.Key));
}
}
catch (Exception e)
{
ModelState.AddModelError(nameof(vm.Enabled),
$"Key was invalid");
}
}
if (!ModelState.IsValid)
{
return View(vm);
}
switch (command?.ToLowerInvariant())
{
case "save":
await _FujiOracleService.SetFujiOracleForStore(storeId, vm);
TempData["SuccessMessage"] = "FujiOracle settings modified";
return RedirectToAction(nameof(UpdateFujiOracleSettings), new {storeId});
default:
return View(vm);
}
}
[AllowAnonymous]
[HttpGet("")]
public async Task<IActionResult> GetOracleInfo(string storeId)
{
var oracle = await _FujiOracleService.GetFujiOracleForStore(storeId);
if (oracle is null || !oracle.Enabled || oracle.Key is null)
{
return NotFound();
}
return Ok(new
{
publicKey = new Key(Encoders.Hex.DecodeData(oracle.Key)).PubKey.ToHex(),
availableTickers = oracle.Pairs.ToArray()
});
}
[AllowAnonymous]
[HttpGet("{pair}")]
public async Task<IActionResult> GetOracleAttestation(string storeId, string pair)
{
var oracle = await _FujiOracleService.GetFujiOracleForStore(storeId);
if (oracle is null || !oracle.Enabled || oracle.Key is null || !oracle.Pairs.Contains(pair))
{
return NotFound();
}
var url = Request.GetAbsoluteUri(Url.Action("GetRates2",
"BitpayRate", new {storeId, currencyPairs = pair}));
var resp = JArray.Parse(await _httpClientFactory.CreateClient().GetStringAsync(url)).First();
var ts = DateTimeOffset.Now.ToUnixTimeSeconds();
var rate =(long)decimal.Truncate( resp["rate"].Value<decimal>());
var messageBytes = BitConverter.GetBytes(ts).Concat(BitConverter.GetBytes(rate)).ToArray();
using var sha256Hash = System.Security.Cryptography.SHA256.Create();
var messageHash = sha256Hash.ComputeHash(messageBytes);
var key = Extensions.ParseKey(oracle.Key);
var buf = new byte[64];
key.SignBIP340(messageHash).WriteToSpan(buf);
var sig = buf.ToHex();
return Ok(new
{
timestamp = ts.ToString(),
lastPrice = rate.ToString(),
attestation = new {
signature= sig,
message= messageBytes.ToHex(),
messageHash= messageHash.ToHex()
},
});
}
}
public static class Extensions
{
public static ECPrivKey ParseKey(string key)
{
return ECPrivKey.Create(key.DecodHexData());
}
public static byte[] DecodHexData(this string encoded)
{
if (encoded == null)
throw new ArgumentNullException(nameof(encoded));
if (encoded.Length % 2 == 1)
throw new FormatException("Invalid Hex String");
var result = new byte[encoded.Length / 2];
for (int i = 0, j = 0; i < encoded.Length; i += 2, j++)
{
var a = IsDigit(encoded[i]);
var b = IsDigit(encoded[i + 1]);
if (a == -1 || b == -1)
throw new FormatException("Invalid Hex String");
result[j] = (byte)(((uint)a << 4) | (uint)b);
}
return result;
}
public static int IsDigit(this char c)
{
if ('0' <= c && c <= '9')
{
return c - '0';
}
else if ('a' <= c && c <= 'f')
{
return c - 'a' + 10;
}
else if ('A' <= c && c <= 'F')
{
return c - 'A' + 10;
}
else
{
return -1;
}
}
public static string ToHex(this byte[] bytes)
{
var builder = new StringBuilder();
foreach (var t in bytes)
{
builder.Append(t.ToHex());
}
return builder.ToString();
}
private static string ToHex(this byte b)
{
return b.ToString("x2");
}
public static string ToHex(this Span<byte> bytes)
{
var builder = new StringBuilder();
foreach (var t in bytes)
{
builder.Append(t.ToHex());
}
return builder.ToString();
}
public static string ToHex(this ECPrivKey key)
{
Span<byte> output = new(new byte[32]);
key.WriteToSpan(output);
return output.ToHex();
}
}
}

View File

@@ -0,0 +1,32 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.FujiOracle
{
public class FujiOraclePlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.FujiOracle";
public override string Name => "Fuji Oracle";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0" }
};
public override string Description =>
"Allows you to become an oracle for the fuji.money platform";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<FujiOracleService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FujiOracle/StoreIntegrationFujiOracleOption",
"store-integrations-list"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FujiOracle/FujiOracleNav",
"store-integrations-nav"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Plugins.FujiOracle
{
public class FujiOracleService
{
private readonly IMemoryCache _memoryCache;
private readonly IStoreRepository _storeRepository;
public FujiOracleService(IMemoryCache memoryCache, IStoreRepository storeRepository)
{
_memoryCache = memoryCache;
_storeRepository = storeRepository;
}
public async Task<FujiOracleSettings> GetFujiOracleForStore(string storeId)
{
var k = $"{nameof(FujiOracleSettings)}_{storeId}";
return await _memoryCache.GetOrCreateAsync(k, async _ =>
{
var res = await _storeRepository.GetSettingAsync<FujiOracleSettings>(storeId,
nameof(FujiOracleSettings));
return res;
});
}
public async Task SetFujiOracleForStore(string storeId, FujiOracleSettings FujiOracleSettings)
{
var k = $"{nameof(FujiOracleSettings)}_{storeId}";
await _storeRepository.UpdateSetting(storeId, nameof(FujiOracleSettings), FujiOracleSettings);
_memoryCache.Set(k, FujiOracleSettings);
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace BTCPayServer.Plugins.FujiOracle
{
public class FujiOracleSettings
{
public bool Enabled { get; set; }
public string Key { get; set; }
public List<string> Pairs { get; set; } = new();
}
}

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.FujiOracle
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.FujiOracle BTCPayServer.Plugins.FujiOracle ../packed

View File

@@ -0,0 +1,55 @@
@using BTCPayServer.Abstractions.Extensions
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Plugins.FujiOracle.FujiOracleSettings
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIStores/_Nav";
ViewData.SetActivePage("Fuji Oracle", "Update Fuji Oracle Settings", null);
}
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Key" class="form-label" data-required>Key to sign with (in hex)</label>
<div class="input-group">
<input asp-for="Key" class="form-control"/>
<button class="btn btn-outline-primary" type="submit" name="command" value="generate">Generate</button>
</div>
<span asp-validation-for="Key" class="text-danger"></span>
</div>
<div class="form-group form-check">
<label asp-for="Enabled" class="form-check-label"></label>
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
</div>
<div class="row mt-4">
<div class="h4 w-100 d-flex justify-content-between">
Pairs <button type="submit" value="add-pair" name="command" class="btn btn-outline-primary btn-sm">Add</button>
</div>
@for (var index = 0; index < Model.Pairs.Count; index++)
{
<div class="input-group">
<input class="form-control" asp-for="Pairs[index]" type="text" placeholder="BTC_USD"/>
<button type="submit" value="remove-pair:@index" name="command" class="btn btn-outline-danger">Remove</button>
</div>
}
</div>
<div class="form-group mt-4">
<input type="submit" value="Save" name="command" class="btn btn-primary"/>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,19 @@
@inject IScopeProvider ScopeProvider
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.FujiOracle
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(FujiOracleController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-area="" asp-controller="FujiOracle" asp-action="UpdateFujiOracleSettings" asp-route-storeId="@storeId" class="nav-link js-scroll-trigger @(isActive? "active": string.Empty)">
<svg role="img" class="icon">
</svg>
<span>FujiOracle</span>
</a>
</li>
}

View File

@@ -0,0 +1,58 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.FujiOracle
@inject IScopeProvider ScopeProvider
@inject FujiOracleService FujiOracleService
@{
var storeId = ScopeProvider.GetCurrentStoreId();
FujiOracleSettings settings = null;
if (!string.IsNullOrEmpty(storeId))
{
try
{
settings = await FujiOracleService.GetFujiOracleForStore(storeId);
}
catch (Exception)
{
}
}
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="list-group-item bg-tile ">
<div class="d-flex align-items-center">
<span class="d-flex flex-wrap flex-fill flex-column flex-sm-row">
<strong class="me-3">
Ticket Tailor
</strong>
<span title="" class="d-flex me-3">
<span class="text-secondary">Sell tickets on Ticket Tailor using BTCPay Server</span>
</span>
</span>
<span class="d-flex align-items-center fw-semibold">
@if (settings?.Enabled is true)
{
<span class="d-flex align-items-center text-success">
<span class="me-2 btcpay-status btcpay-status--enabled"></span>
Active
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="FujiOracle" asp-action="UpdateFujiOracleSettings" asp-route-storeId="@storeId">
Modify
</a>
}
else
{
<span class="d-flex align-items-center text-danger">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="FujiOracle" asp-action="UpdateFujiOracleSettings" asp-route-storeId="@storeId">
Setup
</a>
}
</span>
</div>
</li>
}

View File

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

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<AssemblyVersion>1.0.0</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="LNURL" Version="0.0.22" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.LSP
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/LSP")]
public class LSPController : Controller
{
[AllowAnonymous]
[HttpGet("")]
public async Task<IActionResult> View(string storeId)
{
var config = await _LSPService.GetLSPForStore(storeId);
try
{
if (config?.Enabled is true)
{
return View(new LSPViewModel() {Settings = config});
}
}
catch (Exception e)
{
// ignored
}
return NotFound();
}
[AllowAnonymous]
[HttpPost("")]
public async Task<IActionResult> Purchase(string storeId, string email, uint inbound, bool privateChannel)
{
var config = await _LSPService.GetLSPForStore(storeId);
try
{
if (config?.Enabled is not true || string.IsNullOrEmpty(email) || inbound < config.Minimum ||
inbound > config.Maximum)
{
return RedirectToAction("View", new {storeId});
}
var price = Math.Ceiling((config.FeePerSat == 0 ? 0 : (config.FeePerSat * inbound)) + config.BaseFee);
var btcpayClient = await CreateClient(storeId);
var redirectUrl = Request.GetAbsoluteUri(Url.Action("Connect",
"LSP", new {storeId, invoiceId = "kukkskukkskukks"}));
redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}");
var inv = await btcpayClient.CreateInvoice(storeId,
new CreateInvoiceRequest()
{
Amount = price,
Currency = "sats",
Type = InvoiceType.Standard,
AdditionalSearchTerms = new[] {"LSP"},
Checkout =
{
RequiresRefundEmail = true,
RedirectAutomatically = price > 0,
RedirectURL = redirectUrl,
},
Metadata = JObject.FromObject(new
{
buyerEmail = email,
privateChannel,
inbound,
config.BaseFee,
config.FeePerSat,
orderId = "LSP"
})
});
while (inv.Amount == 0 && inv.Status == InvoiceStatus.New)
{
if (inv.Status == InvoiceStatus.New)
inv = await btcpayClient.GetInvoice(inv.StoreId, inv.Id);
}
if (inv.Status == InvoiceStatus.Settled)
return RedirectToAction("Connect", new {storeId, invoiceId = inv.Id});
return Redirect(inv.CheckoutLink);
}
catch (Exception e)
{
}
return RedirectToAction("View", new {storeId});
}
[AllowAnonymous]
[HttpGet("connect")]
public async Task<IActionResult> Connect(string storeId, string invoiceId)
{
var btcpayClient = await CreateClient(storeId);
try
{
var config = await _LSPService.GetLSPForStore(storeId);
var result = new LSPConnectPage() {InvoiceId = invoiceId, Settings = config};
var invoice = await btcpayClient.GetInvoice(storeId, invoiceId);
result.Status = invoice.Status;
if (invoice.Status != InvoiceStatus.Settled) return View(result);
if (invoice.Metadata.TryGetValue("lsp-channel-complete", out _))
{
return Redirect(invoice.CheckoutLink);
}
result.Invoice = invoice;
result.LNURL = LNURL.LNURL.EncodeUri(new Uri(Request.GetAbsoluteUri(Url.Action(
"LNURLChannelRequest",
"LSP", new {storeId, invoiceId}))), "channelRequest", true).ToString();
return View(result);
}
catch (Exception e)
{
return NotFound();
}
}
private async Task<BTCPayServerClient> CreateClient(string storeId)
{
return await _btcPayServerClientFactory.Create(null, new[] {storeId},
new DefaultHttpContext()
{
Request =
{
Scheme = "https", Host = Request.Host, Path = Request.Path, PathBase = Request.PathBase
}
});
}
public class LSPConnectPage
{
public string LNURL;
public string InvoiceId { get; set; }
public InvoiceStatus Status { get; set; }
public LSPSettings Settings { get; set; }
public InvoiceData Invoice { get; set; }
}
private readonly LSPService _LSPService;
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
public LSPController(IHttpClientFactory httpClientFactory,
LSPService LSPService,
IBTCPayServerClientFactory btcPayServerClientFactory)
{
_LSPService = LSPService;
_btcPayServerClientFactory = btcPayServerClientFactory;
}
[HttpGet("update")]
public async Task<IActionResult> UpdateLSPSettings(string storeId)
{
LSPSettings vm = null;
try
{
vm = await _LSPService.GetLSPForStore(storeId);
}
catch (Exception)
{
// ignored
}
vm ??= new();
return View(vm);
}
[HttpPost("update")]
public async Task<IActionResult> UpdateLSPSettings(string storeId,
LSPSettings vm,
string command)
{
if (!ModelState.IsValid)
{
return View(vm);
}
switch (command?.ToLowerInvariant())
{
case "save":
await _LSPService.SetLSPForStore(storeId, vm);
TempData["SuccessMessage"] = "LSP settings modified";
return RedirectToAction(nameof(UpdateLSPSettings), new {storeId});
default:
return View(vm);
}
}
[AllowAnonymous]
[HttpGet("lnurlc-callback")]
public async Task<IActionResult> LNURLChannelRequestCallback(string storeId, string k1, string remoteId)
{
if (!NodeInfo.TryParse(remoteId, out var remoteNode))
{
return BadRequest();
}
var btcPayClient = await CreateClient(storeId);
var invoice = await btcPayClient.GetInvoice(storeId, k1);
if (invoice?.Status != InvoiceStatus.Settled || invoice.Metadata.TryGetValue("lsp-channel-complete", out _))
{
return NotFound();
}
var settings = await _LSPService.GetLSPForStore(storeId);
if (settings?.Enabled is not true)
{
return BadRequest();
}
if (!invoice.Metadata.TryGetValue("posData", out var posData))
{
posData = JToken.Parse("{}");
}
var inbound = invoice.Metadata["inbound"].Value<long>();
try
{
await btcPayClient.ConnectToLightningNode(storeId, "BTC", new ConnectToNodeRequest(remoteNode));
posData["LSP"] = JToken.FromObject(new Dictionary<string,object>());
posData["LSP"]["Remote Node"] = remoteId;
await btcPayClient.OpenLightningChannel(storeId, "BTC", new OpenLightningChannelRequest()
{
ChannelAmount = new Money(inbound, MoneyUnit.Satoshi),
NodeURI = remoteNode
});
posData["LSP"]["Channel Status"] = "Opened";
invoice.Metadata["posData"] = posData;
invoice.Metadata["lsp-channel-complete"] = true;
await btcPayClient.UpdateInvoice(storeId, invoice.Id,
new UpdateInvoiceRequest() {Metadata = invoice.Metadata});
return Ok(new LNURL.LNUrlStatusResponse()
{
Status = "OK"
});
}
catch (Exception e)
{
posData["Error"] =
$"Channel could not be created. You should refund customer.{Environment.NewLine}{e.Message}";
invoice.Metadata["posData"] = posData;
await btcPayClient.UpdateInvoice(storeId, invoice.Id,
new UpdateInvoiceRequest() {Metadata = invoice.Metadata});
return Ok(new LNURL.LNUrlStatusResponse()
{
Status = "ERROR", Reason = $"Channel could not be created to {remoteId}"
});
}
}
[AllowAnonymous]
[HttpGet("{invoiceId}/lnurlc")]
public async Task<IActionResult> LNURLChannelRequest(string storeId, string invoiceId, string nodeUri)
{
var btcPayClient = await CreateClient(storeId);
var invoice = await btcPayClient.GetInvoice(storeId, invoiceId);
if (invoice?.Status != InvoiceStatus.Settled || invoice.Metadata.TryGetValue("lsp-channel-complete", out _))
{
return NotFound();
}
var settings = await _LSPService.GetLSPForStore(storeId);
if (settings?.Enabled is not true)
{
return BadRequest();
}
return Ok(new LNURL.LNURLChannelRequest()
{
Tag = "channelRequest",
K1 = invoiceId,
Callback = new Uri(Request.GetAbsoluteUri(Url.Action("LNURLChannelRequestCallback",
"LSP", new {storeId}))),
Uri = nodeUri is null
? (await btcPayClient.GetLightningNodeInfo(storeId, "BTC")).NodeURIs
.OrderBy(nodeInfo => nodeInfo.IsTor).First()
: NodeInfo.Parse(nodeUri)
});
}
}
}

View File

@@ -0,0 +1,31 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.LSP
{
public class LSPPlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.LSP";
public override string Name => "LSP";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0"}
};
public override string Description =>
"Allows you to become an LSP selling lightning channels with inbound liquidity";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<LSPService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("LSP/StoreIntegrationLSPOption",
"store-integrations-list"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("LSP/LSPNav",
"store-integrations-nav"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Plugins.LSP;
public class LSPService
{
private readonly IMemoryCache _memoryCache;
private readonly IStoreRepository _storeRepository;
public LSPService(IMemoryCache memoryCache,
IStoreRepository storeRepository)
{
_memoryCache = memoryCache;
_storeRepository = storeRepository;
}
public async Task<LSPSettings> GetLSPForStore(string storeId)
{
var k = $"{nameof(LSPSettings)}_{storeId}";
return await _memoryCache.GetOrCreateAsync(k, async _ =>
{
var res = await _storeRepository.GetSettingAsync<LSPSettings>(storeId,
nameof(LSPSettings));
return res;
});
}
public async Task SetLSPForStore(string storeId, LSPSettings lspSettings)
{
var k = $"{nameof(LSPSettings)}_{storeId}";
await _storeRepository.UpdateSetting(storeId, nameof(LSPSettings), lspSettings);
_memoryCache.Set(k, lspSettings);
}
}

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.LSP
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.LSP BTCPayServer.Plugins.LSP ../packed

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Plugins.LSP;
public class LSPSettings
{
public bool Enabled { get; set; } = true;
public long Minimum { get; set; } = 100000;
public long Maximum { get; set; } = 10000000;
public decimal FeePerSat { get; set; } = 0.01m;
public long BaseFee { get; set; } = 0;
public string CustomCSS { get; set; }
public string Title { get; set; } = "Lightning Liquidity Peddler";
public string Description { get; set; } = "<h3 class='w-100'>Get an inbound channel</h3><p>This will open a public channel to your node.</p>";
}
public class LSPViewModel
{
public LSPSettings Settings { get; set; }
}

View File

@@ -0,0 +1,73 @@
@using BTCPayServer.Client.Models
@model BTCPayServer.Plugins.LSP.LSPController.LSPConnectPage
@inject ContentSecurityPolicies contentSecurityPolicies
@using BTCPayServer.Security
@using NBitcoin
@{
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
contentSecurityPolicies.AllowUnsafeHashes();
Layout = "_LayoutSimple";
var reloadPage = false;
}
<style>
footer {
display: none;
}
@if (!string.IsNullOrEmpty(Model.Settings.CustomCSS))
{
@Safe.Raw(Model.Settings.CustomCSS)
}
</style>
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 w-100 m-auto">
<partial name="_StatusMessage"/>
<div class="mb-4 d-print-none d-flex justify-content-center">
<h1 >Thank you!</h1>
</div>
@if (Model.Status == InvoiceStatus.Processing)
{
reloadPage = true;
<div class="alert alert-info">
The invoice has detected a payment but is still waiting to be settled. This page will refresh periodically until it is settled.
</div>
}
else if (Model.Status != InvoiceStatus.Settled)
{
<div class="alert alert-danger">
The invoice is not settled.
</div>
}
else
{
Model.Invoice.Metadata.TryGetValue("inbound", out var inbound);
<div class="d-inline-flex flex-column" style="width:256px">
<div class="qr-container mb-2">
@await Component.InvokeAsync("QRCode", new {data = Model.LNURL.ToUpperInvariant()})
</div>
<p class="mx-auto">Scan this QR with your wallet to proceed with opening the channel.</p>
<a class="btn btn-secondary mt-3" href=@Model.LNURL>Open in wallet</a>
<p class="text-muted">Opening a channel of at least @inbound.ToString() sats.</p>
</div>
}
<div class="row">
<div class="powered__by__btcpayserver col-12">
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver" rel="noreferrer noopener">BTCPay Server</a>
</div>
</div>
</div>
</div>
@if (reloadPage)
{
<script type="text/javascript" nonce="@nonce">
setTimeout(function(){
window.location.reload();
}, 3000);
</script>
}

View File

@@ -0,0 +1,78 @@
@using BTCPayServer.Abstractions.Extensions
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Plugins.LSP.LSPSettings
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIStores/_Nav";
ViewData.SetActivePage("LSP", "Update Store LSP Settings", null);
}
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" asp-for="Enabled"/>
<label asp-for="Enabled" class="form-check-label"></label>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Title" class="form-label" data-required>LSP Title</label>
<input asp-for="Title" class="form-control" required/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" rows="10" cols="40" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSS" class="form-label">Additional Custom CSS</label>
<textarea asp-for="CustomCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="CustomCSS" class="text-danger"></span>
</div>
<div class="row">
<div class="form-group col-md-6 col-sm-12">
<label asp-for="Minimum" class="form-label"></label>
<input asp-for="Minimum" class="form-control" type="number" inputmode="numeric" min="1" style="max-width:16ch;"/>
<span asp-validation-for="Minimum" class="text-danger"></span>
</div>
<div class="form-group col-md-6 col-sm-12">
<label asp-for="Maximum" class="form-label"></label>
<input asp-for="Maximum" class="form-control" type="number" inputmode="numeric" min="1" style="max-width:16ch;"/>
<span asp-validation-for="Maximum" class="text-danger"></span>
</div>
<div class="form-group col-md-6 col-sm-12">
<label asp-for="BaseFee" class="form-label"></label>
<input asp-for="BaseFee" class="form-control" type="number" inputmode="numeric" style="max-width:16ch;"/>
<span asp-validation-for="BaseFee" class="text-danger"></span>
</div>
<div class="form-group col-md-6 col-sm-12">
<label asp-for="FeePerSat" class="form-label"></label>
<input asp-for="FeePerSat" class="form-control" type="number" min="0" step="any" inputmode="numeric" style="max-width:16ch;"/>
<span asp-validation-for="FeePerSat" class="text-danger"></span>
</div>
</div>
<div class="form-group mt-4">
<input type="submit" value="Save" name="command" class="btn btn-primary"/>
@if (this.ViewContext.ModelState.IsValid && Model.Enabled)
{
<a class="btn btn-secondary" href=" @Url.Action("View", "LSP", new {storeId})">
Purchase page
</a>
}
</div>
</form>
</div>
</div>
@section PageFootContent {
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true"/>
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
<partial name="_ValidationScriptsPartial"/>
}

View File

@@ -0,0 +1,88 @@
@using Microsoft.AspNetCore.Routing
@using BTCPayServer.Plugins.LSP
@model BTCPayServer.Plugins.LSP.LSPViewModel
@inject ContentSecurityPolicies contentSecurityPolicies
@inject IScopeProvider ScopeProvider
@using BTCPayServer.Security
@using NBitcoin
@using BTCPayServer.Abstractions.Contracts
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
contentSecurityPolicies.AllowUnsafeHashes();
Layout = "_LayoutSimple";
}
<style>
footer {
display: none;
}
@if (!string.IsNullOrEmpty(Model.Settings.CustomCSS))
{
@Safe.Raw(Model.Settings.CustomCSS)
}
</style>
<script nonce="@nonce">
const baseFee = @Model.Settings.BaseFee ;
const rate = @Model.Settings.FeePerSat ;
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("inbound").addEventListener("input", ev => {
compute(ev.target.value);
});
function compute(inbound){
const cost = Math.ceil(baseFee + ( rate ===0? 0 : (rate * inbound)));
document.getElementById("cost").textContent = `Cost: ${cost} sats`;
}
compute(document.getElementById("inbound").value);
});
</script>
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100 m-auto">
<partial name="_StatusMessage"/>
<h1 >@Model.Settings.Title</h1>
@if (!string.IsNullOrEmpty(Model.Settings.Description))
{
<div class="row" id="description">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Settings.Description)</div>
</div>
}
<form method="post" asp-controller="LSP" asp-action="Purchase" asp-antiforgery="false" asp-route-storeId="@storeId">
<div class="row g-2 mb-4 justify-content-center" id="form-email-container">
<div class="col-sm-12 col-md-8">
<div class="form-floating">
<input required type="email" name="email" class="form-control"/>
<label >Email</label>
</div>
</div>
<div class="col-sm-12 col-md-8">
<div class="form-floating">
<input required type="number" id="inbound" name="inbound" value="@Model.Settings.Minimum" min="@Model.Settings.Minimum" max="@Model.Settings.Maximum" class="form-control"/>
<label >Inbound Sats</label>
</div>
</div>
<div class="col-sm-12 col-md-8">
<p class="mb-0">Base fee: @Model.Settings.BaseFee sats, fee per inbound sat: @Model.Settings.FeePerSat sats</p>
<p class="mb-2 w-100" id="cost"></p>
</div>
<div class="col-sm-12 col-md-6">
<button type="submit" class="btn btn-primary btn-lg">Purchase</button>
</div>
</div>
</form>
<div class="row">
<div class="powered__by__btcpayserver col-12">
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver" rel="noreferrer noopener">BTCPay Server</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
@using BTCPayServer.Plugins.LSP
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(LSPController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-area="" asp-controller="LSP" asp-action="UpdateLSPSettings" asp-route-storeId="@storeId" class="nav-link js-scroll-trigger @(isActive? "active": string.Empty)">
<svg role="img" class="icon">
</svg>
<span>LSP</span>
</a>
</li>
}

View File

@@ -0,0 +1,61 @@
@using BTCPayServer.Client
@using BTCPayServer.Plugins.LSP
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Microsoft.AspNetCore.Routing
@using BTCPayServer.Abstractions.Contracts
@inject BTCPayServerClient BTCPayServerClient
@inject LSPService LSPService
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
LSPSettings settings = null;
if (!string.IsNullOrEmpty(storeId))
{
try
{
settings = await LSPService.GetLSPForStore(storeId);
}
catch (Exception)
{
}
}
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="list-group-item bg-tile ">
<div class="d-flex align-items-center">
<span class="d-flex flex-wrap flex-fill flex-column flex-sm-row">
<strong class="me-3">
LSP
</strong>
<span title="" class="d-flex me-3">
<span class="text-secondary">Sell lightning channel inbound liquidity using BTCPay Server</span>
</span>
</span>
<span class="d-flex align-items-center fw-semibold">
@if (settings?.Enabled is true)
{
<span class="d-flex align-items-center text-success">
<span class="me-2 btcpay-status btcpay-status--enabled"></span>
Active
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="LSP" asp-action="UpdateLSPSettings" asp-route-storeId="@storeId">
Modify
</a>
}
else
{
<span class="d-flex align-items-center text-danger">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="LSP" asp-action="UpdateLSPSettings" asp-route-storeId="@storeId">
Setup
</a>
}
</span>
</div>
</li>
}

View File

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

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Title>"Liquid+</Title>
<Description>Enhanced support for the liquid network.</Description>
<Authors>Kukks</Authors>
<Version>1.0.8</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Shared" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,114 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Plugins.LiquidPlus.Models;
using BTCPayServer.Plugins.LiquidPlus.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.LiquidPlus.Controllers
{
[Route("plugins/liquid/admin-settings")]
[Authorize(Policy = BTCPayServer.Client.Policies.CanModifyServerSettings,
AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class CustomLiquidAssetsController : Controller
{
private readonly CustomLiquidAssetsRepository _liquidAssetsRepository;
private readonly IHttpClientFactory _httpClientFactory;
public CustomLiquidAssetsController(CustomLiquidAssetsRepository liquidAssetsRepository,
IHttpClientFactory httpClientFactory)
{
_liquidAssetsRepository = liquidAssetsRepository;
_httpClientFactory = httpClientFactory;
}
[HttpGet("")]
public IActionResult Assets()
{
return View(new CustomLiquidAssetsViewModel()
{
Items = (_liquidAssetsRepository.Get()).Items,
PendingChanges = _liquidAssetsRepository.ChangesPending
});
}
[HttpPost("")]
public async Task<IActionResult> Assets(CustomLiquidAssetsViewModel model, string command = null,
string import = null)
{
if (command == "add")
{
ModelState.Clear();
model.Items.Add(new CustomLiquidAssetsSettings.LiquidAssetConfiguration());
return View(model);
}
if (command?.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase) is true)
{
ModelState.Clear();
var index = int.Parse(
command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
CultureInfo.InvariantCulture);
model.Items.RemoveAt(index);
return View(model);
}
if (import != null)
{
try
{
if (!uint256.TryParse(import, out var _))
{
TempData["ErrorMessage"] =
"Asset Id to import was invalid.";
return View(model);
}
var data = JObject.Parse(await _httpClientFactory.CreateClient()
.GetStringAsync($"https://blockstream.info/liquid/api/asset/{import}"));
model.Items.Add(new CustomLiquidAssetsSettings.LiquidAssetConfiguration()
{
DisplayName = data["name"].Value<string>(),
Divisibility = data["precision"].Value<int>(),
AssetId = data["asset_id"].Value<string>(),
CryptoCode = data["ticker"].Value<string>().Replace("-", "").Replace("_", "")
});
}
catch (Exception)
{
TempData["ErrorMessage"] =
"Asset Id to import was invalid.";
return View(model);
}
}
for (int i = 0; i < model.Items.Count; i++)
{
if (!string.IsNullOrEmpty(model.Items[i].AssetId) &&
!uint256.TryParse(model.Items[i].AssetId, out var x))
{
var inputName =
string.Format(CultureInfo.InvariantCulture, "Items[{0}].",
i.ToString(CultureInfo.InvariantCulture)) +
nameof(CustomLiquidAssetsSettings.LiquidAssetConfiguration.AssetId);
ModelState.AddModelError(inputName, "Invalid asset id format");
}
}
if (!ModelState.IsValid)
{
return View(model);
}
await _liquidAssetsRepository.Set(model);
return RedirectToAction(nameof(Assets));
}
}
}

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer.Plugins.LiquidPlus.Controllers
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public class StoreLiquidController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayServerClient _client;
private readonly IExplorerClientProvider _explorerClientProvider;
public StoreLiquidController(BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerClient client, IExplorerClientProvider explorerClientProvider)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_client = client;
_explorerClientProvider = explorerClientProvider;
}
[HttpGet("stores/{storeId}/liquid")]
public async Task<IActionResult> GenerateLiquidScript(string storeId, Dictionary<string, BitcoinExtKey> bitcoinExtKeys = null)
{
Dictionary<string, string> generated = new Dictionary<string, string>();
var allNetworks = _btcPayNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
.GroupBy(network => network.NetworkCryptoCode);
var allNetworkCodes = allNetworks
.SelectMany(networks => networks.Select(network => network.CryptoCode.ToUpperInvariant()))
.ToArray()
.Distinct();
Dictionary<string, BitcoinExtKey> privKeys = bitcoinExtKeys ?? new Dictionary<string, BitcoinExtKey>();
var paymentMethods = (await _client.GetStoreOnChainPaymentMethods(storeId))
.Where(settings => allNetworkCodes.Contains(settings.CryptoCode))
.GroupBy(data => _btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(data.CryptoCode).NetworkCryptoCode);
if (paymentMethods.Any() is false)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Message = "There are no wallets configured that use Liquid or an elements side-chain."
});
return View(new GenerateLiquidImportScripts());
}
foreach (var der in paymentMethods)
{
var network = _btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(der.Key);
var nbxnet = network.NBXplorerNetwork;
var sb = new StringBuilder();
var explorerClient = _explorerClientProvider.GetExplorerClient(der.Key);
var status = await explorerClient.GetStatusAsync();
if (status.BitcoinStatus is null)
{
sb.AppendLine($"{der.Key} node is not available. Try again later.");
generated.Add(der.Key, sb.ToString());
continue;
}
var derivationSchemesForNetwork = der.GroupBy(data => data.DerivationScheme);
foreach (var paymentMethodDerivationScheme in derivationSchemesForNetwork)
{
var derivatonScheme =
nbxnet.DerivationStrategyFactory.Parse(paymentMethodDerivationScheme.Key);
var sameWalletCryptoCodes = paymentMethodDerivationScheme.Select(data => data.CryptoCode).ToArray();
var matchedExistingKey = privKeys.Where(pair => sameWalletCryptoCodes.Contains(pair.Key));
BitcoinExtKey key = null;
if (matchedExistingKey.Any())
{
key = matchedExistingKey.First().Value;
}
else
{
key = await explorerClient.GetMetadataAsync<BitcoinExtKey>(derivatonScheme,
WellknownMetadataKeys.AccountHDKey);
}
if (key != null)
{
foreach (var paymentMethodData in paymentMethodDerivationScheme)
{
privKeys.TryAdd(paymentMethodData.CryptoCode, key);
}
}
var utxos = await explorerClient.GetUTXOsAsync(derivatonScheme, CancellationToken.None);
foreach (var utxo in utxos.GetUnspentUTXOs())
{
var addr = nbxnet.CreateAddress(derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey);
if (key is null)
{
sb.AppendLine(
$"elements-cli importaddress \"{addr}\" \"{utxo.KeyPath} from {derivatonScheme}\" false");
}
else
{
sb.AppendLine(
$"elements-cli importprivkey \"{key.Derive(utxo.KeyPath).PrivateKey.GetWif(nbxnet.NBitcoinNetwork)}\" \"{utxo.KeyPath} from {derivatonScheme}\" false");
}
if (!derivatonScheme.Unblinded())
{
var blindingKey =
NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(
derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey, nbxnet.NBitcoinNetwork);
sb.AppendLine($"elements-cli importblindingkey {addr} {blindingKey.ToHex()}");
}
}
}
if (sb.Length > 0)
{
sb.AppendLine("elements-cli stop");
sb.AppendLine("elementsd -rescan");
}
generated.Add(der.Key, sb.ToString());
}
return View(new GenerateLiquidImportScripts()
{
Wallets = paymentMethods.SelectMany(settings =>
settings.Select(data =>
new GenerateLiquidImportScripts.GenerateLiquidImportScriptWalletKeyVm()
{
CryptoCode = data.CryptoCode,
KeyPresent = privKeys.ContainsKey(data.CryptoCode),
ManualKey = null
}).ToArray()).ToArray(),
Scripts = generated
});
}
[HttpPost("stores/{storeId}/liquid")]
public async Task<IActionResult> GenerateLiquidScript(string storeId, GenerateLiquidImportScripts vm)
{
Dictionary<string, BitcoinExtKey> privKeys = new Dictionary<string, BitcoinExtKey>();
for (var index = 0; index < vm.Wallets.Length; index++)
{
var wallet = vm.Wallets[index];
if (string.IsNullOrEmpty(wallet.ManualKey))
continue;
var n =
_btcPayNetworkProvider.GetNetwork<ElementsBTCPayNetwork>(wallet.CryptoCode);
ExtKey extKey = null;
try
{
var mnemonic = new Mnemonic(wallet.ManualKey);
extKey = mnemonic.DeriveExtKey();
}
catch (Exception)
{
}
if (extKey == null)
{
try
{
extKey = ExtKey.Parse(wallet.ManualKey, n.NBitcoinNetwork);
}
catch (Exception)
{
}
}
if (extKey == null)
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey,
"Invalid key (must be seed or root xprv or account xprv)", this);
continue;
}
var der = n.NBXplorerNetwork.DerivationStrategyFactory.Parse(
(await _client.GetStoreOnChainPaymentMethod(storeId, wallet.CryptoCode)).DerivationScheme);
if (der.GetExtPubKeys().Count() > 1)
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle multsig", this);
continue;
}
var first = der
.GetExtPubKeys().First();
if (first != extKey.Neuter())
{
KeyPath kp = null;
switch (der.ScriptPubKeyType())
{
case ScriptPubKeyType.Legacy:
kp = new KeyPath($"m/44'/{n.CoinType}/0'");
break;
case ScriptPubKeyType.Segwit:
kp = new KeyPath($"m/84'/{n.CoinType}/0'");
break;
case ScriptPubKeyType.SegwitP2SH:
kp = new KeyPath($"m/49'/{n.CoinType}/0'");
break;
default:
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle wallet type",
this);
continue;
}
extKey = extKey.Derive(kp);
if (first != extKey.Neuter())
{
vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "key did not match", this);
continue;
}
}
privKeys.TryAdd(wallet.CryptoCode, extKey.GetWif(n.NBitcoinNetwork));
}
if (!ModelState.IsValid)
{
return View(vm);
}
return await GenerateLiquidScript(storeId, privKeys);
}
public class GenerateLiquidImportScripts
{
public class GenerateLiquidImportScriptWalletKeyVm
{
public string CryptoCode { get; set; }
public bool KeyPresent { get; set; }
public string ManualKey { get; set; }
}
public GenerateLiquidImportScriptWalletKeyVm[] Wallets { get; set; } =
Array.Empty<GenerateLiquidImportScriptWalletKeyVm>();
public Dictionary<string, string> Scripts { get; set; } = new Dictionary<string, string>();
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Plugins.LiquidPlus.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Plugins.LiquidPlus
{
public class LiquidPlusPlugin : BaseBTCPayServerPlugin
{
public override string Identifier { get; } = "BTCPayServer.Plugins.LiquidPlus";
public override string Name { get; } = "Liquid+";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" }
};
public override string Description { get; } = "Enhanced support for the liquid network.";
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("LiquidNav", "store-integrations-nav"));
services.AddSingleton<IUIExtension>(new UIExtension("CustomLiquidAssetsNavExtension", "server-nav"));
services.AddSingleton<IUIExtension>(new UIExtension("StoreNavLiquidExtension", "store-nav"));
services.AddSingleton<CustomLiquidAssetsRepository>();
var originalImplementationFactory = services.Single(descriptor =>
descriptor.Lifetime == ServiceLifetime.Singleton &&
descriptor.ServiceType == typeof(BTCPayNetworkProvider));
services.Replace(ServiceDescriptor.Singleton(provider =>
{
var _customLiquidAssetsRepository = provider.GetService<CustomLiquidAssetsRepository>();
var _logger = provider.GetService<ILogger<LiquidPlusPlugin>>();
var networkProvider =
(originalImplementationFactory.ImplementationInstance ??
originalImplementationFactory.ImplementationFactory.Invoke(provider)) as BTCPayNetworkProvider;
if (networkProvider.Support("LBTC"))
{
var settings = _customLiquidAssetsRepository.Get();
var template = networkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
var additionalNetworks = settings.Items.Select(configuration => new ElementsBTCPayNetwork()
{
CryptoCode = configuration.CryptoCode
.Replace("-", "")
.Replace("_", ""),
DefaultRateRules = configuration.DefaultRateRules ?? Array.Empty<string>(),
AssetId = uint256.Parse(configuration.AssetId),
Divisibility = configuration.Divisibility,
DisplayName = configuration.DisplayName,
CryptoImagePath = configuration.CryptoImagePath,
NetworkCryptoCode = template.NetworkCryptoCode,
DefaultSettings = template.DefaultSettings,
ElectrumMapping = template.ElectrumMapping,
BlockExplorerLink = template.BlockExplorerLink,
ReadonlyWallet = template.ReadonlyWallet,
SupportLightning = false,
SupportPayJoin = false,
ShowSyncSummary = false,
WalletSupported = template.WalletSupported,
LightningImagePath = "",
NBXplorerNetwork = template.NBXplorerNetwork,
CoinType = template.CoinType,
VaultSupported = template.VaultSupported,
MaxTrackedConfirmation = template.MaxTrackedConfirmation,
BlockExplorerLinkDefault = template.BlockExplorerLinkDefault,
SupportRBF = template.SupportRBF
});
var newCryptoCodes = settings.Items.Select(configuration => configuration.CryptoCode).ToArray();
_logger.LogInformation($"Loaded {newCryptoCodes.Length} " +
$"{(!newCryptoCodes.Any()?string.Empty: $"({string.Join(',', newCryptoCodes)})")} additional liquid assets");
var newSupportedChains = networkProvider.GetAll().Select(b => b.CryptoCode).Concat(newCryptoCodes).ToArray();
return new BTCPayNetworkProviderOverride(networkProvider.NetworkType, additionalNetworks).Filter(newSupportedChains);
}
return networkProvider;
}));
}
}
public class BTCPayNetworkProviderOverride : BTCPayNetworkProvider
{
public BTCPayNetworkProviderOverride(ChainName networkType,
IEnumerable<ElementsBTCPayNetwork> elementsBTCPayNetworks) : base(networkType)
{
foreach (ElementsBTCPayNetwork elementsBTCPayNetwork in elementsBTCPayNetworks)
{
_Networks.TryAdd(elementsBTCPayNetwork.CryptoCode.ToUpperInvariant(), elementsBTCPayNetwork);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Plugins.LiquidPlus.Models
{
public class CustomLiquidAssetsSettings
{
public List<LiquidAssetConfiguration> Items { get; set; } = new List<LiquidAssetConfiguration>();
public class LiquidAssetConfiguration
{
[Required] public string AssetId { get; set; }
[Range(0, double.PositiveInfinity)] public int Divisibility { get; set; } = 8;
[Required]
[Display(Name = "Display name")]
public string DisplayName { get; set; }
[Display(Name = "Checkout icon url")] public string CryptoImagePath { get; set; }
[Required]
[Display(Name = "Currency code")]
public string CryptoCode { get; set; }
public string[] DefaultRateRules { get; set; }
}
}
}

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Plugins.LiquidPlus.Models
{
public class CustomLiquidAssetsViewModel: CustomLiquidAssetsSettings
{
public bool PendingChanges { get; set; }
}
}

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.LiquidPlus
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.LiquidPlus BTCPayServer.Plugins.LiquidPlus ../packed

View File

@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Plugins.LiquidPlus.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.LiquidPlus.Services
{
public class CustomLiquidAssetsRepository
{
private readonly ILogger<CustomLiquidAssetsRepository> _logger;
private readonly IOptions<DataDirectories> _options;
private string File => Path.Combine(_options.Value.DataDir, "custom-liquid-assets.json");
public CustomLiquidAssetsRepository(ILogger<CustomLiquidAssetsRepository> logger, IOptions<DataDirectories> options)
{
_logger = logger;
_options = options;
}
public CustomLiquidAssetsSettings Get()
{
try
{
if (System.IO.File.Exists(File))
{
return JObject.Parse(System.IO.File.ReadAllText(File)).ToObject<CustomLiquidAssetsSettings>();
}
}
catch (Exception e)
{
_logger.LogError(e, "could not parse custom liquid assets file");
}
return new CustomLiquidAssetsSettings();
}
public async Task Set(CustomLiquidAssetsSettings settings)
{
try
{
await System.IO.File.WriteAllTextAsync(File, JObject.FromObject(settings).ToString(Formatting.Indented));
ChangesPending = true;
}
catch (Exception e)
{
_logger.LogError(e, "could not write custom liquid assets file");
}
}
public bool ChangesPending { get; private set; }
}
}

View File

@@ -0,0 +1,79 @@
@inject ContentSecurityPolicies contentSecurityPolicies
@using BTCPayServer.Security
@using NBitcoin
@model BTCPayServer.Plugins.LiquidPlus.Models.CustomLiquidAssetsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIServer/_Nav";
ViewData["Title"] = "Custom Liquid Assets";
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
}
@if (Model.PendingChanges)
{
<div class="alert alert-warning">There are saved changes to the custom liquid assets that have not yet been applied. Restart BTCPay Server to load these changes.</div>
}
<form class="form-group" asp-action="Assets" id="assetform">
<div class="list-group list-group-flush">
@if (!Model.Items.Any())
{
<p>No custom assets set up</p>
}
@for (var index = 0; index < Model.Items.Count; index++)
{
<div class="card mb-2">
<div class="card-body">
<div class="form-group">
<label asp-for="Items[index].CryptoCode" class="control-label"></label>
<input asp-for="Items[index].CryptoCode" class="form-control"/>
<span asp-validation-for="Items[index].CryptoCode" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Items[index].AssetId" class="control-label"></label>
<input asp-for="Items[index].AssetId" class="form-control"/>
<span asp-validation-for="Items[index].AssetId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Items[index].Divisibility" class="control-label"></label>
<input asp-for="Items[index].Divisibility" class="form-control"/>
<span asp-validation-for="Items[index].Divisibility" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Items[index].DisplayName" class="control-label"></label>
<input asp-for="Items[index].DisplayName" class="form-control"/>
<span asp-validation-for="Items[index].DisplayName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Items[index].CryptoImagePath" class="control-label"></label>
<input asp-for="Items[index].CryptoImagePath" class="form-control"/>
<span asp-validation-for="Items[index].CryptoImagePath" class="text-danger"></span>
</div>
</div>
<div class="card-footer">
<button type="submit" title="Remove" name="command" value="@($"remove:{index}")"
class="btn btn-danger">
Remove
</button>
</div>
</div>
}
</div>
<div>
<input type="hidden" name="import" id="import"/>
<button type="submit" name="command" value="add" class="btn btn-secondary">Add </button>
<button id="import-liquid-asset-from-registry" type="button" class="btn btn-secondary">Import from Blockstream Asset Registry</button>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</div>
<script type="text/javascript" nonce="@nonce">
document.getElementById("import-liquid-asset-from-registry").addEventListener("click", function (){
var id = prompt("Enter the asset id");
if (id){
document.getElementById("import").value = id;
document.getElementById("assetform").submit();
}
});
</script>
</form>

View File

@@ -0,0 +1,7 @@
@using BTCPayServer.Plugins.LiquidPlus.Controllers
@{
var isActive = ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(CustomLiquidAssetsController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-action="Assets" asp-controller="CustomLiquidAssets">Liquid Assets</a>

View File

@@ -0,0 +1,23 @@
@using BTCPayServer
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.LiquidPlus.Controllers
@using Microsoft.AspNetCore.Routing
@inject BTCPayNetworkProvider BTCPayNetworkProvider;
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(StoreLiquidController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (!string.IsNullOrEmpty(storeId) && (BTCPayNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>().Any()))
{
<li class="nav-item">
<a asp-route-storeId="@storeId" asp-action="GenerateLiquidScript" asp-controller="StoreLiquid" class="nav-link js-scroll-trigger @(isActive ? "active" : string.Empty)">
<svg role="img" class="icon">
</svg>
<span >Liquid+</span>
</a>
</li>
}

View File

@@ -0,0 +1,15 @@
@using BTCPayServer
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.LiquidPlus.Controllers
@using Microsoft.AspNetCore.Routing
@inject BTCPayNetworkProvider BTCPayNetworkProvider;
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(StoreLiquidController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (BTCPayNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>().Any())
{
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-route-storeId="@storeId" asp-action="GenerateLiquidScript" asp-controller="StoreLiquid">Liquid</a>
}

View File

@@ -0,0 +1,65 @@
@model BTCPayServer.Plugins.LiquidPlus.Controllers.StoreLiquidController.GenerateLiquidImportScripts
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIStores/_Nav";
ViewBag.MainTitle = "Store settings";
ViewData["title"] = "Liquid Import";
}
<h4>Liquid import</h4>
<div class="row">
<div class="col-md-8">
<p>Generates commands to import your received liquid funds into an elements node</p>
</div>
</div>
@if (Model.Wallets.Any())
{
<form class="row" asp-action="GenerateLiquidScript" method="post">
<ul class="list-group col-12">
<li class="list-group-item list-group-item-heading h3">Wallets</li>
@for (var index = 0; index < Model.Wallets.Length; index++)
{
var x = Model.Wallets[index];
<input type="hidden" asp-for="Wallets[index].CryptoCode"/>
<input type="hidden" asp-for="Wallets[index].KeyPresent"/>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span> @x.CryptoCode</span>
@if (!x.KeyPresent)
{
<input type="text" class="form-control form-control-sm ms-4" asp-for="Wallets[index].ManualKey" placeholder="Xprv (root or account) or seed"/>
}
else
{
<span class="text-success"><span class="fa fa-check-circle"></span>Keys already available</span>
}
</div>
<span asp-validation-for="Wallets[index].ManualKey" class="text-danger"></span>
</li>
}
@if (!Model.Wallets.All(vm => vm.KeyPresent))
{
<li class="list-group-item">
<button type="submit" class="btn btn-primary">Continue</button>
</li>
}
<li class="list-group-item list-group-item-heading h3">Scripts (per chain)</li>
@foreach (var script in Model.Scripts)
{
<li class="list-group-item list-group-item-heading h4">@script.Key</li>
<li class="list-group-item">
@if (string.IsNullOrEmpty(script.Value))
{
<span>Nothing to generate</span>
}
<pre>@Html.Raw(script.Value)</pre>
</li>
}
</ul>
</form>
}

View File

@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Title>LNURL NFC Support</Title>
<Description>Allows you to support contactless card payments over NFC and LNURL Withdraw!</Description>
<Authors>Kukks</Authors>
<Version>1.0.8</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Shared" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,99 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using LNURL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
namespace BTCPayServer.Plugins.NFC
{
[Route("plugins/NFC")]
public class NFCController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public NFCController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public class SubmitRequest
{
public string Lnurl { get; set; }
public string Destination { get; set; }
}
[AllowAnonymous]
public async Task<IActionResult> SubmitLNURLWithdrawForInvoice([FromBody] SubmitRequest request)
{
Uri uri;
string tag;
try
{
uri = LNURL.LNURL.Parse(request.Lnurl, out tag);
if (uri is null)
{
return BadRequest("lnurl was malformed");
}
}
catch (Exception e)
{
return BadRequest(e.Message);
}
if (!string.IsNullOrEmpty(tag) && !tag.Equals("withdrawRequest"))
{
return BadRequest("lnurl was not lnurl-withdraw");
}
var httpClient = _httpClientFactory.CreateClient(uri.IsOnion()
? "LightningLikePayoutHandlerOnionNamedClient"
: "LightningLikePayoutHandlerClearnetNamedClient");
var info = (await
LNURL.LNURL.FetchInformation(uri, "withdrawRequest", httpClient)) as LNURLWithdrawRequest;
if (info is null)
{
return BadRequest("Could not fetch info from lnurl-withdraw ");
}
httpClient = _httpClientFactory.CreateClient(info.Callback.IsOnion()
? "LightningLikePayoutHandlerOnionNamedClient"
: "LightningLikePayoutHandlerClearnetNamedClient");
try
{
var destinationuri = LNURL.LNURL.Parse(request.Destination, out string _);
var destinfo = (await
LNURL.LNURL.FetchInformation(destinationuri, "payRequest", httpClient)) as LNURLPayRequest;
if (destinfo is null)
{
return BadRequest("Could not fetch bolt11 invoice to pay to.");
}
httpClient = _httpClientFactory.CreateClient(destinfo.Callback.IsOnion()
? "LightningLikePayoutHandlerOnionNamedClient"
: "LightningLikePayoutHandlerClearnetNamedClient");
var destCallback = await destinfo.SendRequest(destinfo.MinSendable, Network.Main, httpClient);
request.Destination = destCallback.Pr;
}
catch (Exception e)
{
}
var result = await info.SendRequest(request.Destination, httpClient);
if (result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase))
{
return Ok(result.Reason);
}
return BadRequest(result.Reason);
}
}
}

View File

@@ -0,0 +1,31 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.FixedFloat
{
public class NFCPlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.NFC";
public override string Name => "LNURL NFC Support";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">1.7.1.0" }
};
public override string Description =>
"Allows you to support contactless card payments over NFC and LNURL Withdraw!";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/CheckoutEnd",
"checkout-end"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/LightningCheckoutPostContent",
"checkout-lightning-post-content"));
base.Execute(applicationBuilder);
}
}
}

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.NFC
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.NFC BTCPayServer.Plugins.NFC ../packed

View File

@@ -0,0 +1,80 @@
Vue.component("LNURLWithdrawContactless", {
data: function () {
return {
supported: ('NDEFReader' in window && window.self === window.top),
scanning: false,
submitting: false,
readerAbortController: null,
}
},
methods: {
startScan: async function () {
try {
if (this.scanning || this.submitting) {
return;
}
const self = this;
self.submitting = false;
self.scanning = true;
if (!this.supported) {
const result = prompt("enter lnurl withdraw");
if (result) {
self.sendData.bind(self)(result);
return;
}
self.scanning = false;
}
ndef = new NDEFReader()
self.readerAbortController = new AbortController()
await ndef.scan({signal: self.readerAbortController.signal})
ndef.addEventListener('readingerror', () => {
self.scanning = false;
self.readerAbortController.abort()
})
ndef.addEventListener('reading', ({message, serialNumber}) => {
//Decode NDEF data from tag
const record = message.records[0]
const textDecoder = new TextDecoder('utf-8')
const lnurl = textDecoder.decode(record.data)
//User feedback, show loader icon
self.scanning = false;
self.sendData.bind(self)(lnurl);
})
} catch(e) {
self.scanning = false;
self.submitting = false;
}
},
sendData: function (lnurl) {
this.submitting = true;
//Post LNURLW data to server
var xhr = new XMLHttpRequest()
xhr.open('POST', window.lnurlWithdrawSubmitUrl, true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify({lnurl, destination: this.$parent.srvModel.btcAddress}))
const self = this;
//User feedback, reset on failure
xhr.onload = function () {
if (xhr.readyState === xhr.DONE) {
console.log(xhr.response);
console.log(xhr.responseText);
self.scanning = false;
self.submitting = false;
if(self.readerAbortController) {
self.readerAbortController.abort()
}
if(xhr.response){
alert(xhr.response)
}
}
}
}
}
});

View File

@@ -0,0 +1,14 @@
@inject ContentSecurityPolicies contentSecurityPolicies
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Security
@using NBitcoin
@{
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
var url = Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"));
}
<script type="text/javascript" nonce="@nonce">
window.lnurlWithdrawSubmitUrl = '@url';
</script>
<script src="~/Resources/js/lnurlwnfc.js"></script>

View File

@@ -0,0 +1,12 @@
<LNURLWithdrawContactless inline-template v-if="!srvModel.isUnsetTopUp">
<bp-loading-button>
<button v-on:click="startScan" class="action-button" style="margin-top: 15px;" v-bind:disabled="scanning || submitting" v-bind:class="{ 'loading': scanning || submitting }">
<span class="button-text" v-if="supported">Pay by NFC & LNURL-Withdraw</span>
<span class="button-text" v-else>Pay by LNURL-Withdraw</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner"/>
</div>
</button>
</bp-loading-button>
</LNURLWithdrawContactless>

View File

@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<AssemblyVersion>1.0.2</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Views\TestExtension\Index.cshtml" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.RockstarStylist
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.RockstarStylist BTCPayServer.Plugins.RockstarStylist ../packed

View File

@@ -0,0 +1,37 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.RockstarStylist
{
public class RockstarStyleProvider
{
private HttpClient _githubClient;
public RockstarStyleProvider(IHttpClientFactory httpClientFactory)
{
_githubClient = httpClientFactory.CreateClient();
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
}
public async Task<RockstarStyle[]> Get()
{
var response = JArray.Parse(await _githubClient.GetStringAsync("https://api.github.com/repos/btcpayserver/BTCPayThemes/contents"));
return response.Where(token => token.Value<string>("type") == "dir").Select(token => new RockstarStyle()
{
StyleName = token.Value<string>("name"),
CssUrl = $"https://btcpayserver.github.io/BTCPayThemes/{token.Value<string>("name")}/btcpay-checkout.custom.css",
PreviewUrl = $"https://btcpayserver.github.io/BTCPayThemes/{token.Value<string>("name")}"
}).ToArray();
}
}
public class RockstarStyle
{
public string StyleName { get; set; }
public string CssUrl { get; set; }
public string PreviewUrl { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.RockstarStylist
{
public class RockstarStylistPlugin : BaseBTCPayServerPlugin
{
public override string Identifier { get; } = "BTCPayServer.Plugins.RockstarStylist";
public override string Name { get; } = "Rockstar hairstylist";
public override string Description { get; } = "Allows your checkout to get a rockstar approved makeover";
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("InvoiceCheckoutThemeOptions",
"invoice-checkout-theme-options"));
services.AddSingleton<RockstarStyleProvider>();
}
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.4.6.0" }
};
}
}

View File

@@ -0,0 +1,26 @@
@using BTCPayServer.Plugins.RockstarStylist
@using BTCPayServer.Security
@using NBitcoin
@inject ContentSecurityPolicies contentSecurityPolicies
@inject RockstarStyleProvider RockstarStyleProvider
@{
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
var themes = await RockstarStyleProvider.Get();
}
@foreach (var theme in themes)
{
<div>
<a href="#" class="set-theme-custom" data-theme="@theme.CssUrl">@theme.StyleName</a>
(<a href="@theme.PreviewUrl" target="_blank">Preview</a>)
</div>
}
<script type="text/javascript" nonce="@nonce">
document.addEventListener("DOMContentLoaded", () => {
delegate('click', '.set-theme-custom', e => {
const data = e.target.closest('.set-theme-custom').getAttribute('data-theme');
document.getElementById('CustomCSS').value = data;
});
});
</script>

View File

@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Title>SideShift</Title>
<Description>Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins.</Description>
<Authors>Kukks</Authors>
<Version>1.0.9</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Shared" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.SideShift
dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.SideShift BTCPayServer.Plugins.SideShift ../packed

View File

@@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.1878 1.91295C11.7421 0.677017 9.90218 -0.00144636 8.00016 2.31513e-06C3.57671 2.31513e-06 3.18626e-06 3.57994 3.18626e-06 8.00684C-0.00169573 9.90996 0.676051 11.7512 1.91123 13.199L13.1878 1.91295Z" fill="url(#paint0_linear_4698_101169)"/>
<path d="M2.75781 14.0459C4.16396 15.262 5.99349 15.9999 8.00042 15.9999C12.4234 15.9999 16.0004 12.4199 16.0004 7.99302C16.0004 5.98435 15.2631 4.15354 14.0482 2.74609L2.75781 14.0462V14.0459Z" fill="url(#paint1_linear_4698_101169)"/>
<defs>
<linearGradient id="paint0_linear_4698_101169" x1="10.7152" y1="12.044" x2="2.73879" y2="1.16127" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_4698_101169" x1="13.5175" y1="14.8401" x2="5.50803" y2="3.91232" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 959 B

View File

@@ -0,0 +1,38 @@
Vue.component("side-shift", {
props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress"],
methods: {
openDialog: function (e) {
if (e && e.preventDefault) {
e.preventDefault();
}
let settleMethodId = "";
let amount = !this.$parent.srvModel.isUnsetTopUp
? this.toCurrencyDue
: undefined;
if (this.toCurrency.toLowerCase() === "lbtc") {
settleMethodId = "liquid";
} else if (this.toCurrency.toLowerCase() === "usdt") {
settleMethodId = "usdtla";
} else if (
this.toCurrency.endsWith("LightningLike") ||
this.toCurrency.endsWith("LNURLPay")
) {
settleMethodId = "ln";
} else {
settleMethodId = this.toCurrency
.replace("_BTCLike", "")
.replace("_MoneroLike", "")
.replace("_ZcashLike", "")
.toLowerCase();
}
window.__SIDESHIFT__ = {
parentAffiliateId: "qg0OrfHJV",
defaultSettleMethodId: settleMethodId,
settleAddress: this.toCurrencyAddress,
settleAmount: amount,
type: !this.$parent.srvModel.isUnsetTopUp ? "fixed" : undefined,
};
window.sideshift.show();
},
},
});

View File

@@ -0,0 +1,82 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.SideShift
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/SideShift")]
public class SideShiftController : Controller
{
private readonly BTCPayServerClient _btcPayServerClient;
private readonly SideShiftService _sideShiftService;
public SideShiftController(BTCPayServerClient btcPayServerClient, SideShiftService sideShiftService)
{
_btcPayServerClient = btcPayServerClient;
_sideShiftService = sideShiftService;
}
[HttpGet("")]
public async Task<IActionResult> UpdateSideShiftSettings(string storeId)
{
var store = await _btcPayServerClient.GetStore(storeId);
UpdateSideShiftSettingsViewModel vm = new UpdateSideShiftSettingsViewModel();
vm.StoreName = store.Name;
SideShiftSettings SideShift = null;
try
{
SideShift = await _sideShiftService.GetSideShiftForStore(storeId);
}
catch (Exception)
{
// ignored
}
SetExistingValues(SideShift, vm);
return View(vm);
}
private void SetExistingValues(SideShiftSettings existing, UpdateSideShiftSettingsViewModel vm)
{
if (existing == null)
return;
vm.Enabled = existing.Enabled;
}
[HttpPost("")]
public async Task<IActionResult> UpdateSideShiftSettings(string storeId, UpdateSideShiftSettingsViewModel vm,
string command)
{
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var sideShiftSettings = new SideShiftSettings()
{
Enabled = vm.Enabled,
};
switch (command)
{
case "save":
await _sideShiftService.SetSideShiftForStore(storeId, sideShiftSettings);
TempData["SuccessMessage"] = "SideShift settings modified";
return RedirectToAction(nameof(UpdateSideShiftSettings), new {storeId});
default:
return View(vm);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.SideShift
{
public class SideShiftPlugin : BaseBTCPayServerPlugin
{
public override string Identifier => "BTCPayServer.Plugins.SideShift";
public override string Name => "SideShift";
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" }
};
public override string Description =>
"Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins.";
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddSingleton<SideShiftService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/SideShiftNav",
"store-integrations-nav"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/StoreIntegrationSideShiftOption",
"store-integrations-list"));
// Checkout v2
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutPaymentMethodExtension",
"checkout-payment-method"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutPaymentExtension",
"checkout-payment"));
// Checkout Classic
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutContentExtension",
"checkout-bitcoin-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutContentExtension",
"checkout-lightning-post-content"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutTabExtension",
"checkout-bitcoin-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutTabExtension",
"checkout-lightning-post-tabs"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutEnd",
"checkout-end"));
base.Execute(applicationBuilder);
}
}
}

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