This commit is contained in:
NicolasDorier 2017-09-13 15:47:34 +09:00
commit b5c6ed3860
228 changed files with 63977 additions and 0 deletions

289
.gitignore vendored Normal file
View file

@ -0,0 +1,289 @@
## 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/
**/Properties/launchSettings.json
*_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

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
<PackageReference Include="NBitcoin.TestFramework" Version="1.4.4" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,141 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Invoicing;
using BTCPayServer.RateProvider;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Tests;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using Xunit;
namespace BTCPayServer.Tests
{
public class BTCPayServerTester : IDisposable
{
private string _Directory;
public BTCPayServerTester(string scope)
{
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
}
public Uri NBXplorerUri
{
get; set;
}
public string CookieFile
{
get; set;
}
public Uri ServerUri
{
get;
set;
}
public ExtKey HDPrivateKey
{
get; set;
}
IWebHost _Host;
public void Start()
{
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
HDPrivateKey = new ExtKey();
var port = Utils.FreeTcpPort();
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"port={port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
ServerUri = new Uri("http://127.0.0.1:" + port + "/");
BTCPayServerOptions options = new BTCPayServerOptions();
options.LoadArgs(new TextFileConfiguration(new string[] { "-datadir", _Directory }));
_Host = new WebHostBuilder()
.ConfigureServices(s =>
{
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
.AddPayServer(options)
.UseKestrel()
.UseStartup<Startup>()
.Build();
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(50);
}
public BTCPayServerRuntime Runtime
{
get; set;
}
public T GetController<T>(string userId = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
context.Request.Scheme = "http";
context.Request.Protocol = "http";
if(userId != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
}
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider;
context.RequestServices = provider;
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
httpAccessor.HttpContext = context;
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
controller.Url = new UrlHelperMock();
controller.ControllerContext = new ControllerContext()
{
HttpContext = context
};
return controller;
}
public void Dispose()
{
if(_Host != null)
_Host.Dispose();
}
}
}

View file

@ -0,0 +1,89 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit.Abstractions;
namespace BTCPayServer.Tests.Logging
{
public interface ILog
{
void LogInformation(string msg);
}
public class XUnitLogProvider : ILoggerProvider
{
ITestOutputHelper _Helper;
public XUnitLogProvider(ITestOutputHelper helper)
{
_Helper = helper;
}
public ILogger CreateLogger(string categoryName)
{
return new XUnitLog(_Helper) { Name = categoryName };
}
public void Dispose()
{
}
}
public class XUnitLog : ILog, ILogger, IDisposable
{
ITestOutputHelper _Helper;
public XUnitLog(ITestOutputHelper helper)
{
_Helper = helper;
}
public string Name
{
get; set;
}
public IDisposable BeginScope<TState>(TState state)
{
return 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)
{
StringBuilder builder = new StringBuilder();
builder.Append(formatter(state, exception));
if(exception != null)
{
builder.AppendLine();
builder.Append(exception.ToString());
}
LogInformation(builder.ToString());
}
public void LogInformation(string msg)
{
if(msg != null)
_Helper.WriteLine(Name + ": " + msg);
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLogProvider LogProvider
{
get;
set;
}
}
}

View file

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Mvc.Routing;
namespace BTCPayServer.Tests.Mocks
{
public class UrlHelperMock : IUrlHelper
{
public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext)
{
return "http://127.0.0.1/mock";
}
public string Content(string contentPath)
{
return "http://127.0.0.1/mock";
}
public bool IsLocalUrl(string url)
{
return false;
}
public string Link(string routeName, object values)
{
return "http://127.0.0.1/mock";
}
public string RouteUrl(UrlRouteContext routeContext)
{
return "http://127.0.0.1/mock";
}
}
}

View file

@ -0,0 +1,106 @@
using NBitcoin;
using NBitcoin.Tests;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Xunit;
namespace BTCPayServer.Tests
{
public class NBXplorerTester : IDisposable
{
private string _Directory;
public NBXplorerTester(string scope)
{
if(scope == null)
throw new ArgumentNullException(nameof(scope));
this._Directory = scope;
}
Process _Process;
public CoreNode Node
{
get; set;
}
public void Start()
{
ProcessLauncher launcher = new ProcessLauncher();
launcher.GoTo("Repositories", true);
if(!launcher.GoTo(new[] { "nxbplorer", "NBXplorer" }))
{
launcher.Run("git", "clone https://github.com/dgarage/NBXplorer nxbplorer");
Assert.True(launcher.GoTo(new[] { "nxbplorer", "NBXplorer" }), "Could not clone nxbplorer");
}
launcher.PushDirectory();
if(!launcher.GoTo(new[] { "bin", "Release", "netcoreapp2.0" }) || !launcher.Exists("NBXplorer.dll"))
{
launcher.PopDirectory();
launcher.Run("git", "pull");
launcher.Run("git", "checkout master");
launcher.Run("dotnet", "build /p:Configuration=Release");
Assert.True(launcher.GoTo(new[] { "bin", "Release", "netcoreapp2.0" }), "Could not build NBXplorer");
launcher.AssertExists("NBXplorer.dll");
}
var port = Utils.FreeTcpPort();
var launcher2 = new ProcessLauncher();
launcher2.GoTo(_Directory, true);
launcher2.GoTo("nbxplorer-datadir", true);
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"port={port}");
config.AppendLine($"rpc.url={Node.RPCUri.AbsoluteUri}");
config.AppendLine($"rpc.auth={Node.AuthenticationString}");
config.AppendLine($"node.endpoint={Node.NodeEndpoint.Address}:{Node.NodeEndpoint.Port}");
File.WriteAllText(Path.Combine(launcher2.CurrentDirectory, "settings.config"), config.ToString());
_Process = launcher.Start("dotnet", $"NBXplorer.dll -datadir \"{launcher2.CurrentDirectory}\"");
ExplorerClient = new NBXplorer.ExplorerClient(Node.Network, new Uri($"http://127.0.0.1:{port}/"));
CookieFile = Path.Combine(launcher2.CurrentDirectory, ".cookie");
File.Create(CookieFile).Close(); //Will be wipedout when the client starts
ExplorerClient.SetCookieFile(CookieFile);
try
{
var cancellationSource = new CancellationTokenSource(10000);
ExplorerClient.WaitServerStarted(cancellationSource.Token);
}
catch(OperationCanceledException)
{
Assert.False(_Process.HasExited, "NBXplorer failed to launch");
throw;
}
}
public NBXplorer.ExplorerClient ExplorerClient
{
get; set;
}
public string CookieFile
{
get;
set;
}
public static NBXplorerTester Create([CallerMemberNameAttribute] string scope = null)
{
return new NBXplorerTester(scope);
}
public void Dispose()
{
if(_Process != null && !_Process.HasExited)
_Process.Kill();
}
}
}

View file

@ -0,0 +1,112 @@
using BTCPayServer.Tests.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using Xunit;
namespace BTCPayServer.Tests
{
public class ProcessLauncher
{
string _CurrentDirectory;
public string CurrentDirectory
{
get
{
return _CurrentDirectory;
}
}
public ProcessLauncher()
{
_CurrentDirectory = Directory.GetCurrentDirectory();
}
public bool GoTo(string[] directories, bool createIfNotExists = false)
{
var original = _CurrentDirectory;
foreach(var dir in directories)
{
var newDirectory = Path.Combine(_CurrentDirectory, dir);
if(!Directory.Exists(newDirectory))
{
if(createIfNotExists)
Directory.CreateDirectory(newDirectory);
else
{
_CurrentDirectory = original;
return false;
}
}
_CurrentDirectory = newDirectory;
}
return true;
}
Stack<string> _Directories = new Stack<string>();
public void PushDirectory()
{
_Directories.Push(_CurrentDirectory);
}
public void PopDirectory()
{
_CurrentDirectory = _Directories.Pop();
}
public bool GoTo(string directory, bool createIfNotExists = false)
{
return GoTo(new[] { directory }, createIfNotExists);
}
public void Run(string processName, string args)
{
Start(processName, args).WaitForExit();
}
public Process Start(string processName, string args)
{
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
StringBuilder builder = new StringBuilder();
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
WorkingDirectory = _CurrentDirectory,
FileName = processName,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
try
{
process.OutputDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
Logs.Tester.LogInformation(e.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
catch(Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
return process;
}
public void AssertExists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
Assert.True(File.Exists(path), $"The file {path} should exist");
}
public bool Exists(string file)
{
var path = Path.Combine(_CurrentDirectory, file);
return File.Exists(path);
}
}
}

View file

@ -0,0 +1,89 @@
using BTCPayServer.Controllers;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Tests;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
public class ServerTester : IDisposable
{
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
{
return new ServerTester(scope);
}
string _Directory;
NodeBuilder _Builder;
public ServerTester(string scope)
{
_Directory = scope;
}
public void Start()
{
if(Directory.Exists(_Directory))
Utils.DeleteDirectory(_Directory);
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
_Builder = NodeBuilder.Create(_Directory, "0.14.2");
ExplorerNode = _Builder.CreateNode(false);
ExplorerNode.WhiteBind = true;
ExplorerNode.Start();
ExplorerNode.CreateRPCClient().Generate(101);
ExplorerTester = NBXplorerTester.Create(Path.Combine(_Directory, "explorer"));
ExplorerTester.Node = ExplorerNode;
ExplorerTester.Start();
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerTester.ExplorerClient.Address,
CookieFile = ExplorerTester.CookieFile
};
PayTester.Start();
}
public TestAccount CreateAccount()
{
return new TestAccount(this);
}
public CoreNode ExplorerNode
{
get; set;
}
public BTCPayServerTester PayTester
{
get; set;
}
public NBXplorerTester ExplorerTester
{
get; set;
}
public Network Network
{
get;
set;
} = Network.RegTest;
public void Dispose()
{
if(PayTester != null)
PayTester.Dispose();
if(ExplorerTester != null)
ExplorerTester.Dispose();
if(_Builder != null)
_Builder.Dispose();
}
}
}

View file

@ -0,0 +1,65 @@
using BTCPayServer.Controllers;
using BTCPayServer.Invoicing;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace BTCPayServer.Tests
{
public class TestAccount
{
ServerTester parent;
public TestAccount(ServerTester parent)
{
this.parent = parent;
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
}
public void GrantAccess()
{
GrantAccessAsync().GetAwaiter().GetResult();
}
public async Task GrantAccessAsync()
{
var extKey = new ExtKey().GetWif(parent.Network);
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
{
Email = "Bob@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
});
UserId = account.RegisteredUserId;
StoreId = account.RegisteredStoreId;
var manage = parent.PayTester.GetController<ManageController>(account.RegisteredUserId);
await manage.Index(new Models.ManageViewModels.IndexViewModel()
{
ExtPubKey = extKey.Neuter().ToString(),
SpeedPolicy = SpeedPolicy.MediumSpeed
});
Assert.IsType<ViewResult>(await manage.AskPairing(pairingCode.ToString()));
await manage.Pairs(pairingCode.ToString());
}
public Bitpay BitPay
{
get; set;
}
public string UserId
{
get; set;
}
public string StoreId
{
get; set;
}
}
}

View file

@ -0,0 +1,255 @@
using BTCPayServer.Tests.Logging;
using System.Linq;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitpayClient;
using System;
using System.Threading;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using BTCPayServer.Invoicing;
namespace BTCPayServer.Tests
{
public class UnitTest1
{
public UnitTest1(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
}
[Fact]
public void CanPayUsingBIP70()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.CreateAccount();
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.False(invoice.Refundable);
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
var request = url.GetPaymentRequest();
var payment = request.CreatePayment();
Transaction tx = new Transaction();
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
var cashCow = tester.ExplorerNode.CreateRPCClient();
tx = cashCow.FundRawTransaction(tx).Transaction;
tx = cashCow.SignRawTransaction(tx);
payment.Transactions.Add(tx);
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
var ack = payment.SubmitPayment();
Assert.NotNull(ack);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable);
});
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.CreateAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new Invoicing.InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new Invoicing.InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.Equal("false", invoice.ExceptionStatus);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
var firstPayment = Money.Coins(0.04m);
var txFee = Money.Zero;
var rate = user.BitPay.GetRates();
var cashCow = tester.ExplorerNode.CreateRPCClient();
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Money secondPayment = Money.Zero;
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidPartial", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
secondPayment = localInvoice.BtcDue;
});
cashCow.SendToAddress(invoiceAddress, secondPayment);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("false", localInvoice.ExceptionStatus);
});
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
cashCow.Generate(5); //Now should be complete
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
});
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidOver", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
});
cashCow.Generate(1);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
});
}
}
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
while(true)
{
try
{
act();
break;
}
catch(XunitException) when(!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
}
}

View file

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace BTCPayServer.Tests
{
public class Utils
{
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
// http://stackoverflow.com/a/14933880/2061103
public static void DeleteDirectory(string destinationDir)
{
const int magicDust = 10;
for(var gnomes = 1; gnomes <= magicDust; gnomes++)
{
try
{
Directory.Delete(destinationDir, true);
}
catch(DirectoryNotFoundException)
{
return; // good!
}
catch(IOException)
{
if(gnomes == magicDust)
throw;
// System.IO.IOException: The directory is not empty
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
catch(UnauthorizedAccessException)
{
if(gnomes == magicDust)
throw;
// Wait, maybe another software make us authorized a little later
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
Thread.Sleep(100);
continue;
}
return;
}
// depending on your use case, consider throwing an exception here
}
}
}

View file

@ -0,0 +1,35 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;
namespace BTCPayServer.Authentication
{
public class BitIdentity : IIdentity
{
public BitIdentity(PubKey key)
{
PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
}
string _Name;
public string SIN
{
get;
}
public PubKey PubKey
{
get;
}
public string AuthenticationType => "BitID";
public bool IsAuthenticated => true;
public string Name => _Name;
}
}

View file

@ -0,0 +1,44 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Name
{
get; set;
}
public string Value
{
get; set;
}
public DateTimeOffset DateCreated
{
get; set;
}
public bool Active
{
get; set;
}
public string PairedId
{
get; set;
}
public string Label
{
get; set;
}
public DateTimeOffset PairingTime
{
get; set;
}
public string SIN
{
get;
set;
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Authentication
{
public class PairingCodeEntity
{
public string Id
{
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTimeOffset PairingTime
{
get;
set;
}
public DateTimeOffset PairingExpiration
{
get;
set;
}
public string Token
{
get;
set;
}
public bool IsExpired()
{
return DateTimeOffset.UtcNow > PairingExpiration;
}
}
}

View file

@ -0,0 +1,179 @@
using DBreeze;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Authentication
{
public class TokenRepository
{
public TokenRepository(DBreezeEngine engine)
{
_Engine = engine;
}
private readonly DBreezeEngine _Engine;
public DBreezeEngine Engine
{
get
{
return _Engine;
}
}
public Task<BitTokenEntity[]> GetTokens(string sin)
{
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
foreach(var row in tx.SelectForward<string, byte[]>($"T_{sin}"))
{
var token = ToObject<BitTokenEntity>(row.Value);
tokens.Add(token);
}
}
return Task.FromResult(tokens.ToArray());
}
public Task<BitTokenEntity> CreateToken(string sin, string tokenName)
{
var token = new BitTokenEntity
{
Name = tokenName,
Value = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
DateCreated = DateTimeOffset.UtcNow
};
using(var tx = _Engine.GetTransaction())
{
tx.Insert<string, byte[]>($"T_{sin}", token.Name, ToBytes(token));
tx.Commit();
}
return Task.FromResult(token);
}
public Task<bool> PairWithAsync(string pairingCode, string pairedId)
{
if(pairedId == null)
throw new ArgumentNullException(nameof(pairedId));
using(var tx = _Engine.GetTransaction())
{
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
if(row == null || !row.Exists)
return Task.FromResult(false);
tx.RemoveKey<string>("PairingCodes", pairingCode);
try
{
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
if(pairingEntity.IsExpired())
return Task.FromResult(false);
row = tx.Select<string, byte[]>($"T_{pairingEntity.SIN}", pairingEntity.Facade);
if(row == null || !row.Exists)
return Task.FromResult(false);
var token = ToObject<BitTokenEntity>(row.Value);
if(token.Active)
return Task.FromResult(false);
token.Active = true;
token.PairedId = pairedId;
token.SIN = pairingEntity.SIN;
token.Label = pairingEntity.Label;
token.PairingTime = DateTimeOffset.UtcNow;
tx.Insert($"TbP_{pairedId}", token.Value, ToBytes(token));
tx.Insert($"T_{pairingEntity.SIN}", pairingEntity.Facade, ToBytes(token));
}
finally
{
tx.Commit();
}
}
return Task.FromResult(true);
}
public Task<BitTokenEntity[]> GetTokensByPairedIdAsync(string pairedId)
{
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = false;
foreach(var row in tx.SelectForward<string, byte[]>($"TbP_{pairedId}"))
{
tokens.Add(ToObject<BitTokenEntity>(row.Value));
}
}
return Task.FromResult(tokens.ToArray());
}
public Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{
using(var tx = _Engine.GetTransaction())
{
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
if(row == null || !row.Exists)
return Task.FromResult<PairingCodeEntity>(null);
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
if(pairingEntity.IsExpired())
return Task.FromResult<PairingCodeEntity>(null);
return Task.FromResult(pairingEntity);
}
}
public Task<PairingCodeEntity> AddPairingCodeAsync(PairingCodeEntity pairingCodeEntity)
{
pairingCodeEntity = Clone(pairingCodeEntity);
pairingCodeEntity.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
using(var tx = _Engine.GetTransaction())
{
tx.Insert("PairingCodes", pairingCodeEntity.Id, ToBytes(pairingCodeEntity));
tx.Commit();
}
return Task.FromResult(pairingCodeEntity);
}
private byte[] ToBytes<T>(T obj)
{
return ZipUtils.Zip(JsonConvert.SerializeObject(obj));
}
private T ToObject<T>(byte[] value)
{
return JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(value));
}
private T Clone<T>(T obj)
{
return ToObject<T>(ToBytes(obj));
}
public async Task DeleteToken(string sin, string tokenName)
{
var token = await GetToken(sin, tokenName);
using(var tx = _Engine.GetTransaction())
{
tx.RemoveKey<string>($"T_{sin}", tokenName);
if(token.PairedId != null)
tx.RemoveKey<string>($"TbP_" + token.PairedId, token.Value);
tx.Commit();
}
}
public Task<BitTokenEntity> GetToken(string sin, string tokenName)
{
using(var tx = _Engine.GetTransaction())
{
tx.ValuesLazyLoadingIsOn = true;
var row = tx.Select<string, byte[]>($"T_{sin}", tokenName);
if(row == null || !row.Exists)
return Task.FromResult<BitTokenEntity>(null);
var token = ToObject<BitTokenEntity>(row.Value);
if(!token.Active)
return Task.FromResult<BitTokenEntity>(null);
return Task.FromResult(token);
}
}
}
}

View file

@ -0,0 +1,89 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.34" />
<PackageReference Include="NBitpayClient" Version="1.0.0.6" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.7" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\img\bitcoin-symbol.svg" />
<None Include="wwwroot\js\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />
<None Include="wwwroot\js\site.min.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less" />
<None Include="wwwroot\vendor\font-awesome\less\core.less" />
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less" />
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less" />
<None Include="wwwroot\vendor\font-awesome\less\icons.less" />
<None Include="wwwroot\vendor\font-awesome\less\larger.less" />
<None Include="wwwroot\vendor\font-awesome\less\list.less" />
<None Include="wwwroot\vendor\font-awesome\less\mixins.less" />
<None Include="wwwroot\vendor\font-awesome\less\path.less" />
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less" />
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less" />
<None Include="wwwroot\vendor\font-awesome\less\stacked.less" />
<None Include="wwwroot\vendor\font-awesome\less\variables.less" />
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
<None Include="wwwroot\vendor\jquery\jquery.js" />
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.min.js" />
<None Include="wwwroot\vendor\popper\popper.js" />
<None Include="wwwroot\vendor\popper\popper.min.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.min.js" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer
{
public class BitpayHttpException : Exception
{
public BitpayHttpException(int code, string message) : base(message)
{
StatusCode = code;
}
public int StatusCode
{
get; set;
}
}
}

View file

@ -0,0 +1,205 @@
using BTCPayServer.Logging;
using System.Linq;
using Microsoft.Extensions.Logging;
using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace BTCPayServer.Configuration
{
public class BTCPayServerOptions
{
public Network Network
{
get; set;
}
public Uri Explorer
{
get; set;
}
public string CookieFile
{
get; set;
}
public string ConfigurationFile
{
get;
private set;
}
public string DataDir
{
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public void LoadArgs(TextFileConfiguration consoleConfig)
{
ConfigurationFile = consoleConfig.GetOrDefault<string>("conf", null);
DataDir = consoleConfig.GetOrDefault<string>("datadir", null);
if(DataDir != null && ConfigurationFile != null)
{
var isRelativePath = Path.GetFullPath(ConfigurationFile).Length > ConfigurationFile.Length;
if(isRelativePath)
{
ConfigurationFile = Path.Combine(DataDir, ConfigurationFile);
}
}
Network = consoleConfig.GetOrDefault<bool>("testnet", false) ? Network.TestNet :
consoleConfig.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
null;
if(DataDir != null && ConfigurationFile == null)
{
ConfigurationFile = GetDefaultConfigurationFile(Network != null);
}
if(ConfigurationFile != null)
{
AssetConfigFileExists();
var configTemp = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
Network = Network ?? (configTemp.GetOrDefault<bool>("testnet", false) ? Network.TestNet :
configTemp.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
null);
}
Network = Network ?? Network.Main;
if(DataDir == null)
{
DataDir = DefaultDataDirectory.GetDefaultDirectory("BTCPayServer", Network, true);
ConfigurationFile = GetDefaultConfigurationFile(true);
}
if(!Directory.Exists(DataDir))
throw new ConfigurationException("Data directory does not exists");
var config = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
consoleConfig.MergeInto(config, true);
Logs.Configuration.LogInformation("Network: " + Network);
Logs.Configuration.LogInformation("Data directory set to " + DataDir);
Logs.Configuration.LogInformation("Configuration file set to " + ConfigurationFile);
var defaultPort = config.GetOrDefault<int>("port", GetDefaultPort(Network));
Listen = config
.GetAll("bind")
.Select(p => ConvertToEndpoint(p, defaultPort))
.ToList();
if(Listen.Count == 0)
{
Listen.Add(new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort));
}
Explorer = config.GetOrDefault<Uri>("explorer.url", GetDefaultNXplorerUri());
CookieFile = config.GetOrDefault<string>("explorer.cookiefile", GetExplorerDefaultCookiePath());
ExternalUrl = config.GetOrDefault<Uri>("externalurl", null);
if(ExternalUrl == null)
{
var ip = Listen.Where(u => !u.Address.ToString().Equals("0.0.0.0", StringComparison.OrdinalIgnoreCase)).FirstOrDefault()
?? new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort);
ExternalUrl = new Uri($"http://{ip.Address}:{ip.Port}/");
}
}
public Uri ExternalUrl
{
get; set;
}
private Uri GetDefaultNXplorerUri()
{
return new Uri("http://localhost:" + GetNXplorerDefaultPort(Network));
}
public string[] GetUrls()
{
return Listen.Select(b => "http://" + b + "/").ToArray();
}
private void AssetConfigFileExists()
{
if(!File.Exists(ConfigurationFile))
throw new ConfigurationException("Configuration file does not exists");
}
public static IPEndPoint ConvertToEndpoint(string str, int defaultPort)
{
var portOut = defaultPort;
var hostOut = "";
int colon = str.LastIndexOf(':');
// if a : is found, and it either follows a [...], or no other : is in the string, treat it as port separator
bool fHaveColon = colon != -1;
bool fBracketed = fHaveColon && (str[0] == '[' && str[colon - 1] == ']'); // if there is a colon, and in[0]=='[', colon is not 0, so in[colon-1] is safe
bool fMultiColon = fHaveColon && (str.LastIndexOf(':', colon - 1) != -1);
if(fHaveColon && (colon == 0 || fBracketed || !fMultiColon))
{
int n;
if(int.TryParse(str.Substring(colon + 1), out n) && n > 0 && n < 0x10000)
{
str = str.Substring(0, colon);
portOut = n;
}
}
if(str.Length > 0 && str[0] == '[' && str[str.Length - 1] == ']')
hostOut = str.Substring(1, str.Length - 2);
else
hostOut = str;
return new IPEndPoint(IPAddress.Parse(hostOut), portOut);
}
const string DefaultConfigFile = "settings.config";
private string GetDefaultConfigurationFile(bool createIfNotExist)
{
var config = Path.Combine(DataDir, DefaultConfigFile);
Logs.Configuration.LogInformation("Configuration file set to " + config);
if(createIfNotExist && !File.Exists(config))
{
Logs.Configuration.LogInformation("Creating configuration file");
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0");
builder.AppendLine("#Put here the xpub key of your hardware wallet");
builder.AppendLine("#hdpubkey=xpub...");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + GetDefaultPort(Network));
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine("#externalurl=http://127.0.0.1/");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + GetDefaultNXplorerUri());
builder.AppendLine("#explorer.cookiefile=" + GetExplorerDefaultCookiePath());
File.WriteAllText(config, builder.ToString());
}
return config;
}
private string GetExplorerDefaultCookiePath()
{
return Path.Combine(DefaultDataDirectory.GetDefaultDirectory("NBXplorer", Network, false), ".cookie");
}
private int GetNXplorerDefaultPort(Network network)
{
return network == Network.Main ? 24444 :
network == Network.TestNet ? 24445 : 24446;
}
private int GetDefaultPort(Network network)
{
return network == Network.Main ? 23000 :
network == Network.TestNet ? 23001 : 23002;
}
}
}

View file

@ -0,0 +1,114 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Invoicing;
using BTCPayServer.Logging;
using BTCPayServer.Wallet;
using DBreeze;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Data;
namespace BTCPayServer.Configuration
{
public class BTCPayServerRuntime : IDisposable
{
public ExplorerClient Explorer
{
get;
private set;
}
public void Configure(BTCPayServerOptions opts)
{
ConfigureAsync(opts).GetAwaiter().GetResult();
}
public async Task ConfigureAsync(BTCPayServerOptions opts)
{
Network = opts.Network;
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
Explorer.SetCookieFile(opts.CookieFile);
CancellationTokenSource cts = new CancellationTokenSource(5000);
try
{
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
Logs.Configuration.LogInformation("Connection successfull");
}
catch(Exception ex)
{
throw new ConfigurationException($"Could not connect to NBXplorer, {ex.Message}");
}
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db);
TokenRepository = new TokenRepository(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db);
var dbContext = new ApplicationDbContextFactory(Path.Combine(opts.DataDir, "sqllite.db"));
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
db = new DBreezeEngine(CreateDBPath(opts, "AddressMapping"));
_Resources.Add(db);
Wallet = new BTCPayWallet(Explorer, db);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)
{
var dbpath = Path.Combine(opts.DataDir, name);
if(!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return dbpath;
}
List<IDisposable> _Resources = new List<IDisposable>();
public void Dispose()
{
lock(_Resources)
{
foreach(var r in _Resources)
{
r.Dispose();
}
_Resources.Clear();
}
}
public Network Network
{
get;
private set;
}
public TokenRepository TokenRepository
{
get; set;
}
public InvoiceRepository InvoiceRepository
{
get;
set;
}
public BTCPayWallet Wallet
{
get;
set;
}
public ApplicationDbContextFactory DBFactory
{
get;
set;
}
}
}

View file

@ -0,0 +1,52 @@
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace BTCPayServer.Configuration
{
public class DefaultDataDirectory
{
public static string GetDefaultDirectory(string appName, Network network, bool createDirectory)
{
string directory = null;
var home = Environment.GetEnvironmentVariable("HOME");
if(!string.IsNullOrEmpty(home))
{
if(createDirectory)
Logs.Configuration.LogInformation("Using HOME environment variable for initializing application data");
directory = home;
directory = Path.Combine(directory, "." + appName.ToLowerInvariant());
}
else
{
var localAppData = Environment.GetEnvironmentVariable("APPDATA");
if(!string.IsNullOrEmpty(localAppData))
{
if(createDirectory)
Logs.Configuration.LogInformation("Using APPDATA environment variable for initializing application data");
directory = localAppData;
directory = Path.Combine(directory, appName);
}
else
{
throw new DirectoryNotFoundException("Could not find suitable datadir");
}
}
if(!Directory.Exists(directory) && createDirectory)
{
Directory.CreateDirectory(directory);
}
directory = Path.Combine(directory, network.Name);
if(!Directory.Exists(directory) && createDirectory)
{
Logs.Configuration.LogInformation("Creating data directory");
Directory.CreateDirectory(directory);
}
return directory;
}
}
}

View file

@ -0,0 +1,221 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class ConfigurationException : Exception
{
public ConfigurationException(string message) : base(message)
{
}
}
public class TextFileConfiguration
{
private Dictionary<string, List<string>> _Args;
public TextFileConfiguration(string[] args)
{
_Args = new Dictionary<string, List<string>>();
string noValueParam = null;
Action flushNoValueParam = () =>
{
if(noValueParam != null)
{
Add(noValueParam, "1", false);
noValueParam = null;
}
};
foreach(var arg in args)
{
bool isParamName = arg.StartsWith("-", StringComparison.Ordinal);
if(isParamName)
{
var splitted = arg.Split('=');
if(splitted.Length > 1)
{
var value = String.Join("=", splitted.Skip(1).ToArray());
flushNoValueParam();
Add(splitted[0], value, false);
}
else
{
flushNoValueParam();
noValueParam = splitted[0];
}
}
else
{
if(noValueParam != null)
{
Add(noValueParam, arg, false);
noValueParam = null;
}
}
}
flushNoValueParam();
}
private void Add(string key, string value, bool sourcePriority)
{
key = NormalizeKey(key);
List<string> list;
if(!_Args.TryGetValue(key, out list))
{
list = new List<string>();
_Args.Add(key, list);
}
if(sourcePriority)
list.Insert(0, value);
else
list.Add(value);
}
private static string NormalizeKey(string key)
{
key = key.ToLowerInvariant();
while(key.Length > 0 && key[0] == '-')
{
key = key.Substring(1);
}
key = key.Replace(".", "");
return key;
}
public void MergeInto(TextFileConfiguration destination, bool sourcePriority)
{
foreach(var kv in _Args)
{
foreach(var v in kv.Value)
destination.Add(kv.Key, v, sourcePriority);
}
}
public TextFileConfiguration(Dictionary<string, List<string>> args)
{
_Args = args;
}
public static TextFileConfiguration Parse(string data)
{
Dictionary<string, List<string>> result = new Dictionary<string, List<string>>();
var lines = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
int lineCount = -1;
foreach(var l in lines)
{
lineCount++;
var line = l.Trim();
if(line.StartsWith("#", StringComparison.Ordinal))
continue;
var split = line.Split('=');
if(split.Length == 0)
continue;
if(split.Length == 1)
throw new FormatException("Line " + lineCount + ": No value are set");
var key = split[0];
key = NormalizeKey(key);
List<string> values;
if(!result.TryGetValue(key, out values))
{
values = new List<string>();
result.Add(key, values);
}
var value = String.Join("=", split.Skip(1).ToArray());
values.Add(value);
}
return new TextFileConfiguration(result);
}
public bool Contains(string key)
{
List<string> values;
return _Args.TryGetValue(key, out values);
}
public string[] GetAll(string key)
{
List<string> values;
if(!_Args.TryGetValue(key, out values))
return new string[0];
return values.ToArray();
}
private List<Tuple<string, string>> _Aliases = new List<Tuple<string, string>>();
public void AddAlias(string from, string to)
{
from = NormalizeKey(from);
to = NormalizeKey(to);
_Aliases.Add(Tuple.Create(from, to));
}
public T GetOrDefault<T>(string key, T defaultValue)
{
key = NormalizeKey(key);
var aliases = _Aliases
.Where(a => a.Item1 == key || a.Item2 == key)
.Select(a => a.Item1 == key ? a.Item2 : a.Item1)
.ToList();
aliases.Insert(0, key);
foreach(var alias in aliases)
{
List<string> values;
if(!_Args.TryGetValue(alias, out values))
continue;
if(values.Count == 0)
continue;
try
{
return ConvertValue<T>(values[0]);
}
catch(FormatException) { throw new ConfigurationException("Key " + key + " should be of type " + typeof(T).Name); }
}
return defaultValue;
}
private T ConvertValue<T>(string str)
{
if(typeof(T) == typeof(bool))
{
var trueValues = new[] { "1", "true" };
var falseValues = new[] { "0", "false" };
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)true;
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
return (T)(object)false;
throw new FormatException();
}
else if(typeof(T) == typeof(Uri))
return (T)(object)new Uri(str, UriKind.Absolute);
else if(typeof(T) == typeof(string))
return (T)(object)str;
else if(typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":");
if(separator == -1)
throw new FormatException();
var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
}
else if(typeof(T) == typeof(int))
{
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
}
else
{
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
}
}
}
}

View file

@ -0,0 +1,62 @@
using BTCPayServer.Authentication;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
public AccessTokenController(TokenRepository tokenRepository)
{
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
}
[HttpGet]
[Route("tokens")]
public async Task<GetTokensResponse> GetTokens()
{
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
return new GetTokensResponse(tokens);
}
[HttpPost]
[Route("tokens")]
public async Task<DataWrapper<List<PairingCodeResponse>>> GetPairingCode([FromBody] PairingCodeRequest token)
{
var now = DateTimeOffset.UtcNow;
var pairingEntity = new PairingCodeEntity()
{
Facade = token.Facade,
Label = token.Label,
SIN = token.Id,
PairingTime = now,
PairingExpiration = now + TimeSpan.FromMinutes(15)
};
var grantedToken = await _TokenRepository.CreateToken(token.Id, token.Facade);
pairingEntity.Token = grantedToken.Name;
pairingEntity = await _TokenRepository.AddPairingCodeAsync(pairingEntity);
var pairingCodes = new List<PairingCodeResponse>
{
new PairingCodeResponse()
{
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.PairingExpiration,
DateCreated = pairingEntity.PairingTime,
Facade = grantedToken.Name,
Token = grantedToken.Value,
Label = pairingEntity.Label
}
};
return DataWrapper.Create(pairingCodes);
}
}
}

View file

@ -0,0 +1,486 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Stores;
namespace BTCPayServer.Controllers
{
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
public AccountController(
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
ILogger<AccountController> logger)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
}
[TempData]
public string ErrorMessage { get; set; }
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return RedirectToLocal(returnUrl);
}
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
}
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
}
[HttpGet]
[AllowAnonymous]
public IActionResult Lockout()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
var store = await storeRepository.CreateStore(user.Id);
RegisteredStoreId = store.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId
{
get; set;
}
/// <summary>
/// Test property
/// </summary>
public string RegisteredStoreId
{
get; set;
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
throw new ApplicationException("Error loading external login information during confirmation.");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
ViewData["ReturnUrl"] = returnUrl;
return View(nameof(ExternalLogin), model);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string code = null)
{
if (code == null)
{
throw new ApplicationException("A code must be supplied for password reset.");
}
var model = new ResetPasswordViewModel { Code = code };
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction(nameof(ResetPasswordConfirmation));
}
AddErrors(result);
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
return View();
}
[HttpGet]
public IActionResult AccessDenied()
{
return View();
}
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
#endregion
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View file

@ -0,0 +1,104 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Filters;
using BTCPayServer.Invoicing;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await CreateInvoiceCore(invoice, store);
}
[HttpGet]
[Route("invoices/{id}")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if(invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = EntityToDTO(invoice);
return new DataWrapper<InvoiceResponse>(resp);
}
[HttpGet]
[Route("invoices")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token,
DateTimeOffset? dateStart = null,
DateTimeOffset? dateEnd = null,
string orderId = null,
string itemCode = null,
string status = null,
int? limit = null,
int? offset = null)
{
if(dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var query = new InvoiceQuery()
{
Count = limit,
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
Status = status,
StoreId = store.Id
};
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select(EntityToDTO).ToArray();
return DataWrapper.Create(entities);
}
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string exptectedToken)
{
if(facade == null)
throw new ArgumentNullException(nameof(facade));
var actualToken = await _TokenRepository.GetToken(this.GetBitIdentity().SIN, facade.ToString());
if(exptectedToken == null || actualToken == null || !actualToken.Value.Equals(exptectedToken, StringComparison.Ordinal))
{
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
throw new BitpayHttpException(401, "This endpoint does not support the `user` facade");
}
return actualToken;
}
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.PairedId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;
}
}
}

View file

@ -0,0 +1,72 @@
using BTCPayServer.Filters;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
return NotFound();
var dto = EntityToDTO(invoice);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(_ExternalUrl.GetAbsolute($"i/{invoice.Id}"), UriKind.Absolute);
var store = await _StoreRepository.FindStore(invoice.StoreId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
if(store.StoreCertificate != null)
{
try
{
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
}
catch(Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
}
}
return new PaymentRequestActionResult(request);
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null || invoice.IsExpired())
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
}

View file

@ -0,0 +1,185 @@
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Invoicing;
using BTCPayServer.Models.InvoicingModels;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
public async Task<IActionResult> Payment(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null)
return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = EntityToDTO(invoice);
var model = new PaymentModel()
{
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BTCAddress = invoice.DepositAddress.ToString(),
BTCAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
BTCTotalDue = invoice.GetTotalCryptoDue().ToString(),
BTCDue = invoice.GetCryptoDue().ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString(),
RedirectUrl = invoice.RedirectURL,
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.Payments.Count + 1,
Status = invoice.Status
};
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
return View(model);
}
private string PrettyPrint(TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
if(expiration.Days >= 1)
builder.Append(expiration.Days.ToString());
if(expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00"));
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
return builder.ToString();
}
[HttpGet]
[Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if(invoice == null)
return NotFound();
return Content(invoice.Status);
}
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok();
}
[HttpGet]
[Route("Invoices")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Index(string searchTerm = null, int skip = 0, int count = 20)
{
var store = await FindStore(User);
var model = new InvoicesModel();
foreach(var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = searchTerm,
Count = count,
Skip = skip,
StoreId = store.Id
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpGet]
[Route("Invoices/Create")]
[BitpayAPIConstraint(false)]
public IActionResult CreateInvoice()
{
return View();
}
[HttpPost]
[Route("Invoices/Create")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var store = await FindStore(User);
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = "USD",
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail
}, store);
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction("Index");
}
[HttpPost]
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
{
return RedirectToAction("Index", new
{
searchTerm = invoices.SearchTerm,
skip = invoices.Skip,
count = invoices.Count,
});
}
[TempData]
public string StatusMessage
{
get;
set;
}
private async Task<StoreData> FindStore(ClaimsPrincipal user)
{
var usr = await _UserManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_UserManager.GetUserId(User)}'.");
}
return await _StoreRepository.GetStore(usr.Id);
}
}
}

View file

@ -0,0 +1,165 @@
using BTCPayServer.Authentication;
using System.Reflection;
using System.Linq;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Newtonsoft.Json;
using BTCPayServer.Invoicing;
using BTCPayServer.Wallet;
using System.Globalization;
using NBitcoin;
using NBitcoin.DataEncoders;
using BTCPayServer.RateProvider;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Net;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin.Payment;
using BTCPayServer.Data;
using BTCPayServer.Stores;
using BTCPayServer.Models.InvoicingModels;
using System.Security.Claims;
using BTCPayServer.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
TokenRepository _TokenRepository;
InvoiceRepository _InvoiceRepository;
IExternalUrlProvider _ExternalUrl;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
BTCPayWallet wallet,
IExternalUrlProvider externalUrl,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
IFeeProvider feeProvider)
{
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_ExternalUrl = externalUrl;
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
static Regex _Email;
bool IsEmail(string str)
{
if(String.IsNullOrWhiteSpace(str))
return false;
if(_Email == null)
_Email = new Regex("^((([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromSeconds(2.0));
return _Email.IsMatch(str);
}
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
{
var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This user has not configured his derivation strategy")
};
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.RefundMail = IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
entity.SpeedPolicy = store.SpeedPolicy;
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(derivationStrategy);
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Wallet.MapAsync(entity.DepositAddress, entity.Id);
await _Watcher.WatchAsync(entity.Id);
var resp = EntityToDTO(entity);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private InvoiceResponse EntityToDTO(InvoiceEntity entity)
{
InvoiceResponse dto = new InvoiceResponse
{
Id = entity.Id,
OrderId = entity.OrderId,
PosData = entity.PosData,
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = entity.InvoiceTime,
ExpirationTime = entity.ExpirationTime,
BTCPrice = Money.Coins((decimal)(1.0 / entity.Rate)).ToString(),
Status = entity.Status,
Url = _ExternalUrl.GetAbsolute("invoice?id=" + entity.Id),
Currency = entity.ProductInformation.Currency,
Flags = new Flags() { Refundable = entity.Refundable }
};
Populate(entity.ProductInformation, dto);
Populate(entity.BuyerInformation, dto);
dto.ExRates = new Dictionary<string, double>
{
{ entity.ProductInformation.Currency, entity.Rate }
};
dto.PaymentUrls = new InvoicePaymentUrls()
{
BIP72 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}&r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
BIP72b = $"bitcoin:?r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
BIP73 = _ExternalUrl.GetAbsolute($"i/{entity.Id}"),
BIP21 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}",
};
dto.BitcoinAddress = entity.DepositAddress.ToString();
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
var paid = entity.Payments.Select(p => p.Output.Value).Sum();
dto.BTCPaid = paid.ToString();
dto.BTCDue = entity.GetCryptoDue().ToString();
dto.ExceptionStatus = entity.ExceptionStatus == null ? new JValue(false) : new JValue(entity.ExceptionStatus);
return dto;
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
}
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
{
var str = JsonConvert.SerializeObject(from);
JsonConvert.PopulateObject(str, dest);
}
}
}

View file

@ -0,0 +1,678 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using BTCPayServer.Models;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Services;
using BTCPayServer.Authentication;
using BTCPayServer.Wallet;
using Microsoft.AspNetCore.Hosting;
using NBitpayClient;
using NBitcoin;
using BTCPayServer.Stores;
namespace BTCPayServer.Controllers
{
[Authorize]
[Route("[controller]/[action]")]
public class ManageController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
private readonly BTCPayWallet _Wallet;
IHostingEnvironment _Env;
IExternalUrlProvider _UrlProvider;
StoreRepository _StoreRepository;
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWallet wallet,
StoreRepository storeRepository,
IHostingEnvironment env,
IExternalUrlProvider urlProvider)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Wallet = wallet;
_Env = env;
_UrlProvider = urlProvider;
_StoreRepository = storeRepository;
}
[TempData]
public string StatusMessage
{
get; set;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var store = await _StoreRepository.GetStore(user.Id);
var model = new IndexViewModel
{
Username = user.UserName,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
IsEmailConfirmed = user.EmailConfirmed,
StatusMessage = StatusMessage,
ExtPubKey = store.DerivationStrategy,
StoreWebsite = store.StoreWebsite,
StoreName = store.StoreName,
SpeedPolicy = store.SpeedPolicy
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IndexViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
bool needUpdate = false;
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var store = await _StoreRepository.GetStore(user.Id);
if(model.ExtPubKey != store.DerivationStrategy)
{
store.DerivationStrategy = model.ExtPubKey;
await _Wallet.TrackAsync(store.DerivationStrategy);
needUpdate = true;
}
if(model.SpeedPolicy != store.SpeedPolicy)
{
store.SpeedPolicy = model.SpeedPolicy;
needUpdate = true;
}
if(model.StoreName != store.StoreName)
{
store.StoreName = model.StoreName;
needUpdate = true;
}
if(model.StoreWebsite != store.StoreWebsite)
{
store.StoreWebsite = model.StoreWebsite;
needUpdate = true;
}
var email = user.Email;
if(model.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, model.Email);
if(!setEmailResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
}
var phoneNumber = user.PhoneNumber;
if(model.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber);
if(!setPhoneResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'.");
}
}
if(needUpdate)
{
var result = await _userManager.UpdateAsync(user);
await _StoreRepository.UpdateStore(store);
if(!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred updating user with ID '{user.Id}'.");
}
}
StatusMessage = "Your profile has been updated";
return RedirectToAction(nameof(Index));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SendVerificationEmail(IndexViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(Index));
}
[HttpGet]
public async Task<IActionResult> ChangePassword()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if(!hasPassword)
{
return RedirectToAction(nameof(SetPassword));
}
var model = new ChangePasswordViewModel { StatusMessage = StatusMessage };
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
if(!changePasswordResult.Succeeded)
{
AddErrors(changePasswordResult);
return View(model);
}
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User changed their password successfully.");
StatusMessage = "Your password has been changed.";
return RedirectToAction(nameof(ChangePassword));
}
[HttpGet]
public async Task<IActionResult> SetPassword()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if(hasPassword)
{
return RedirectToAction(nameof(ChangePassword));
}
var model = new SetPasswordViewModel { StatusMessage = StatusMessage };
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword);
if(!addPasswordResult.Succeeded)
{
AddErrors(addPasswordResult);
return View(model);
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "Your password has been set.";
return RedirectToAction(nameof(SetPassword));
}
[HttpGet]
public async Task<IActionResult> ExternalLogins()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) };
model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1;
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LinkLogin(string provider)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action(nameof(LinkLoginCallback));
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return new ChallengeResult(provider, properties);
}
[HttpGet]
public async Task<IActionResult> LinkLoginCallback()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
if(info == null)
{
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
}
var result = await _userManager.AddLoginAsync(user, info);
if(!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'.");
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
StatusMessage = "The external login was added.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
[Route("/api-access-request")]
public async Task<IActionResult> AskPairing(string pairingCode)
{
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if(pairing == null)
{
StatusMessage = "Unknown pairing code";
return RedirectToAction(nameof(Pairs));
}
else
{
return View(new PairingModel()
{
Id = pairing.Id,
Facade = pairing.Facade,
Label = pairing.Label,
SIN = pairing.SIN
});
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Pairs(string pairingCode)
{
var store = await _StoreRepository.GetStore(_userManager.GetUserId(User));
if(pairingCode != null && await _TokenRepository.PairWithAsync(pairingCode, store.Id))
{
StatusMessage = "Pairing is successfull";
return RedirectToAction(nameof(Tokens));
}
else
{
StatusMessage = "Pairing failed";
return RedirectToAction(nameof(Tokens));
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel model)
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
if(!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'.");
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "The external login was removed.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if(!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if(!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if(string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if(!is2faTokenValid)
{
ModelState.AddModelError("model.Code", "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddToken(AddTokenViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
string storeId = await GetStoreId();
var url = new Uri(_UrlProvider.GetAbsolute(""));
var bitpay = new Bitpay(new NBitcoin.Key(), url);
var pairing = await bitpay.RequestClientAuthorizationAsync(model.Label, new Facade(model.Facade));
var link = pairing.CreateLink(url).ToString();
await _TokenRepository.PairWithAsync(pairing.ToString(), storeId);
StatusMessage = "New access token paired to this store";
return RedirectToAction("Tokens");
}
private async Task<string> GetStoreId()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
return (await _StoreRepository.GetStore(user.Id)).Id;
}
[HttpGet]
public IActionResult AddToken()
{
var model = new AddTokenViewModel();
model.Facade = "merchant";
if(_Env.IsDevelopment())
{
model.PublicKey = new Key().PubKey.ToHex();
}
return View(model);
}
[HttpPost]
public async Task<IActionResult> DeleteToken(string name, string sin)
{
await _TokenRepository.DeleteToken(sin, name);
StatusMessage = "Token revoked";
return RedirectToAction("Tokens");
}
[HttpGet]
public async Task<IActionResult> Tokens()
{
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByPairedIdAsync(await GetStoreId());
model.StatusMessage = StatusMessage;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Name,
Label = t.Label,
SIN = t.SIN,
Id = t.Value
}).ToArray();
return View(model);
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if(user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if(!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach(var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while(currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if(currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),
unformattedKey);
}
#endregion
}
}

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,40 @@
using BTCPayServer.Models;
using BTCPayServer.RateProvider;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProvider _RateProvider;
CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
{
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
{
var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r =>
new NBitpayClient.Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
}).Where(n => n.Name != null).ToArray());
}
}
}

269
BTCPayServer/Currencies.txt Normal file
View file

@ -0,0 +1,269 @@
Afghani AFN 2
Bitcoin BTC 8 Ƀ
Euro EUR 2 €
Lek ALL 2
Algerian Dinar DZD 2
US Dollar USD 2 $
Euro EUR 2
Kwanza AOA 2
East Caribbean Dollar XCD 2
No universal currency
East Caribbean Dollar XCD 2
Argentine Peso ARS 2
Armenian Dram AMD 2
Aruban Florin AWG 2
Australian Dollar AUD 2 A$
Euro EUR 2
Azerbaijanian Manat AZN 2
Bahamian Dollar BSD 2
Bahraini Dinar BHD 3
Taka BDT 2
Barbados Dollar BBD 2
Belarusian Ruble BYN 2
Belarusian Ruble BYR 0
Euro EUR 2
Belize Dollar BZD 2
CFA Franc BCEAO XOF 0
Bermudian Dollar BMD 2
Indian Rupee INR 2
Ngultrum BTN 2
Boliviano BOB 2
Mvdol BOV 2
US Dollar USD 2
Convertible Mark BAM 2
Pula BWP 2
Norwegian Krone NOK 2
Brazilian Real BRL 2
US Dollar USD 2
Brunei Dollar BND 2
Bulgarian Lev BGN 2
CFA Franc BCEAO XOF 0
Burundi Franc BIF 0
Cabo Verde Escudo CVE 2
Riel KHR 2
CFA Franc BEAC XAF 0
Canadian Dollar CAD 2 CA$
Cayman Islands Dollar KYD 2
CFA Franc BEAC XAF 0
CFA Franc BEAC XAF 0
Chilean Peso CLP 0
Unidad de Fomento CLF 4
Yuan Renminbi CNY 2 CN¥
Australian Dollar AUD 2 A$
Colombian Peso COP 2
Unidad de Valor Real COU 2
Comoro Franc KMF 0
Congolese Franc CDF 2
CFA Franc BEAC XAF 0
New Zealand Dollar NZD 2 NZ$
Costa Rican Colon CRC 2
CFA Franc BCEAO XOF 0
Kuna HRK 2
Cuban Peso CUP 2
Peso Convertible CUC 2
Netherlands Antillean Guilder ANG 2
Euro EUR 2
Czech Koruna CZK 2
Danish Krone DKK 2
Djibouti Franc DJF 0
East Caribbean Dollar XCD 2
Dominican Peso DOP 2
US Dollar USD 2
Egyptian Pound EGP 2
El Salvador Colon SVC 2
US Dollar USD 2
CFA Franc BEAC XAF 0
Nakfa ERN 2
Euro EUR 2
Ethiopian Birr ETB 2
Euro EUR 2
Falkland Islands Pound FKP 2
Danish Krone DKK 2
Fiji Dollar FJD 2
Euro EUR 2
Euro EUR 2
Euro EUR 2
CFP Franc XPF 0
Euro EUR 2
CFA Franc BEAC XAF 0
Dalasi GMD 2
Lari GEL 2
Euro EUR 2
Ghana Cedi GHS 2
Gibraltar Pound GIP 2
Euro EUR 2
Danish Krone DKK 2
East Caribbean Dollar XCD 2
Euro EUR 2
US Dollar USD 2
Quetzal GTQ 2
Pound Sterling GBP 2 £
Guinea Franc GNF 0
CFA Franc BCEAO XOF 0
Guyana Dollar GYD 2
Gourde HTG 2
US Dollar USD 2
Australian Dollar AUD 2
Euro EUR 2
Lempira HNL 2
Hong Kong Dollar HKD 2
Forint HUF 2
Iceland Krona ISK 0
Indian Rupee INR 2
Rupiah IDR 2
SDR (Special Drawing Right) XDR N.A.
Iranian Rial IRR 2
Iraqi Dinar IQD 3
Euro EUR 2
Pound Sterling GBP 2
New Israeli Sheqel ILS 2
Euro EUR 2
Jamaican Dollar JMD 2
Yen JPY 0 ¥
Pound Sterling GBP 2
Jordanian Dinar JOD 3
Tenge KZT 2
Kenyan Shilling KES 2
Australian Dollar AUD 2
North Korean Won KPW 2
Won KRW 0 ₩
Kuwaiti Dinar KWD 3
Som KGS 2
Kip LAK 2
Euro EUR 2
Lebanese Pound LBP 2
Loti LSL 2
Rand ZAR 2
Liberian Dollar LRD 2
Libyan Dinar LYD 3
Swiss Franc CHF 2
Euro EUR 2
Euro EUR 2
Pataca MOP 2
Denar MKD 2
Malagasy Ariary MGA 2
Malawi Kwacha MWK 2
Malaysian Ringgit MYR 2
Rufiyaa MVR 2
CFA Franc BCEAO XOF 0
Euro EUR 2
US Dollar USD 2
Euro EUR 2
Ouguiya MRO 2
Mauritius Rupee MUR 2
Euro EUR 2
ADB Unit of Account XUA N.A.
Mexican Peso MXN 2
Mexican Unidad de Inversion (UDI) MXV 2
US Dollar USD 2
Moldovan Leu MDL 2
Euro EUR 2
Tugrik MNT 2
Euro EUR 2
East Caribbean Dollar XCD 2
Moroccan Dirham MAD 2
Mozambique Metical MZN 2
Kyat MMK 2
Namibia Dollar NAD 2
Rand ZAR 2
Australian Dollar AUD 2
Nepalese Rupee NPR 2
Euro EUR 2
CFP Franc XPF 0
New Zealand Dollar NZD 2
Cordoba Oro NIO 2
CFA Franc BCEAO XOF 0
Naira NGN 2
New Zealand Dollar NZD 2
Australian Dollar AUD 2
US Dollar USD 2
Norwegian Krone NOK 2
Rial Omani OMR 3
Pakistan Rupee PKR 2
US Dollar USD 2
No universal currency
Balboa PAB 2
US Dollar USD 2
Kina PGK 2
Guarani PYG 0
Sol PEN 2
Philippine Peso PHP 2
New Zealand Dollar NZD 2
Zloty PLN 2
Euro EUR 2
US Dollar USD 2
Qatari Rial QAR 2
Euro EUR 2
Romanian Leu RON 2
Russian Ruble RUB 2
Rwanda Franc RWF 0
Euro EUR 2
Saint Helena Pound SHP 2
East Caribbean Dollar XCD 2
East Caribbean Dollar XCD 2
Euro EUR 2
Euro EUR 2
East Caribbean Dollar XCD 2
Tala WST 2
Euro EUR 2
Dobra STD 2
Saudi Riyal SAR 2
CFA Franc BCEAO XOF 0
Serbian Dinar RSD 2
Seychelles Rupee SCR 2
Leone SLL 2
Singapore Dollar SGD 2
Netherlands Antillean Guilder ANG 2
Sucre XSU N.A.
Euro EUR 2
Euro EUR 2
Solomon Islands Dollar SBD 2
Somali Shilling SOS 2
Rand ZAR 2
No universal currency
South Sudanese Pound SSP 2
Euro EUR 2
Sri Lanka Rupee LKR 2
Sudanese Pound SDG 2
Surinam Dollar SRD 2
Norwegian Krone NOK 2
Lilangeni SZL 2
Swedish Krona SEK 2
Swiss Franc CHF 2
WIR Euro CHE 2
WIR Franc CHW 2
Syrian Pound SYP 2
New Taiwan Dollar TWD 2
Somoni TJS 2
Tanzanian Shilling TZS 2
Baht THB 2
US Dollar USD 2
CFA Franc BCEAO XOF 0
New Zealand Dollar NZD 2
Paanga TOP 2
Trinidad and Tobago Dollar TTD 2
Tunisian Dinar TND 3
Turkish Lira TRY 2
Turkmenistan New Manat TMT 2
US Dollar USD 2
Australian Dollar AUD 2
Uganda Shilling UGX 0
Hryvnia UAH 2
UAE Dirham AED 2
Pound Sterling GBP 2
US Dollar USD 2
US Dollar USD 2
US Dollar (Next day) USN 2
Peso Uruguayo UYU 2
Uruguay Peso en Unidades Indexadas (URUIURUI) UYI 0
Uzbekistan Sum UZS 2
Vatu VUV 0
Bolívar VEF 2
Dong VND 0
US Dollar USD 2
US Dollar USD 2
CFP Franc XPF 0
Moroccan Dirham MAD 2
Yemeni Rial YER 2
Zambian Kwacha ZMW 2
Zimbabwe Dollar ZWL 2

View file

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
namespace BTCPayServer.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<InvoiceData> Invoices
{
get; set;
}
public DbSet<RefundAddressesData> RefundAddresses
{
get; set;
}
public DbSet<PaymentData> Payments
{
get; set;
}
public DbSet<StoreData> Stores
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var options = optionsBuilder.Options.FindExtension<SqliteOptionsExtension>();
if(options?.ConnectionString == null)
optionsBuilder.UseSqlite("Data Source=temp.db");
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasKey(t => new {
t.ApplicationUserId,
t.StoreDataId
});
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)
.HasForeignKey(pt => pt.ApplicationUserId);
builder.Entity<UserStore>()
.HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId);
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class ApplicationDbContextFactory
{
string _Path;
public ApplicationDbContextFactory(string path)
{
_Path = path ?? throw new ArgumentNullException(nameof(path));
}
public ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlite("Data Source=" + _Path);
return new ApplicationDbContext(builder.Options);
}
}
}

View file

@ -0,0 +1,70 @@
using BTCPayServer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceData
{
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
public string Id
{
get; set;
}
public DateTimeOffset Created
{
get; set;
}
public List<PaymentData> Payments
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;
}
public byte[] Blob
{
get; set;
}
public string ItemCode
{
get;
set;
}
public string OrderId
{
get;
set;
}
public string Status
{
get;
set;
}
public string ExceptionStatus
{
get;
set;
}
public string CustomerEmail
{
get;
set;
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PaymentData
{
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class RefundAddressesData
{
public string Id
{
get; set;
}
public string InvoiceDataId
{
get; set;
}
public InvoiceData InvoiceData
{
get; set;
}
public byte[] Blob
{
get; set;
}
}
}

View file

@ -0,0 +1,48 @@
using BTCPayServer.Invoicing;
using BTCPayServer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class StoreData
{
public string Id
{
get;
set;
}
public List<UserStore> UserStores
{
get; set;
}
public string DerivationStrategy
{
get; set;
}
public string StoreName
{
get; set;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
public string StoreWebsite
{
get; set;
}
public byte[] StoreCertificate
{
get; set;
}
}
}

View file

@ -0,0 +1,29 @@
using BTCPayServer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class UserStore
{
public string ApplicationUserId
{
get; set;
}
public ApplicationUser ApplicationUser
{
get; set;
}
public string StoreDataId
{
get; set;
}
public StoreData StoreData
{
get; set;
}
}
}

View file

@ -0,0 +1,18 @@
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer
{
public static class Extensions
{
public static BitIdentity GetBitIdentity(this Controller controller)
{
if(!(controller.User.Identity is BitIdentity))
throw new UnauthorizedAccessException("no-bitid");
return (BitIdentity)controller.User.Identity;
}
}
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Services;
namespace BTCPayServer.Services
{
public static class EmailSenderExtensions
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
namespace Microsoft.AspNetCore.Mvc
{
public static class UrlHelperExtensions
{
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
{
return urlHelper.Action(
action: nameof(AccountController.ConfirmEmail),
controller: "Account",
values: new { userId, code },
protocol: scheme);
}
public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
{
return urlHelper.Action(
action: nameof(AccountController.ResetPassword),
controller: "Account",
values: new { userId, code },
protocol: scheme);
}
}
}

View file

@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http.Extensions;
namespace BTCPayServer
{
public interface IExternalUrlProvider
{
string GetEncodedUrl();
string GetAbsolute(string path);
}
public class DefaultExternalUrlProvider : IExternalUrlProvider
{
IHttpContextAccessor _ContextAccessor;
public DefaultExternalUrlProvider(IHttpContextAccessor contextAccessor)
{
if(contextAccessor == null)
throw new ArgumentNullException(nameof(contextAccessor));
_ContextAccessor = contextAccessor;
}
public string GetAbsolute(string path)
{
var request = _ContextAccessor.HttpContext.Request;
var builder = new UriBuilder()
{
Scheme = request.Scheme,
Host = request.Host.Host,
};
if(request.Host.Port.HasValue)
builder.Port = request.Host.Port.Value;
return builder.Uri.AbsoluteUri + path;
}
public string GetEncodedUrl()
{
var request = _ContextAccessor.HttpContext.Request;
return request.GetEncodedUrl();
}
}
public class FixedExternalUrlProvider : IExternalUrlProvider
{
string _Url;
IHttpContextAccessor _ContextAccessor;
public FixedExternalUrlProvider(Uri url, IHttpContextAccessor contextAccessor)
{
if(url == null)
throw new ArgumentNullException(nameof(url));
if(contextAccessor == null)
throw new ArgumentNullException(nameof(contextAccessor));
_ContextAccessor = contextAccessor;
_Url = url.AbsoluteUri;
}
public string GetAbsolute(string path)
{
var uri = new Uri(_Url, UriKind.Absolute);
var builder = new UriBuilder()
{
Scheme = uri.Scheme,
Host = uri.Host,
};
if(!uri.IsDefaultPort)
builder.Port = uri.Port;
return builder.Uri.AbsoluteUri + path;
}
public string GetEncodedUrl()
{
var req = _ContextAccessor.HttpContext.Request;
return BuildAbsolute(req.Path, req.QueryString); ;
}
private string BuildAbsolute(PathString path = new PathString(),
QueryString query = new QueryString(),
FragmentString fragment = new FragmentString())
{
var combinedPath = path.HasValue ? path.Value.Substring(1) : "";
var encodedQuery = query.ToString();
var encodedFragment = fragment.ToString();
// PERF: Calculate string length to allocate correct buffer size for StringBuilder.
var length = _Url.Length + combinedPath.Length + encodedQuery.Length + encodedFragment.Length;
return new StringBuilder(length)
.Append(_Url)
.Append(combinedPath)
.Append(encodedQuery)
.Append(encodedFragment)
.ToString();
}
}
}

View file

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.Extensions.Primitives;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Filters
{
public class MediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public MediaTypeConstraintAttribute(string mediaType)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
}
public string MediaType
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal);
return match.HasValue && match.Value;
}
}
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
{
IsBitpayAPI = isBitpayAPI;
}
public bool IsBitpayAPI
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any() == IsBitpayAPI;
}
}
public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint
{
public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
ExpectedValue = expectedValue;
}
public bool ExpectedValue
{
get; set;
}
public string MediaType
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
var match = context.RouteContext.HttpContext.Request.Headers["Accept"].FirstOrDefault()?.StartsWith(MediaType, StringComparison.Ordinal);
return (match.HasValue && match.Value) == ExpectedValue;
}
}
}

View file

@ -0,0 +1,92 @@
using BTCPayServer.Configuration;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Http;
using BTCPayServer.Wallet;
using BTCPayServer.RateProvider;
using NBitpayClient;
using NBitcoin;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using System.IO;
using Microsoft.Data.Sqlite;
using BTCPayServer.Invoicing;
using NBXplorer;
using BTCPayServer.Stores;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Services;
namespace BTCPayServer.Hosting
{
public static class BTCPayServerServices
{
public static IWebHostBuilder AddPayServer(this IWebHostBuilder builder, BTCPayServerOptions options)
{
return
builder
.ConfigureServices(c =>
{
c.AddDbContext<ApplicationDbContext>(o =>
{
var path = Path.Combine(options.DataDir, "sqllite.db");
o.UseSqlite("Data Source=" + path);
});
c.AddSingleton(options);
c.AddSingleton<BTCPayServerRuntime>(o =>
{
var runtime = new BTCPayServerRuntime();
runtime.Configure(options);
return runtime;
});
c.AddSingleton<Network>(options.Network);
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().TokenRepository);
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
c.AddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory);
c.AddSingleton<StoreRepository>();
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().Wallet);
c.AddSingleton<CurrencyNameTable>();
c.AddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
{
Fallback = new FeeRate(100, 1),
BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>()
});
c.AddSingleton<ExplorerClient>(o =>
{
var runtime = o.GetRequiredService<BTCPayServerRuntime>();
return runtime.Explorer;
});
c.AddSingleton<Bitpay>(o =>
{
if(options.Network == Network.Main)
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
c.TryAddSingleton<IRateProvider, BitpayRateProvider>();
c.AddSingleton<InvoiceWatcher>();
c.AddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
c.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
c.AddSingleton<IExternalUrlProvider>(o => new FixedExternalUrlProvider(options.ExternalUrl, o.GetRequiredService<IHttpContextAccessor>()));
})
.UseUrls(options.GetUrls());
}
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{
using(var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
}
app.UseMiddleware<BTCPayMiddleware>();
return app;
}
}
}

View file

@ -0,0 +1,102 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using Microsoft.AspNetCore.Http.Internal;
using System.IO;
using BTCPayServer.Authentication;
using System.Security.Principal;
using NBitpayClient.Extensions;
using BTCPayServer.Logging;
using Newtonsoft.Json;
using BTCPayServer.Models;
using BTCPayServer.Configuration;
namespace BTCPayServer.Hosting
{
public class BTCPayMiddleware
{
TokenRepository _TokenRepository;
RequestDelegate _Next;
IExternalUrlProvider _ExternalUrl;
public BTCPayMiddleware(RequestDelegate next, TokenRepository tokenRepo, IExternalUrlProvider externalUrl)
{
_ExternalUrl = externalUrl ?? throw new ArgumentNullException(nameof(externalUrl));
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task Invoke(HttpContext httpContext)
{
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
if(!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
{
httpContext.Request.EnableRewind();
string body = string.Empty;
if(httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using(StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = _ExternalUrl.GetEncodedUrl();
try
{
var key = new PubKey(id);
if(BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, new string[0]);
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
}
}
catch(FormatException) { }
if(!(httpContext.User.Identity is BitIdentity))
Logs.PayServer.LogDebug("BitId signature check failed");
}
try
{
await _Next(httpContext);
}
catch(UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
}
catch(BitpayHttpException ex)
{
await HandleBitpayHttpException(httpContext, ex);
}
catch(Exception ex)
{
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
throw;
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;
using(var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, true))
{
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
writer.Write(result);
await writer.FlushAsync();
}
}
}
}

View file

@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Hosting;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using BTCPayServer.Authentication;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using BTCPayServer.Services;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
namespace BTCPayServer.Hosting
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
//services.AddSingleton<IObjectModelValidator, NoObjectModelValidator>();
services.AddMvcCore(o =>
{
//o.Filters.Add(new NBXplorerExceptionFilter());
o.OutputFormatters.Clear();
o.InputFormatters.Clear();
})
.AddJsonFormatters()
.AddFormatterMappings();
services.AddMvc();
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
Logs.Configure(loggerFactory);
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

View file

@ -0,0 +1,245 @@
using NBitcoin;
using System.Linq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Invoicing
{
public class BuyerInformation
{
[JsonProperty(PropertyName = "buyerName")]
public string BuyerName
{
get; set;
}
[JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail
{
get; set;
}
[JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry
{
get; set;
}
[JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip
{
get; set;
}
[JsonProperty(PropertyName = "buyerState")]
public string BuyerState
{
get; set;
}
[JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity
{
get; set;
}
[JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2
{
get; set;
}
[JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1
{
get; set;
}
[JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone
{
get; set;
}
}
public class ProductInformation
{
[JsonProperty(PropertyName = "itemDesc")]
public string ItemDesc
{
get; set;
}
[JsonProperty(PropertyName = "itemCode")]
public string ItemCode
{
get; set;
}
[JsonProperty(PropertyName = "physical")]
public bool Physical
{
get; set;
}
[JsonProperty(PropertyName = "price")]
public double Price
{
get; set;
}
[JsonProperty(PropertyName = "currency")]
public string Currency
{
get; set;
}
}
public enum SpeedPolicy
{
HighSpeed = 0,
MediumSpeed = 1,
LowSpeed = 2
}
public class InvoiceEntity
{
public string Id
{
get; set;
}
public string StoreId
{
get; set;
}
public string OrderId
{
get; set;
}
public Money GetTotalCryptoDue()
{
return Calculate().TotalDue;
}
private (Money TotalDue, Money Paid) Calculate()
{
var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee;
var paid = Money.Zero;
var payments =
Payments
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
paid += _.Output.Value;
return _;
})
.TakeWhile(_ =>
{
var paidEnough = totalDue <= paid;
if(!paidEnough)
totalDue += TxFee;
return !paidEnough;
})
.ToArray();
return (totalDue, paid);
}
public Money GetTotalPaid()
{
return Calculate().Paid;
}
public Money GetCryptoDue()
{
var o = Calculate();
var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
public double Rate
{
get; set;
}
public DateTimeOffset InvoiceTime
{
get; set;
}
public DateTimeOffset ExpirationTime
{
get; set;
}
public BitcoinAddress DepositAddress
{
get; set;
}
public ProductInformation ProductInformation
{
get; set;
}
public BuyerInformation BuyerInformation
{
get; set;
}
public string PosData
{
get;
set;
}
public string DerivationStrategy
{
get;
set;
}
public string Status
{
get;
set;
}
public string ExceptionStatus
{
get; set;
}
public List<PaymentEntity> Payments
{
get; set;
}
public bool Refundable
{
get;
set;
}
public string RefundMail
{
get;
set;
}
public string RedirectURL
{
get;
set;
}
public Money TxFee
{
get;
set;
}
public bool IsExpired()
{
return DateTimeOffset.UtcNow > ExpirationTime;
}
}
public class PaymentEntity
{
public DateTimeOffset ReceivedTime
{
get; set;
}
public OutPoint Outpoint
{
get; set;
}
public TxOut Output
{
get; set;
}
}
}

View file

@ -0,0 +1,382 @@
using DBreeze;
using System;
using System.Collections.Generic;
using System.Text;
using NBitpayClient;
using Newtonsoft.Json;
using System.Linq;
using NBitcoin;
using NBitcoin.DataEncoders;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using BTCPayServer.Models;
using System.Threading.Tasks;
using BTCPayServer.Data;
using System.Globalization;
using BTCPayServer.Models.InvoicingModels;
namespace BTCPayServer.Invoicing
{
public class InvoiceRepository
{
private readonly DBreezeEngine _Engine;
public DBreezeEngine Engine
{
get
{
return _Engine;
}
}
Network _Network;
public Network Network
{
get
{
return _Network;
}
set
{
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network)
{
_Engine = engine;
_Network = network;
_ContextFactory = contextFactory;
}
public Task AddPendingInvoice(string invoiceId)
{
using(var tx = _Engine.GetTransaction())
{
tx.Insert<string, byte[]>("T-Pending", invoiceId, new byte[0]);
tx.Commit();
}
return Task.FromResult(true);
}
public Task RemovePendingInvoice(string invoiceId)
{
using(var tx = _Engine.GetTransaction())
{
tx.RemoveKey("T-Pending", invoiceId);
tx.Commit();
}
return Task.FromResult(true);
}
public string[] GetPendingInvoices()
{
List<string> pending = new List<string>();
using(var tx = _Engine.GetTransaction())
{
foreach(var row in tx.SelectForward<string, byte[]>("T-Pending"))
{
pending.Add(row.Key);
}
}
return pending.ToArray();
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{
using(var ctx = _ContextFactory.CreateContext())
{
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
if(invoiceData == null)
return;
if(invoiceData.CustomerEmail == null && data.Email != null)
{
invoiceData.CustomerEmail = data.Email;
}
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
{
invoice = Clone(invoice);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
invoice.Payments = new List<PaymentEntity>();
invoice.StoreId = storeId;
using(var context = _ContextFactory.CreateContext())
{
await context.AddAsync(new InvoiceData()
{
StoreDataId = storeId,
Id = invoice.Id,
Created = invoice.InvoiceTime,
Blob = ToBytes(invoice),
OrderId = invoice.OrderId,
Status = invoice.Status,
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
}).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);
}
AddToTextSearch(invoice.Id,
invoice.Id,
invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture),
invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture),
invoice.GetTotalCryptoDue().ToString(),
invoice.OrderId,
ToString(invoice.BuyerInformation),
ToString(invoice.ProductInformation),
invoice.StoreId
);
return invoice;
}
private string[] SearchInvoice(string searchTerms)
{
using(var tx = _Engine.GetTransaction())
{
return tx.TextSearch("InvoiceSearch").Block(searchTerms)
.GetDocumentIDs()
.Select(id => Encoders.Base58.EncodeData(id))
.ToArray();
}
}
void AddToTextSearch(string invoiceId, params string[] terms)
{
using(var tx = _Engine.GetTransaction())
{
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.Commit();
}
}
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
{
using(var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if(invoiceData == null)
return;
invoiceData.Status = status;
invoiceData.ExceptionStatus = exceptionStatus;
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<InvoiceEntity> GetInvoice(string storeId, string id)
{
using(var context = _ContextFactory.CreateContext())
{
IQueryable<InvoiceData> query =
context
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses)
.Where(i => i.Id == id);
if(storeId != null)
query = query.Where(i => i.StoreDataId == storeId);
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
if(invoice == null)
return null;
return ToEntity(invoice);
}
}
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
entity.Payments = invoice.Payments.Select(p => ToObject<PaymentEntity>(p.Blob)).ToList();
entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail;
entity.Refundable = invoice.RefundAddresses.Count != 0;
return entity;
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
using(var context = _ContextFactory.CreateContext())
{
IQueryable<InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(!string.IsNullOrEmpty(queryObject.StoreId))
{
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
}
if(!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if(ids.Count == 0)
return new InvoiceEntity[0];
query = query.Where(i => ids.Contains(i.Id));
}
if(queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if(queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if(queryObject.ItemCode != null)
query = query.Where(i => i.ItemCode == queryObject.ItemCode);
if(queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId);
if(queryObject.Status != null)
query = query.Where(i => i.Status == queryObject.Status);
query = query.OrderByDescending(q => q.Created);
if(queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if(queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
var data = await query.ToArrayAsync().ConfigureAwait(false);
return data.Select(ToEntity).ToArray();
}
}
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs)
{
if(outputs.Length == 0)
return;
outputs = outputs.Take(10).ToArray();
using(var context = _ContextFactory.CreateContext())
{
int i = 0;
foreach(var output in outputs)
{
await context.RefundAddresses.AddAsync(new RefundAddressesData()
{
Id = invoiceId + "-" + i,
InvoiceDataId = invoiceId,
Blob = ToBytes(output)
}).ConfigureAwait(false);
i++;
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray();
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
}
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin)
{
using(var context = _ContextFactory.CreateContext())
{
PaymentEntity entity = new PaymentEntity
{
Outpoint = receivedCoin.Outpoint,
Output = receivedCoin.TxOut,
ReceivedTime = DateTime.UtcNow
};
PaymentData data = new PaymentData
{
Id = receivedCoin.Outpoint.ToString(),
Blob = ToBytes(entity),
InvoiceDataId = invoiceId
};
await context.Payments.AddAsync(data).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);
return entity;
}
}
private T ToObject<T>(byte[] value)
{
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
}
private byte[] ToBytes<T>(T obj)
{
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj));
}
private T Clone<T>(T invoice)
{
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network);
}
private string ToString<T>(T data)
{
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
}
}
public class InvoiceQuery
{
public string StoreId
{
get; set;
}
public string TextSearch
{
get; set;
}
public DateTimeOffset? StartDate
{
get; set;
}
public DateTimeOffset? EndDate
{
get; set;
}
public int? Skip
{
get; set;
}
public int? Count
{
get; set;
}
public string OrderId
{
get; set;
}
public string ItemCode
{
get; set;
}
public string Status
{
get; set;
}
}
}

View file

@ -0,0 +1,270 @@
using NBXplorer;
using Microsoft.Extensions.Logging;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
namespace BTCPayServer.Invoicing
{
public class InvoiceWatcher : IHostedService
{
InvoiceRepository _InvoiceRepository;
ExplorerClient _ExplorerClient;
DerivationStrategyFactory _DerivationFactory;
public InvoiceWatcher(ExplorerClient explorerClient, InvoiceRepository invoiceRepository)
{
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
}
private async Task StartWatchInvoice(string invoiceId)
{
Logs.PayServer.LogInformation("Watching invoice " + invoiceId);
UTXOChanges changes = null;
while(true)
{
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId).ConfigureAwait(false);
if(invoice == null)
break;
var stateBefore = invoice.Status;
var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false);
changes = result.Changes;
if(result.NeedSave)
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
if(stateBefore != invoice.Status)
{
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
}
if(invoice.Status == "complete" || invoice.Status == "invalid")
{
await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false);
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break;
}
}
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
{
break;
}
catch(Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
}
}
}
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
{
if(invoice.Status == "invalid" && (invoice.Status == "new" || invoice.Status == "paidPartial"))
{
return (false, changes);
}
bool needSave = false;
bool shouldWait = true;
if(invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
invoice.Status = "invalid";
}
if(invoice.Status == "new" || invoice.Status == "paidPartial")
{
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, false, _Cts.Token).ConfigureAwait(false);
shouldWait = false; //should not wait, Sync is blocking call
List<Coin> receivedCoins = new List<Coin>();
foreach(var received in changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs))
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
invoice.Payments.Add(payment);
if(invoice.Status == "new")
{
invoice.Status = "paidPartial";
needSave = true;
}
}
}
if(invoice.Status == "paidPartial")
{
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
if(totalPaid == invoice.GetTotalCryptoDue())
{
invoice.Status = "paid";
invoice.ExceptionStatus = null;
needSave = true;
}
if(totalPaid > invoice.GetTotalCryptoDue())
{
invoice.Status = "paidOver";
invoice.ExceptionStatus = "paidOver";
needSave = true;
}
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.ExceptionStatus == null)
{
invoice.ExceptionStatus = "paidPartial";
needSave = true;
}
}
if(invoice.Status == "paid" || invoice.Status == "paidOver")
{
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
await Task.WhenAll(getTransactions).ConfigureAwait(false);
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
bool confirmed = false;
var minConf = transactions.Select(t => t.Confirmations).Min();
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
if(minConf > 0)
confirmed = true;
else
confirmed = !transactions.Any(t => t.Transaction.RBF);
}
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
confirmed = minConf >= 1;
}
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
confirmed = minConf >= 6;
}
if(confirmed)
{
invoice.Status = "confirmed";
needSave = true;
}
}
if(invoice.Status == "confirmed")
{
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
await Task.WhenAll(getTransactions).ConfigureAwait(false);
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
var minConf = transactions.Select(t => t.Confirmations).Min();
if(minConf >= 6)
{
invoice.Status = "complete";
needSave = true;
}
}
shouldWait = shouldWait && !needSave;
if(shouldWait)
{
await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false);
}
return (needSave, changes);
}
public TimeSpan PollInterval
{
get; set;
} = TimeSpan.FromSeconds(10);
public async Task WatchAsync(string invoiceId)
{
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
_WatchRequests.Add(invoiceId);
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
public void Dispose()
{
_Cts.Cancel();
}
Thread _Thread;
TaskCompletionSource<bool> _RunningTask;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
foreach(var pending in _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
_Thread.Start();
return Task.CompletedTask;
}
void Run()
{
Logs.PayServer.LogInformation("Start watching invoices");
List<Task> watching = new List<Task>();
try
{
foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
{
watching.Add(StartWatchInvoice(item));
foreach(var task in watching.ToList())
{
if(task.Status != TaskStatus.Running)
{
watching.Remove(task);
}
}
}
}
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
{
try
{
Task.WaitAll(watching.ToArray());
}
catch(AggregateException) { }
_RunningTask.TrySetResult(true);
}
catch(Exception ex)
{
_Cts.Cancel();
_RunningTask.TrySetException(ex);
Logs.PayServer.LogCritical(ex, "Error in the InvoiceWatcher loop");
}
finally
{
Logs.PayServer.LogInformation("Stop watching invoices");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

View file

@ -0,0 +1,400 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class CustomConsoleLogProvider : ILoggerProvider
{
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a,b) => true, false, _Processor);
}
public void Dispose()
{
}
}
/// <summary>
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
/// </summary>
public class CustomConsoleLogger : ILogger
{
private static readonly string _loglevelPadding = ": ";
private static readonly string _messagePadding;
private static readonly string _newLineWithMessagePadding;
// ConsoleColor does not have a value to specify the 'Default' color
private readonly ConsoleColor? DefaultConsoleColor = null;
private readonly ConsoleLoggerProcessor _queueProcessor;
private Func<string, LogLevel, bool> _filter;
[ThreadStatic]
private static StringBuilder _logBuilder;
static CustomConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Filter = filter ?? ((category, logLevel) => true);
IncludeScopes = includeScopes;
_queueProcessor = loggerProcessor;
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console = new WindowsLogConsole();
}
else
{
Console = new AnsiLogConsole(new AnsiSystemConsole());
}
}
public IConsole Console
{
get
{
return _queueProcessor.Console;
}
set
{
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
}
}
public Func<string, LogLevel, bool> Filter
{
get
{
return _filter;
}
set
{
_filter = value ?? throw new ArgumentNullException(nameof(value));
}
}
public bool IncludeScopes
{
get; set;
}
public string Name
{
get;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if(!IsEnabled(logLevel))
{
return;
}
if(formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if(!string.IsNullOrEmpty(message) || exception != null)
{
WriteMessage(logLevel, Name, eventId.Id, message, exception);
}
}
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
{
var logBuilder = _logBuilder;
_logBuilder = null;
if(logBuilder == null)
{
logBuilder = new StringBuilder();
}
var logLevelColors = default(ConsoleColors);
var logLevelString = string.Empty;
// Example:
// INFO: ConsoleApp.Program[10]
// Request received
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");
var lenAfter = logBuilder.ToString().Length;
while(lenAfter++ < 18)
logBuilder.Append(" ");
// scope information
if(IncludeScopes)
{
GetScopeInformation(logBuilder);
}
if(!string.IsNullOrEmpty(message))
{
// message
//logBuilder.Append(_messagePadding);
var len = logBuilder.Length;
logBuilder.AppendLine(message);
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
}
// Example:
// System.InvalidOperationException
// at Namespace.Class.Function() in File:line X
if(exception != null)
{
// exception message
logBuilder.AppendLine(exception.ToString());
}
if(logBuilder.Length > 0)
{
var hasLevel = !string.IsNullOrEmpty(logLevelString);
// Queue log message
_queueProcessor.EnqueueMessage(new LogMessageEntry()
{
Message = logBuilder.ToString(),
MessageColor = DefaultConsoleColor,
LevelString = hasLevel ? logLevelString : null,
LevelBackground = hasLevel ? logLevelColors.Background : null,
LevelForeground = hasLevel ? logLevelColors.Foreground : null
});
}
logBuilder.Clear();
if(logBuilder.Capacity > 1024)
{
logBuilder.Capacity = 1024;
}
_logBuilder = logBuilder;
}
public bool IsEnabled(LogLevel logLevel)
{
return Filter(Name, logLevel);
}
public IDisposable BeginScope<TState>(TState state)
{
if(state == null)
{
throw new ArgumentNullException(nameof(state));
}
return ConsoleLogScope.Push(Name, state);
}
private static string GetLogLevelString(LogLevel logLevel)
{
switch(logLevel)
{
case LogLevel.Trace:
return "trce";
case LogLevel.Debug:
return "dbug";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "fail";
case LogLevel.Critical:
return "crit";
default:
throw new ArgumentOutOfRangeException(nameof(logLevel));
}
}
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
// We must explicitly set the background color if we are setting the foreground color,
// since just setting one can look bad on the users console.
switch(logLevel)
{
case LogLevel.Critical:
return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
case LogLevel.Error:
return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red);
case LogLevel.Warning:
return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black);
case LogLevel.Information:
return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black);
case LogLevel.Debug:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
case LogLevel.Trace:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
default:
return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor);
}
}
private void GetScopeInformation(StringBuilder builder)
{
var current = ConsoleLogScope.Current;
string scopeLog = string.Empty;
var length = builder.Length;
while(current != null)
{
if(length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
{
scopeLog = $"=> {current} ";
}
builder.Insert(length, scopeLog);
current = current.Parent;
}
if(builder.Length > length)
{
builder.Insert(length, _messagePadding);
builder.AppendLine();
}
}
private struct ConsoleColors
{
public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
{
Foreground = foreground;
Background = background;
}
public ConsoleColor? Foreground
{
get;
}
public ConsoleColor? Background
{
get;
}
}
private class AnsiSystemConsole : IAnsiSystemConsole
{
public void Write(string message)
{
System.Console.Write(message);
}
public void WriteLine(string message)
{
System.Console.WriteLine(message);
}
}
}
public class ConsoleLoggerProcessor : IDisposable
{
private const int _maxQueuedMessages = 1024;
private readonly BlockingCollection<LogMessageEntry> _messageQueue = new BlockingCollection<LogMessageEntry>(_maxQueuedMessages);
private readonly Task _outputTask;
public IConsole Console;
public ConsoleLoggerProcessor()
{
// Start Console message queue processor
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
}
public virtual void EnqueueMessage(LogMessageEntry message)
{
if(!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(message);
return;
}
catch(InvalidOperationException) { }
}
// Adding is completed so just log the message
WriteMessage(message);
}
// for testing
internal virtual void WriteMessage(LogMessageEntry message)
{
if(message.LevelString != null)
{
Console.Write(message.LevelString, message.LevelBackground, message.LevelForeground);
}
Console.Write(message.Message, message.MessageColor, message.MessageColor);
Console.Flush();
}
private void ProcessLogQueue()
{
foreach(var message in _messageQueue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
private static void ProcessLogQueue(object state)
{
var consoleLogger = (ConsoleLoggerProcessor)state;
consoleLogger.ProcessLogQueue();
}
public void Dispose()
{
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(1500); // with timeout in-case Console is locked by user input
}
catch(TaskCanceledException) { }
catch(AggregateException ex) when(ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) { }
}
}
public struct LogMessageEntry
{
public string LevelString;
public ConsoleColor? LevelBackground;
public ConsoleColor? LevelForeground;
public ConsoleColor? MessageColor;
public string Message;
}
}

View file

@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class Logs
{
static Logs()
{
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
}
public static void Configure(ILoggerFactory factory)
{
Configuration = factory.CreateLogger("Configuration");
PayServer = factory.CreateLogger("PayServer");
}
public static ILogger Configuration
{
get; set;
}
public static ILogger PayServer
{
get; set;
}
public const int ColumnLength = 16;
}
public class FuncLoggerFactory : ILoggerFactory
{
private Func<string, ILogger> createLogger;
public FuncLoggerFactory(Func<string, ILogger> createLogger)
{
this.createLogger = createLogger;
}
public void AddProvider(ILoggerProvider provider)
{
}
public ILogger CreateLogger(string categoryName)
{
return createLogger(categoryName);
}
public void Dispose()
{
}
}
}

View file

@ -0,0 +1,355 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Invoicing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20170901023716_Init")]
partial class Init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,356 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class Init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Stores",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
StoreCertificate = table.Column<byte[]>(type: "BLOB", nullable: true),
StoreName = table.Column<string>(type: "TEXT", nullable: true),
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Stores", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Invoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
OrderId = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Invoices", x => x.Id);
table.ForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "UserStore",
columns: table => new
{
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserStore", x => new { x.ApplicationUserId, x.StoreDataId });
table.ForeignKey(
name: "FK_UserStore_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserStore_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Payments",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Payments", x => x.Id);
table.ForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "RefundAddresses",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RefundAddresses", x => x.Id);
table.ForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Invoices_StoreDataId",
table: "Invoices",
column: "StoreDataId");
migrationBuilder.CreateIndex(
name: "IX_Payments_InvoiceDataId",
table: "Payments",
column: "InvoiceDataId");
migrationBuilder.CreateIndex(
name: "IX_RefundAddresses_InvoiceDataId",
table: "RefundAddresses",
column: "InvoiceDataId");
migrationBuilder.CreateIndex(
name: "IX_UserStore_StoreDataId",
table: "UserStore",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "Payments");
migrationBuilder.DropTable(
name: "RefundAddresses");
migrationBuilder.DropTable(
name: "UserStore");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "Invoices");
migrationBuilder.DropTable(
name: "AspNetUsers");
migrationBuilder.DropTable(
name: "Stores");
}
}
}

View file

@ -0,0 +1,354 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Invoicing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class ExternalLoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class ForgotPasswordViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class LoginWith2faViewModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string TwoFactorCode { get; set; }
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
public bool RememberMe { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class LoginWithRecoveryCodeViewModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; }
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AccountViewModels
{
public class ResetPasswordViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string Code { get; set; }
}
}

View file

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Invoicing;
using BTCPayServer.Data;
namespace BTCPayServer.Models
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser
{
public List<UserStore> UserStores
{
get;
set;
}
}
}

View file

@ -0,0 +1,39 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Models
{
public class BitpayErrorsModel
{
public BitpayErrorsModel()
{
}
public BitpayErrorsModel(BitpayHttpException ex)
{
Error = ex.Message;
}
[JsonProperty("errors", DefaultValueHandling = DefaultValueHandling.Ignore)]
public BitpayErrorModel[] Errors
{
get; set;
}
[JsonProperty("error", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Error
{
get; set;
}
}
public class BitpayErrorModel
{
[JsonProperty("error")]
public string Error
{
get; set;
}
}
}

View file

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Models
{
public class DataWrapper
{
public static DataWrapper<T> Create<T>(T obj)
{
return new DataWrapper<T>(obj);
}
}
public class DataWrapper<T>
{
public DataWrapper()
{
}
public DataWrapper(T data)
{
Data = data;
}
[JsonProperty("facade", NullValueHandling = NullValueHandling.Ignore)]
public string Facade
{
get; set;
}
[JsonProperty("data")]
public T Data
{
get; set;
}
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace BTCPayServer.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

View file

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using NBitcoin.DataEncoders;
using Microsoft.AspNetCore.Http;
using System.IO;
namespace BTCPayServer.Models
{
//{"data":[{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"},{"pos/invoice":"H1pwwh2tMeSCri9rh5VvHWEHokGdf2EGtghfZkUEbeZv"},{"merchant":"89zEBr9orAc6wgybAABp8ioGcjYeFrUaZgMzjxNuqYty"},{"merchant/invoice":"8e7ijDxGfJsWXWgJuKXjjNgxnX1xpsBM8cTZCFnU7ehj"}]}
public class GetTokensResponse : IActionResult
{
BitTokenEntity[] _Tokens;
public GetTokensResponse(BitTokenEntity[] tokens)
{
if(tokens == null)
throw new ArgumentNullException(nameof(tokens));
this._Tokens = tokens;
}
[JsonProperty(PropertyName = "data")]
//{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"}
public JArray Data
{
get; set;
}
public async Task ExecuteResultAsync(ActionContext context)
{
JObject jobj = new JObject();
JArray jarray = new JArray();
jobj.Add("data", jarray);
foreach(var token in _Tokens)
{
JObject item = new JObject();
jarray.Add(item);
JProperty jProp = new JProperty(token.Name);
item.Add(jProp);
jProp.Value = token.Value;
}
context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json"));
var str = JsonConvert.SerializeObject(jobj);
using(var writer = new StreamWriter(context.HttpContext.Response.Body, Encoding.UTF8, 1024 * 10, true))
{
await writer.WriteLineAsync(str);
}
}
}
}

View file

@ -0,0 +1,225 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Models
{
class DateTimeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTimeOffset);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var v = (long)reader.Value;
Check(v);
return unixRef + TimeSpan.FromMilliseconds((long)v);
}
static DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var date = ((DateTimeOffset)value).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds;
Check(v);
writer.WriteValue(v);
}
private static void Check(long v)
{
if(v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
}
}
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
public string Url
{
get; set;
}
//"posData":"posData"
[JsonProperty("posData")]
public string PosData
{
get; set;
}
//status":"new"
[JsonProperty("status")]
public string Status
{
get; set;
}
//"btcPrice":"0.001157"
[JsonProperty("btcPrice")]
public string BTCPrice
{
get; set;
}
//"btcDue":"0.001160"
[JsonProperty("btcDue")]
public string BTCDue
{
get; set;
}
//"price":5
[JsonProperty("price")]
public double Price
{
get; set;
}
//"currency":"USD"
[JsonProperty("currency")]
public string Currency
{
get; set;
}
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
public Dictionary<string, double> ExRates
{
get; set;
}
//"buyerTotalBtcAmount":"0.001160"
[JsonProperty("buyerTotalBtcAmount")]
public string BuyerTotalBtcAmount
{
get; set;
}
//"itemDesc":"Some description"
[JsonProperty("itemDesc")]
public string ItemDesc
{
get; set;
}
//"orderId":"orderId"
[JsonProperty("orderId")]
public string OrderId
{
get; set;
}
//"guid":"e238ce2a-06da-47e9-aefd-2588d4aa5f8d"
[JsonProperty("guid")]
public string Guid
{
get; set;
}
//"id":"9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("id")]
public string Id
{
get; set;
}
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("invoiceTime")]
public DateTimeOffset InvoiceTime
{
get; set;
}
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("expirationTime")]
public DateTimeOffset ExpirationTime
{
get; set;
}
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty("currentTime")]
public DateTimeOffset CurrentTime
{
get; set;
}
//"lowFeeDetected":false
[JsonProperty("lowFeeDetected")]
public bool LowFeeDetected
{
get; set;
}
//"btcPaid":"0.000000"
[JsonProperty("btcPaid")]
public string BTCPaid
{
get; set;
}
//"rate":4320.02
[JsonProperty("rate")]
public double Rate
{
get; set;
}
//"exceptionStatus":false
//Can be `paidPartial`, `paidOver`, or false
[JsonProperty("exceptionStatus")]
public JToken ExceptionStatus
{
get; set;
}
//"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"}
[JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
}
//"refundAddressRequestPending":false
[JsonProperty("refundAddressRequestPending")]
public bool RefundAddressRequestPending
{
get; set;
}
//"buyerPaidBtcMinerFee":"0.000003"
[JsonProperty("buyerPaidBtcMinerFee")]
public string BuyerPaidBtcMinerFee
{
get; set;
}
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
[JsonProperty("bitcoinAddress")]
public string BitcoinAddress
{
get; set;
}
//"token":"9jF3TU7A8inKHDRQXFrKcRnMkLXWGQ2yKf7pnjMKGHEfpwTNV35HytrD9FXDBy25Li"
[JsonProperty("token")]
public string Token
{
get; set;
}
[JsonProperty("flags")]
public Flags Flags
{
get; set;
}
}
public class Flags
{
[JsonProperty("refundable")]
public bool Refundable
{
get; set;
}
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels
{
public class CreateInvoiceModel
{
[Required]
public double? Amount
{
get; set;
}
public string OrderId
{
get; set;
}
public string ItemDesc
{
get; set;
}
public string PosData
{
get; set;
}
[EmailAddress]
public string BuyerEmail
{
get; set;
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels
{
public class InvoicesModel
{
public int Skip
{
get; set;
}
public int Count
{
get; set;
}
public string SearchTerm
{
get; set;
}
public List<InvoiceModel> Invoices
{
get; set;
} = new List<InvoiceModel>();
public string StatusMessage
{
get;
set;
}
}
public class InvoiceModel
{
public DateTimeOffset Date
{
get; set;
}
public string InvoiceId
{
get; set;
}
public string Status
{
get; set;
}
public string AmountCurrency
{
get; set;
}
public string StatusMessage
{
get; set;
}
}
}

View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels
{
public class PaymentModel
{
public string InvoiceId
{
get; set;
}
public string OrderId
{
get; set;
}
public string BTCAddress
{
get; set;
}
public string BTCDue
{
get; set;
}
public string CustomerEmail
{
get; set;
}
public int ExpirationSeconds
{
get; set;
}
public int MaxTimeSeconds
{
get; set;
}
public string TimeLeft
{
get; set;
}
public string RedirectUrl
{
get; set;
}
public string StoreName
{
get; set;
}
public string ItemDesc
{
get; set;
}
public string Rate
{
get; set;
}
public string BTCAmount
{
get; set;
}
public string TxFees
{
get; set;
}
public string InvoiceBitcoinUrl
{
get;
internal set;
}
public string BTCTotalDue
{
get;
set;
}
public int TxCount
{
get; set;
}
public string StoreEmail
{
get; set;
}
public string Status
{
get;
set;
}
}
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels
{
public class UpdateCustomerModel
{
[EmailAddress]
[Required]
public string Email
{
get; set;
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class ChangePasswordViewModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string StatusMessage { get; set; }
}
}

View file

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class EnableAuthenticatorViewModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
[ReadOnly(true)]
public string SharedKey { get; set; }
public string AuthenticatorUri { get; set; }
}
}

View file

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
namespace BTCPayServer.Models.ManageViewModels
{
public class ExternalLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationScheme> OtherLogins { get; set; }
public bool ShowRemoveButton { get; set; }
public string StatusMessage { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class GenerateRecoveryCodesViewModel
{
public string[] RecoveryCodes { get; set; }
}
}

View file

@ -0,0 +1,53 @@
using BTCPayServer.Invoicing;
using BTCPayServer.Validations;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class IndexViewModel
{
public string Username { get; set; }
public bool IsEmailConfirmed { get; set; }
[Required]
[EmailAddress]
[MaxLength(50)]
public string Email { get; set; }
[ExtPubKeyValidator]
public string ExtPubKey { get; set; }
[Display(Name = "Store Name")]
[MaxLength(50)]
public string StoreName
{
get; set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
public SpeedPolicy SpeedPolicy
{
get; set;
}
[Phone]
[Display(Name = "Phone number")]
[MaxLength(50)]
public string PhoneNumber { get; set; }
public string StatusMessage { get; set; }
[Url]
[Display(Name = "Store Website")]
public string StoreWebsite
{
get;
set;
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class PairingModel
{
public string Id
{
get; set;
}
public string Label
{
get; set;
}
public string Facade
{
get; set;
}
public string SIN
{
get; set;
}
}
}

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class RemoveLoginViewModel
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
}
}

View file

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class SetPasswordViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string StatusMessage { get; set; }
}
}

View file

@ -0,0 +1,60 @@
using BTCPayServer.Validations;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class AddTokenViewModel
{
[PubKeyValidatorAttribute]
public string PublicKey
{
get; set;
}
[Required]
public string Label
{
get; set;
}
[Required]
public string Facade
{
get; set;
}
}
public class TokenViewModel
{
public string Id
{
get; set;
}
public string Label
{
get; set;
}
public string SIN
{
get; set;
}
public string Facade
{
get; set;
}
}
public class TokensViewModel
{
public TokenViewModel[] Tokens
{
get; set;
}
public string StatusMessage
{
get;
set;
}
}
}

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ManageViewModels
{
public class TwoFactorAuthenticationViewModel
{
public bool HasAuthenticator { get; set; }
public int RecoveryCodesLeft { get; set; }
public bool Is2faEnabled { get; set; }
}
}

View file

@ -0,0 +1,82 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
namespace BTCPayServer.Models
{
public class PairingCodeRequest
{
[JsonProperty(PropertyName = "id")]
public string Id
{
get; set;
}
[JsonProperty(PropertyName = "guid")]
public string Guid
{
get; set;
}
[JsonProperty(PropertyName = "facade")]
public string Facade
{
get; set;
}
[JsonProperty(PropertyName = "count")]
public int Count
{
get; set;
}
[JsonProperty(PropertyName = "label")]
public string Label
{
get; set;
}
}
public class PairingCodeResponse
{
[JsonProperty(PropertyName = "pairingCode")]
public string PairingCode
{
get; set;
}
[JsonProperty(PropertyName = "pairingExpiration")]
[JsonConverter(typeof(DateTimeJsonConverter))]
public DateTimeOffset PairingExpiration
{
get; set;
}
[JsonProperty(PropertyName = "dateCreated")]
[JsonConverter(typeof(DateTimeJsonConverter))]
public DateTimeOffset DateCreated
{
get; set;
}
[JsonProperty(PropertyName = "facade")]
public string Facade
{
get;
set;
}
[JsonProperty(PropertyName = "token")]
public string Token
{
get;
set;
}
[JsonProperty(PropertyName = "label")]
public string Label
{
get;
set;
}
}
}

88
BTCPayServer/Program.cs Normal file
View file

@ -0,0 +1,88 @@
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using System;
using BTCPayServer.Hosting;
using NBitcoin;
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.InteropServices;
using System.Linq;
using System.Diagnostics;
using System.IO;
using System.Net;
namespace BTCPayServer
{
class Program
{
static void Main(string[] args)
{
ServicePointManager.DefaultConnectionLimit = 100;
IWebHost host = null;
try
{
var conf = new BTCPayServerOptions();
conf.LoadArgs(new TextFileConfiguration(args));
host = new WebHostBuilder()
.AddPayServer(conf)
.UseKestrel()
.UseIISIntegration()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureServices(services =>
{
services.AddLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider());
});
})
.UseStartup<Startup>()
.Build();
var running = host.RunAsync();
OpenBrowser(conf.GetUrls().Select(url => url.Replace("0.0.0.0", "127.0.0.1")).First());
running.GetAwaiter().GetResult();
}
catch(ConfigurationException ex)
{
if(!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message);
}
catch(Exception exception)
{
Logs.PayServer.LogError("Exception thrown while running the server");
Logs.PayServer.LogError(exception.ToString());
}
finally
{
if(host != null)
host.Dispose();
}
}
public static void OpenBrowser(string url)
{
try
{
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); // Works ok on windows
}
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url); // Works ok on linux
}
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url); // Not tested
}
else
{
}
}
catch { }
}
}
}

View file

@ -0,0 +1,36 @@
using NBitpayClient;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.RateProvider
{
public class BitpayRateProvider : IRateProvider
{
Bitpay _Bitpay;
public BitpayRateProvider(Bitpay bitpay)
{
if(bitpay == null)
throw new ArgumentNullException(nameof(bitpay));
_Bitpay = bitpay;
}
public async Task<decimal> GetRateAsync(string currency)
{
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
var rate = rates.GetRate(currency);
if(rate == 0m)
throw new RateUnavailableException(currency);
return (decimal)rate;
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
.ToList();
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Text;
namespace BTCPayServer.RateProvider
{
public class CurrencyData
{
public string Name
{
get;
internal set;
}
public string Code
{
get;
internal set;
}
public int Divisibility
{
get;
internal set;
}
public string Symbol
{
get;
internal set;
}
}
public class CurrencyNameTable
{
public CurrencyNameTable()
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
}
Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt");
string content = null;
using(var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>();
foreach(var currency in currencies)
{
var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
if(splitted.Length < 3)
continue;
CurrencyData info = new CurrencyData();
info.Name = splitted[0];
info.Code = splitted[1];
int divisibility;
if(!int.TryParse(splitted[2], out divisibility))
continue;
info.Divisibility = divisibility;
if(!dico.ContainsKey(info.Code))
dico.Add(info.Code, info);
if(splitted.Length >= 4)
{
info.Symbol = splitted[3];
}
}
return dico.Values.ToArray();
}
public CurrencyData GetCurrencyData(string currency)
{
CurrencyData result;
_Currencies.TryGetValue(currency.ToUpperInvariant(), out result);
return result;
}
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.RateProvider
{
public class Rate
{
public Rate()
{
}
public Rate(string currency, decimal value)
{
Value = value;
Currency = currency;
}
public string Currency
{
get; set;
}
public decimal Value
{
get; set;
}
}
public interface IRateProvider
{
Task<decimal> GetRateAsync(string currency);
Task<ICollection<Rate>> GetRatesAsync();
}
}

View file

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.RateProvider
{
public class MockRateProvider : IRateProvider
{
List<Rate> _Rates;
public MockRateProvider(params Rate[] rates)
{
_Rates = new List<Rate>(rates);
}
public MockRateProvider(List<Rate> rates)
{
_Rates = rates;
}
public Task<decimal> GetRateAsync(string currency)
{
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
if(rate == null)
throw new RateUnavailableException(currency);
return Task.FromResult(rate.Value);
}
public Task<ICollection<Rate>> GetRatesAsync()
{
ICollection<Rate> rates = _Rates;
return Task.FromResult(rates);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.RateProvider
{
public class RateUnavailableException : Exception
{
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
{
if(currency == null)
throw new ArgumentNullException(nameof(currency));
Currency = currency;
}
public string Currency
{
get; set;
}
}
}

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
// This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string message)
{
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
}
}

View file

@ -0,0 +1,60 @@
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IFeeProvider
{
Task<FeeRate> GetFeeRateAsync();
}
public class NBXplorerFeeProvider : IFeeProvider
{
public ExplorerClient ExplorerClient
{
get; set;
}
public FeeRate Fallback
{
get; set;
}
public int BlockTarget
{
get; set;
}
public async Task<FeeRate> GetFeeRateAsync()
{
try
{
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
}
catch(NBXplorerException ex) when( ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
{
return Fallback;
}
}
}
public class FixedFeeProvider : IFeeProvider
{
public FixedFeeProvider(FeeRate feeRate)
{
FeeRate = feeRate;
}
public FeeRate FeeRate
{
get; set;
}
public Task<FeeRate> GetFeeRateAsync()
{
return Task.FromResult(FeeRate);
}
}
}

View file

@ -0,0 +1,70 @@
using BTCPayServer.Data;
using BTCPayServer.Models;
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Stores
{
public class StoreRepository
{
private ApplicationDbContextFactory _ContextFactory;
public StoreRepository(ApplicationDbContextFactory contextFactory)
{
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
}
public async Task<StoreData> FindStore(string storeId)
{
using(var ctx = _ContextFactory.CreateContext())
{
return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
}
}
public async Task<StoreData> CreateStore(string userId)
{
using(var ctx = _ContextFactory.CreateContext())
{
StoreData store = new StoreData
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
};
var userStore = new UserStore
{
StoreDataId = store.Id,
ApplicationUserId = userId
};
await ctx.AddAsync(store).ConfigureAwait(false);
await ctx.AddAsync(userStore).ConfigureAwait(false);
await ctx.SaveChangesAsync().ConfigureAwait(false);
return store;
}
}
public async Task<StoreData> GetStore(string userId)
{
using(var ctx = _ContextFactory.CreateContext())
{
return await ctx
.Stores
.Where(s => s.UserStores.Any(us => us.ApplicationUserId == userId))
.FirstOrDefaultAsync().ConfigureAwait(false);
}
}
public async Task UpdateStore(StoreData store)
{
using(var ctx = _ContextFactory.CreateContext())
{
var existing = await ctx.FindAsync<StoreData>(store.Id);
ctx.Entry(existing).CurrentValues.SetValues(store);
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
}
}
}

View file

@ -0,0 +1,31 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
{
public class ExtPubKeyValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value == null)
{
return ValidationResult.Success;
}
var network = (Network)validationContext.GetService(typeof(Network));
if(network == null)
return new ValidationResult("No Network specified");
try
{
new BitcoinExtPubKey((string)value, network);
return ValidationResult.Success;
}
catch(Exception ex)
{
return new ValidationResult(ex.Message);
}
}
}
}

View file

@ -0,0 +1,28 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
{
public class PubKeyValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value == null)
{
return ValidationResult.Success;
}
try
{
new PubKey((string)value);
return ValidationResult.Success;
}
catch(Exception ex)
{
return new ValidationResult(ex.Message);
}
}
}
}

View file

@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Access denied";
}
<header>
<h2 class="text-danger">ViewData["Title"]</h2>
<p class="text-danger">You do not have access to this resource.</p>
</header>

View file

@ -0,0 +1,10 @@
@{
ViewData["Title"] = "Confirm email";
}
<h2>@ViewData["Title"]</h2>
<div>
<p>
Thank you for confirming your email.
</p>
</div>

View file

@ -0,0 +1,32 @@
@model ExternalLoginViewModel
@{
ViewData["Title"] = "Register";
}
<h2>@ViewData["Title"]</h2>
<h4>Associate your @ViewData["LoginProvider"] account.</h4>
<hr />
<p class="text-info">
You've successfully authenticated with <strong>@ViewData["LoginProvider"]</strong>.
Please enter an email address for this site below and click the Register button to finish
logging in.
</p>
<div class="row">
<div class="col-md-4">
<form asp-action="ExternalLoginConfirmation" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View file

@ -0,0 +1,25 @@
@model ForgotPasswordViewModel
@{
ViewData["Title"] = "Forgot your password?";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View file

@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Forgot password confirmation";
}
<h2>@ViewData["Title"]</h2>
<p>
Please check your email to reset your password.
</p>

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