This commit is contained in:
nicolas.dorier 2022-11-08 10:43:22 +09:00
commit 2b51c24cdf
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
42 changed files with 15101 additions and 0 deletions

124
.dockerignore Normal file
View File

@ -0,0 +1,124 @@
# Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
**/[Bb]in/
**/[Oo]bj/
node_modules/
dist/
# mstest test results
TestResults
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.log
*.vspscc
*.vssscc
.builds
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Publish Web Output
*.Publish.xml
# NuGet Packages Directory
packages
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
[Bb]in
[Oo]bj
sql
TestResults
[Tt]est[Rr]esult*
*.Cache
ClientBin
[Ss]tyle[Cc]op.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
src/Rapporteringsregisteret.Web/assets/less/*.css
MetricResults/
*.sln.ide/
_configs/
# vnext stuff
bower_components
output
.vs
**/launchSettings.json

153
.editorconfig Normal file
View File

@ -0,0 +1,153 @@
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8
[launchSettings.json]
indent_size = 2
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_within_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# only use var when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:none
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion
# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Code style defaults
dotnet_sort_system_directives_first = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
csharp_style_prefer_null_check_over_type_check = true:warning
csharp_prefer_simple_using_statement = true:warning
# C++ Files
[*.{cpp,h,in}]
curly_bracket_next_line = true
indent_brace_style = Allman
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
[*.{cmd, bat}]
end_of_line = crlf

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Declare files that will always have CRLF line endings on checkout.
*.sh text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

304
.gitignore vendored Normal file
View File

@ -0,0 +1,304 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
.vs/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
/BTCPayServer/Build/dockerfiles
# Bundling JS/CSS
BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
!.vscode/extensions.json
BTCPayServer/testpwd
.DS_Store
Packed Plugins
Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json

View File

@ -0,0 +1,19 @@
#nullable disable
using System.Reflection;
using Microsoft.Build.Framework;
namespace PluginBuilder.Targets;
public class Packer : ITask
{
public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; }
public bool Execute()
{
return true;
}
public string PluginDll { get; set; }
[Output]
public string Output { get; set; }
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginBuilder\PluginBuilder.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace PluginBuilder.Tests
{
public class ServerTester : IAsyncDisposable
{
public ServerTester(string testFolder, XUnitLogger logs)
{
TestFolder = testFolder;
Logs = logs;
}
public string TestFolder { get; }
public T GetService<T>() where T : notnull
{
return WebApp.Services.GetRequiredService<T>();
}
public XUnitLogger Logs { get; }
List<IAsyncDisposable> disposables = new List<IAsyncDisposable>();
WebApplication? _WebApp;
public WebApplication WebApp
{
get
{
return _WebApp ?? throw new InvalidOperationException("Webapp not initialized");
}
}
public bool ReuseDatabase { get; set; } = true;
public async Task Start()
{
string dbName = TestFolder;
if (!ReuseDatabase)
{
dbName = TestFolder + "_" + (DateTimeOffset.UtcNow.Ticks / 100000).ToString();
}
dbName = dbName.ToLowerInvariant();
Logs.LogInformation($"DbName: {dbName}");
Environment.SetEnvironmentVariable("PB_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=61932;Database=" + dbName);
Environment.SetEnvironmentVariable("PB_STORAGE_CONNECTION_STRING", "BlobEndpoint=http://127.0.0.1:32827/satoshi;AccountName=satoshi;AccountKey=Rxb41pUHRe+ibX5XS311tjXpjvu7mVi2xYJvtmq1j2jlUpN+fY/gkzyBMjqwzgj42geXGdYSbPEcu5i5wjSjPw==");
var host = new PluginBuilder.Program();
var webappBuilder = host.CreateWebApplicationBuilder();
webappBuilder.Logging.AddFilter(typeof(ProcessRunner).FullName, LogLevel.Trace);
webappBuilder.Logging.AddProvider(Logs);
var webapp = webappBuilder.Build();
host.Configure(webapp);
disposables.Add(webapp);
await webapp.StartAsync();
_WebApp = webapp;
}
public async ValueTask DisposeAsync()
{
foreach (var d in disposables)
{
await d.DisposeAsync();
}
}
}
}

View File

@ -0,0 +1,53 @@
using System.Threading.Tasks;
using PluginBuilder.Services;
using Xunit;
using Xunit.Abstractions;
namespace PluginBuilder.Tests;
public class UnitTest1 : UnitTestBase
{
public UnitTest1(ITestOutputHelper logs) : base(logs)
{
}
[Fact]
public async Task Test1()
{
await using var tester = await Start();
}
[Theory]
[InlineData("test-6", true)]
[InlineData("test-6-", false)]
[InlineData("6test-6", false)]
[InlineData("-test-6", false)]
[InlineData("te", false)]
[InlineData("teqoeteqoeteqoeteqoeteqoeteqoee", false)]
[InlineData("teqoeteqoeteqoeteqoeteqoet", true)]
public void IsValidSlugTest(string slug, bool expected)
{
Assert.Equal(expected, PluginSlug.IsValidSlugName(slug));
}
[Fact]
public async Task CanPackPlugin()
{
await using var tester = Create();
tester.ReuseDatabase = false;
await tester.Start();
var buildService = tester.GetService<BuildService>();
using var conn = await tester.GetService<DBConnectionFactory>().Open();
await conn.NewPlugin("rockstar-stylist");
//https://github.com/Kukks/btcpayserver/tree/plugins/collection/Plugins/BTCPayServer.Plugins.RockstarStylist
await buildService.Build("rockstar-stylist",
new PluginBuildParameters("https://github.com/Kukks/btcpayserver")
{
PluginDirectory = "Plugins/BTCPayServer.Plugins.RockstarStylist",
GitRef = "plugins/collection"
});
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
namespace PluginBuilder.Tests
{
public class UnitTestBase
{
public UnitTestBase(ITestOutputHelper log)
{
Log = new XUnitLogger("Tests", log);
}
public XUnitLogger Log { get; }
public async Task<ServerTester> Start([CallerMemberName] string? caller = null)
{
ServerTester tester = Create(caller);
await tester.Start();
return tester;
}
public ServerTester Create([CallerMemberName] string? caller = null)
{
return new ServerTester(caller ?? "Default", Log);
}
}
}

View File

@ -0,0 +1,82 @@
#nullable disable
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit.Abstractions;
namespace PluginBuilder.Tests
{
public class XUnitLogger : ILogger, ILoggerProvider, ILoggerFactory
{
public XUnitLogger(ITestOutputHelper log)
{
this.log = log;
}
string category;
public ITestOutputHelper log;
public XUnitLogger(string category, ITestOutputHelper log)
{
this.category = category;
this.log = log;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLogger(categoryName, log);
}
public ILogger<T> CreateLogger<T>()
{
return new Logger<T>(this);
}
public void Dispose()
{
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
log.WriteLine($"[{Simplified(category)}] {Simplified(logLevel)}: {formatter(state, exception)}");
if (exception is Exception)
{
log.WriteLine($"Exception: {exception}");
}
}
private string Simplified(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Information:
return "Info";
case LogLevel.Warning:
return "Warn";
default:
return logLevel.ToString();
}
}
private string Simplified(string category)
{
return category.Split('.').Last();
//return category;
}
public void AddProvider(ILoggerProvider provider)
{
}
}
}

View File

@ -0,0 +1,18 @@
version: "3"
services:
postgres:
image: postgres:15.0
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- "61932:5432"
expose:
- "5432"
azureblob:
image: mcr.microsoft.com/azure-blob-storage
environment:
LOCAL_STORAGE_ACCOUNT_NAME: satoshi
LOCAL_STORAGE_ACCOUNT_KEY: Rxb41pUHRe+ibX5XS311tjXpjvu7mVi2xYJvtmq1j2jlUpN+fY/gkzyBMjqwzgj42geXGdYSbPEcu5i5wjSjPw==
ports:
- "32827:11002"

View File

@ -0,0 +1,26 @@
namespace PluginBuilder
{
public class ConfigurationRequiredException : ConfigurationException
{
public ConfigurationRequiredException(string key) : base(key, $"Required environment variable")
{
}
}
public class ConfigurationException : Exception
{
public ConfigurationException(string key, string message) : base ($"[{key}] {message}")
{
Key = key;
}
public string Key { get; }
}
public static class ConfigurationExtensions
{
public static string GetRequired(this IConfiguration configuration, string key)
{
if (configuration[key] is string v && !string.IsNullOrWhiteSpace(v))
return v;
throw new ConfigurationRequiredException(key);
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using PluginBuilder.Services;
namespace PluginBuilder.Controllers
{
public class HomeController : Controller
{
public DBConnectionFactory ConnectionFactory { get; }
public HomeController(DBConnectionFactory connectionFactory)
{
ConnectionFactory = connectionFactory;
}
[HttpGet("/")]
public IActionResult HomePage()
{
return View();
}
[HttpPost("/plugins/add")]
public IActionResult AddPlugin(
string name,
string repository,
string reference,
string csprojPath)
{
// Wouter style: https://github.com/storefront-bvba/btcpayserver-kraken-plugin
// Dennis style: https://github.com/dennisreimann/btcpayserver
// Kukks sytle: https://github.com/Kukks/btcpayserver/tree/plugins/collection/Plugins
return View();
}
}
}

View File

@ -0,0 +1,3 @@
CREATE TABLE migrations (
script_name TEXT NOT NULL PRIMARY KEY,
executed_at timestamptz DEFAULT CURRENT_TIMESTAMP);

View File

@ -0,0 +1,35 @@
CREATE TABLE plugins
(
slug TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE builds_ids
(
plugin_slug TEXT NOT NULL,
curr_id BIGINT NOT NULL,
PRIMARY KEY (plugin_slug),
FOREIGN KEY (plugin_slug) REFERENCES plugins (slug) ON DELETE CASCADE
);
CREATE TABLE builds
(
plugin_slug TEXT NOT NULL,
id BIGINT NOT NULL,
state TEXT NOT NULL,
manifest_info JSONB,
build_info JSONB,
PRIMARY KEY (plugin_slug, id),
FOREIGN KEY (plugin_slug) REFERENCES plugins (slug) ON DELETE CASCADE
);
CREATE TABLE versions
(
plugin_slug TEXT NOT NULL,
ver TEXT NOT NULL,
build_id BIGINT NOT NULL,
btcpay_min_ver INT[] NOT NULL,
PRIMARY KEY (plugin_slug, ver),
FOREIGN KEY (plugin_slug) REFERENCES plugins (slug) ON DELETE CASCADE
FOREIGN KEY (build_id) REFERENCES builds (id) ON DELETE CASCADE
);

View File

@ -0,0 +1,22 @@
using System.Text.RegularExpressions;
namespace PluginBuilder
{
public record FullBuildId
{
public FullBuildId(PluginSlug PluginSlug, long BuildId)
{
ArgumentNullException.ThrowIfNull(PluginSlug);
if (BuildId < 0)
throw new ArgumentException("BuildId should be more than 0", nameof(BuildId));
this.PluginSlug = PluginSlug;
this.BuildId = BuildId;
}
public PluginSlug PluginSlug { get; }
public long BuildId { get; }
public override string ToString()
{
return $"{PluginSlug}/{BuildId}";
}
}
}

View File

@ -0,0 +1,24 @@
using PluginBuilder.Services;
namespace PluginBuilder.HostedServices
{
public class AzureStartupHostedService : IHostedService
{
public AzureStartupHostedService(AzureStorageClient azureStorageClient)
{
AzureStorageClient = azureStorageClient;
}
public AzureStorageClient AzureStorageClient { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
await AzureStorageClient.EnsureDefaultContainerExists(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,84 @@
using System.Globalization;
using System.Text;
using Dapper;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PluginBuilder.Services;
namespace PluginBuilder.HostedServices
{
public class DatabaseStartupHostedService : IHostedService
{
ILogger Logger { get; }
public DatabaseStartupHostedService(ILogger<DatabaseStartupHostedService> logger, DBConnectionFactory connectionFactory)
{
ConnectionFactory = connectionFactory;
Logger = logger;
}
public DBConnectionFactory ConnectionFactory { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
retry:
try
{
await using var conn = await ConnectionFactory.Open();
await RunScripts(conn);
}
catch (Npgsql.NpgsqlException pgex) when (pgex.SqlState == "3D000")
{
var builder = new Npgsql.NpgsqlConnectionStringBuilder(ConnectionFactory.ConnectionString.ToString());
var dbname = builder.Database;
Logger.LogInformation($"Database '{dbname}' doesn't exists, creating it...");
builder.Database = null;
var conn2Str = builder.ToString();
var conn2 = new Npgsql.NpgsqlConnection(conn2Str);
await conn2.OpenAsync();
await conn2.ExecuteAsync($"CREATE DATABASE {dbname} TEMPLATE 'template0' LC_CTYPE 'C' LC_COLLATE 'C' ENCODING 'UTF8'");
goto retry;
}
}
private async Task RunScripts(Npgsql.NpgsqlConnection conn)
{
await using (conn)
{
HashSet<string> executed;
try
{
executed = (await conn.QueryAsync<string>("SELECT script_name FROM migrations")).ToHashSet();
}
catch (Npgsql.NpgsqlException ex) when (ex.SqlState == "42P01")
{
executed = new HashSet<string>();
}
foreach (var resource in System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames()
.Where(n => n.EndsWith(".sql", System.StringComparison.InvariantCulture))
.OrderBy(n => n))
{
var parts = resource.Split('.');
if (!int.TryParse(parts[^3], NumberStyles.Any, CultureInfo.InvariantCulture, out _))
continue;
var scriptName = $"{parts[^3]}.{parts[^2]}";
if (executed.Contains(scriptName))
continue;
var stream = System.Reflection.Assembly.GetExecutingAssembly()
.GetManifestResourceStream(resource)!;
string content;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
Logger.LogInformation($"Execute script {scriptName}...");
await conn.ExecuteAsync($"{content}; INSERT INTO migrations VALUES (@scriptName)", new { scriptName });
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,76 @@
namespace PluginBuilder.HostedServices
{
public class DockerStartupException : Exception
{
public DockerStartupException(string message) : base(message)
{
}
}
public class DockerStartupHostedService : IHostedService
{
public DockerStartupHostedService(ILogger<DockerStartupHostedService> logger, IWebHostEnvironment env, ProcessRunner processRunner)
{
Logger = logger;
ProcessRunner = processRunner;
ContentRootPath = env.ContentRootPath;
}
public ILogger<DockerStartupHostedService> Logger { get; }
public ProcessRunner ProcessRunner { get; }
public string ContentRootPath { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Building the PluginBuilder docker image");
var result = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = new[]
{
"build",
"-f", "PluginBuilder.Dockerfile",
"-t", "plugin-builder",
"."
},
WorkingDirectory = ContentRootPath
}, cancellationToken);
if (result != 0)
throw new DockerStartupException("The build of PluginBuilder.Dockerfile failed");
var output = new OutputCapture();
await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = new[]
{
"volume", "ls",
"-f", "label=BTCPAY_PLUGIN_BUILD",
"--format", "{{ .Name }}"
},
OutputCapture = output
}, cancellationToken);
if (result != 0)
throw new DockerStartupException("docker volume ls failed");
if (output.Lines.Any())
{
Logger.LogInformation("Cleaning dangling volumes");
var args = new List<string>();
args.Add("volume");
args.Add("rm");
args.AddRange(output.Lines);
await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = args.ToArray()
}, cancellationToken);
if (result != 0)
throw new DockerStartupException("docker volume rm failed");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,51 @@
using Dapper;
using Newtonsoft.Json.Linq;
using Npgsql;
namespace PluginBuilder
{
public static class NpgsqlConnectionExtensions
{
public static async Task NewPlugin(this NpgsqlConnection connection, PluginSlug pluginSlug)
{
await connection.ExecuteAsync("INSERT INTO plugins (slug) VALUES (@id);",
new
{
id = pluginSlug.ToString(),
});
}
public static async Task UpdateBuild(this NpgsqlConnection connection, FullBuildId fullBuildId, string newState, JObject? buildInfo, JObject? manifestInfo = null)
{
await connection.ExecuteAsync(
"UPDATE builds " +
"SET state=@state, " +
"build_info=COALESCE(build_info || @build_info::JSONB, @build_info::JSONB, build_info), " +
"manifest_info=COALESCE(@manifest_info::JSONB, manifest_info) " +
"WHERE plugin_slug=@plugin_slug AND id=@buildId",
new
{
state = newState,
build_info = buildInfo?.ToString(),
manifest_info = manifestInfo?.ToString(),
plugin_slug = fullBuildId.PluginSlug.ToString(),
buildId = fullBuildId.BuildId
});
}
public static Task<long> NewBuild(this NpgsqlConnection connection, PluginSlug pluginSlug)
{
return connection.ExecuteScalarAsync<long>("" +
"WITH cte AS " +
"( " +
" INSERT INTO builds_ids AS bi VALUES (@plugin_slug, 0)" +
" ON CONFLICT (plugin_slug) DO UPDATE SET curr_id=bi.curr_id+1 " +
" RETURNING curr_id " +
") " +
"INSERT INTO builds (plugin_slug, id, state) VALUES (@plugin_slug, (SELECT * FROM cte), @state) RETURNING id;",
new
{
plugin_slug = pluginSlug.ToString(),
state = "queued"
});
}
}
}

View File

@ -0,0 +1,14 @@
namespace PluginBuilder
{
public class PluginBuildParameters
{
public PluginBuildParameters(string gitRepository)
{
GitRepository = gitRepository;
}
public string GitRepository { get; set; }
public string? GitRef { get; set; }
public string? PluginDirectory { get; set; }
public string? BuildConfig { get; set; }
}
}

View File

@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN apt-get update && apt-get install -y git jq && rm -rf /var/lib/apt/lists/*
RUN useradd -r --create-home dotnet
USER dotnet
WORKDIR /build-tools
ENV PLUGIN_PACKER_VERSION=https://github.com/btcpayserver/btcpayserver
RUN git clone --depth 1 -b v1.6.12 --single-branch https://github.com/btcpayserver/btcpayserver && \
cd btcpayserver/BTCPayServer.PluginPacker && \
dotnet build -c Release -o "/build-tools/PluginPacker" && \
rm -rf /build-tools/btcpayserver
WORKDIR /out
WORKDIR /build
COPY --chown=dotnet:dotnet entrypoint.sh /entrypoint.sh
CMD [ "/entrypoint.sh" ]

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-PluginBuilder-4D3592EF-6E6A-41BD-960D-231C299188A2</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
<Folder Include="wwwroot\img\" />
<Folder Include="wwwroot\scripts\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Npgsql" Version="6.0.7" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
<PropertyGroup>
<DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Scripts\*.sql" />
</ItemGroup>
<ItemGroup>
<None Remove="Data\Scripts\01.migrations.sql" />
<None Remove="Data\Scripts\02.Init.sql" />
</ItemGroup>
<ItemGroup>
<None Update="entrypoint.sh">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="PluginBuilder.Dockerfile">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace PluginBuilder
{
public record PluginSlug
{
static Regex SlugRegex = new Regex("^[a-z]{1,}[a-z0-9\\-]{0,}$");
public static bool IsValidSlugName(string slug)
{
ArgumentNullException.ThrowIfNull(slug);
if (!SlugRegex.IsMatch(slug))
return false;
if (slug[^1] == '-')
return false;
if (slug.Length > 30)
return false;
if (slug.Length < 4)
return false;
return true;
}
public static bool TryParse(string str, [MaybeNullWhen(false)] out PluginSlug slug)
{
ArgumentNullException.ThrowIfNull(str);
if (!IsValidSlugName(str))
{
slug = null;
return false;
}
slug = new PluginSlug(str, false);
return true;
}
public static PluginSlug Parse(string str)
{
if (TryParse(str, out var slug))
return slug;
throw new FormatException("Invalid slug name");
}
public PluginSlug(string slug) : this(slug, true)
{ }
PluginSlug(string slug, bool check)
{
if (check)
{
if (!IsValidSlugName(slug))
throw new ArgumentException("Invalid slug name", nameof(slug));
}
this.slug = slug;
}
public static implicit operator PluginSlug(string str)
{
return new PluginSlug(str);
}
readonly string slug;
public override string ToString()
{
return slug;
}
}
}

View File

@ -0,0 +1,294 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PluginBuilder
{
public interface IOutputCapture
{
void AddLine(string line);
}
public class OutputCapture : IOutputCapture
{
private readonly List<string> _lines = new List<string>();
public IEnumerable<string> Lines => _lines;
public void AddLine(string line) => _lines.Add(line);
public override string ToString()
{
return String.Join(Environment.NewLine, _lines);
}
}
public class ProcessSpec
{
public string? Executable { get; set; }
public string? WorkingDirectory { get; set; }
public ProcessSpecEnvironmentVariables EnvironmentVariables { get; } = new();
public IReadOnlyList<string>? Arguments { get; set; }
public string? EscapedArguments { get; set; }
public IOutputCapture? OutputCapture { get; set; }
public IOutputCapture? ErrorCapture { get; set; }
public DataReceivedEventHandler? OnOutput { get; set; }
public DataReceivedEventHandler? OnError { get; set; }
public string? Input { get; set; }
public sealed class ProcessSpecEnvironmentVariables : Dictionary<string, string>
{
public List<string> DotNetStartupHooks { get; } = new();
public List<string> AspNetCoreHostingStartupAssemblies { get; } = new();
}
}
public class ProcessRunner
{
private static readonly Func<string, string?> _getEnvironmentVariable = static key => Environment.GetEnvironmentVariable(key);
public ILogger<ProcessRunner> Logger { get; }
public ProcessRunner(ILogger<ProcessRunner> logger)
{
Logger = logger;
}
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(processSpec, nameof(processSpec));
int exitCode;
var stopwatch = new Stopwatch();
using (var process = CreateProcess(processSpec))
using (var processState = new ProcessState(process))
{
cancellationToken.Register(() => processState.TryKill());
var readOutput = false;
var readError = false;
if (processSpec.OutputCapture is not null)
{
readOutput = true;
process.OutputDataReceived += (_, a) =>
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
}
else if (processSpec.OnOutput != null)
{
readOutput = true;
process.OutputDataReceived += processSpec.OnOutput;
}
if (processSpec.ErrorCapture is not null)
{
readError = true;
process.ErrorDataReceived += (_, a) =>
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.ErrorCapture.AddLine(a.Data);
}
};
}
else if (processSpec.OnError is not null)
{
readError = true;
process.ErrorDataReceived += processSpec.OnError;
}
if (Logger.IsEnabled(LogLevel.Trace))
{
readOutput = true;
readError = true;
process.OutputDataReceived += (s, a) =>
{
// a.Data.EndsWith("\u001b[K")
Logger.LogInformation(a.Data);
};
process.ErrorDataReceived += (s, a) =>
{
Logger.LogWarning(a.Data);
};
}
stopwatch.Start();
process.Start();
if (readOutput)
{
process.BeginOutputReadLine();
}
if (readError)
{
process.BeginErrorReadLine();
}
if (processSpec.Input is not null)
{
await process.StandardInput.WriteLineAsync(processSpec.Input);
await process.StandardInput.FlushAsync();
process.StandardInput.Close();
}
await processState.Task;
exitCode = process.ExitCode;
stopwatch.Stop();
}
return exitCode;
}
private Process CreateProcess(ProcessSpec processSpec)
{
var process = new Process
{
EnableRaisingEvents = true,
StartInfo =
{
FileName = processSpec.Executable,
UseShellExecute = false,
WorkingDirectory = processSpec.WorkingDirectory,
RedirectStandardOutput = processSpec.OutputCapture is not null || (processSpec.OnOutput is not null) || Logger.IsEnabled(LogLevel.Trace),
RedirectStandardError = processSpec.ErrorCapture is not null || (processSpec.OnError is not null) || Logger.IsEnabled(LogLevel.Trace),
RedirectStandardInput = processSpec.Input is not null
}
};
if (processSpec.EscapedArguments is not null)
{
process.StartInfo.Arguments = processSpec.EscapedArguments;
}
else if (processSpec.Arguments is not null)
{
for (var i = 0; i < processSpec.Arguments.Count; i++)
{
process.StartInfo.ArgumentList.Add(processSpec.Arguments[i]);
}
}
foreach (var env in processSpec.EnvironmentVariables)
{
process.StartInfo.Environment.Add(env.Key, env.Value);
}
SetEnvironmentVariable(process.StartInfo, "DOTNET_STARTUP_HOOKS", processSpec.EnvironmentVariables.DotNetStartupHooks, Path.PathSeparator, _getEnvironmentVariable);
SetEnvironmentVariable(process.StartInfo, "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", processSpec.EnvironmentVariables.AspNetCoreHostingStartupAssemblies, ';', _getEnvironmentVariable);
return process;
}
internal static void SetEnvironmentVariable(ProcessStartInfo processStartInfo, string envVarName, List<string> envVarValues, char separator, Func<string, string?> getEnvironmentVariable)
{
if (envVarValues is { Count: 0 })
{
return;
}
var existing = getEnvironmentVariable(envVarName);
if (processStartInfo.Environment.TryGetValue(envVarName, out var value))
{
existing = CombineEnvironmentVariable(existing, value, separator);
}
string result;
if (!string.IsNullOrEmpty(existing))
{
result = existing + separator + string.Join(separator, envVarValues);
}
else
{
result = string.Join(separator, envVarValues);
}
processStartInfo.EnvironmentVariables[envVarName] = result;
static string? CombineEnvironmentVariable(string? a, string? b, char separator)
{
if (!string.IsNullOrEmpty(a))
{
return !string.IsNullOrEmpty(b) ? (a + separator + b) : a;
}
return b;
}
}
private class ProcessState : IDisposable
{
private readonly Process _process;
private readonly TaskCompletionSource _tcs = new TaskCompletionSource();
private volatile bool _disposed;
public ProcessState(Process process)
{
_process = process;
_process.Exited += OnExited;
Task = _tcs.Task.ContinueWith(_ =>
{
try
{
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
// this code used Process.Exited, which could result in us missing some output due to the ordering of
// events.
//
// See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
if (!_process.WaitForExit(Int32.MaxValue))
{
throw new TimeoutException();
}
_process.WaitForExit();
}
catch (InvalidOperationException)
{
// suppress if this throws if no process is associated with this object anymore.
}
});
}
public Task Task { get; }
public void TryKill()
{
if (_disposed)
{
return;
}
try
{
if (_process is not null && !_process.HasExited)
{
_process.Kill(entireProcessTree: true);
}
}
catch (Exception)
{
}
}
private void OnExited(object? sender, EventArgs args)
=> _tcs.TrySetResult();
public void Dispose()
{
if (!_disposed)
{
TryKill();
_disposed = true;
_process.Exited -= OnExited;
_process.Dispose();
}
}
}
}
}

60
PluginBuilder/Program.cs Normal file
View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Identity;
using PluginBuilder.HostedServices;
using PluginBuilder.Services;
namespace PluginBuilder;
public class Program
{
public static Task Main(string[] args)
{
var host = new Program();
return new Program().Start(args);
}
public Task Start(string[]? args = null)
{
WebApplication app = CreateWebApplication(args);
return app.RunAsync();
}
public WebApplication CreateWebApplication(string[]? args = null)
{
WebApplicationBuilder builder = CreateWebApplicationBuilder(args);
var app = builder.Build();
Configure(app);
return app;
}
public WebApplicationBuilder CreateWebApplicationBuilder(string[]? args = null)
{
var builder = WebApplication.CreateBuilder(args ?? Array.Empty<string>());
builder.Configuration.AddEnvironmentVariables("PB_");
AddServices(builder.Configuration, builder.Services);
return builder;
}
public void Configure(WebApplication app)
{
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// app.MapControllerRoute(
//name: "default",
//pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllers();
}
public void AddServices(IConfiguration configuration, IServiceCollection services)
{
services.AddControllersWithViews();
services.AddHostedService<DatabaseStartupHostedService>();
services.AddHostedService<DockerStartupHostedService>();
services.AddHostedService<AzureStartupHostedService>();
services.AddSingleton<DBConnectionFactory>();
services.AddSingleton<BuildService>();
services.AddSingleton<ProcessRunner>();
services.AddSingleton<AzureStorageClient>();
}
}

View File

@ -0,0 +1,14 @@
{
"profiles": {
"Debug Profile": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7259;http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"PB_POSTGRES": "lo"
}
}
}
}

View File

@ -0,0 +1,135 @@
using Microsoft.WindowsAzure.Storage;
using Newtonsoft.Json.Linq;
namespace PluginBuilder.Services
{
public class AzureStorageClientException : Exception
{
public AzureStorageClientException(string message) : base(message)
{
}
}
/// <summary>
/// A wrapper around "az" utility inside a docker image
///
/// While we could theorically use the Azure Storage library directly instead of this,
/// the files to upload on azure are stored in a docker volume, so using the library
/// would require us to copy the files to upload out of the docker volume.
///
/// This wouldn't be ideal, as we would need to make sure to properly clean it up.
/// And we also don't have any datadir for this project.
///
/// Another solution I tried was to directly use fetch the files via MountPoint of the docker volume
/// Sadly, on windows docker run on a VM, so the file system isn't local to the machine.
/// </summary>
public class AzureStorageClient
{
string scheme;
bool isLocalhost;
public AzureStorageClient(ProcessRunner processRunner, IConfiguration configuration)
{
ProcessRunner = processRunner;
StorageConnectionString = configuration.GetRequired("STORAGE_CONNECTION_STRING");
if (!CloudStorageAccount.TryParse(StorageConnectionString, out var acc))
throw new ConfigurationException("STORAGE_CONNECTION_STRING", "Invalid storage connection string");
scheme = acc.BlobEndpoint.Scheme;
isLocalhost = acc.BlobEndpoint.Host == "localhost" || acc.BlobEndpoint.Host == "127.0.0.1";
DefaultContainer = "artifacts";
}
public ProcessRunner ProcessRunner { get; }
public string StorageConnectionString { get; }
public string DefaultContainer { get; }
public async Task<bool> EnsureDefaultContainerExists(CancellationToken cancellationToken = default)
{
var error = new OutputCapture();
var output = new OutputCapture();
var code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = CreateArguments("az", "storage", "container", "create", "--name", DefaultContainer, "--public-access", "blob"),
ErrorCapture = error,
OutputCapture = output
}, cancellationToken);
if (code != 0)
throw new AzureStorageClientException($"Impossible to create container ({error})");
return ToJson(output)["created"]!.Value<bool>();
}
public async Task<string> Upload(string volume, string fileInVolume, string blobName)
{
var error = new OutputCapture();
var output = new OutputCapture();
var code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = CreateArguments(
new[]
{
"-v", $"{volume}:/out"
},
new[]
{
"az", "storage", "blob", "upload",
"-f", $"/out/{fileInVolume}", "-c", DefaultContainer,
"-n", blobName,
"--content-type", "application/zip"
}),
ErrorCapture = error,
OutputCapture = output
}, default);
if (code != 0)
throw new AzureStorageClientException($"Impossible to upload ({error})");
error = new OutputCapture();
output = new OutputCapture();
code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = CreateArguments("az", "storage", "blob", "url", "--container-name", DefaultContainer, "--name", blobName, "--protocol", scheme),
ErrorCapture = error,
OutputCapture = output
}, default);
if (code != 0)
throw new AzureStorageClientException($"Impossible to get the public url of the blob ({error})");
return ToString(output);
}
private static JObject ToJson(OutputCapture output)
{
var txt = output.ToString();
// Remove some crap at the end present for god knows why
txt = txt.Substring(0, txt.LastIndexOf('}') + 1);
return JObject.Parse(txt)!;
}
private static string ToString(OutputCapture output)
{
var txt = output.ToString();
// Remove some crap at the end present for god knows why
txt = txt.Substring(0, txt.LastIndexOf('"') + 1);
return JValue.Parse(txt)!.Value<string>()!;
}
private string[] CreateArguments(params string[] args)
{
return CreateArguments(null, args);
}
private string[] CreateArguments(string[]? dockerArgs, string[] args)
{
List<string> a = new List<string>();
a.AddRange(new[] { "run", "-ti", "--rm", "--env", $"AZURE_STORAGE_CONNECTION_STRING={StorageConnectionString}" });
if (isLocalhost)
{
// Not needed in prod, but we need it in tests to connect to the azure containers running in docker-compose
a.AddRange(new[] { "--network", "host" });
}
if (dockerArgs is not null)
a.AddRange(dockerArgs);
a.Add("mcr.microsoft.com/azure-cli:2.9.1");
a.AddRange(args);
return a.ToArray();
}
}
}

View File

@ -0,0 +1,139 @@
using System.Security.Cryptography;
using Dapper;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json.Linq;
namespace PluginBuilder.Services
{
public class BuildServiceException : Exception
{
public BuildServiceException(string message) : base(message)
{
}
}
public class BuildService
{
public BuildService(
ILogger<BuildService> logger,
ProcessRunner processRunner,
DBConnectionFactory connectionFactory,
AzureStorageClient azureStorageClient)
{
Logger = logger;
ProcessRunner = processRunner;
ConnectionFactory = connectionFactory;
AzureStorageClient = azureStorageClient;
}
public ILogger<BuildService> Logger { get; }
public ProcessRunner ProcessRunner { get; }
public DBConnectionFactory ConnectionFactory { get; }
public AzureStorageClient AzureStorageClient { get; }
public async Task Build(PluginSlug pluginSlug, PluginBuildParameters buildParameters)
{
var fullBuildId = await CreateNewBuild(pluginSlug);
List<string> args = new List<string>();
// Create the volumes where the artifacts will be stored
args.AddRange(new[] { "volume", "create" });
args.AddRange(new[] { "--label", $"BTCPAY_PLUGIN_BUILD={fullBuildId}" });
var output = new OutputCapture();
var code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = args.ToArray(),
OutputCapture = output
}, default);
if (code != 0)
throw new BuildServiceException("docker volume create failed");
var volume = output.ToString().Trim();
args.Clear();
// Then let's build by running our image plugin-builder (built in DockerStartupHostedService)
var info = new JObject();
args.Add("run");
args.AddRange(new[] { "--env", $"GIT_REPO={buildParameters.GitRepository}" });
info["gitRepository"] = buildParameters.GitRepository;
info["dockerVolume"] = volume;
if (buildParameters.GitRef != null)
{
args.AddRange(new[] { "--env", $"GIT_REF={buildParameters.GitRef}" });
info["gitRef"] = buildParameters.GitRef;
}
if (buildParameters.PluginDirectory != null)
{
args.AddRange(new[] { "--env", $"PLUGIN_DIR={buildParameters.PluginDirectory}" });
info["pluginDir"] = buildParameters.PluginDirectory;
}
if (buildParameters.BuildConfig != null)
{
args.AddRange(new[] { "--env", $"BUILD_CONFIG={buildParameters.BuildConfig}" });
info["buildConfig"] = buildParameters.BuildConfig;
}
args.AddRange(new[] { "-v", $"{volume}:/out" });
args.AddRange(new[] { "-ti", "--rm" });
args.Add("plugin-builder");
await UpdateBuild(fullBuildId, "running", info);
JObject buildEnv;
try
{
code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = args.ToArray()
}, default);
if (code != 0)
throw new BuildServiceException("docker build failed");
string buildEnvStr = await ReadFileInVolume(volume, "build-env.json");
buildEnv = JObject.Parse(buildEnvStr);
}
catch (Exception err)
{
await UpdateBuild(fullBuildId, "failed", new JObject() { ["error"] = err.Message });
throw;
}
var pluginName = buildEnv["pluginName"]!.Value<string>();
string manifestStr = await ReadFileInVolume(volume, $"{pluginName}.btcpay.json");
var manifest = JObject.Parse(manifestStr);
await UpdateBuild(fullBuildId, "waiting-upload", buildEnv, manifest);
await UpdateBuild(fullBuildId, "uploading", null, null);
var url = await AzureStorageClient.Upload(volume, $"{pluginName}.btcpay", $"{fullBuildId}/{pluginName}.btcpay");
await UpdateBuild(fullBuildId, "uploaded", new JObject() { ["url"] = url }, null);
}
private async Task<string> ReadFileInVolume(string volume, string file)
{
var output = new OutputCapture();
// Let's read the build-env.json
int code = await ProcessRunner.RunAsync(new ProcessSpec()
{
Executable = "docker",
Arguments = new[] {
"run", "-ti", "--rm", "-v", $"{volume}:/out", "plugin-builder", "cat", $"/out/{file}" },
OutputCapture = output
}, default);
if (code != 0)
throw new BuildServiceException("docker run to read a file in volume");
return output.ToString();
}
private async Task UpdateBuild(FullBuildId fullBuildId, string newState, JObject? buildInfo, JObject? manifestInfo = null)
{
await using var connection = await ConnectionFactory.Open();
await connection.UpdateBuild(fullBuildId, newState, buildInfo, manifestInfo);
}
private async Task<FullBuildId> CreateNewBuild(PluginSlug pluginSlug)
{
await using var connection = await ConnectionFactory.Open();
return new FullBuildId(pluginSlug, await connection.NewBuild(pluginSlug));
}
}
}

View File

@ -0,0 +1,46 @@
using Npgsql;
namespace PluginBuilder.Services
{
public class DBConnectionFactory
{
public NpgsqlConnectionStringBuilder ConnectionString { get; }
public DBConnectionFactory(IConfiguration config)
{
try
{
ConnectionString = new NpgsqlConnectionStringBuilder(config.GetRequired("POSTGRES"));
}
catch (Exception ex) when (ex is not ConfigurationException)
{
throw new ConfigurationException("POSTGRES", ex.Message);
}
}
public async Task<NpgsqlConnection> Open(CancellationToken cancellationToken = default)
{
int maxRetries = 10;
int retries = maxRetries;
retry:
var conn = new Npgsql.NpgsqlConnection(ConnectionString.ToString());
try
{
await conn.OpenAsync(cancellationToken);
}
catch (PostgresException ex) when (ex.IsTransient && retries > 0)
{
retries--;
await conn.DisposeAsync();
await Task.Delay((maxRetries - retries) * 100, cancellationToken);
goto retry;
}
catch
{
conn.Dispose();
throw;
}
return conn;
}
}
}

View File

@ -0,0 +1,33 @@
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en" class="uie" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link href="~/styles/btcpayserver-variables.css" rel="stylesheet" asp-append-version="true" />
<link href="~/styles/btcpayserver-bootstrap.css" rel="stylesheet" asp-append-version="true" />
<link href="~/styles/btcpayserver-main.css" rel="stylesheet" asp-append-version="true" />
<title>BTCPay Server - Plugin Builder</title>
<style>
.navigation-logo {
display: block;
margin-bottom: var(--uie-space-l);
margin-left: auto;
margin-right: auto;
max-height: 120px;
min-height: 120px;
max-width: 90%;
}
.navigation-logo-caption {
font-size: x-large;
}
</style>
</head>
<body>
</body>
</html>

View File

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

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -e
git --version
dotnet --info
: "${BUILD_CONFIG:=Release}"
BRANCH_OPTS=""
[[ "$GIT_REF" ]] && BRANCH_OPTS="-b ${GIT_REF}"
git clone --depth 1 --recurse-submodules $BRANCH_OPTS --single-branch "${GIT_REPO}" .
GIT_COMMIT="$(git rev-parse HEAD)"
GIT_COMMIT_DATE=$(git show -s --format=%ci)
# To UTC
GIT_COMMIT_DATE=$(date -d "$GIT_COMMIT_DATE" --iso-8601=seconds --utc)
[[ "$PLUGIN_DIR" ]] && cd "${PLUGIN_DIR}"
PLUGIN_NAME="$(ls *.csproj)"
PLUGIN_NAME="${PLUGIN_NAME/.csproj/}"
dotnet publish -c "${BUILD_CONFIG}" -o "/tmp/publish"
# PluginPacker crash because of no gpg, but we don't use it anyway...
/build-tools/PluginPacker/BTCPayServer.PluginPacker "/tmp/publish" "${PLUGIN_NAME}" "/tmp/publish-package" || true
cp /tmp/publish-package/*/*/* /out
rm /out/SHA256SUMS.asc /out/SHA256SUMS
BUILD_DATE=$(date --iso-8601=seconds --utc)
# To UTC
BUILD_DATE=$(date -d "$BUILD_DATE" --iso-8601=seconds --utc)
BUILD_HASH=($(sha256sum /out/${PLUGIN_NAME}.btcpay))
jq --null-input \
--arg buildConfig "$BUILD_CONFIG" \
--arg gitRef "$GIT_REF" \
--arg gitRepository "$GIT_REPO" \
--arg pluginDir "$PLUGIN_DIR" \
--arg buildConfig "$BUILD_CONFIG" \
--arg gitCommit "$GIT_COMMIT" \
--arg gitCommitDate "$GIT_COMMIT_DATE" \
--arg buildDate "$BUILD_DATE" \
--arg buildHash "$BUILD_HASH" \
--arg pluginName "$PLUGIN_NAME" \
'{
"pluginName": $pluginName,
"gitRepository": $gitRepository,
"gitRef": $gitRef,
"pluginDir": $pluginDir,
"buildConfig": $buildConfig,
"gitCommit": $gitCommit,
"gitCommitDate": $gitCommitDate,
"buildDate": $buildDate,
"buildHash": $buildHash
}' > /out/build-env.json
# {
# "gitRepository": "https://github.com/Kukks/btcpayserver",
# "gitRef": "plugins/collection",
# "pluginDir": "Plugins/BTCPayServer.Plugins.AOPP",
# "gitCommit": "bed25814a7a47f7bf13b2a1cb9a2dcf544d268dd",
# "gitCommitDate": "2022-10-31T10:03:52+00:00",
# "buildDate": "2022-11-07T06:10:20+00:00",
# "buildHash": "f56cec255e2fc92c1b2c0d39546d87548daa98a7fa0e9f7a7f28f6dc129a31b6"
# }
# ls /out/
# BTCPayServer.Plugins.AOPP.btcpay BTCPayServer.Plugins.AOPP.btcpay.json build-env.json

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,943 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: reduce) {
* {
-webkit-animation: none !important;
animation: none !important;
transition: none !important;
}
}
body {
font-family: var(--btcpay-font-family-base);
font-size: var(--btcpay-font-size-base);
font-weight: var(--btcpay-font-weight-normal);
line-height: 1.6;
color: var(--btcpay-body-text);
background-color: var(--btcpay-body-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: var(--btcpay-font-family-monospace);
border-radius: var(--btcpay-border-radius);
padding: var(--btcpay-space-xs);
color: var(--btcpay-code-text);
background-color: var(--btcpay-code-bg);
}
pre {
font-family: var(--btcpay-font-family-monospace);
border-radius: var(--btcpay-border-radius);
color: var(--btcpay-pre-text);
background-color: var(--btcpay-pre-bg);
}
pre code {
padding: var(--btcpay-space-m) !important;
background: var(--btcpay-pre-bg);
}
a {
color: var(--btcpay-body-link);
}
a:focus,
a:hover {
color: var(--btcpay-body-link-accent);
}
/* components */
.btcpay-header {
color: var(--btcpay-header-text);
background-color: var(--btcpay-header-bg);
}
.btcpay-header a {
color: var(--btcpay-header-link);
text-decoration: none;
}
.btcpay-header a:focus,
.btcpay-header a:hover {
color: var(--btcpay-header-link-accent);
}
.btcpay-header-columns {
display: flex;
flex-wrap: wrap;
width: 100%;
align-items: center;
justify-content: space-between;
}
.btcpay-header-logo {
height: 45px;
}
.btcpay-pills input {
display: none;
}
.btcpay-pills label,
.btcpay-pill {
display: inline-block;
padding: var(--btcpay-space-s) 1.5rem;
color: var(--btcpay-body-link);
background: transparent;
font-weight: var(--btcpay-font-weight-semibold);
margin-right: var(--btcpay-space-m);
border: 1px solid var(--btcpay-secondary-border);
cursor: pointer;
border-radius: 5rem;
text-decoration: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btcpay-pills input:not(:checked):not([disabled]) + label:hover,
.btcpay-pill:hover {
color: var(--btcpay-body-link);
border-color: var(--btcpay-secondary-border-hover);
text-decoration: none;
}
.btcpay-pills input {
display: none;
}
.btcpay-pills input:checked + label,
.btcpay-pill.active {
color: var(--btcpay-body-text-active);
background: var(--btcpay-body-bg-active);
border-color: var(--btcpay-body-bg-active);
}
.btcpay-pills input[disabled] + label,
.btcpay-pill.disabled {
color: var(--btcpay-body-text-muted) !important;
border-color: var(--btcpay-secondary-border) !important;
opacity: .5 !important;
}
.btcpay-theme-switch {
--btcpay-theme-switch-light-color: var(--btcpay-white);
--btcpay-theme-switch-dark-color: var(--btcpay-neutral-900);
--btcpay-theme-switch-icon-size: 1.25rem;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--btcpay-white);
background: none;
cursor: pointer;
height: 40px;
width: 40px;
border: 0;
}
.btcpay-theme-switch svg {
height: var(--btcpay-theme-switch-icon-size);
width: var(--btcpay-theme-switch-icon-size);
}
.btcpay-theme-switch path {
stroke-width: .5px;
fill: none;
}
.btcpay-theme-switch:hover .btcpay-theme-switch-light, .btcpay-theme-switch:focus .btcpay-theme-switch-light {
fill: var(--btcpay-theme-switch-light-color);
}
.btcpay-theme-switch:hover .btcpay-theme-switch-dark, .btcpay-theme-switch:focus .btcpay-theme-switch-dark {
fill: var(--btcpay-theme-switch-dark-color);
}
.btcpay-theme-switch-dark {
display: inline-block;
stroke: var(--btcpay-theme-switch-dark-color);
}
[data-btcpay-theme="dark"]:root .btcpay-theme-switch-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="dark"]) .btcpay-theme-switch-dark {
display: inline-block;
}
}
.btcpay-theme-switch-light {
display: none;
stroke: var(--btcpay-theme-switch-light-color);
}
[data-btcpay-theme="dark"]:root .btcpay-theme-switch-light {
display: inline-block;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="light"]) .btcpay-theme-switch-light {
display: inline-block;
}
}
.btcpay-footer {
color: var(--btcpay-footer-text);
background-color: var(--btcpay-footer-bg);
}
.btcpay-footer a {
color: var(--btcpay-footer-link);
text-decoration: none;
}
.btcpay-footer a:focus,
.btcpay-footer a:hover {
color: var(--btcpay-footer-link-accent);
}
.btcpay-footer-columns {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.btcpay-footer-columns > * {
flex: 1 1 100%;
margin-bottom: var(--btcpay-space-m);
}
.btcpay-footer-columns h4,
.btcpay-footer-columns h5,
.btcpay-footer-columns h6 {
margin: var(--btcpay-space-m) 0;
font-weight: var(--btcpay-font-weight-bold);
}
.btcpay-footer-columns ul {
margin: 0;
padding: 0;
list-style: none;
}
.btcpay-footer-columns ul li {
padding: var(--btcpay-space-xs) 0;
}
@media (min-width: 576px) {
.btcpay-footer-columns > * {
flex: 1;
}
.btcpay-footer-columns > * + * {
margin-left: var(--btcpay-space-m);
}
}

View File

@ -0,0 +1,578 @@
:root {
--btcpay-border-radius: .25rem;
--btcpay-border-radius-l: .5rem;
--btcpay-border-radius-xl: 1rem;
--btcpay-border-radius-xxl: 1.5rem;
--btcpay-transition-duration-fast: .2s;
--btcpay-transition-duration-default: .3s;
--btcpay-transition-duration-slow: .5s;
--btcpay-brand-primary: #51b13e;
--btcpay-brand-secondary: #CEDC21;
--btcpay-brand-tertiary: #1e7a44;
--btcpay-brand-dark: #0F3B21;
--btcpay-white: #ffffff;
--btcpay-white-rgb: 255, 255, 255;
--btcpay-black: #000000;
--btcpay-black-rgb: 0, 0, 0;
--btcpay-neutral-light-100: #f8f9fa;
--btcpay-neutral-light-200: #e9ecef;
--btcpay-neutral-light-300: #dee2e6;
--btcpay-neutral-light-400: #ced4da;
--btcpay-neutral-light-500: #8f979e;
--btcpay-neutral-light-600: #6c757d;
--btcpay-neutral-light-700: #495057;
--btcpay-neutral-light-800: #343a40;
--btcpay-neutral-light-900: #292929;
--btcpay-neutral-light-rgb: 143,151,158;
--btcpay-neutral-dark-100: #F0F6FC;
--btcpay-neutral-dark-200: #C9D1D9;
--btcpay-neutral-dark-300: #B1BAC4;
--btcpay-neutral-dark-400: #8B949E;
--btcpay-neutral-dark-500: #6E7681;
--btcpay-neutral-dark-600: #484F58;
--btcpay-neutral-dark-700: #30363D;
--btcpay-neutral-dark-800: #21262D;
--btcpay-neutral-dark-900: #161B22;
--btcpay-neutral-dark-rgb: 110,118,129;
--btcpay-primary-100: #c7e6c1;
--btcpay-primary-200: #b5dead;
--btcpay-primary-300: #9dd392;
--btcpay-primary-400: #7cc46e;
--btcpay-primary-500: #44a431;
--btcpay-primary-600: #389725;
--btcpay-primary-700: #2e8a1b;
--btcpay-primary-800: #247d12;
--btcpay-primary-900: #1c710b;
--btcpay-primary-rgb: 68,164,49;
--btcpay-green-100: #EEFAEB;
--btcpay-green-200: #C7E8C0;
--btcpay-green-300: #A0D695;
--btcpay-green-400: #78C369;
--btcpay-green-500: #51B13E;
--btcpay-green-600: #419437;
--btcpay-green-700: #307630;
--btcpay-green-800: #205928;
--btcpay-green-900: #0F3B21;
--btcpay-green-rgb: 81,177,62;
--btcpay-blue-100: #b5e1e8;
--btcpay-blue-200: #9dd7e1;
--btcpay-blue-300: #7ccad7;
--btcpay-blue-400: #51b9c9;
--btcpay-blue-500: #17a2b8;
--btcpay-blue-600: #03899e;
--btcpay-blue-700: #007d91;
--btcpay-blue-800: #007284;
--btcpay-blue-900: #006778;
--btcpay-blue-rgb: 23,162,184;
--btcpay-yellow-100: #FFFAF0;
--btcpay-yellow-200: #FFF2D9;
--btcpay-yellow-300: #FFE3AC;
--btcpay-yellow-400: #FFCF70;
--btcpay-yellow-500: #FFC043;
--btcpay-yellow-600: #BC8B2C;
--btcpay-yellow-700: #997328;
--btcpay-yellow-800: #674D1B;
--btcpay-yellow-900: #543D10;
--btcpay-yellow-rgb: 255,192,67;
--btcpay-red-100: #FFEFED;
--btcpay-red-200: #FED7D2;
--btcpay-red-300: #F1998E;
--btcpay-red-400: #E85C4A;
--btcpay-red-500: #E11900;
--btcpay-red-600: #AB1300;
--btcpay-red-700: #870F00;
--btcpay-red-800: #5A0A00;
--btcpay-red-900: #420105;
--btcpay-red-rgb: 225,25,0;
--btcpay-purple-100: #F4F1FA;
--btcpay-purple-200: #E3DDF2;
--btcpay-purple-300: #C1B5E3;
--btcpay-purple-400: #957FCE;
--btcpay-purple-500: #7356BF;
--btcpay-purple-600: #574191;
--btcpay-purple-700: #453473;
--btcpay-purple-800: #2E224C;
--btcpay-purple-900: #1A1033;
--btcpay-purple-rgb: 115,86,191;
--btcpay-orange-100: #FFF3EF;
--btcpay-orange-200: #FFE1D6;
--btcpay-orange-300: #FABDA5;
--btcpay-orange-400: #FA9269;
--btcpay-orange-500: #FF6937;
--btcpay-orange-600: #C14F29;
--btcpay-orange-700: #9A3F21;
--btcpay-orange-800: #672A16;
--btcpay-orange-900: #3D1300;
--btcpay-orange-rgb: 255,105,55;
--btcpay-brown-100: #F6F0EA;
--btcpay-brown-200: #EBE0DB;
--btcpay-brown-300: #D2BBB0;
--btcpay-brown-400: #B18977;
--btcpay-brown-500: #99644C;
--btcpay-brown-600: #744C3A;
--btcpay-brown-700: #5C3C2E;
--btcpay-brown-800: #3D281E;
--btcpay-brown-900: #241914;
--btcpay-brown-rgb: 153,100,76;
--btcpay-pink-100: #FFEDF9;
--btcpay-pink-200: #FFCEE5;
--btcpay-pink-300: #FFACD6;
--btcpay-pink-400: #FE82C2;
--btcpay-pink-500: #F162AF;
--btcpay-pink-600: #BB4183;
--btcpay-pink-700: #7D2457;
--btcpay-pink-800: #5E103E;
--btcpay-pink-900: #4A042E;
--btcpay-pink-rgb: 241,98,175;
--btcpay-space-xs: 4px;
--btcpay-space-s: 8px;
--btcpay-space-m: 16px;
--btcpay-space-l: 32px;
--btcpay-space-xl: 64px;
--btcpay-space-xxl: 80px;
--btcpay-font-family-base: "Open Sans", "Helvetica Neue", Arial, sans-serif;
--btcpay-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--btcpay-font-size-xs: 10px;
--btcpay-font-size-s: 12px;
--btcpay-font-size-m: 14px;
--btcpay-font-size-l: 18px;
--btcpay-font-size-xl: 36px;
--btcpay-font-size-xxl: 45px;
--btcpay-font-weight-normal: 400;
--btcpay-font-weight-semibold: 600;
--btcpay-font-weight-bold: 700;
--btcpay-neutral-100: var(--btcpay-neutral-light-100);
--btcpay-neutral-200: var(--btcpay-neutral-light-200);
--btcpay-neutral-300: var(--btcpay-neutral-light-300);
--btcpay-neutral-400: var(--btcpay-neutral-light-400);
--btcpay-neutral-500: var(--btcpay-neutral-light-500);
--btcpay-neutral-600: var(--btcpay-neutral-light-600);
--btcpay-neutral-700: var(--btcpay-neutral-light-700);
--btcpay-neutral-800: var(--btcpay-neutral-light-800);
--btcpay-neutral-900: var(--btcpay-neutral-light-900);
--btcpay-font-size-base: var(--btcpay-font-size-m);
--btcpay-bg-tile: var(--btcpay-white);
--btcpay-bg-dark: var(--btcpay-brand-dark);
--btcpay-body-bg: var(--btcpay-neutral-100);
--btcpay-body-bg-light: var(--btcpay-white);
--btcpay-body-bg-medium: var(--btcpay-neutral-200);
--btcpay-body-bg-striped: var(--btcpay-neutral-200);
--btcpay-body-bg-hover: var(--btcpay-white);
--btcpay-body-bg-active: var(--btcpay-primary);
--btcpay-body-bg-rgb: 248, 249, 250;
--btcpay-body-border-light: var(--btcpay-neutral-200);
--btcpay-body-border-medium: var(--btcpay-neutral-300);
--btcpay-body-text: var(--btcpay-neutral-900);
--btcpay-body-text-striped: var(--btcpay-body-text);
--btcpay-body-text-hover: var(--btcpay-body-text);
--btcpay-body-text-active: var(--btcpay-white);
--btcpay-body-text-muted: var(--btcpay-neutral-500);
--btcpay-body-text-rgb: 41, 41, 41;
--btcpay-body-link: var(--btcpay-primary);
--btcpay-body-link-accent: var(--btcpay-primary-accent);
--btcpay-body-shadow: rgba(25, 135, 84, 0.33);
--btcpay-wizard-bg: var(--btcpay-body-bg);
--btcpay-wizard-text: var(--btcpay-body-text);
--btcpay-header-bg: var(--btcpay-white);
--btcpay-header-text: var(--btcpay-body-text);
--btcpay-header-link: var(--btcpay-header-text);
--btcpay-header-link-accent: var(--btcpay-primary);
--btcpay-header-link-active: var(--btcpay-primary);
--btcpay-nav-link: var(--btcpay-neutral-600);
--btcpay-nav-link-accent: var(--btcpay-neutral-700);
--btcpay-nav-link-active: var(--btcpay-neutral-900);
--btcpay-nav-bg: transparent;
--btcpay-nav-bg-hover: transparent;
--btcpay-nav-bg-active: transparent;
--btcpay-nav-border: transparent;
--btcpay-nav-border-hover: transparent;
--btcpay-nav-border-active: var(--btcpay-primary);
--btcpay-form-bg: var(--btcpay-white);
--btcpay-form-bg-hover: var(--btcpay-form-bg);
--btcpay-form-bg-addon: var(--btcpay-neutral-300);
--btcpay-form-bg-disabled: var(--btcpay-neutral-200);
--btcpay-form-text: var(--btcpay-neutral-900);
--btcpay-form-text-label: var(--btcpay-neutral-700);
--btcpay-form-text-addon: var(--btcpay-neutral-700);
--btcpay-form-border: var(--btcpay-neutral-300);
--btcpay-form-border-check: var(--btcpay-neutral-400);
--btcpay-form-border-hover: var(--btcpay-primary);
--btcpay-form-border-focus: var(--btcpay-primary);
--btcpay-form-border-active: var(--btcpay-form-border);
--btcpay-form-border-disabled: var(--btcpay-form-border);
--btcpay-form-shadow-size: 2px;
--btcpay-form-shadow-focus: var(--btcpay-primary-shadow);
--btcpay-form-shadow-valid: var(--btcpay-success-shadow);
--btcpay-form-shadow-invalid: var(--btcpay-danger-shadow);
--btcpay-toggle-bg: var(--btcpay-neutral-500);
--btcpay-toggle-bg-hover: var(--btcpay-neutral-600);
--btcpay-toggle-bg-active: var(--btcpay-primary);
--btcpay-toggle-bg-active-hover: var(--btcpay-primary-600);
--btcpay-footer-bg: var(--btcpay-body-bg);
--btcpay-footer-text: var(--btcpay-neutral-500);
--btcpay-footer-link: var(--btcpay-neutral-500);
--btcpay-footer-link-accent: var(--btcpay-neutral-600);
--btcpay-code-text: var(--btcpay-body-text);
--btcpay-code-bg: transparent;
--btcpay-pre-text: var(--btcpay-white);
--btcpay-pre-bg: var(--btcpay-neutral-900);
--btcpay-primary: var(--btcpay-brand-primary);
--btcpay-primary-accent: var(--btcpay-brand-tertiary);
--btcpay-primary-text: var(--btcpay-white);
--btcpay-primary-text-hover: var(--btcpay-white);
--btcpay-primary-text-active: var(--btcpay-white);
--btcpay-primary-bg-hover: var(--btcpay-primary-accent);
--btcpay-primary-bg-active: var(--btcpay-primary-accent);
--btcpay-primary-border: var(--btcpay-primary);
--btcpay-primary-border-hover: var(--btcpay-primary-bg-hover);
--btcpay-primary-border-active: var(--btcpay-primary-bg-active);
--btcpay-primary-dim-bg: var(--btcpay-primary-500);
--btcpay-primary-dim-bg-striped: var(--btcpay-primary-400);
--btcpay-primary-dim-bg-hover: var(--btcpay-primary-600);
--btcpay-primary-dim-bg-active: var(--btcpay-primary-700);
--btcpay-primary-dim-border: var(--btcpay-primary-dim-bg);
--btcpay-primary-dim-border-active: var(--btcpay-primary-dim-bg-active);
--btcpay-primary-dim-text: var(--btcpay-white);
--btcpay-primary-dim-text-striped: var(--btcpay-primary-dim-text);
--btcpay-primary-dim-text-hover: var(--btcpay-primary-dim-text);
--btcpay-primary-dim-text-active: var(--btcpay-primary-900);
--btcpay-primary-shadow: rgba(81, 177, 62, 0.5);
--btcpay-primary-rgb: 81, 177, 62;
--btcpay-secondary: var(--btcpay-white);
--btcpay-secondary-accent: var(--btcpay-secondary);
--btcpay-secondary-text: var(--btcpay-primary);
--btcpay-secondary-text-hover: var(--btcpay-primary);
--btcpay-secondary-text-active: var(--btcpay-brand-dark);
--btcpay-secondary-bg-hover: var(--btcpay-secondary-accent);
--btcpay-secondary-bg-active: var(--btcpay-secondary-accent);
--btcpay-secondary-border: var(--btcpay-neutral-300);
--btcpay-secondary-border-hover: var(--btcpay-primary);
--btcpay-secondary-border-active: var(--btcpay-neutral-500);
--btcpay-secondary-dim-bg: var(--btcpay-neutral-200);
--btcpay-secondary-dim-bg-striped: var(--btcpay-neutral-300);
--btcpay-secondary-dim-bg-hover: var(--btcpay-neutral-300);
--btcpay-secondary-dim-bg-active: var(--btcpay-neutral-400);
--btcpay-secondary-dim-border: var(--btcpay-secondary-dim-bg);
--btcpay-secondary-dim-border-active: var(--btcpay-secondary-dim-bg-active);
--btcpay-secondary-dim-text: var(--btcpay-neutral-800);
--btcpay-secondary-dim-text-striped: var(--btcpay-secondary-dim-text);
--btcpay-secondary-dim-text-hover: var(--btcpay-secondary-dim-text);
--btcpay-secondary-dim-text-active: var(--btcpay-neutral-900);
--btcpay-secondary-shadow: rgba(130, 138, 145, 0.33);
--btcpay-secondary-rgb: 255, 255, 255;
--btcpay-success: var(--btcpay-green-500);
--btcpay-success-accent: var(--btcpay-green-700);
--btcpay-success-text: var(--btcpay-white);
--btcpay-success-text-hover: var(--btcpay-white);
--btcpay-success-text-active: var(--btcpay-white);
--btcpay-success-bg-hover: var(--btcpay-success-accent);
--btcpay-success-bg-active: var(--btcpay-success-accent);
--btcpay-success-border: var(--btcpay-success);
--btcpay-success-border-hover: var(--btcpay-success-bg-hover);
--btcpay-success-border-active: var(--btcpay-success-bg-active);
--btcpay-success-dim-bg: var(--btcpay-green-100);
--btcpay-success-dim-bg-striped: var(--btcpay-green-200);
--btcpay-success-dim-bg-hover: var(--btcpay-green-200);
--btcpay-success-dim-bg-active: var(--btcpay-green-300);
--btcpay-success-dim-border: var(--btcpay-success-dim-bg);
--btcpay-success-dim-border-active: var(--btcpay-success-dim-bg-active);
--btcpay-success-dim-text: var(--btcpay-green-800);
--btcpay-success-dim-text-striped: var(--btcpay-success-dim-text);
--btcpay-success-dim-text-hover: var(--btcpay-success-dim-text);
--btcpay-success-dim-text-active: var(--btcpay-green-900);
--btcpay-success-shadow: rgba(60, 153, 110, 0.33);
--btcpay-success-rgb: var(--btcpay-green-rgb);
--btcpay-info: var(--btcpay-blue-500);
--btcpay-info-accent: var(--btcpay-blue-700);
--btcpay-info-text: var(--btcpay-white);
--btcpay-info-text-hover: var(--btcpay-white);
--btcpay-info-text-active: var(--btcpay-white);
--btcpay-info-bg-hover: var(--btcpay-info-accent);
--btcpay-info-bg-active: var(--btcpay-info-accent);
--btcpay-info-border: var(--btcpay-info);
--btcpay-info-border-hover: var(--btcpay-info-bg-hover);
--btcpay-info-border-active: var(--btcpay-info-bg-active);
--btcpay-info-dim-bg: var(--btcpay-blue-100);
--btcpay-info-dim-bg-striped: var(--btcpay-blue-200);
--btcpay-info-dim-bg-hover: var(--btcpay-blue-200);
--btcpay-info-dim-bg-active: var(--btcpay-blue-300);
--btcpay-info-dim-border: var(--btcpay-info-dim-bg);
--btcpay-info-dim-border-active: var(--btcpay-info-dim-bg-active);
--btcpay-info-dim-text: var(--btcpay-blue-800);
--btcpay-info-dim-text-striped: var(--btcpay-info-dim-text);
--btcpay-info-dim-text-hover: var(--btcpay-info-dim-text);
--btcpay-info-dim-text-active: var(--btcpay-blue-900);
--btcpay-info-shadow: rgba(11, 172, 204, 0.33);
--btcpay-info-rgb: var(--btcpay-blue-rgb);
--btcpay-warning: var(--btcpay-yellow-500);
--btcpay-warning-accent: var(--btcpay-yellow-700);
--btcpay-warning-text: var(--btcpay-neutral-900);
--btcpay-warning-text-hover: var(--btcpay-neutral-900);
--btcpay-warning-text-active: var(--btcpay-neutral-900);
--btcpay-warning-bg-hover: var(--btcpay-warning-accent);
--btcpay-warning-bg-active: var(--btcpay-warning-accent);
--btcpay-warning-border: var(--btcpay-warning);
--btcpay-warning-border-hover: var(--btcpay-warning-bg-hover);
--btcpay-warning-border-active: var(--btcpay-warning-bg-active);
--btcpay-warning-dim-bg: var(--btcpay-yellow-100);
--btcpay-warning-dim-bg-striped: var(--btcpay-yellow-200);
--btcpay-warning-dim-bg-hover: var(--btcpay-yellow-200);
--btcpay-warning-dim-bg-active: var(--btcpay-yellow-300);
--btcpay-warning-dim-border: var(--btcpay-warning-dim-bg);
--btcpay-warning-dim-border-active: var(--btcpay-warning-dim-bg-active);
--btcpay-warning-dim-text: var(--btcpay-neutral-800);
--btcpay-warning-dim-text-striped: var(--btcpay-warning-dim-text);
--btcpay-warning-dim-text-hover: var(--btcpay-warning-dim-text);
--btcpay-warning-dim-text-active: var(--btcpay-yellow-900);
--btcpay-warning-shadow: rgba(217, 164, 6, 0.33);
--btcpay-warning-rgb: var(--btcpay-yellow-rgb);
--btcpay-danger: var(--btcpay-red-500);
--btcpay-danger-accent: var(--btcpay-red-700);
--btcpay-danger-text: var(--btcpay-white);
--btcpay-danger-text-hover: var(--btcpay-white);
--btcpay-danger-text-active: var(--btcpay-white);
--btcpay-danger-bg-hover: var(--btcpay-danger-accent);
--btcpay-danger-bg-active: var(--btcpay-danger-accent);
--btcpay-danger-border: var(--btcpay-danger);
--btcpay-danger-border-hover: var(--btcpay-danger-bg-hover);
--btcpay-danger-border-active: var(--btcpay-danger-bg-active);
--btcpay-danger-dim-bg: var(--btcpay-red-100);
--btcpay-danger-dim-bg-striped: var(--btcpay-red-200);
--btcpay-danger-dim-bg-hover: var(--btcpay-red-200);
--btcpay-danger-dim-bg-active: var(--btcpay-red-300);
--btcpay-danger-dim-border: var(--btcpay-danger-dim-bg);
--btcpay-danger-dim-border-active: var(--btcpay-danger-dim-bg-active);
--btcpay-danger-dim-text: var(--btcpay-red-800);
--btcpay-danger-dim-text-striped: var(--btcpay-danger-dim-text);
--btcpay-danger-dim-text-hover: var(--btcpay-danger-dim-text);
--btcpay-danger-dim-text-active: var(--btcpay-red-900);
--btcpay-danger-shadow: rgba(225, 83, 97, 0.33);
--btcpay-danger-rgb: var(--btcpay-red-rgb);
--btcpay-light: var(--btcpay-neutral-200);
--btcpay-light-accent: var(--btcpay-neutral-400);
--btcpay-light-text: var(--btcpay-neutral-800);
--btcpay-light-text-hover: var(--btcpay-neutral-800);
--btcpay-light-text-active: var(--btcpay-neutral-800);
--btcpay-light-bg-hover: var(--btcpay-light-accent);
--btcpay-light-bg-active: var(--btcpay-light-accent);
--btcpay-light-border: var(--btcpay-light);
--btcpay-light-border-hover: var(--btcpay-light-bg-hover);
--btcpay-light-border-active: var(--btcpay-light-bg-active);
--btcpay-light-dim-bg: var(--btcpay-white);
--btcpay-light-dim-bg-striped: var(--btcpay-neutral-200);
--btcpay-light-dim-bg-hover: var(--btcpay-neutral-200);
--btcpay-light-dim-bg-active: var(--btcpay-neutral-300);
--btcpay-light-dim-border: var(--btcpay-light-dim-bg);
--btcpay-light-dim-border-active: var(--btcpay-light-dim-bg-active);
--btcpay-light-dim-text: var(--btcpay-neutral-800);
--btcpay-light-dim-text-striped: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-hover: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-active: var(--btcpay-neutral-900);
--btcpay-light-shadow: rgba(211, 212, 213, 0.33);
--btcpay-light-rgb: 233, 236, 239;
--btcpay-dark: var(--btcpay-neutral-800);
--btcpay-dark-accent: var(--btcpay-black);
--btcpay-dark-text: var(--btcpay-neutral-200);
--btcpay-dark-text-hover: var(--btcpay-neutral-200);
--btcpay-dark-text-active: var(--btcpay-neutral-200);
--btcpay-dark-bg-hover: var(--btcpay-dark-accent);
--btcpay-dark-bg-active: var(--btcpay-dark-accent);
--btcpay-dark-border: var(--btcpay-dark);
--btcpay-dark-border-hover: var(--btcpay-dark-bg-hover);
--btcpay-dark-border-active: var(--btcpay-dark-bg-active);
--btcpay-dark-dim-bg: var(--btcpay-neutral-900);
--btcpay-dark-dim-bg-striped: var(--btcpay-neutral-900);
--btcpay-dark-dim-bg-hover: var(--btcpay-neutral-800);
--btcpay-dark-dim-bg-active: var(--btcpay-neutral-700);
--btcpay-dark-dim-border: var(--btcpay-dark-dim-bg);
--btcpay-dark-dim-border-active: var(--btcpay-dark-dim-bg-active);
--btcpay-dark-dim-text: var(--btcpay-neutral-200);
--btcpay-dark-dim-text-striped: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-hover: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-active: var(--btcpay-neutral-100);
--btcpay-dark-shadow: rgba(66, 70, 73, 0.33);
--btcpay-dark-rgb: 33, 38, 45;
}
:root[data-theme="dark"],
:root[data-btcpay-theme="dark"] {
--btcpay-neutral-100: var(--btcpay-neutral-dark-100);
--btcpay-neutral-200: var(--btcpay-neutral-dark-200);
--btcpay-neutral-300: var(--btcpay-neutral-dark-300);
--btcpay-neutral-400: var(--btcpay-neutral-dark-400);
--btcpay-neutral-500: var(--btcpay-neutral-dark-500);
--btcpay-neutral-600: var(--btcpay-neutral-dark-600);
--btcpay-neutral-700: var(--btcpay-neutral-dark-700);
--btcpay-neutral-800: var(--btcpay-neutral-dark-800);
--btcpay-neutral-900: var(--btcpay-neutral-dark-900);
--btcpay-neutral-950: #0D1117;
--btcpay-bg-dark: var(--btcpay-neutral-950);
--btcpay-bg-tile: var(--btcpay-bg-dark);
--btcpay-body-bg: var(--btcpay-neutral-900);
--btcpay-body-bg-light: var(--btcpay-neutral-950);
--btcpay-body-bg-medium: var(--btcpay-neutral-800);
--btcpay-body-bg-striped: var(--btcpay-neutral-800);
--btcpay-body-bg-hover: var(--btcpay-neutral-950);
--btcpay-body-bg-rgb: 41, 41, 41;
--btcpay-body-border-light: var(--btcpay-neutral-800);
--btcpay-body-border-medium: var(--btcpay-neutral-700);
--btcpay-body-text: var(--btcpay-white);
--btcpay-body-text-rgb: 255, 255, 255;
--btcpay-body-link-accent: var(--btcpay-primary-300);
--btcpay-form-bg: var(--btcpay-neutral-950);
--btcpay-form-bg-addon: var(--btcpay-neutral-700);
--btcpay-form-bg-disabled: var(--btcpay-neutral-800);
--btcpay-form-text: var(--btcpay-neutral-200);
--btcpay-form-text-label: var(--btcpay-neutral-100);
--btcpay-form-text-addon: var(--btcpay-neutral-300);
--btcpay-form-border: var(--btcpay-neutral-800);
--btcpay-form-border-check: var(--btcpay-neutral-600);
--btcpay-header-bg: var(--btcpay-bg-dark);
--btcpay-nav-link: var(--btcpay-neutral-500);
--btcpay-nav-link-accent: var(--btcpay-neutral-300);
--btcpay-nav-link-active: var(--btcpay-white);
--btcpay-footer-text: var(--btcpay-neutral-400);
--btcpay-footer-link: var(--btcpay-neutral-400);
--btcpay-footer-link-accent: var(--btcpay-neutral-200);
--btcpay-pre-bg: var(--btcpay-bg-dark);
--btcpay-secondary: transparent;
--btcpay-secondary-text-active: var(--btcpay-primary);
--btcpay-secondary-border: var(--btcpay-neutral-700);
--btcpay-secondary-rgb: 22, 27, 34;
--btcpay-light: var(--btcpay-neutral-800);
--btcpay-light-accent: var(--btcpay-black);
--btcpay-light-text: var(--btcpay-neutral-200);
--btcpay-light-text-hover: var(--btcpay-neutral-200);
--btcpay-light-text-active: var(--btcpay-neutral-200);
--btcpay-light-bg-hover: var(--btcpay-light-accent);
--btcpay-light-bg-active: var(--btcpay-light-accent);
--btcpay-light-border: var(--btcpay-light);
--btcpay-light-border-hover: var(--btcpay-light-bg-hover);
--btcpay-light-border-active: var(--btcpay-light-bg-active);
--btcpay-light-dim-bg: var(--btcpay-neutral-950);
--btcpay-light-dim-bg-striped: var(--btcpay-neutral-900);
--btcpay-light-dim-bg-hover: var(--btcpay-neutral-800);
--btcpay-light-dim-bg-active: var(--btcpay-neutral-700);
--btcpay-light-dim-border: var(--btcpay-light-dim-bg);
--btcpay-light-dim-border-active: var(--btcpay-light-dim-bg-active);
--btcpay-light-dim-text: var(--btcpay-neutral-200);
--btcpay-light-dim-text-striped: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-hover: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-active: var(--btcpay-neutral-100);
--btcpay-light-shadow: rgba(66, 70, 73, 0.33);
--btcpay-light-rgb: 33, 38, 45;
--btcpay-dark: var(--btcpay-neutral-200);
--btcpay-dark-accent: var(--btcpay-neutral-400);
--btcpay-dark-text: var(--btcpay-neutral-800);
--btcpay-dark-text-hover: var(--btcpay-neutral-800);
--btcpay-dark-text-active: var(--btcpay-neutral-800);
--btcpay-dark-bg-hover: var(--btcpay-dark-accent);
--btcpay-dark-bg-active: var(--btcpay-dark-accent);
--btcpay-dark-border: var(--btcpay-dark);
--btcpay-dark-border-hover: var(--btcpay-dark-bg-hover);
--btcpay-dark-border-active: var(--btcpay-dark-bg-active);
--btcpay-dark-dim-bg: var(--btcpay-white);
--btcpay-dark-dim-bg-striped: var(--btcpay-neutral-200);
--btcpay-dark-dim-bg-hover: var(--btcpay-neutral-200);
--btcpay-dark-dim-bg-active: var(--btcpay-neutral-300);
--btcpay-dark-dim-border: var(--btcpay-dark-dim-bg);
--btcpay-dark-dim-border-active: var(--btcpay-dark-dim-bg-active);
--btcpay-dark-dim-text: var(--btcpay-neutral-800);
--btcpay-dark-dim-text-striped: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-hover: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-active: var(--btcpay-neutral-900);
--btcpay-dark-shadow: rgba(211, 212, 213, 0.33);
--btcpay-dark-rgb: 201, 209, 217;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme],[data-theme]) {
--btcpay-neutral-100: var(--btcpay-neutral-dark-100);
--btcpay-neutral-200: var(--btcpay-neutral-dark-200);
--btcpay-neutral-300: var(--btcpay-neutral-dark-300);
--btcpay-neutral-400: var(--btcpay-neutral-dark-400);
--btcpay-neutral-500: var(--btcpay-neutral-dark-500);
--btcpay-neutral-600: var(--btcpay-neutral-dark-600);
--btcpay-neutral-700: var(--btcpay-neutral-dark-700);
--btcpay-neutral-800: var(--btcpay-neutral-dark-800);
--btcpay-neutral-900: var(--btcpay-neutral-dark-900);
--btcpay-neutral-950: #0D1117;
--btcpay-bg-dark: var(--btcpay-neutral-950);
--btcpay-bg-tile: var(--btcpay-bg-dark);
--btcpay-body-bg: var(--btcpay-neutral-900);
--btcpay-body-bg-light: var(--btcpay-neutral-950);
--btcpay-body-bg-medium: var(--btcpay-neutral-800);
--btcpay-body-bg-striped: var(--btcpay-neutral-800);
--btcpay-body-bg-hover: var(--btcpay-neutral-950);
--btcpay-body-bg-rgb: 41, 41, 41;
--btcpay-body-border-light: var(--btcpay-neutral-800);
--btcpay-body-border-medium: var(--btcpay-neutral-700);
--btcpay-body-text: var(--btcpay-white);
--btcpay-body-text-rgb: 255, 255, 255;
--btcpay-body-link-accent: var(--btcpay-primary-300);
--btcpay-form-bg: var(--btcpay-neutral-950);
--btcpay-form-bg-addon: var(--btcpay-neutral-700);
--btcpay-form-bg-disabled: var(--btcpay-neutral-800);
--btcpay-form-text: var(--btcpay-neutral-200);
--btcpay-form-text-label: var(--btcpay-neutral-100);
--btcpay-form-text-addon: var(--btcpay-neutral-300);
--btcpay-form-border: var(--btcpay-neutral-800);
--btcpay-form-border-check: var(--btcpay-neutral-600);
--btcpay-header-bg: var(--btcpay-bg-dark);
--btcpay-nav-link: var(--btcpay-neutral-500);
--btcpay-nav-link-accent: var(--btcpay-neutral-300);
--btcpay-nav-link-active: var(--btcpay-white);
--btcpay-footer-text: var(--btcpay-neutral-400);
--btcpay-footer-link: var(--btcpay-neutral-400);
--btcpay-footer-link-accent: var(--btcpay-neutral-200);
--btcpay-pre-bg: var(--btcpay-bg-dark);
--btcpay-secondary: transparent;
--btcpay-secondary-text-active: var(--btcpay-primary);
--btcpay-secondary-border: var(--btcpay-neutral-700);
--btcpay-secondary-rgb: 22, 27, 34;
--btcpay-light: var(--btcpay-neutral-800);
--btcpay-light-accent: var(--btcpay-black);
--btcpay-light-text: var(--btcpay-neutral-200);
--btcpay-light-text-hover: var(--btcpay-neutral-200);
--btcpay-light-text-active: var(--btcpay-neutral-200);
--btcpay-light-bg-hover: var(--btcpay-light-accent);
--btcpay-light-bg-active: var(--btcpay-light-accent);
--btcpay-light-border: var(--btcpay-light);
--btcpay-light-border-hover: var(--btcpay-light-bg-hover);
--btcpay-light-border-active: var(--btcpay-light-bg-active);
--btcpay-light-dim-bg: var(--btcpay-neutral-950);
--btcpay-light-dim-bg-striped: var(--btcpay-neutral-900);
--btcpay-light-dim-bg-hover: var(--btcpay-neutral-800);
--btcpay-light-dim-bg-active: var(--btcpay-neutral-700);
--btcpay-light-dim-border: var(--btcpay-light-dim-bg);
--btcpay-light-dim-border-active: var(--btcpay-light-dim-bg-active);
--btcpay-light-dim-text: var(--btcpay-neutral-200);
--btcpay-light-dim-text-striped: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-hover: var(--btcpay-light-dim-text);
--btcpay-light-dim-text-active: var(--btcpay-neutral-100);
--btcpay-light-shadow: rgba(66, 70, 73, 0.33);
--btcpay-light-rgb: 33, 38, 45;
--btcpay-dark: var(--btcpay-neutral-200);
--btcpay-dark-accent: var(--btcpay-neutral-400);
--btcpay-dark-text: var(--btcpay-neutral-800);
--btcpay-dark-text-hover: var(--btcpay-neutral-800);
--btcpay-dark-text-active: var(--btcpay-neutral-800);
--btcpay-dark-bg-hover: var(--btcpay-dark-accent);
--btcpay-dark-bg-active: var(--btcpay-dark-accent);
--btcpay-dark-border: var(--btcpay-dark);
--btcpay-dark-border-hover: var(--btcpay-dark-bg-hover);
--btcpay-dark-border-active: var(--btcpay-dark-bg-active);
--btcpay-dark-dim-bg: var(--btcpay-white);
--btcpay-dark-dim-bg-striped: var(--btcpay-neutral-200);
--btcpay-dark-dim-bg-hover: var(--btcpay-neutral-200);
--btcpay-dark-dim-bg-active: var(--btcpay-neutral-300);
--btcpay-dark-dim-border: var(--btcpay-dark-dim-bg);
--btcpay-dark-dim-border-active: var(--btcpay-dark-dim-bg-active);
--btcpay-dark-dim-text: var(--btcpay-neutral-800);
--btcpay-dark-dim-text-striped: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-hover: var(--btcpay-dark-dim-text);
--btcpay-dark-dim-text-active: var(--btcpay-neutral-900);
--btcpay-dark-shadow: rgba(211, 212, 213, 0.33);
--btcpay-dark-rgb: 201, 209, 217;
}
}

View File

@ -0,0 +1,10 @@
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/>
<path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/>
<path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/>
<path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/>
<path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/>
<path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" class="logo-brand-text"/>
</g>
</svg>

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Introduction
This project hosts a server with a front end which can be used to build BTCPay Server plugins and store the binaries on some storage.
## Prerequisite:
It assumes you installed docker on your system.
## Configuration
All parameters are configured via environment variables.
* `PB_POSTGRES`: Connection to a postgres database (example: `User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=61932;Database=blah`)

View File

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginBuilder.Tests", "PluginBuilder.Tests\PluginBuilder.Tests.csproj", "{973D71D2-B713-4A95-A98C-D714FFB98AC6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginBuilder", "PluginBuilder\PluginBuilder.csproj", "{A1928D9E-3939-42A7-BC39-01D7F2746C7B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginBuilder.Targets", "PluginBuilder.Targets\PluginBuilder.Targets.csproj", "{D76DBE30-BEA4-4AF0-9CF4-D0781E127D2A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{973D71D2-B713-4A95-A98C-D714FFB98AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{973D71D2-B713-4A95-A98C-D714FFB98AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{973D71D2-B713-4A95-A98C-D714FFB98AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{973D71D2-B713-4A95-A98C-D714FFB98AC6}.Release|Any CPU.Build.0 = Release|Any CPU
{A1928D9E-3939-42A7-BC39-01D7F2746C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1928D9E-3939-42A7-BC39-01D7F2746C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1928D9E-3939-42A7-BC39-01D7F2746C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1928D9E-3939-42A7-BC39-01D7F2746C7B}.Release|Any CPU.Build.0 = Release|Any CPU
{D76DBE30-BEA4-4AF0-9CF4-D0781E127D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D76DBE30-BEA4-4AF0-9CF4-D0781E127D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D76DBE30-BEA4-4AF0-9CF4-D0781E127D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D76DBE30-BEA4-4AF0-9CF4-D0781E127D2A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal