Compare commits
248 Commits
Author | SHA1 | Date | |
---|---|---|---|
030cb09af8 | |||
9f734349da | |||
2d5a861df0 | |||
061f428a54 | |||
9a539fd350 | |||
4149fe10e9 | |||
1014083160 | |||
dfa3167c18 | |||
7e09efb9a3 | |||
fb736c0d0f | |||
e4a7263e9b | |||
c52926f2b0 | |||
b6138b36be | |||
04bce3ae00 | |||
68ca162dd3 | |||
100bb02cd5 | |||
856249d52c | |||
309d6fdfe0 | |||
f289420364 | |||
f05e85de5f | |||
4138849546 | |||
7052e4e1dc | |||
ffb3e4f1fb | |||
297834be66 | |||
5924f1730c | |||
adc6bea4dc | |||
ef431f688f | |||
c8923af573 | |||
3d33ecf397 | |||
82d38da18e | |||
200e259b82 | |||
2d1f4e5e0a | |||
7fe64612ad | |||
5e452a679e | |||
10be8aec82 | |||
0e1a1fd2cd | |||
3f07010de8 | |||
2e45c8b190 | |||
b4f4401cdc | |||
a6b92a0dd5 | |||
2f3238c65e | |||
65f5a38b4a | |||
271cbf682f | |||
a634593903 | |||
af94de93d1 | |||
35f669aa15 | |||
4795bd8108 | |||
29aed99fd1 | |||
aa4519ac30 | |||
752133b01c | |||
fe0c21ba08 | |||
90904a6b5e | |||
8e3f7ea68d | |||
a239104a28 | |||
3bc232e1da | |||
a1ee09cd85 | |||
b898cc030c | |||
0602353dd2 | |||
9d406923ae | |||
aa8565e3cc | |||
5de330b1f9 | |||
66597aed46 | |||
3071826f06 | |||
c3684eb064 | |||
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
770bed54d1 | |||
774817d4ac | |||
b8068b2ae8 | |||
3007a6bbc8 | |||
1c0c8fece2 | |||
c52eee47f0 | |||
f88c98b9d9 | |||
b0e9e10f7e | |||
39d47e33f6 | |||
4b7b6c6327 | |||
a59edc5e8c | |||
5ba322ea6a | |||
b47b4b10cb | |||
c88f391935 | |||
26d3178e93 | |||
1ad27c7827 | |||
4200a8eed5 | |||
daceb7af8e | |||
f703b53bce | |||
2762224f0f | |||
86fc64d184 | |||
726cd6fd49 | |||
be1c4666e0 | |||
b75dfc4191 | |||
97815f8daf | |||
af16e1db69 | |||
5f6913b3a2 | |||
2b31af80cb | |||
3f9889d374 | |||
f8189c64a4 | |||
c9b5f89f17 | |||
ecb82f2cc9 | |||
f340c6eb7f | |||
ba0e080816 | |||
bb3d107309 | |||
8517b222bf | |||
aed32204b5 | |||
6b4eeff3f1 | |||
e3cc589ebb | |||
4a152e8ffc | |||
d54a9474d1 | |||
98472211fc | |||
099c9fa1f9 | |||
5226b77ffc | |||
290779ee39 | |||
4f39a8060c | |||
92caa98dfb | |||
df7bb9e2f8 | |||
02a039d695 | |||
b5e4c803aa | |||
2b7c70622f | |||
88779e7129 | |||
6beb7abfd2 | |||
a1ebedc0d1 | |||
d5ad0cdb39 | |||
39fb8dbb6a | |||
58194cb060 | |||
ef165e15bf | |||
bf871c46ec | |||
52331e057f | |||
b59021a0be | |||
8596e16feb | |||
223558c01d | |||
3a91965187 | |||
55d50af39d | |||
3ff293ab7f | |||
7bcf2b5472 | |||
983f34814f | |||
a33e20b46b | |||
bafdcb04ed | |||
cb4468d3b3 | |||
de6f0008a6 | |||
7618eacef1 | |||
842e083ebe | |||
1c510df3fc | |||
c80ffe396e | |||
401a31e5c2 | |||
2df60bd121 | |||
6d10c8a6c1 | |||
44898b5e23 | |||
c0f53db561 | |||
133fb96d28 | |||
98b7ad62af | |||
3069fe0dd9 | |||
729555b96f | |||
b4040ba7ad | |||
863752a471 | |||
6ae9d13c43 | |||
0c735f4e29 | |||
76d50b018b | |||
31672a2587 | |||
a048494f34 | |||
c513d6bd44 | |||
c3d37b1f78 | |||
5910644cda | |||
a16cd3e287 | |||
e3a0122eb3 | |||
1cda0eff16 | |||
6003aa4236 | |||
8753dd15de | |||
6ae6335c6d | |||
e3a1eed8b3 | |||
eb44203475 | |||
80e878c2f5 | |||
6cb1649fc2 | |||
63fceed5f4 | |||
781b2885cc | |||
2f9afda0ab | |||
108146ca92 | |||
bb46294a6d | |||
f8aad6ac80 | |||
658d1f1df0 | |||
ee3144f34a | |||
9a34fe46fb | |||
766d96c02d | |||
7445c89773 | |||
28ac5608a5 | |||
44c925a4ba | |||
51cd89f177 | |||
ab188ad54f | |||
513835ed0f | |||
a863812f90 | |||
a37fdde214 | |||
d5ef36fe50 | |||
7430ceb23d | |||
395b550c21 | |||
774565b121 | |||
72d1344002 | |||
b4ee5dcb0d | |||
a0f0ff0bf1 | |||
db2cc8f951 | |||
24007f1515 | |||
3d7445f359 | |||
34760afe77 | |||
417209b057 | |||
9026378b86 | |||
9b3dca1683 | |||
cde593a935 | |||
f0755260a6 | |||
582e1eb4f8 | |||
aaadda3e0f | |||
9d7f5b5b6e | |||
99040597dc | |||
d9794216dd | |||
84bb6056d3 | |||
dfed2daa8e |
.gitignore
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csChargeTester.csDockerfileEclairTester.cs
Logging
README.mdServerTester.csTestAccount.csUnitTest1.csUnitTestPeusa.csdocker-compose.ymldocker-litecoin-cli.ps1BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojBTCPayServer.rulesetPaymentMethodExtensions.csPaymentMethodId.csPaymentTypes.csProgram.csbundleconfig.json
DockerfileLICENSEREADME.mdbtcpayserver.slnglobal.jsonConfiguration
Controllers
AccountController.csCallbackController.csInvoiceController.API.csInvoiceController.PaymentProtocol.csInvoiceController.UI.csInvoiceController.csManageController.csRateController.csStoresController.BTCLike.csStoresController.LightningLike.csStoresController.cs
Data
AddressInvoiceData.csApplicationDbContext.csHistoricalAddressInvoiceData.csInvoiceData.csInvoiceEventData.csStoreData.cs
DerivationStrategy.csEventAggregator.csEvents
InvoiceDataChangedEvent.csInvoiceEvent.csInvoiceIPNEvent.csInvoiceNeedUpdateEvent.csInvoiceNewAddressEvent.csInvoiceStopWatchedEvent.csNBXplorerStateChangedEvent.csNewBlockEvent.cs
ExplorerClientProvider.csExtensions.csHostedServices
Hosting
JsonConverters
Logging
Migrations
20171012020112_PendingInvoices.cs20171221054550_AltcoinSupport.Designer.cs20171221054550_AltcoinSupport.cs20180106095215_DerivationStrategies.Designer.cs20180106095215_DerivationStrategies.cs20180109021122_defaultcrypto.Designer.cs20180109021122_defaultcrypto.cs20180114123253_events.Designer.cs20180114123253_events.csApplicationDbContextModelSnapshot.cs
Models
MultiValueDictionary.csNBXplorerWaiter.csPayments
Bitcoin
BitcoinLikeOnChainPaymentMethod.csBitcoinLikePaymentData.csBitcoinLikePaymentHandler.csNBXplorerListener.cs
IPaymentMethodDetails.csIPaymentMethodHandler.csISupportedPaymentMethod.csLightning
CLightning
ChargeListener.csEclair
LightMoney.csLightningLikePaymentData.csLightningLikePaymentHandler.csLightningLikePaymentMethodDetails.csLightningSupportedPaymentMethod.csProperties
SearchString.csServices
BTCPayServerEnvironment.cs
Fees
HardwareWalletService.csInvoices
InvoiceEntity.csInvoiceNotificationManager.csInvoiceRepository.csInvoiceWatcher.csPaymentMethodDictionary.cs
Mails
Rates
CachedDefaultRateProviderFactory.csCachedRateProvider.csCoinAverageRateProvider.csIRateProviderFactory.csMockRateProvider.csTweakRateProvider.cs
Stores
Wallets
Views
Home
Invoice
Shared
Stores
wwwroot
bundles
css
img
imlegacy
js
vendor
animatecss
bootstrap/css
clipboard.js
jquery-validate-unobtrusive
jquery-validate
4
.gitignore
vendored
4
.gitignore
vendored
@ -287,3 +287,7 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
/BTCPayServer/Build/dockerfiles
|
||||
|
||||
# Bundling JS/CSS
|
||||
BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
@ -4,10 +4,11 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
@ -43,21 +43,15 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Uri LTCNBXplorerUri { get; set; }
|
||||
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ExtKey HDPrivateKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
@ -73,27 +67,44 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
||||
|
||||
HDPrivateKey = new ExtKey();
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"regtest=1");
|
||||
config.AppendLine($"{chain.ToLowerInvariant()}=1");
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"explorer.cookiefile={CookieFile}");
|
||||
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
|
||||
config.AppendLine($"chains=btc,ltc");
|
||||
|
||||
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"btc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"internallightningnode={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
|
||||
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
@ -107,12 +118,6 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
|
||||
var waiter = ((NBXplorerWaiterAccessor)_Host.Services.GetService(typeof(NBXplorerWaiterAccessor))).Instance;
|
||||
while(waiter.State != NBXplorerState.Ready)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -121,6 +126,7 @@ namespace BTCPayServer.Tests
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
|
23
BTCPayServer.Tests/ChargeTester.cs
Normal file
23
BTCPayServer.Tests/ChargeTester.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChargeTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public ChargeTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = new ChargeClient(new Uri(url), network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.0-sdk
|
||||
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
@ -2,17 +2,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Eclair;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class EclairTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
|
||||
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
|
||||
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
@ -33,5 +34,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
return GetNodeInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,11 @@ namespace BTCPayServer.Tests.Logging
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if (msg != null)
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
try
|
||||
{
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
|
@ -1,12 +1,14 @@
|
||||
# How to run the tests
|
||||
# How to be started for development
|
||||
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
|
||||
|
||||
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
docker-compose up dev
|
||||
```
|
||||
|
||||
You can run the tests while it is running through your favorite IDE, or with
|
||||
@ -43,4 +45,16 @@ docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitc
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
```
|
||||
|
||||
For sending to litecoin, use .\docker-litecoin-cli.ps1 instead.
|
||||
|
||||
## FAQ
|
||||
|
||||
`docker-compose up dev` failed or tests are not passing, what should I do?
|
||||
|
||||
1. Run `docker-compose down --v` (this will reset your test environment)
|
||||
2. Run `docker-compose pull` (this will ensure you have the lastest images)
|
||||
3. Run again with `docker-compose up dev`
|
||||
|
||||
If you still have issues, try to restart docker.
|
||||
|
@ -17,7 +17,8 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Eclair;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -47,20 +48,27 @@ namespace BTCPayServer.Tests
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
|
||||
FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true"));
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
|
||||
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
IntegratedLightning = MerchantCharge.Client.Uri
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.Start();
|
||||
|
||||
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
|
||||
}
|
||||
|
||||
|
||||
@ -77,21 +85,62 @@ namespace BTCPayServer.Tests
|
||||
// Activate segwit
|
||||
var blockCount = ExplorerNode.GetBlockCountAsync();
|
||||
// Fetch node info, but that in cache
|
||||
var merchant = MerchantEclair.GetNodeInfoAsync();
|
||||
var merchantInfo = MerchantCharge.Client.GetInfoAsync();
|
||||
var customer = CustomerEclair.GetNodeInfoAsync();
|
||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
|
||||
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
|
||||
|
||||
// Mine until segwit is activated
|
||||
if (blockCount.Result <= 432)
|
||||
var info = await merchantInfo;
|
||||
var clightning = new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port);
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(clightning);
|
||||
await Task.WhenAll(blockCount, customer, channels, connect);
|
||||
|
||||
// If the channel is not created, let's do it
|
||||
if (channels.Result.Length == 0)
|
||||
{
|
||||
ExplorerNode.Generate(433 - blockCount.Result);
|
||||
var c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
bool generated = false;
|
||||
bool createdChannel = false;
|
||||
CancellationTokenSource timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
while (c.Length == 0 || c[0].State != "NORMAL")
|
||||
{
|
||||
if (timeout.IsCancellationRequested)
|
||||
{
|
||||
timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
createdChannel = c.Length == 0;
|
||||
generated = false;
|
||||
}
|
||||
if (!createdChannel)
|
||||
{
|
||||
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
|
||||
createdChannel = true;
|
||||
}
|
||||
if (!generated && c.Length != 0 && c[0].State == "WAIT_FOR_FUNDING_CONFIRMED")
|
||||
{
|
||||
ExplorerNode.Generate(6);
|
||||
generated = true;
|
||||
}
|
||||
c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task SendLightningPaymentAsync(Invoice invoice)
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerEclair.RPC.SendAsync(bolt11);
|
||||
}
|
||||
|
||||
public EclairTester MerchantEclair { get; set; }
|
||||
public EclairTester CustomerEclair { get; set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
@ -104,20 +153,22 @@ namespace BTCPayServer.Tests
|
||||
return new TestAccount(this);
|
||||
}
|
||||
|
||||
public bool FakeCallback
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public RPCClient LTCExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ExplorerClient LTCExplorerClient { get; set; }
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
@ -213,55 +264,12 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost.
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
public void SimulateCallback(BitcoinAddress address = null)
|
||||
{
|
||||
if (!FakeCallback) //The callback of NBXplorer should work
|
||||
return;
|
||||
|
||||
var req = new MockHttpRequest(PayTester.ServerUri);
|
||||
var controller = PayTester.GetController<CallbackController>();
|
||||
if (address != null)
|
||||
{
|
||||
|
||||
var match = new TransactionMatch();
|
||||
match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey });
|
||||
var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json");
|
||||
var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult();
|
||||
|
||||
HttpRequestMessage message = new HttpRequestMessage();
|
||||
message.Method = HttpMethod.Post;
|
||||
message.RequestUri = uri;
|
||||
message.Content = content;
|
||||
|
||||
_Http.SendAsync(message).GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult();
|
||||
HttpRequestMessage message = new HttpRequestMessage();
|
||||
message.Method = HttpMethod.Post;
|
||||
message.RequestUri = uri;
|
||||
_Http.SendAsync(message).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Network.RegTest;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (PayTester != null)
|
||||
|
@ -50,21 +50,40 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
return CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<StoresController> CreateStoreAsync()
|
||||
{
|
||||
ExtKey = new ExtKey().GetWif(parent.Network);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
await store.UpdateStore(StoreId, new StoreViewModel()
|
||||
{
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
SpeedPolicy = SpeedPolicy.MediumSpeed
|
||||
}, "Save");
|
||||
return store;
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
});
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
private async Task RegisterAsync()
|
||||
@ -92,5 +111,20 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode)
|
||||
{
|
||||
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,15 @@ using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Eclair;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -34,37 +42,196 @@ namespace BTCPayServer.Tests
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue2()
|
||||
{
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest);
|
||||
#pragma warning disable CS0618
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 };
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Rate = 10513.44m,
|
||||
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
TxFee = Money.Coins(0.00000100m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Rate = 216.79m
|
||||
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
TxFee = Money.Coins(0.00010000m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
invoiceEntity.SetPaymentMethods(paymentMethods);
|
||||
|
||||
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
var accounting = btc.Calculate();
|
||||
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||
}));
|
||||
accounting = btc.Calculate();
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Output = new TxOut() { Value = accounting.Due }
|
||||
}));
|
||||
accounting = btc.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Zero, accounting.DueUncapped);
|
||||
|
||||
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = ltc.Calculate();
|
||||
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
|
||||
Assert.True(accounting.DueUncapped < Money.Zero);
|
||||
|
||||
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2, null);
|
||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
#pragma warning disable CS0618
|
||||
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());
|
||||
// Some check that handling legacy stuff does not break things
|
||||
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
paymentMethod.Calculate();
|
||||
Assert.NotNull(paymentMethod);
|
||||
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
////////////////////
|
||||
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
|
||||
|
||||
accounting = paymentMethod.Calculate();
|
||||
//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());
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Rate = 1000,
|
||||
TxFee = Money.Coins(0.1m)
|
||||
});
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Rate = 500,
|
||||
TxFee = Money.Coins(0.01m)
|
||||
});
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
|
||||
Assert.Equal(1, accounting.TxRequired);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -75,6 +242,7 @@ namespace BTCPayServer.Tests
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
@ -108,7 +276,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(url.Address);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.True(localInvoice.Refundable);
|
||||
@ -121,6 +288,41 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var light = LightMoney.MilliSatoshis(1);
|
||||
Assert.Equal("0.00000000001", light.ToString());
|
||||
|
||||
light = LightMoney.MilliSatoshis(200000);
|
||||
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSetLightningServer()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "test").GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(storeController.ModelState.IsValid);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save").GetAwaiter().GetResult());
|
||||
|
||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
|
||||
Assert.Single(storeVm.LightningNodes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -131,9 +333,57 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.Start();
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC");
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.01,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||
});
|
||||
|
||||
|
||||
Task.WaitAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||
.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Price = 0.01,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
await tester.SendLightningPaymentAsync(invoice);
|
||||
await EventuallyAsync(async () =>
|
||||
{
|
||||
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUseServerInitiatedPairingCode()
|
||||
{
|
||||
@ -169,6 +419,7 @@ namespace BTCPayServer.Tests
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0,
|
||||
@ -177,12 +428,12 @@ namespace BTCPayServer.Tests
|
||||
OrderId = "orderId",
|
||||
NotificationURL = callbackServer.GetUri().AbsoluteUri,
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
Thread.Sleep(5000);
|
||||
tester.SimulateCallback(url.Address);
|
||||
callbackServer.ProcessNextRequest((ctx) =>
|
||||
{
|
||||
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
|
||||
@ -209,7 +460,7 @@ namespace BTCPayServer.Tests
|
||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||
var store2 = acc.CreateStore();
|
||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,31 +472,32 @@ namespace BTCPayServer.Tests
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD"
|
||||
}, Facade.Merchant);
|
||||
|
||||
var payment1 = Money.Coins(0.04m);
|
||||
var payment2 = Money.Coins(0.08m);
|
||||
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
|
||||
var payment2 = invoice.BtcDue;
|
||||
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
|
||||
{
|
||||
invoice.BitcoinAddress.ToString(),
|
||||
invoice.BitcoinAddress,
|
||||
payment1.ToString(),
|
||||
null, //comment
|
||||
null, //comment_to
|
||||
false, //subtractfeefromamount
|
||||
true, //replaceable
|
||||
}).ResultString);
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment1, invoice.BtcPaid);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
Assert.Equal("paidOver", invoice.ExceptionStatus.ToString());
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||
});
|
||||
|
||||
var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1));
|
||||
@ -253,19 +505,17 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
input.ScriptSig = Script.Empty; //Strip signatures
|
||||
}
|
||||
var change = tx.Outputs.First(o => o.Value != payment1);
|
||||
var output = tx.Outputs.First(o => o.Value == payment1);
|
||||
output.Value = payment2;
|
||||
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
|
||||
change.Value -= (payment2 - payment1) * 2; //Add more fees
|
||||
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
|
||||
tester.ExplorerNode.SendRawTransaction(replaced);
|
||||
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
|
||||
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment2, invoice.BtcPaid);
|
||||
Assert.Equal("False", invoice.ExceptionStatus.ToString());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -282,7 +532,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
public void TestAccessBitpayAPI()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
@ -290,21 +540,224 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CanTweakRate()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
||||
Assert.Equal(1.0, vm.RateMultiplier);
|
||||
vm.RateMultiplier = 0.5;
|
||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
||||
|
||||
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanHaveLTCOnlyStore()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||
var cashCow = tester.LTCExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.1m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||
});
|
||||
|
||||
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
|
||||
|
||||
var controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
|
||||
//////////////////////
|
||||
|
||||
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
|
||||
Assert.Null(invoice.BitcoinAddress);
|
||||
Assert.NotEqual(1.0, invoice.Rate);
|
||||
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal("paid", checkout.Status);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithTwoCurrencies()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
cashCow.Generate(2); // get some money in case
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
});
|
||||
|
||||
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
|
||||
|
||||
var controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("BTC", checkout.CryptoCode);
|
||||
|
||||
//////////////////////
|
||||
|
||||
// Retry now with LTC enabled
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
cashCow = tester.ExplorerNode;
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
});
|
||||
|
||||
cashCow = tester.LTCExplorerNode;
|
||||
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
|
||||
Assert.NotNull(ltcCryptoInfo);
|
||||
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
|
||||
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
|
||||
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(Money.Zero, invoice.BtcDue);
|
||||
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
|
||||
Assert.Equal(Money.Zero, ltcPaid.Due);
|
||||
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
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 repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
|
||||
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
|
||||
Eventually(() =>
|
||||
{
|
||||
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
@ -312,26 +765,26 @@ namespace BTCPayServer.Tests
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
Assert.Single(textSearchResult);
|
||||
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
Assert.Single(textSearchResult);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
|
||||
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);
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
|
||||
|
||||
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
@ -348,27 +801,27 @@ namespace BTCPayServer.Tests
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
|
||||
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
|
||||
Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length);
|
||||
Assert.Single(invoiceEntity.HistoricalAddresses);
|
||||
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
|
||||
|
||||
Money secondPayment = Money.Zero;
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("new", localInvoice.Status);
|
||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
||||
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
||||
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
||||
Assert.True(IsMapped(invoice, ctx));
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
|
||||
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
|
||||
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == invoice.BitcoinAddress.ToString());
|
||||
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
|
||||
Assert.NotNull(historical1.UnAssigned);
|
||||
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == localInvoice.BitcoinAddress.ToString());
|
||||
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
|
||||
Assert.Null(historical2.UnAssigned);
|
||||
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
|
||||
secondPayment = localInvoice.BtcDue;
|
||||
@ -378,21 +831,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount);
|
||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
@ -401,9 +853,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.NotEqual(0.0, localInvoice.Rate);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -423,7 +875,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
@ -434,7 +885,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
@ -446,27 +896,31 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var coinAverage = new CoinAverageRateProvider();
|
||||
var coinAverage = new CoinAverageRateProvider("BTC");
|
||||
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
|
||||
var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
|
||||
var cached = new CachedRateProvider("BTC", coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
|
||||
cached.CacheSpan = TimeSpan.FromSeconds(10);
|
||||
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
//Manually check that cache get hit after 10 sec
|
||||
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
|
||||
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
|
||||
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
|
||||
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.Address == h) != null;
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@ -480,5 +934,22 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,25 +13,27 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
[Fact]
|
||||
public void BitpayCheckout()
|
||||
{
|
||||
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
var url = new Uri("https://test.bitpay.com/");
|
||||
var btcpay = new Bitpay(key, url);
|
||||
var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
{
|
||||
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
ItemDesc = "Hello from the otherside"
|
||||
}, Facade.Merchant);
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// go to invoice.Url
|
||||
Console.WriteLine(invoice.Url);
|
||||
}
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: "3"
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
@ -10,101 +10,62 @@ services:
|
||||
context: ..
|
||||
dockerfile: BTCPayServer.Tests/Dockerfile
|
||||
environment:
|
||||
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_NBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_FAKECALLBACK: 'false'
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_ECLAIR1: http://eclair1:8080/
|
||||
TEST_ECLAIR2: http://eclair2:8080/
|
||||
TEST_ECLAIR: http://eclair-cli:gpwefwmmewci@eclair:8080/
|
||||
TEST_CHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
- bitcoind
|
||||
- nbxplorer
|
||||
- eclair1
|
||||
- eclair2
|
||||
- dev
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- bitcoind
|
||||
- nbxplorer
|
||||
- eclair1
|
||||
- eclair2
|
||||
- postgres
|
||||
- eclair
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.32
|
||||
image: nicolasdorier/nbxplorer:1.0.1.16
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
NBXPLORER_RPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_RPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_RPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_NODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_CHAINS: "btc,ltc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_LTCRPCURL: http://litecoind:43782/
|
||||
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
|
||||
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
- bitcoind
|
||||
- postgres
|
||||
|
||||
eclair1:
|
||||
image: acinq/eclair:latest
|
||||
environment:
|
||||
JAVA_OPTS: >
|
||||
-Xmx512m
|
||||
-Declair.printToConsole
|
||||
-Declair.bitcoind.host=bitcoind
|
||||
-Declair.bitcoind.rpcport=43782
|
||||
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
|
||||
-Declair.bitcoind.rpcpassword=DwubwWsoo3
|
||||
-Declair.bitcoind.zmq=tcp://bitcoind:29000
|
||||
-Declair.chain=regtest
|
||||
-Declair.api.binding-ip=0.0.0.0
|
||||
links:
|
||||
- bitcoind
|
||||
ports:
|
||||
- "30992:8080" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "8080" # api port
|
||||
|
||||
eclair2:
|
||||
image: acinq/eclair:latest
|
||||
environment:
|
||||
JAVA_OPTS: >
|
||||
-Xmx512m
|
||||
-Declair.printToConsole
|
||||
-Declair.bitcoind.host=bitcoind
|
||||
-Declair.bitcoind.rpcport=43782
|
||||
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
|
||||
-Declair.bitcoind.rpcpassword=DwubwWsoo3
|
||||
-Declair.bitcoind.zmq=tcp://bitcoind:29000
|
||||
-Declair.chain=regtest
|
||||
-Declair.api.binding-ip=0.0.0.0
|
||||
links:
|
||||
- bitcoind
|
||||
ports:
|
||||
- "30993:8080" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "8080" # api port
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
@ -117,12 +78,73 @@ services:
|
||||
zmqpubrawblock=tcp://0.0.0.0:29000
|
||||
zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
txindex=1
|
||||
ports:
|
||||
- "43782:43782" # RPC
|
||||
# Eclair is still using addwitnessaddress
|
||||
deprecatedrpc=addwitnessaddress
|
||||
ports:
|
||||
- "43782:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.1
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
SKIP_BITCOIND: 1
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
eclair:
|
||||
image: acinq/eclair@sha256:758eaf02683046a096ee03390d3a54df8fcfca50883f7560ab946a36ee4e81d8
|
||||
environment:
|
||||
JAVA_OPTS: >
|
||||
-Xmx512m
|
||||
-Declair.printToConsole
|
||||
-Declair.bitcoind.host=bitcoind
|
||||
-Declair.bitcoind.rpcport=43782
|
||||
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
|
||||
-Declair.bitcoind.rpcpassword=DwubwWsoo3
|
||||
-Declair.bitcoind.zmq=tcp://bitcoind:29000
|
||||
-Declair.api.enabled=true
|
||||
-Declair.api.password=gpwefwmmewci
|
||||
-Declair.chain=regtest
|
||||
-Declair.api.binding-ip=0.0.0.0
|
||||
links:
|
||||
- bitcoind
|
||||
ports:
|
||||
- "30992:8080" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "8080" # api port
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
ports:
|
||||
- "43783:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "29000" # zmq
|
||||
|
||||
postgres:
|
||||
image: postgres:9.6.5
|
||||
@ -130,3 +152,6 @@ services:
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
|
1
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
1
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args
|
78
BTCPayServer/BTCPayNetwork.cs
Normal file
78
BTCPayServer/BTCPayNetwork.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
{
|
||||
public Network NBitcoinNetwork { get; set; }
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public IRateProvider DefaultRateProvider { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return CryptoCode == "BTC";
|
||||
}
|
||||
}
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public string LightningImagePath { get; set; }
|
||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string CLightningNetworkName { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
38
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProvider("BTC");
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
|
||||
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
|
||||
ChainType == ChainType.Test ? "testnet" :
|
||||
ChainType == ChainType.Regtest ? "regtest" : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
|
||||
var ltcRate = new CoinAverageRateProvider("LTC");
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
|
||||
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
|
||||
ChainType == ChainType.Test ? "litecoin-testnet" : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
92
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
92
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
|
||||
|
||||
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return _NBXplorerNetworkProvider;
|
||||
}
|
||||
}
|
||||
|
||||
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||
{
|
||||
ChainType = filtered.ChainType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
|
||||
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||
foreach (var network in filtered._Networks)
|
||||
{
|
||||
if(cryptoCodes.Contains(network.Key))
|
||||
{
|
||||
_Networks.Add(network.Key, network.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ChainType ChainType { get; set; }
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
ChainType = chainType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keep only the specified crypto
|
||||
/// </summary>
|
||||
/// <param name="cryptoCodes">Crypto to support</param>
|
||||
/// <returns></returns>
|
||||
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
|
||||
{
|
||||
return new BTCPayNetworkProvider(this, cryptoCodes);
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
public BTCPayNetwork BTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(BTCPayNetwork network)
|
||||
{
|
||||
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
|
||||
}
|
||||
|
||||
public IEnumerable<BTCPayNetwork> GetAll()
|
||||
{
|
||||
return _Networks.Values.ToArray();
|
||||
}
|
||||
|
||||
public bool Support(string cryptoCode)
|
||||
{
|
||||
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,13 +2,18 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.41</Version>
|
||||
<Version>1.0.1.40</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="Build\dockerfiles\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="Build\dockerfiles\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="Build\dockerfiles\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
@ -17,26 +22,31 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.362" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.50" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.13" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.56" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.20" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -98,5 +108,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
72
BTCPayServer/BTCPayServer.ruleset
Normal file
72
BTCPayServer/BTCPayServer.ruleset
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
|
||||
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
|
||||
<Name Resource="MinimumRecommendedRules_Name" />
|
||||
<Description Resource="MinimumRecommendedRules_Description" />
|
||||
</Localization>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1009" Action="Warning" />
|
||||
<Rule Id="CA1016" Action="Warning" />
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1049" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1061" Action="Warning" />
|
||||
<Rule Id="CA1063" Action="Warning" />
|
||||
<Rule Id="CA1065" Action="Warning" />
|
||||
<Rule Id="CA1301" Action="Warning" />
|
||||
<Rule Id="CA1400" Action="Warning" />
|
||||
<Rule Id="CA1401" Action="Warning" />
|
||||
<Rule Id="CA1403" Action="Warning" />
|
||||
<Rule Id="CA1404" Action="Warning" />
|
||||
<Rule Id="CA1405" Action="Warning" />
|
||||
<Rule Id="CA1410" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1821" Action="Warning" />
|
||||
<Rule Id="CA1900" Action="Warning" />
|
||||
<Rule Id="CA1901" Action="Warning" />
|
||||
<Rule Id="CA2002" Action="Warning" />
|
||||
<Rule Id="CA2100" Action="Warning" />
|
||||
<Rule Id="CA2101" Action="Warning" />
|
||||
<Rule Id="CA2108" Action="Warning" />
|
||||
<Rule Id="CA2111" Action="Warning" />
|
||||
<Rule Id="CA2112" Action="Warning" />
|
||||
<Rule Id="CA2114" Action="Warning" />
|
||||
<Rule Id="CA2116" Action="Warning" />
|
||||
<Rule Id="CA2117" Action="Warning" />
|
||||
<Rule Id="CA2122" Action="Warning" />
|
||||
<Rule Id="CA2123" Action="Warning" />
|
||||
<Rule Id="CA2124" Action="Warning" />
|
||||
<Rule Id="CA2126" Action="Warning" />
|
||||
<Rule Id="CA2131" Action="Warning" />
|
||||
<Rule Id="CA2132" Action="Warning" />
|
||||
<Rule Id="CA2133" Action="Warning" />
|
||||
<Rule Id="CA2134" Action="Warning" />
|
||||
<Rule Id="CA2137" Action="Warning" />
|
||||
<Rule Id="CA2138" Action="Warning" />
|
||||
<Rule Id="CA2140" Action="Warning" />
|
||||
<Rule Id="CA2141" Action="Warning" />
|
||||
<Rule Id="CA2146" Action="Warning" />
|
||||
<Rule Id="CA2147" Action="Warning" />
|
||||
<Rule Id="CA2149" Action="Warning" />
|
||||
<Rule Id="CA2200" Action="Warning" />
|
||||
<Rule Id="CA2202" Action="Warning" />
|
||||
<Rule Id="CA2207" Action="Warning" />
|
||||
<Rule Id="CA2212" Action="Warning" />
|
||||
<Rule Id="CA2213" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="Warning" />
|
||||
<Rule Id="CA2216" Action="Warning" />
|
||||
<Rule Id="CA2220" Action="Warning" />
|
||||
<Rule Id="CA2229" Action="Warning" />
|
||||
<Rule Id="CA2231" Action="Warning" />
|
||||
<Rule Id="CA2232" Action="Warning" />
|
||||
<Rule Id="CA2235" Action="Warning" />
|
||||
<Rule Id="CA2236" Action="Warning" />
|
||||
<Rule Id="CA2237" Action="Warning" />
|
||||
<Rule Id="CA2238" Action="Warning" />
|
||||
<Rule Id="CA2240" Action="Warning" />
|
||||
<Rule Id="CA2241" Action="Warning" />
|
||||
<Rule Id="CA2242" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
|
||||
</RuleSet>
|
@ -9,21 +9,20 @@ using System.Net;
|
||||
using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class NBXplorerConnectionSetting
|
||||
{
|
||||
public string CryptoCode { get; internal set; }
|
||||
public Uri ExplorerUri { get; internal set; }
|
||||
public string CookieFile { get; internal set; }
|
||||
}
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri Explorer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string CookieFile
|
||||
public ChainType ChainType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -43,22 +42,50 @@ namespace BTCPayServer.Configuration
|
||||
set;
|
||||
}
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
var networkInfo = DefaultConfiguration.GetNetwork(conf);
|
||||
Network = networkInfo?.Network;
|
||||
if (Network == null)
|
||||
throw new ConfigException("Invalid network");
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
|
||||
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + Network);
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
{
|
||||
if (NetworkProvider.GetNetwork(chain) == null)
|
||||
throw new ConfigException($"Invalid chains \"{chain}\"");
|
||||
}
|
||||
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in NetworkProvider.GetAll())
|
||||
{
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
|
||||
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
InternalUrl = conf.GetOrDefault<Uri>("internalurl", null);
|
||||
InternalLightningNode = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
}
|
||||
|
||||
public Uri InternalLightningNode { get; set; }
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
@ -69,6 +96,10 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri InternalUrl { get; private set; }
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
|
||||
if (str == null)
|
||||
return defaultValue;
|
||||
if (typeof(T) == typeof(bool))
|
||||
@ -27,17 +27,24 @@ namespace BTCPayServer.Configuration
|
||||
throw new FormatException();
|
||||
}
|
||||
else if (typeof(T) == typeof(Uri))
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
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(":");
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
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));
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (typeof(T) == typeof(int))
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using System.Text;
|
||||
using CommandLine;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -17,20 +18,28 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
|
||||
Name = "BTCPay"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
}
|
||||
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--internalurl", $"The expected internal url of this service, this set NBXplorer callback addresses (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--internallightningnode", $"An internal lightning node which can be used without https requirement and easily configured by the admin (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -38,54 +47,65 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return GetNetwork(conf).DefaultDataDirectory;
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
|
||||
return Path.Combine(dataDir, fileName);
|
||||
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
|
||||
chainDir = Path.Combine(dataDir, chainDir);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(chainDir))
|
||||
Directory.CreateDirectory(chainDir);
|
||||
}
|
||||
catch { }
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
public static NetworkInformation GetNetwork(IConfiguration conf)
|
||||
public static ChainType GetChainType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
{
|
||||
var info = NetworkInformation.GetNetworkByName(network);
|
||||
if (info == null)
|
||||
throw new ConfigException($"Invalid network name {network}");
|
||||
return info;
|
||||
var n = Network.GetNetwork(network);
|
||||
if (n == null)
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
|
||||
|
||||
return NetworkInformation.GetNetworkByName(net.Name);
|
||||
return net;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#testnet=0");
|
||||
builder.AppendLine("#regtest=0");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + network.DefaultPort);
|
||||
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
|
||||
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@ -93,7 +113,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class NetworkInformation
|
||||
{
|
||||
static NetworkInformation()
|
||||
{
|
||||
_Networks = new Dictionary<string, NetworkInformation>();
|
||||
foreach (var network in Network.GetNetworks())
|
||||
{
|
||||
NetworkInformation info = new NetworkInformation();
|
||||
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
|
||||
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
|
||||
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
|
||||
info.Network = network;
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
|
||||
info.DefaultPort = 23002;
|
||||
_Networks.Add(network.Name, info);
|
||||
if (network == Network.Main)
|
||||
{
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
|
||||
Main = info;
|
||||
info.DefaultPort = 23000;
|
||||
}
|
||||
if (network == Network.TestNet)
|
||||
{
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
|
||||
info.DefaultPort = 23001;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, NetworkInformation> _Networks;
|
||||
public static NetworkInformation GetNetworkByName(string name)
|
||||
{
|
||||
var value = _Networks.TryGet(name);
|
||||
if (value != null)
|
||||
return value;
|
||||
|
||||
//Maybe alias ?
|
||||
var network = Network.GetNetwork(name);
|
||||
if (network != null)
|
||||
{
|
||||
value = _Networks.TryGet(network.Name);
|
||||
if (value != null)
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static NetworkInformation Main
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DefaultConfigurationFile
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DefaultDataDirectory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri DefaultExplorerUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public int DefaultPort
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DefaultExplorerCookieFile
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Network.ToString();
|
||||
}
|
||||
|
||||
public static string ToStringAll()
|
||||
{
|
||||
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger _logger;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository,
|
||||
ILogger<AccountController> logger)
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
|
||||
if (admin.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Admin created.");
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
}
|
||||
@ -273,7 +271,6 @@ namespace BTCPayServer.Controllers
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
RegisteredUserId = user.Id;
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
@ -292,7 +289,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
|
@ -1,126 +0,0 @@
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class CallbackController : Controller
|
||||
{
|
||||
public class CallbackSettings
|
||||
{
|
||||
public string Token
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
SettingsRepository _Settings;
|
||||
Network _Network;
|
||||
InvoiceWatcher _Watcher;
|
||||
ExplorerClient _Explorer;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public CallbackController(SettingsRepository repo,
|
||||
ExplorerClient explorer,
|
||||
InvoiceWatcherAccessor watcher,
|
||||
BTCPayServerOptions options,
|
||||
Network network)
|
||||
{
|
||||
_Settings = repo;
|
||||
_Network = network;
|
||||
_Watcher = watcher.Instance;
|
||||
_Explorer = explorer;
|
||||
_Options = options;
|
||||
}
|
||||
|
||||
[Route("callbacks/transactions")]
|
||||
[HttpPost]
|
||||
public async Task NewTransaction(string token)
|
||||
{
|
||||
await AssertToken(token);
|
||||
Logs.PayServer.LogInformation("New transaction callback");
|
||||
//We don't want to register all the json converter at MVC level, so we parse here
|
||||
var serializer = new NBXplorer.Serializer(_Network);
|
||||
var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync();
|
||||
var match = serializer.ToObject<TransactionMatch>(content);
|
||||
|
||||
foreach (var output in match.Outputs)
|
||||
{
|
||||
await _Watcher.NotifyReceived(output.ScriptPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("callbacks/blocks")]
|
||||
[HttpPost]
|
||||
public async Task NewBlock(string token)
|
||||
{
|
||||
await AssertToken(token);
|
||||
Logs.PayServer.LogInformation("New block callback");
|
||||
await _Watcher.NotifyBlock();
|
||||
}
|
||||
|
||||
private async Task AssertToken(string token)
|
||||
{
|
||||
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
|
||||
if (await GetToken() != token)
|
||||
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
|
||||
}
|
||||
|
||||
public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
|
||||
{
|
||||
string token = await GetToken();
|
||||
return BuildCallbackUri(request, "callbacks/transactions?token=" + token);
|
||||
}
|
||||
|
||||
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
|
||||
{
|
||||
var uri = await GetCallbackUriAsync(request);
|
||||
await _Explorer.SubscribeToWalletAsync(uri, derivationScheme);
|
||||
}
|
||||
|
||||
private async Task<string> GetToken()
|
||||
{
|
||||
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
|
||||
if (callback == null)
|
||||
{
|
||||
callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() };
|
||||
await _Settings.UpdateSetting(callback);
|
||||
}
|
||||
var token = callback.Token;
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
|
||||
{
|
||||
string token = await GetToken();
|
||||
return BuildCallbackUri(request, "callbacks/blocks?token=" + token);
|
||||
}
|
||||
|
||||
private Uri BuildCallbackUri(HttpRequest request, string callbackPath)
|
||||
{
|
||||
string baseUrl = _Options.InternalUrl == null ? request.GetAbsoluteRoot() : _Options.InternalUrl.AbsolutePath;
|
||||
baseUrl = baseUrl.WithTrailingSlash();
|
||||
return new Uri(baseUrl + callbackPath);
|
||||
}
|
||||
|
||||
public async Task<Uri> RegisterCallbackBlockUriAsync(Uri uri)
|
||||
{
|
||||
await _Explorer.SubscribeToBlocksAsync(uri);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
@ -24,16 +24,19 @@ namespace BTCPayServer.Controllers
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO();
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO()).ToArray();
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
@ -9,29 +9,36 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.IsExpired())
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
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.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
@ -57,15 +64,23 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}", Order = 99)]
|
||||
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId)
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.IsExpired())
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
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());
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
@ -16,27 +16,16 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public IActionResult Invoice(string invoiceId, string command)
|
||||
{
|
||||
if (command == "refresh")
|
||||
{
|
||||
_Watcher.Watch(invoiceId);
|
||||
}
|
||||
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
|
||||
return RedirectToAction(nameof(Invoice), new
|
||||
{
|
||||
invoiceId = invoiceId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
@ -44,50 +33,95 @@ namespace BTCPayServer.Controllers
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId
|
||||
InvoiceId = invoiceId,
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Rate = invoice.Rate,
|
||||
Fiat = dto.Price + " " + dto.Currency,
|
||||
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
|
||||
BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
|
||||
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
|
||||
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
BitcoinAddress = invoice.DepositAddress,
|
||||
PaymentUrl = dto.PaymentUrls.BIP72
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events
|
||||
};
|
||||
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if(onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
.Payments
|
||||
.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
|
||||
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
}
|
||||
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Addresses = invoice.HistoricalAddresses;
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
@ -95,80 +129,188 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId)
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto();
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if(!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BtcAddress = invoice.DepositAddress.ToString(),
|
||||
BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
|
||||
BtcTotalDue = invoice.GetTotalCryptoDue().ToString(),
|
||||
BtcDue = invoice.GetCryptoDue().ToString(),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
|
||||
Rate = FormatCurrency(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
|
||||
TxCount = invoice.GetTxCount(),
|
||||
BtcPaid = invoice.GetTotalPaid().ToString(),
|
||||
Status = invoice.Status
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
throw new NotSupportedException(),
|
||||
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetpaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return model;
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
||||
}
|
||||
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString());
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00"));
|
||||
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId)
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
||||
{
|
||||
var model = await GetInvoiceModel(invoiceId);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status/ws")]
|
||||
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
if (message.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
|
||||
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
|
||||
{
|
||||
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
|
||||
return;
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
try
|
||||
{
|
||||
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
catch { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
@ -203,8 +345,9 @@ namespace BTCPayServer.Controllers
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = invoice.InvoiceTime,
|
||||
Date = Prettify(invoice.InvoiceTime),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
@ -214,6 +357,30 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string Prettify(DateTimeOffset invoiceTime)
|
||||
{
|
||||
var ago = DateTime.UtcNow - invoiceTime;
|
||||
|
||||
if(ago.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
|
||||
}
|
||||
if (ago.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalMinutes} minute{Plural((int)ago.TotalMinutes)} ago";
|
||||
}
|
||||
if (ago.Days < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalHours} hour{Plural((int)ago.TotalHours)} ago";
|
||||
}
|
||||
return $"{(int)ago.TotalDays} day{Plural((int)ago.TotalDays)} ago";
|
||||
}
|
||||
|
||||
private string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
@ -241,7 +408,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
if (string.IsNullOrEmpty(store.DerivationStrategy))
|
||||
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
@ -300,6 +467,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -38,65 +38,86 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
private InvoiceWatcher _Watcher;
|
||||
IRateProviderFactory _RateProviders;
|
||||
StoreRepository _StoreRepository;
|
||||
Network _Network;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProvider _FeeProvider;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
ExplorerClient _Explorer;
|
||||
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly BTCPayWalletProvider _WalletProvider;
|
||||
IServiceProvider _ServiceProvider;
|
||||
public InvoiceController(
|
||||
Network network,
|
||||
IServiceProvider serviceProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayWallet wallet,
|
||||
IRateProvider rateProvider,
|
||||
IRateProviderFactory rateProviders,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceWatcherAccessor watcher,
|
||||
ExplorerClient explorerClient,
|
||||
IFeeProvider feeProvider)
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_UserManager = userManager;
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
|
||||
IsAvailable: Task.FromResult(false)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(c =>
|
||||
{
|
||||
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
|
||||
return c;
|
||||
})
|
||||
.ToList();
|
||||
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
|
||||
{
|
||||
if(!await supportedPaymentMethod.IsAvailable)
|
||||
{
|
||||
supportedPaymentMethods.Remove(supportedPaymentMethod);
|
||||
}
|
||||
}
|
||||
if (supportedPaymentMethods.Count == 0)
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
var storeBlob = store.GetStoreBlob(_Network);
|
||||
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
//Another way of passing buyer info to support
|
||||
@ -112,19 +133,66 @@ namespace BTCPayServer.Controllers
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
var getFeeRate = _FeeProvider.GetFeeRateAsync();
|
||||
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
|
||||
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
|
||||
entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await getRate;
|
||||
var methods = supportedPaymentMethods
|
||||
.Select(async o =>
|
||||
{
|
||||
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
});
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var method in methods)
|
||||
{
|
||||
paymentMethods.Add(await method);
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
|
||||
{
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
|
||||
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
|
||||
if (feeProvider != null && rateProvider != null)
|
||||
{
|
||||
var gettingFee = feeProvider.GetFeeRateAsync();
|
||||
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
|
||||
entity.Rate = await gettingRate;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity.DepositAddress = await getAddress;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
_Watcher.Watch(entity.Id);
|
||||
var resp = entity.EntityToDTO();
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if (transactionSpeed == null)
|
||||
@ -153,9 +221,9 @@ namespace BTCPayServer.Controllers
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
|
||||
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
|
@ -20,6 +20,7 @@ using NBitcoin;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -33,7 +34,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
private readonly BTCPayWallet _Wallet;
|
||||
IHostingEnvironment _Env;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
ILogger<ManageController> logger,
|
||||
UrlEncoder urlEncoder,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
@ -57,7 +57,6 @@ namespace BTCPayServer.Controllers
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
_Wallet = wallet;
|
||||
_Env = env;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
@ -436,7 +435,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
@ -526,7 +525,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
|
@ -7,34 +7,68 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProvider _RateProvider;
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
var allRates = (await _RateProvider.GetRatesAsync());
|
||||
return new DataWrapper<NBitpayClient.Rate[]>
|
||||
(allRates.Select(r =>
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
|
||||
}
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
302
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
302
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
|
||||
{
|
||||
selectedScheme = selectedScheme ?? "BTC";
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, vm.CryptoCurrency);
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
if (vm.Confirmation)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/ws/ledger")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string storeId,
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for substract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test();
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
result = await hw.GetExtPubKey(network);
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
||||
feeRateValue,
|
||||
changeAddress.Item1,
|
||||
changeAddress.Item2);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(strategyBase);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = GetDerivationStrategy(store, network);
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
if (!directStrategy.Segwit)
|
||||
return null;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
||||
}
|
||||
|
||||
return strategy.DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
}
|
134
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
134
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
|
||||
{
|
||||
selectedCrypto = selectedCrypto ?? "BTC";
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
|
||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
|
||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
||||
if (network == null || network.CLightningNetworkName == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.Url))
|
||||
{
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (uri.Scheme != "https")
|
||||
{
|
||||
var internalNode = GetInternalLightningNodeIfAuthorized();
|
||||
if (internalNode == null || GetDomain(internalNode) != GetDomain(uri.AbsoluteUri))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningChargeUrl(uri);
|
||||
}
|
||||
if (command == "save")
|
||||
{
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else // if(command == "test")
|
||||
{
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
|
||||
return View(vm);
|
||||
}
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
await handler.Test(paymentMethod, network);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
vm.StatusMessage = "Connection to the lightning node succeed";
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInternalLightningNodeIfAuthorized()
|
||||
{
|
||||
if (_BtcpayServerOptions.InternalLightningNode != null &&
|
||||
CanUseInternalLightning())
|
||||
{
|
||||
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning()
|
||||
{
|
||||
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
||||
}
|
||||
|
||||
string GetDomain(string uri)
|
||||
{
|
||||
return new UriBuilder(uri).Host;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,25 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -25,30 +28,45 @@ namespace BTCPayServer.Controllers
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = "CanAccessStore")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class StoresController : Controller
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
public StoresController(
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
StoreRepository repo,
|
||||
TokenRepository tokenRepo,
|
||||
CallbackController callbackController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWallet wallet,
|
||||
Network network,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_TokenController = tokenController;
|
||||
_Wallet = wallet;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
_Network = network;
|
||||
_CallbackController = callbackController;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
}
|
||||
Network _Network;
|
||||
CallbackController _CallbackController;
|
||||
BTCPayWallet _Wallet;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private ExplorerClientProvider _ExplorerProvider;
|
||||
private MvcJsonOptions _MvcJsonOptions;
|
||||
private IFeeProviderFactory _FeeRateProvider;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
AccessTokenController _TokenController;
|
||||
StoreRepository _Repo;
|
||||
TokenRepository _TokenRepository;
|
||||
@ -87,14 +105,40 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet")]
|
||||
public async Task<IActionResult> Wallet(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
result.StatusMessage = StatusMessage;
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy, null))).ToArray();
|
||||
var balances = stores
|
||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
@ -103,12 +147,27 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
Balance = await balances[i]
|
||||
Balances = balances[i].Select(t => t.Result).ToArray()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
@ -145,115 +204,128 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob(_Network);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.DerivationScheme = store.DerivationStrategy;
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.StatusMessage = StatusMessage;
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
{
|
||||
foreach(var strategy in store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>())
|
||||
{
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = strategy.PaymentId.CryptoCode,
|
||||
Value = strategy.DerivationStrategyBase.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
foreach(var lightning in store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
{
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||
{
|
||||
CryptoCode = lightning.CryptoCode,
|
||||
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
AddPaymentMethods(store, model);
|
||||
|
||||
if (command == "Save")
|
||||
bool needUpdate = false;
|
||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
if (store.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
}
|
||||
if (store.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
if (store.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
}
|
||||
if (store.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
if (store.DerivationStrategy != model.DerivationScheme)
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
needUpdate = true;
|
||||
try
|
||||
var rate = await client.GetAsync(model.RateSource);
|
||||
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(model.DerivationScheme))
|
||||
{
|
||||
var strategy = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
|
||||
await _Wallet.TrackAsync(strategy);
|
||||
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
|
||||
}
|
||||
store.DerivationStrategy = model.DerivationScheme;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob(_Network);
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
|
||||
if (store.SetStoreBlob(blob, _Network))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
}
|
||||
else
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(model.DerivationScheme))
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheme = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
|
||||
var line = scheme.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
}
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format)
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
|
||||
{
|
||||
if (format == "Electrum")
|
||||
{
|
||||
@ -262,12 +334,12 @@ namespace BTCPayServer.Controllers
|
||||
//var p2wsh = 0x2aa7ed3U;
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = _Network == Network.Main ? 0x0488b21eU : 0x043587cf;
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, new string[] { });
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var data = Encoders.Base58Check.DecodeData(derivationScheme);
|
||||
if (data.Length < 4)
|
||||
@ -275,19 +347,19 @@ namespace BTCPayServer.Controllers
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
|
||||
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
|
||||
var standardPrefix = Utils.ToBytes(standard, false);
|
||||
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
data[i] = standardPrefix[i];
|
||||
|
||||
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), _Network).ToString();
|
||||
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
|
||||
foreach (var label in labels)
|
||||
{
|
||||
derivationScheme = derivationScheme + $"-[{label}]";
|
||||
}
|
||||
}
|
||||
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
|
||||
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -353,6 +425,7 @@ namespace BTCPayServer.Controllers
|
||||
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
|
||||
}
|
||||
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
pairingCode = pairingCode,
|
||||
@ -360,6 +433,8 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
public string GeneratedPairingCode { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
|
@ -2,16 +2,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class AddressInvoiceData
|
||||
{
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
/// </summary>
|
||||
[Obsolete("Use GetHash instead")]
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
Address = address + "#" + paymentMethodId?.ToString();
|
||||
return this;
|
||||
}
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
|
||||
if (index == -1)
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.Parse(Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
@ -26,5 +63,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<InvoiceEventData> InvoiceEvents
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
|
||||
{
|
||||
get; set;
|
||||
@ -113,7 +118,9 @@ namespace BTCPayServer.Data
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
builder.Entity<PairingCodeData>()
|
||||
.HasKey(o => o.Id);
|
||||
@ -128,7 +135,18 @@ namespace BTCPayServer.Data
|
||||
.HasKey(o => new
|
||||
{
|
||||
o.InvoiceDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.Address
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
o.InvoiceDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,42 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
/// </summary>
|
||||
[Obsolete("Use GetCryptoCode instead")]
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use GetCryptoCode instead")]
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetCryptoCode()
|
||||
{
|
||||
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
|
||||
}
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public HistoricalAddressInvoiceData SetAddress(string depositAddress, string cryptoCode)
|
||||
{
|
||||
Address = depositAddress + "#" + cryptoCode;
|
||||
CryptoCode = cryptoCode;
|
||||
return this;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
public DateTimeOffset Assigned
|
||||
{
|
||||
get; set;
|
||||
|
@ -32,6 +32,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceEventData> Events
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
|
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class InvoiceEventData
|
||||
{
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -26,11 +29,101 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
bool btcReturned = false;
|
||||
|
||||
// Legacy stuff which should go away
|
||||
if (!string.IsNullOrEmpty(DerivationStrategy))
|
||||
{
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(DerivationStrategies))
|
||||
{
|
||||
JObject strategies = JObject.Parse(DerivationStrategies);
|
||||
foreach (var strat in strategies.Properties())
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
|
||||
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network != null)
|
||||
{
|
||||
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
|
||||
continue;
|
||||
if (strat.Value.Type == JTokenType.Null)
|
||||
continue;
|
||||
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set or remove a new supported payment method for the store
|
||||
/// </summary>
|
||||
/// <param name="paymentMethodId">The paymentMethodId</param>
|
||||
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
|
||||
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
throw new InvalidOperationException("Argument mismatch");
|
||||
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
bool existing = false;
|
||||
foreach (var strat in strategies.Properties().ToList())
|
||||
{
|
||||
var stratId = PaymentMethodId.Parse(strat.Name);
|
||||
if (stratId.IsBTCOnChain)
|
||||
{
|
||||
// Legacy stuff which should go away
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
if (stratId == paymentMethodId)
|
||||
{
|
||||
if (supportedPaymentMethod == null)
|
||||
{
|
||||
strat.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
|
||||
}
|
||||
existing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
else if (!existing)
|
||||
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
|
||||
DerivationStrategies = strategies.ToString();
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
@ -61,16 +154,31 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
|
||||
public StoreBlob GetStoreBlob(Network network)
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
return DefaultCrypto ?? "BTC";
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
{
|
||||
DefaultCrypto = defaultCryptoCurrency;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
static Network Dummy = Network.Main;
|
||||
|
||||
public StoreBlob GetStoreBlob()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
}
|
||||
|
||||
public bool SetStoreBlob(StoreBlob storeBlob, Network network)
|
||||
public bool SetStoreBlob(StoreBlob storeBlob)
|
||||
{
|
||||
var original = new Serializer(network).ToString(GetStoreBlob(network));
|
||||
var newBlob = new Serializer(network).ToString(storeBlob);
|
||||
var original = new Serializer(Dummy).ToString(GetStoreBlob());
|
||||
var newBlob = new Serializer(Dummy).ToString(storeBlob);
|
||||
if (original == newBlob)
|
||||
return false;
|
||||
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
|
||||
@ -78,10 +186,27 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
{
|
||||
public RateRule()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public double Multiplier { get; set; }
|
||||
|
||||
public decimal Apply(BTCPayNetwork network, decimal rate)
|
||||
{
|
||||
return rate * (decimal)Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
@ -95,5 +220,59 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule>();
|
||||
RateRules.Add(new RateRule() { Multiplier = rate });
|
||||
}
|
||||
public decimal GetRateMultiplier()
|
||||
{
|
||||
decimal rate = 1.0m;
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rate;
|
||||
foreach (var rule in RateRules)
|
||||
{
|
||||
rate = rule.Apply(null, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
|
||||
if (rateProvider is CachedRateProvider cachedRateProvider)
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
rateProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rateProvider;
|
||||
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
BTCPayServer/DerivationStrategy.cs
Normal file
44
BTCPayServer/DerivationStrategy.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy : ISupportedPaymentMethod
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
}
|
||||
|
||||
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationStrategy(result, network);
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _DerivationStrategy.ToString();
|
||||
}
|
||||
}
|
||||
}
|
154
BTCPayServer/EventAggregator.cs
Normal file
154
BTCPayServer/EventAggregator.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public interface IEventAggregatorSubscription : IDisposable
|
||||
{
|
||||
void Unsubscribe();
|
||||
void Resubscribe();
|
||||
}
|
||||
public class EventAggregator : IDisposable
|
||||
{
|
||||
class Subscription : IEventAggregatorSubscription
|
||||
{
|
||||
private EventAggregator aggregator;
|
||||
Type t;
|
||||
public Subscription(EventAggregator aggregator, Type t)
|
||||
{
|
||||
this.aggregator = aggregator;
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
public Action<Object> Act { get; set; }
|
||||
|
||||
bool _Disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (_Disposed)
|
||||
return;
|
||||
_Disposed = true;
|
||||
lock (this.aggregator._Subscriptions)
|
||||
{
|
||||
if (this.aggregator._Subscriptions.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
if (actions.Remove(this))
|
||||
{
|
||||
if (actions.Count == 0)
|
||||
this.aggregator._Subscriptions.Remove(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Resubscribe()
|
||||
{
|
||||
aggregator.Subscribe(t, this);
|
||||
}
|
||||
|
||||
public void Unsubscribe()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return WaitNext<T>(o => true, cancellation);
|
||||
}
|
||||
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||
var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
|
||||
using (cancellation.Register(() => { tcs.TrySetCanceled(); subscription.Unsubscribe(); }))
|
||||
{
|
||||
return await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish<T>(T evt) where T : class
|
||||
{
|
||||
Publish(evt, typeof(T));
|
||||
}
|
||||
|
||||
public void Publish(object evt, Type evtType)
|
||||
{
|
||||
if (evt == null)
|
||||
throw new ArgumentNullException(nameof(evt));
|
||||
List<Action<object>> actionList = new List<Action<object>>();
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if (_Subscriptions.TryGetValue(evtType, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
actionList = actions.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var log = evt.ToString();
|
||||
if(!String.IsNullOrEmpty(log))
|
||||
Logs.Events.LogInformation(log);
|
||||
foreach (var sub in actionList)
|
||||
{
|
||||
try
|
||||
{
|
||||
sub(evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Events.LogError(ex, $"Error while calling event handler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription)
|
||||
{
|
||||
var eventType = typeof(T);
|
||||
var s = new Subscription(this, eventType);
|
||||
s.Act = (o) => subscription(s, (T)o);
|
||||
return Subscribe(eventType, s);
|
||||
}
|
||||
|
||||
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if (!_Subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions))
|
||||
{
|
||||
actions = new Dictionary<Subscription, Action<object>>();
|
||||
_Subscriptions.Add(eventType, actions);
|
||||
}
|
||||
actions.Add(subscription, subscription.Act);
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription)
|
||||
{
|
||||
return Subscribe(new Action<T>((t) => subscription(t)));
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<IEventAggregatorSubscription, T, TReturn> subscription)
|
||||
{
|
||||
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t)));
|
||||
}
|
||||
|
||||
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
|
||||
{
|
||||
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal file
33
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceDataChangedEvent
|
||||
{
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceIPNEvent
|
||||
{
|
||||
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = eventCode;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public string Error { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string ipnType = "IPN";
|
||||
if(EventCode.HasValue)
|
||||
{
|
||||
ipnType = $"IPN ({EventCode.Value} {Name})";
|
||||
}
|
||||
if (Error == null)
|
||||
return $"{ipnType} sent for invoice {InvoiceId}";
|
||||
return $"Error while sending {ipnType}: {Error}";
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNeedUpdateEvent
|
||||
{
|
||||
public InvoiceNeedUpdateEvent(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
InvoiceId = invoiceId;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNewAddressEvent
|
||||
{
|
||||
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
|
||||
{
|
||||
Address = address;
|
||||
InvoiceId = invoiceId;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public string Address { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
|
||||
}
|
||||
}
|
||||
}
|
20
BTCPayServer/Events/InvoiceStopWatchedEvent.cs
Normal file
20
BTCPayServer/Events/InvoiceStopWatchedEvent.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStopWatchedEvent
|
||||
{
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} is not monitored anymore.";
|
||||
}
|
||||
}
|
||||
}
|
42
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal file
42
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NBXplorerErrorEvent
|
||||
{
|
||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
||||
{
|
||||
Message = errorMessage;
|
||||
Network = network;
|
||||
}
|
||||
public string Message { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
||||
}
|
||||
}
|
||||
public class NBXplorerStateChangedEvent
|
||||
{
|
||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||
{
|
||||
Network = network;
|
||||
NewState = newState;
|
||||
OldState = old;
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public NBXplorerState NewState { get; set; }
|
||||
public NBXplorerState OldState { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"NBXplorer {Network.CryptoCode}: {OldState} => {NewState}";
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/Events/NewBlockEvent.cs
Normal file
16
BTCPayServer/Events/NewBlockEvent.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NewBlockEvent
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{CryptoCode}: New block";
|
||||
}
|
||||
}
|
||||
}
|
104
BTCPayServer/ExplorerClientProvider.cs
Normal file
104
BTCPayServer/ExplorerClientProvider.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ExplorerClientProvider
|
||||
{
|
||||
BTCPayNetworkProvider _NetworkProviders;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
_Options = options;
|
||||
|
||||
foreach (var setting in options.NBXplorerConnectionSettings)
|
||||
{
|
||||
var cookieFile = setting.CookieFile;
|
||||
if (cookieFile.Trim() == "0" || string.IsNullOrEmpty(cookieFile.Trim()))
|
||||
cookieFile = null;
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Explorer url is {(setting.ExplorerUri.AbsoluteUri ?? "not set")}");
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
explorer.SetNoAuth();
|
||||
}
|
||||
if(!explorer.SetCookieAuth(cookieFile))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
|
||||
}
|
||||
return explorer;
|
||||
}
|
||||
|
||||
Dictionary<string, ExplorerClient> _Clients = new Dictionary<string, ExplorerClient>();
|
||||
|
||||
public ExplorerClient GetExplorerClient(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
|
||||
return client;
|
||||
}
|
||||
|
||||
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return GetExplorerClient(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return IsAvailable(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
if (_Clients.ContainsKey(network.CryptoCode))
|
||||
return network;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
|
||||
{
|
||||
foreach (var net in _NetworkProviders.GetAll())
|
||||
{
|
||||
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
|
||||
{
|
||||
yield return (net, explorer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,12 +17,49 @@ using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using System.IO;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this ExplorerClient client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
}
|
||||
public static async Task CloseSocket(this WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
var transactions = hashes
|
||||
@ -33,7 +70,7 @@ namespace BTCPayServer
|
||||
}
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/"))
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
|
364
BTCPayServer/HostedServices/InvoiceNotificationManager.cs
Normal file
364
BTCPayServer/HostedServices/InvoiceNotificationManager.cs
Normal file
@ -0,0 +1,364 @@
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire.Annotations;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceNotificationManager : IHostedService
|
||||
{
|
||||
public static HttpClient _Client = new HttpClient();
|
||||
|
||||
public class ScheduledJob
|
||||
{
|
||||
public int TryCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceEntity Invoice
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IBackgroundJobClient _JobClient;
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
var jobId = GetHttpJobId(job.Invoice);
|
||||
|
||||
if (!_Executing.TryAdd(jobId, jobId))
|
||||
return; //For some reason, Hangfire fire the job several time
|
||||
|
||||
Logger.LogInformation("Running " + jobId);
|
||||
bool reschedule = false;
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " timed out");
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
reschedule = true;
|
||||
|
||||
List<string> messages = new List<string>();
|
||||
while (ex != null)
|
||||
{
|
||||
messages.Add(ex.Message);
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if (job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
|
||||
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoicePaymentNotificationEvent
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
public class InvoicePaymentNotificationEventWrapper
|
||||
{
|
||||
[JsonProperty("event")]
|
||||
public InvoicePaymentNotificationEvent Event { get; set; }
|
||||
[JsonProperty("data")]
|
||||
public InvoicePaymentNotification Data { get; set; }
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
InvoicePaymentNotification notification = new InvoicePaymentNotification()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Currency = dto.Currency,
|
||||
CurrentTime = dto.CurrentTime,
|
||||
ExceptionStatus = dto.ExceptionStatus,
|
||||
ExpirationTime = dto.ExpirationTime,
|
||||
InvoiceTime = dto.InvoiceTime,
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
notification.Url = dto.Url;
|
||||
notification.BTCDue = dto.BTCDue;
|
||||
notification.BTCPaid = dto.BTCPaid;
|
||||
notification.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
string notificationString = null;
|
||||
if (eventCode.HasValue)
|
||||
{
|
||||
var wrapper = new InvoicePaymentNotificationEventWrapper();
|
||||
wrapper.Data = notification;
|
||||
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
|
||||
notificationString = JsonConvert.SerializeObject(wrapper);
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationString = JsonConvert.SerializeObject(notification);
|
||||
}
|
||||
|
||||
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
return response;
|
||||
}
|
||||
|
||||
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Will make sure only one callback is called at once on the same invoiceId
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="sendRequest"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
|
||||
{
|
||||
Task<T> sending = null;
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
|
||||
{
|
||||
var completion = new TaskCompletionSource<T>();
|
||||
sending = completion.Task;
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
executing.ContinueWith(_ =>
|
||||
{
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
sending = sendRequest();
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
}
|
||||
sending.ContinueWith(o =>
|
||||
{
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
return await sending;
|
||||
}
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
{
|
||||
return $"{invoice.Id}-{invoice.Status}-HTTP";
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
)
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task SaveEvent(string invoiceId, object evt)
|
||||
{
|
||||
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
341
BTCPayServer/HostedServices/InvoiceWatcher.cs
Normal file
341
BTCPayServer/HostedServices/InvoiceWatcher.cs
Normal file
@ -0,0 +1,341 @@
|
||||
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;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceWatcher : IHostedService
|
||||
{
|
||||
class UpdateInvoiceContext
|
||||
{
|
||||
public UpdateInvoiceContext(InvoiceEntity invoice)
|
||||
{
|
||||
Invoice = invoice;
|
||||
}
|
||||
public InvoiceEntity Invoice { get; set; }
|
||||
public List<object> Events { get; set; } = new List<object>();
|
||||
|
||||
bool _Dirty = false;
|
||||
public void MarkDirty()
|
||||
{
|
||||
_Dirty = true;
|
||||
}
|
||||
|
||||
public bool Dirty => _Dirty;
|
||||
}
|
||||
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceWatcher(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
||||
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider);
|
||||
if (paymentMethod == null)
|
||||
return;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
if (accounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
PaymentMethod result = null;
|
||||
accounting = null;
|
||||
decimal nearestToZero = 0.0m;
|
||||
foreach (var paymentMethod in allPaymentMethods)
|
||||
{
|
||||
if (networkProvider != null && networkProvider.GetNetwork(paymentMethod.GetId().CryptoCode) == null)
|
||||
continue;
|
||||
var currentAccounting = paymentMethod.Calculate();
|
||||
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
|
||||
if (result == null || distanceFromZero < nearestToZero)
|
||||
{
|
||||
result = paymentMethod;
|
||||
nearestToZero = distanceFromZero;
|
||||
accounting = currentAccounting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (invoice.ExpirationTime > now)
|
||||
{
|
||||
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
now = DateTimeOffset.UtcNow;
|
||||
if (invoice.MonitoringExpiration > now)
|
||||
{
|
||||
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
Task _Loop;
|
||||
Task _WaitingInvoices;
|
||||
CancellationTokenSource _Cts;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
_WaitingInvoices = WaitPendingInvoices();
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
_WaitingInvoices = null;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
try
|
||||
{
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
return Task.WhenAll(waitingPendingInvoices, _Loop);
|
||||
}
|
||||
}
|
||||
}
|
222
BTCPayServer/HostedServices/NBXplorerWaiter.cs
Normal file
222
BTCPayServer/HostedServices/NBXplorerWaiter.cs
Normal file
@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public enum NBXplorerState
|
||||
{
|
||||
NotConnected,
|
||||
Synching,
|
||||
Ready
|
||||
}
|
||||
|
||||
public class NBXplorerDashboard
|
||||
{
|
||||
public class NBXplorerSummary
|
||||
{
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public NBXplorerState State { get; set; }
|
||||
public StatusResult Status { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
ConcurrentDictionary<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
|
||||
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status, string error)
|
||||
{
|
||||
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status, Error = error };
|
||||
_Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary);
|
||||
}
|
||||
|
||||
public bool IsFullySynched()
|
||||
{
|
||||
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public bool IsFullySynched(string cryptoCode)
|
||||
{
|
||||
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
}
|
||||
}
|
||||
|
||||
public class NBXplorerWaiters : IHostedService
|
||||
{
|
||||
List<NBXplorerWaiter> _Waiters = new List<NBXplorerWaiter>();
|
||||
public NBXplorerWaiters(NBXplorerDashboard dashboard, ExplorerClientProvider explorerClientProvider, EventAggregator eventAggregator)
|
||||
{
|
||||
foreach (var explorer in explorerClientProvider.GetAll())
|
||||
{
|
||||
_Waiters.Add(new NBXplorerWaiter(dashboard, explorer.Item1, explorer.Item2, eventAggregator));
|
||||
}
|
||||
}
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.WhenAll(_Waiters.Select(w => w.StartAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.WhenAll(_Waiters.Select(w => w.StopAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class NBXplorerWaiter : IHostedService
|
||||
{
|
||||
|
||||
public NBXplorerWaiter(NBXplorerDashboard dashboard, BTCPayNetwork network, ExplorerClient client, EventAggregator aggregator)
|
||||
{
|
||||
_Network = network;
|
||||
_Client = client;
|
||||
_Aggregator = aggregator;
|
||||
_Dashboard = dashboard;
|
||||
}
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayNetwork _Network;
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClient _Client;
|
||||
|
||||
CancellationTokenSource _Cts;
|
||||
Task _Loop;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
|
||||
try
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (await StepAsync(cancellation))
|
||||
{
|
||||
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync(CancellationToken cancellation)
|
||||
{
|
||||
var oldState = State;
|
||||
string error = null;
|
||||
StatusResult status = null;
|
||||
try
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
|
||||
|
||||
if(status == null && error == null)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer does not support this cryptocurrency";
|
||||
|
||||
if(status != null && error == null)
|
||||
{
|
||||
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
status = null;
|
||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
||||
}
|
||||
|
||||
_Dashboard.Publish(_Network, State, status, error);
|
||||
if (oldState != State)
|
||||
{
|
||||
if (State == NBXplorerState.Synching)
|
||||
{
|
||||
PollInterval = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
else
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
public NBXplorerState State { get; private set; }
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return _Loop;
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@ using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -102,11 +104,11 @@ namespace BTCPayServer.Hosting
|
||||
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return new InvoiceRepository(dbContext, dbpath, opts.Network);
|
||||
return new InvoiceRepository(dbContext, dbpath);
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -124,49 +126,46 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
return dbContext;
|
||||
});
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWallet>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
BlockTarget = 20,
|
||||
ExplorerClient = o.GetRequiredService<ExplorerClient>()
|
||||
});
|
||||
|
||||
services.TryAddSingleton<NBXplorerWaiterAccessor>();
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiter>();
|
||||
services.TryAddSingleton<ExplorerClient>(o =>
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
var explorer = new ExplorerClient(opts.Network, opts.Explorer);
|
||||
if (!explorer.SetCookieAuth(opts.CookieFile))
|
||||
explorer.SetNoAuth();
|
||||
return explorer;
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main)
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
|
||||
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<IRateProvider>(o =>
|
||||
{
|
||||
var coinaverage = new CoinAverageRateProvider();
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
return new CachedRateProvider(new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay }), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
|
||||
});
|
||||
|
||||
services.TryAddSingleton<InvoiceNotificationManager>();
|
||||
services.TryAddSingleton<IRateProviderFactory, CachedDefaultRateProviderFactory>();
|
||||
|
||||
services.TryAddSingleton<InvoiceWatcherAccessor>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<CallbackController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
@ -184,6 +183,18 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
});
|
||||
|
||||
// bundling
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
var bundle = new BundleOptions();
|
||||
bundle.UseMinifiedFiles = opts.BundleJsCss;
|
||||
bundle.AppendVersion = true;
|
||||
return bundle;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -197,8 +208,9 @@ namespace BTCPayServer.Hosting
|
||||
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
|
||||
});
|
||||
}
|
||||
|
||||
app.UseMiddleware<BTCPayMiddleware>();
|
||||
return app;
|
||||
return app;
|
||||
}
|
||||
|
||||
static void Retry(Action act)
|
||||
@ -211,11 +223,9 @@ namespace BTCPayServer.Hosting
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch
|
||||
catch when(!cts.IsCancellationRequested)
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
throw;
|
||||
Thread.Sleep(1000);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -29,31 +30,21 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
RequestDelegate _Next;
|
||||
CallbackController _CallbackController;
|
||||
BTCPayServerOptions _Options;
|
||||
private NBXplorerWaiterAccessor _NbxplorerAwaiter;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
BTCPayServerOptions options,
|
||||
NBXplorerWaiterAccessor nbxplorerAwaiter,
|
||||
CallbackController callbackController)
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_CallbackController = callbackController;
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_NbxplorerAwaiter = (nbxplorerAwaiter ?? throw new ArgumentNullException(nameof(nbxplorerAwaiter)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool _Registered;
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
RewriteHostIfNeeded(httpContext);
|
||||
await EnsureBlockCallbackRegistered(httpContext);
|
||||
|
||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||
var sig = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||
@ -79,7 +70,7 @@ namespace BTCPayServer.Hosting
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var bitid = new BitIdentity(key);
|
||||
httpContext.User = new GenericPrincipal(bitid, new string[0]);
|
||||
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
|
||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
|
||||
}
|
||||
}
|
||||
@ -92,6 +83,8 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{ }
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
|
||||
@ -109,39 +102,73 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
reverseProxyScheme = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
ushort? reverseProxyPort = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
reverseProxyPort = pp;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
{
|
||||
ushort? p = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = scheme;
|
||||
if (scheme == "http")
|
||||
p = 80;
|
||||
if (scheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
p = pp;
|
||||
}
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
@ -154,28 +181,6 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureBlockCallbackRegistered(HttpContext httpContext)
|
||||
{
|
||||
if (!_Registered)
|
||||
{
|
||||
var callback = await _CallbackController.GetCallbackBlockUriAsync(httpContext.Request);
|
||||
var unused = _NbxplorerAwaiter.Instance.WhenReady(async c =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _CallbackController.RegisterCallbackBlockUriAsync(callback);
|
||||
Logs.PayServer.LogInformation($"Registering block callback to " + callback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Could not register block callback");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
_Registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = ex.StatusCode;
|
||||
|
@ -6,6 +6,7 @@ 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;
|
||||
@ -36,6 +37,9 @@ using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -79,6 +83,24 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
});
|
||||
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequiredLength = 7;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
});
|
||||
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
//{
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// {
|
||||
// l.UseHttps("devtest.pfx", "toto");
|
||||
// });
|
||||
//});
|
||||
}
|
||||
|
||||
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||
@ -144,6 +166,7 @@ namespace BTCPayServer.Hosting
|
||||
app.UseAuthentication();
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
|
||||
app.UseWebSockets();
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(
|
||||
|
40
BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs
Normal file
40
BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class LightMoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
reader.TokenType == JsonToken.Integer ? new LightMoney((long)reader.Value) :
|
||||
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||
: null;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,13 +6,18 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
public class CustomConsoleLogProvider : ILoggerProvider
|
||||
{
|
||||
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
|
||||
ConsoleLoggerProcessor _Processor;
|
||||
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
|
||||
{
|
||||
_Processor = processor;
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
|
||||
@ -329,7 +334,8 @@ namespace BTCPayServer.Logging
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueue,
|
||||
this,
|
||||
TaskCreationOptions.LongRunning);
|
||||
default(CancellationToken),
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||
|
@ -17,6 +17,7 @@ namespace BTCPayServer.Logging
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
@ -26,6 +27,12 @@ namespace BTCPayServer.Logging
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public static ILogger Events
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public const int ColumnLength = 16;
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Name",
|
||||
@ -30,11 +30,6 @@ namespace BTCPayServer.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
private bool SupportDropColumn(string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
481
BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs
generated
Normal file
481
BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs
generated
Normal file
@ -0,0 +1,481 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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("20171221054550_AltcoinSupport")]
|
||||
partial class AltcoinSupport
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
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.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
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.Property<string>("Role");
|
||||
|
||||
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<bool>("RequiresEmailConfirmation");
|
||||
|
||||
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.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs
Normal file
24
BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class AltcoinSupport : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CryptoCode",
|
||||
table: "HistoricalAddressInvoices",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CryptoCode",
|
||||
table: "HistoricalAddressInvoices");
|
||||
}
|
||||
}
|
||||
}
|
483
BTCPayServer/Migrations/20180106095215_DerivationStrategies.Designer.cs
generated
Normal file
483
BTCPayServer/Migrations/20180106095215_DerivationStrategies.Designer.cs
generated
Normal file
@ -0,0 +1,483 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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("20180106095215_DerivationStrategies")]
|
||||
partial class DerivationStrategies
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
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.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
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.Property<string>("Role");
|
||||
|
||||
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<bool>("RequiresEmailConfirmation");
|
||||
|
||||
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.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class DerivationStrategies : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DerivationStrategies",
|
||||
table: "Stores",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DerivationStrategies",
|
||||
table: "Stores");
|
||||
}
|
||||
}
|
||||
}
|
485
BTCPayServer/Migrations/20180109021122_defaultcrypto.Designer.cs
generated
Normal file
485
BTCPayServer/Migrations/20180109021122_defaultcrypto.Designer.cs
generated
Normal file
@ -0,0 +1,485 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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("20180109021122_defaultcrypto")]
|
||||
partial class defaultcrypto
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
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.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
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.Property<string>("Role");
|
||||
|
||||
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<bool>("RequiresEmailConfirmation");
|
||||
|
||||
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.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Migrations/20180109021122_defaultcrypto.cs
Normal file
24
BTCPayServer/Migrations/20180109021122_defaultcrypto.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class defaultcrypto : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DefaultCrypto",
|
||||
table: "Stores",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultCrypto",
|
||||
table: "Stores");
|
||||
}
|
||||
}
|
||||
}
|
508
BTCPayServer/Migrations/20180114123253_events.Designer.cs
generated
Normal file
508
BTCPayServer/Migrations/20180114123253_events.Designer.cs
generated
Normal file
@ -0,0 +1,508 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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("20180114123253_events")]
|
||||
partial class events
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
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.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
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.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
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.Property<string>("Role");
|
||||
|
||||
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<bool>("RequiresEmailConfirmation");
|
||||
|
||||
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.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Migrations/20180114123253_events.cs
Normal file
38
BTCPayServer/Migrations/20180114123253_events.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class events : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InvoiceEvents",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceDataId = table.Column<string>(nullable: false),
|
||||
UniqueId = table.Column<string>(nullable: false),
|
||||
Message = table.Column<string>(nullable: true),
|
||||
Timestamp = table.Column<DateTimeOffset>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
|
||||
column: x => x.InvoiceDataId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "InvoiceEvents");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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
|
||||
@ -18,7 +15,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -44,6 +41,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
@ -79,6 +78,21 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -188,6 +202,10 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
@ -382,7 +400,7 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany()
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
@ -401,6 +419,14 @@ namespace BTCPayServer.Migrations
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -41,6 +42,7 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
|
||||
[JsonProperty("url")]
|
||||
[Obsolete("Use CryptoInfo.Url instead")]
|
||||
public string Url
|
||||
{
|
||||
get; set;
|
||||
@ -59,6 +61,7 @@ namespace BTCPayServer.Models
|
||||
}
|
||||
//"btcPrice":"0.001157"
|
||||
[JsonProperty("btcPrice")]
|
||||
[Obsolete("Use CryptoInfo.Price instead")]
|
||||
public string BTCPrice
|
||||
{
|
||||
get; set;
|
||||
@ -66,11 +69,15 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"btcDue":"0.001160"
|
||||
[JsonProperty("btcDue")]
|
||||
[Obsolete("Use CryptoInfo.Due instead")]
|
||||
public string BTCDue
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty("cryptoInfo")]
|
||||
public List<NBitpayClient.InvoiceCryptoInfo> CryptoInfo { get; set; }
|
||||
|
||||
//"price":5
|
||||
[JsonProperty("price")]
|
||||
public double Price
|
||||
@ -87,6 +94,7 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"exRates":{"USD":4320.02}
|
||||
[JsonProperty("exRates")]
|
||||
[Obsolete("Use CryptoInfo.ExRates instead")]
|
||||
public Dictionary<string, double> ExRates
|
||||
{
|
||||
get; set;
|
||||
@ -156,6 +164,7 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"btcPaid":"0.000000"
|
||||
[JsonProperty("btcPaid")]
|
||||
[Obsolete("Use CryptoInfo.Paid instead")]
|
||||
public string BTCPaid
|
||||
{
|
||||
get; set;
|
||||
@ -163,7 +172,8 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"rate":4320.02
|
||||
[JsonProperty("rate")]
|
||||
public double Rate
|
||||
[Obsolete("Use CryptoInfo.Rate instead")]
|
||||
public decimal Rate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -178,6 +188,7 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"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")]
|
||||
[Obsolete("Use CryptoInfo.PaymentsUrls instead")]
|
||||
public NBitpayClient.InvoicePaymentUrls PaymentUrls
|
||||
{
|
||||
get; set;
|
||||
@ -197,6 +208,7 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
|
||||
[JsonProperty("bitcoinAddress")]
|
||||
[Obsolete("Use CryptoInfo.Address instead")]
|
||||
public string BitcoinAddress
|
||||
{
|
||||
get; set;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
@ -9,9 +10,19 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class InvoiceDetailsModel
|
||||
{
|
||||
public class CryptoPayment
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string Due { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
}
|
||||
public class Payment
|
||||
{
|
||||
public int Confirmations
|
||||
public string CryptoCode { get; set; }
|
||||
public string Confirmations
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -37,6 +48,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Replaced { get; set; }
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
@ -48,16 +61,18 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<Payment> Payments
|
||||
public List<CryptoPayment> CryptoPayments
|
||||
{
|
||||
get; set;
|
||||
} = new List<Payment>();
|
||||
} = new List<CryptoPayment>();
|
||||
|
||||
public List<Payment> Payments { get; set; } = new List<Payment>();
|
||||
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StatusException { get; set; }
|
||||
public DateTimeOffset CreatedDate
|
||||
{
|
||||
get; set;
|
||||
@ -82,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string TransactionSpeed { get; set; }
|
||||
public object StoreName
|
||||
{
|
||||
get;
|
||||
@ -92,55 +109,25 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public double Rate
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string NotificationUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public string RedirectUrl { get; set; }
|
||||
public string Fiat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTC
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTCDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTCPaid
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public String NetworkFee
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public BitcoinAddress BitcoinAddress
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string PaymentUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public HistoricalAddressInvoiceData[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +33,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
|
||||
public class InvoiceModel
|
||||
{
|
||||
public DateTimeOffset Date
|
||||
public string Date
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
|
@ -7,6 +7,14 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class PaymentModel
|
||||
{
|
||||
public class AvailableCrypto
|
||||
{
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string BtcAddress { get; set; }
|
||||
@ -22,14 +30,18 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string ItemDesc { get; set; }
|
||||
public string TimeLeft { get; set; }
|
||||
public string Rate { get; set; }
|
||||
public string BtcAmount { get; set; }
|
||||
public string TxFees { get; set; }
|
||||
public string OrderAmount { get; set; }
|
||||
public string InvoiceBitcoinUrl { get; set; }
|
||||
public string BtcTotalDue { get; set; }
|
||||
public string InvoiceBitcoinUrlQR { get; set; }
|
||||
public int TxCount { get; set; }
|
||||
public string BtcPaid { get; set; }
|
||||
public string StoreEmail { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string NetworkFeeDescription { get; internal set; }
|
||||
public int MaxTimeMinutes { get; internal set; }
|
||||
public string PaymentType { get; internal set; }
|
||||
public string PaymentMethodId { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class DerivationSchemeViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public DerivationSchemeViewModel()
|
||||
{
|
||||
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
|
||||
DerivationSchemeFormat = btcPay.Value;
|
||||
DerivationSchemeFormats = new SelectList(new Format[]
|
||||
{
|
||||
btcPay,
|
||||
new Format { Name = "Electrum", Value = "Electrum" },
|
||||
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
|
||||
}
|
||||
public string DerivationScheme
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<(string KeyPath, string Address)> AddressSamples
|
||||
{
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
[Display(Name = "Derivation Scheme format")]
|
||||
public string DerivationSchemeFormat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Confirmation { get; set; }
|
||||
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList DerivationSchemeFormats { get; set; }
|
||||
|
||||
public string ServerUrl { get; set; }
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class LightningNodeViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
[Display(Name = "Lightning charge url")]
|
||||
public string Url
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
public string InternalLightningNode { get; internal set; }
|
||||
|
||||
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
|
||||
{
|
||||
var choices = networkProvider.GetAll()
|
||||
.Where(n => n.CLightningNetworkName != null)
|
||||
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
public class DerivationScheme
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
@ -18,14 +23,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
}
|
||||
public StoreViewModel()
|
||||
{
|
||||
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
|
||||
DerivationSchemeFormat = btcPay.Value;
|
||||
DerivationSchemeFormats = new SelectList(new Format[]
|
||||
{
|
||||
btcPay,
|
||||
new Format { Name = "Electrum", Value = "Electrum" },
|
||||
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
|
||||
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
[Display(Name = "Store Name")]
|
||||
[Required]
|
||||
@ -45,21 +45,36 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
public string DerivationScheme
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get; set;
|
||||
get
|
||||
{
|
||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Derivation Scheme format")]
|
||||
public string DerivationSchemeFormat
|
||||
[Display(Name = "Multiply the original rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public SelectList DerivationSchemeFormats { get; set; }
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 31)]
|
||||
public int InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
|
||||
[Range(10, 60 * 24 * 31)]
|
||||
public int MonitoringExpiration
|
||||
{
|
||||
@ -79,14 +94,31 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<(string KeyPath, string Address)> AddressSamples
|
||||
{
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
|
||||
[Display(Name = "Default crypto currency on checkout")]
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
|
||||
public class LightningNode
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
}
|
||||
public List<LightningNode> LightningNodes
|
||||
{
|
||||
get; set;
|
||||
} = new List<LightningNode>();
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == defaultCrypto) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultCryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Money Balance
|
||||
public string[] Balances
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal file
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class WalletModel
|
||||
{
|
||||
public string ServerUrl { get; set; }
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
1114
BTCPayServer/MultiValueDictionary.cs
Normal file
1114
BTCPayServer/MultiValueDictionary.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,184 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class NBXplorerWaiterAccessor
|
||||
{
|
||||
public NBXplorerWaiter Instance { get; set; }
|
||||
}
|
||||
public enum NBXplorerState
|
||||
{
|
||||
NotConnected,
|
||||
Synching,
|
||||
Ready
|
||||
}
|
||||
public class NBXplorerWaiter : IHostedService
|
||||
{
|
||||
public NBXplorerWaiter(ExplorerClient client, NBXplorerWaiterAccessor accessor)
|
||||
{
|
||||
_Client = client;
|
||||
accessor.Instance = this;
|
||||
}
|
||||
|
||||
ExplorerClient _Client;
|
||||
Timer _Timer;
|
||||
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Callback(object state)
|
||||
{
|
||||
if (!_Idle.IsSet)
|
||||
return;
|
||||
try
|
||||
{
|
||||
_Idle.Reset();
|
||||
CheckStatus().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Idle.Set();
|
||||
}
|
||||
}
|
||||
|
||||
async Task CheckStatus()
|
||||
{
|
||||
while (await StepAsync())
|
||||
{
|
||||
|
||||
}
|
||||
List<Task> tasks = new List<Task>();
|
||||
if (State == NBXplorerState.Ready)
|
||||
{
|
||||
while (_WhenReady.TryDequeue(out Func<ExplorerClient, Task> act))
|
||||
{
|
||||
tasks.Add(act(_Client));
|
||||
}
|
||||
}
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync()
|
||||
{
|
||||
var oldState = State;
|
||||
|
||||
StatusResult status = null;
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched())
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched())
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched())
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
LastStatus = status;
|
||||
if (oldState != State)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"NBXplorerWaiter status changed: {oldState} => {State}");
|
||||
}
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
public Task<T> WhenReady<T>(Func<ExplorerClient, Task<T>> act)
|
||||
{
|
||||
if (State == NBXplorerState.Ready)
|
||||
return act(_Client);
|
||||
TaskCompletionSource<T> completion = new TaskCompletionSource<T>();
|
||||
_WhenReady.Enqueue(async client =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await act(client);
|
||||
completion.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completion.SetException(ex);
|
||||
}
|
||||
});
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
ConcurrentQueue<Func<ExplorerClient, Task>> _WhenReady = new ConcurrentQueue<Func<ExplorerClient, Task>>();
|
||||
|
||||
private async Task<StatusResult> GetStatusWithTimeout()
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
using (cts)
|
||||
{
|
||||
var cancellation = cts.Token;
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
|
||||
return status;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public NBXplorerState State { get; private set; }
|
||||
|
||||
public StatusResult LastStatus { get; private set; }
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer.Dispose();
|
||||
_Timer = null;
|
||||
_Idle.Wait();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public class BitcoinLikeOnChainPaymentMethod : IPaymentMethodDetails
|
||||
{
|
||||
public PaymentTypes GetPaymentType()
|
||||
{
|
||||
return PaymentTypes.BTCLike;
|
||||
}
|
||||
|
||||
public string GetPaymentDestination()
|
||||
{
|
||||
return DepositAddress?.ToString();
|
||||
}
|
||||
|
||||
public decimal GetTxFee()
|
||||
{
|
||||
return TxFee.ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public void SetNoTxFee()
|
||||
{
|
||||
TxFee = Money.Zero;
|
||||
}
|
||||
|
||||
|
||||
public void SetPaymentDestination(string newPaymentDestination)
|
||||
{
|
||||
if (newPaymentDestination == null)
|
||||
DepositAddress = null;
|
||||
else
|
||||
DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network);
|
||||
}
|
||||
|
||||
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
|
||||
[JsonIgnore]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
[JsonIgnore]
|
||||
public Money TxFee { get; set; }
|
||||
[JsonIgnore]
|
||||
public BitcoinAddress DepositAddress { get; set; }
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
78
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs
Normal file
78
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
|
||||
public class BitcoinLikePaymentData : CryptoPaymentData
|
||||
{
|
||||
public PaymentTypes GetPaymentType()
|
||||
{
|
||||
return PaymentTypes.BTCLike;
|
||||
}
|
||||
public BitcoinLikePaymentData()
|
||||
{
|
||||
|
||||
}
|
||||
public BitcoinLikePaymentData(Coin coin, bool rbf)
|
||||
{
|
||||
Outpoint = coin.Outpoint;
|
||||
Output = coin.TxOut;
|
||||
ConfirmationCount = 0;
|
||||
RBF = rbf;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
[JsonIgnore]
|
||||
public TxOut Output { get; set; }
|
||||
public int ConfirmationCount { get; set; }
|
||||
public bool RBF { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
|
||||
/// </summary>
|
||||
public bool Legacy { get; set; }
|
||||
|
||||
public string GetPaymentId()
|
||||
{
|
||||
return Outpoint.ToString();
|
||||
}
|
||||
|
||||
public string[] GetSearchTerms()
|
||||
{
|
||||
return new[] { Outpoint.Hash.ToString() };
|
||||
}
|
||||
|
||||
public decimal GetValue()
|
||||
{
|
||||
return Output.Value.ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
|
||||
{
|
||||
return ConfirmationCount >= network.MaxTrackedConfirmation;
|
||||
}
|
||||
|
||||
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
|
||||
{
|
||||
if (speedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 1 || !RBF;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 1;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 6;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
46
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs
Normal file
46
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase<DerivationStrategy>
|
||||
{
|
||||
ExplorerClientProvider _ExplorerProvider;
|
||||
private IFeeProviderFactory _FeeRateProviderFactory;
|
||||
private Services.Wallets.BTCPayWalletProvider _WalletProvider;
|
||||
|
||||
public BitcoinLikePaymentHandler(ExplorerClientProvider provider,
|
||||
IFeeProviderFactory feeRateProviderFactory,
|
||||
Services.Wallets.BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
_ExplorerProvider = provider;
|
||||
this._FeeRateProviderFactory = feeRateProviderFactory;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
|
||||
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
onchainMethod.FeeRate = await getFeeRate;
|
||||
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
onchainMethod.DepositAddress = await getAddress;
|
||||
return onchainMethod;
|
||||
}
|
||||
|
||||
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
|
||||
}
|
||||
}
|
||||
}
|
404
BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs
Normal file
404
BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs
Normal file
@ -0,0 +1,404 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using System.Collections.Concurrent;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
/// <summary>
|
||||
/// This class listener NBXplorer instances to detect incoming on-chain, bitcoin like payment
|
||||
/// </summary>
|
||||
public class NBXplorerListener : IHostedService
|
||||
{
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
IApplicationLifetime _Lifetime;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
private TaskCompletionSource<bool> _RunningTask;
|
||||
private CancellationTokenSource _Cts;
|
||||
BTCPayWalletProvider _Wallets;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public NBXplorerListener(ExplorerClientProvider explorerClients,
|
||||
BTCPayWalletProvider wallets,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
EventAggregator aggregator, IApplicationLifetime lifetime)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Wallets = wallets;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_ExplorerClients = explorerClients;
|
||||
_Aggregator = aggregator;
|
||||
_Lifetime = lifetime;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
ConcurrentDictionary<string, NotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, NotificationSession>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_ListenPoller != null)
|
||||
{
|
||||
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
|
||||
{
|
||||
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
|
||||
{
|
||||
var wallet = _Wallets.GetWallet(nbxplorerEvent.Network);
|
||||
if (_Wallets.IsAvailable(wallet.Network))
|
||||
{
|
||||
await Listen(wallet);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
_ListenPoller = new Timer(async s =>
|
||||
{
|
||||
foreach (var wallet in _Wallets.GetWallets())
|
||||
{
|
||||
if (_Wallets.IsAvailable(wallet.Network))
|
||||
{
|
||||
await Listen(wallet);
|
||||
}
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
|
||||
await Task.WhenAll(invoice.GetSupportedPaymentMethod<DerivationStrategy>(_NetworkProvider)
|
||||
.Select(s => (Session: _SessionsByCryptoCode.TryGet(s.PaymentId.CryptoCode),
|
||||
DerivationStrategy: s.DerivationStrategyBase))
|
||||
.Where(s => s.Session != null)
|
||||
.Select(s => s.Session.ListenDerivationSchemesAsync(new[] { s.DerivationStrategy }))
|
||||
.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Listen(BTCPayWallet wallet)
|
||||
{
|
||||
var network = wallet.Network;
|
||||
bool cleanup = false;
|
||||
try
|
||||
{
|
||||
if (_SessionsByCryptoCode.ContainsKey(network.CryptoCode))
|
||||
return;
|
||||
var client = _ExplorerClients.GetExplorerClient(network);
|
||||
if (client == null)
|
||||
return;
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
|
||||
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
cleanup = true;
|
||||
|
||||
using (session)
|
||||
{
|
||||
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
|
||||
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
|
||||
int paymentCount = await FindPaymentViaPolling(wallet, network);
|
||||
Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");
|
||||
|
||||
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
|
||||
switch (newEvent)
|
||||
{
|
||||
case NBXplorer.Models.NewBlockEvent evt:
|
||||
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(invoiceId => UpdatePaymentStates(wallet, invoiceId))
|
||||
.ToArray());
|
||||
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
|
||||
break;
|
||||
case NBXplorer.Models.NewTransactionEvent evt:
|
||||
wallet.InvalidateCache(evt.DerivationStrategy);
|
||||
foreach (var output in evt.Outputs)
|
||||
{
|
||||
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
|
||||
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
|
||||
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
|
||||
if (invoice != null)
|
||||
{
|
||||
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
|
||||
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
||||
if (!alreadyExist)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdatePaymentStates(wallet, invoice.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (cleanup)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out NotificationSession unused);
|
||||
if (_SessionsByCryptoCode.Count == 0 && _Cts.IsCancellationRequested)
|
||||
{
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
|
||||
{
|
||||
return invoice.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
|
||||
}
|
||||
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
||||
.Select(p => p.Outpoint.Hash)
|
||||
.ToArray());
|
||||
var conflicts = GetConflicts(transactions.Select(t => t.Value));
|
||||
foreach (var payment in invoice.GetPayments(wallet.Network))
|
||||
{
|
||||
if (payment.GetpaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
||||
continue;
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
|
||||
continue;
|
||||
var txId = tx.Transaction.GetHash();
|
||||
var txConflict = conflicts.GetConflict(txId);
|
||||
var accounted = txConflict == null || txConflict.IsWinner(txId);
|
||||
|
||||
bool updated = false;
|
||||
if (accounted != payment.Accounted)
|
||||
{
|
||||
updated = true;
|
||||
payment.Accounted = accounted;
|
||||
}
|
||||
|
||||
if (paymentData.ConfirmationCount != tx.Confirmations)
|
||||
{
|
||||
if (wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
|
||||
{
|
||||
paymentData.ConfirmationCount = tx.Confirmations;
|
||||
payment.SetCryptoPaymentData(paymentData);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated)
|
||||
updatedPaymentEntities.Add(payment);
|
||||
}
|
||||
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
|
||||
if (updatedPaymentEntities.Count != 0)
|
||||
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
|
||||
return invoice;
|
||||
}
|
||||
|
||||
class TransactionConflict
|
||||
{
|
||||
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
|
||||
|
||||
|
||||
uint256 _Winner;
|
||||
public bool IsWinner(uint256 txId)
|
||||
{
|
||||
if (_Winner == null)
|
||||
{
|
||||
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
|
||||
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
|
||||
{
|
||||
_Winner = confirmed.Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
|
||||
_Winner = Transactions
|
||||
.OrderByDescending(t => t.Value.Timestamp)
|
||||
.First()
|
||||
.Key;
|
||||
}
|
||||
}
|
||||
return _Winner == txId;
|
||||
}
|
||||
}
|
||||
class TransactionConflicts : List<TransactionConflict>
|
||||
{
|
||||
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TransactionConflict GetConflict(uint256 txId)
|
||||
{
|
||||
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
|
||||
}
|
||||
}
|
||||
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
|
||||
{
|
||||
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
var hash = tx.Transaction.GetHash();
|
||||
foreach (var input in tx.Transaction.Inputs)
|
||||
{
|
||||
TransactionConflict conflict = new TransactionConflict();
|
||||
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
|
||||
{
|
||||
conflict = conflictsByOutpoint[input.PrevOut];
|
||||
}
|
||||
if (!conflict.Transactions.ContainsKey(hash))
|
||||
conflict.Transactions.Add(hash, tx);
|
||||
}
|
||||
}
|
||||
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
|
||||
}
|
||||
|
||||
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
|
||||
{
|
||||
int totalPayment = 0;
|
||||
var invoices = await _InvoiceRepository.GetPendingInvoices();
|
||||
foreach (var invoiceId in invoices)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
|
||||
var strategy = GetDerivationStrategy(invoice, network);
|
||||
if (strategy == null)
|
||||
continue;
|
||||
var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
if (!invoice.Support(cryptoId))
|
||||
continue;
|
||||
var coins = (await wallet.GetUnspentCoins(strategy))
|
||||
.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + cryptoId))
|
||||
.ToArray();
|
||||
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
|
||||
{
|
||||
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
return totalPayment;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(InvoiceEntity invoice, BTCPayNetwork network)
|
||||
{
|
||||
return invoice.GetSupportedPaymentMethod<DerivationStrategy>(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), _ExplorerClients.NetworkProviders)
|
||||
.Select(d => d.DerivationStrategyBase)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
{
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
btc.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address;
|
||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
|
||||
{
|
||||
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
|
||||
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var strategy = GetDerivationStrategy(invoice, network);
|
||||
if (strategy != null)
|
||||
strategies.Add(strategy);
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
32
BTCPayServer/Payments/IPaymentMethodDetails.cs
Normal file
32
BTCPayServer/Payments/IPaymentMethodDetails.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent information necessary to track a payment
|
||||
/// </summary>
|
||||
public interface IPaymentMethodDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A string representation of the payment destination
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetPaymentDestination();
|
||||
PaymentTypes GetPaymentType();
|
||||
/// <summary>
|
||||
/// Returns what a merchant would need to pay to cashout this payment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
decimal GetTxFee();
|
||||
void SetNoTxFee();
|
||||
/// <summary>
|
||||
/// Change the payment destination (internal plumbing)
|
||||
/// </summary>
|
||||
/// <param name="newPaymentDestination"></param>
|
||||
void SetPaymentDestination(string newPaymentDestination);
|
||||
}
|
||||
}
|
62
BTCPayServer/Payments/IPaymentMethodHandler.cs
Normal file
62
BTCPayServer/Payments/IPaymentMethodHandler.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
/// <summary>
|
||||
/// This class customize invoice creation by the creation of payment details for the PaymentMethod during invoice creation
|
||||
/// </summary>
|
||||
public interface IPaymentMethodHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the dependencies for a specific payment method are satisfied.
|
||||
/// </summary>
|
||||
/// <param name="supportedPaymentMethod"></param>
|
||||
/// <param name="network"></param>
|
||||
/// <returns>true if this payment method is available</returns>
|
||||
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
/// <summary>
|
||||
/// Create needed to track payments of this invoice
|
||||
/// </summary>
|
||||
/// <param name="supportedPaymentMethod"></param>
|
||||
/// <param name="paymentMethod"></param>
|
||||
/// <param name="network"></param>
|
||||
/// <returns></returns>
|
||||
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
|
||||
}
|
||||
|
||||
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
|
||||
{
|
||||
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
|
||||
}
|
||||
|
||||
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
|
||||
{
|
||||
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
|
||||
|
||||
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (supportedPaymentMethod is T method)
|
||||
{
|
||||
return CreatePaymentMethodDetails(method, paymentMethod, network);
|
||||
}
|
||||
throw new NotSupportedException("Invalid supportedPaymentMethod");
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if(supportedPaymentMethod is T method)
|
||||
{
|
||||
return IsAvailable(method, network);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer/Payments/ISupportedPaymentMethod.cs
Normal file
18
BTCPayServer/Payments/ISupportedPaymentMethod.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represent a mode of payment supported by a store.
|
||||
/// It is stored at the store level and cloned to the invoice during invoice creation.
|
||||
/// This object will be serialized in database in json
|
||||
/// </summary>
|
||||
public interface ISupportedPaymentMethod
|
||||
{
|
||||
PaymentMethodId PaymentId { get; }
|
||||
}
|
||||
}
|
130
BTCPayServer/Payments/Lightning/CLightning/ChargeClient.cs
Normal file
130
BTCPayServer/Payments/Lightning/CLightning/ChargeClient.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class ChargeClient
|
||||
{
|
||||
private Uri _Uri;
|
||||
public Uri Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Uri;
|
||||
}
|
||||
}
|
||||
private Network _Network;
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public ChargeClient(Uri uri, Network network)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
this._Uri = uri;
|
||||
this._Network = network;
|
||||
if (uri.UserInfo == null)
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
||||
var userInfo = uri.UserInfo.Split(':');
|
||||
if (userInfo.Length != 2)
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
||||
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
|
||||
}
|
||||
|
||||
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var message = CreateMessage(HttpMethod.Post, "invoice");
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
|
||||
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||
message.Content = new FormUrlEncodedContent(parameters);
|
||||
var result = await _Client.SendAsync(message, cancellation);
|
||||
result.EnsureSuccessStatusCode();
|
||||
var content = await result.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
|
||||
}
|
||||
|
||||
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var socket = new ClientWebSocket();
|
||||
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
|
||||
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
|
||||
if (!uri.EndsWith('/'))
|
||||
uri += "/";
|
||||
uri += "ws";
|
||||
uri = ToWebsocketUri(uri);
|
||||
await socket.ConnectAsync(new Uri(uri), cancellation);
|
||||
return new ChargeSession(socket);
|
||||
}
|
||||
|
||||
private static string ToWebsocketUri(string uri)
|
||||
{
|
||||
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
|
||||
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
|
||||
return uri;
|
||||
}
|
||||
|
||||
public NetworkCredential Credentials { get; set; }
|
||||
|
||||
public GetInfoResponse GetInfo()
|
||||
{
|
||||
return GetInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<ChargeInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}");
|
||||
var message = await _Client.SendAsync(request, cancellation);
|
||||
if (message.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
message.EnsureSuccessStatusCode();
|
||||
var content = await message.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<ChargeInvoice>(content);
|
||||
}
|
||||
|
||||
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var request = CreateMessage(HttpMethod.Get, "info");
|
||||
var message = await _Client.SendAsync(request, cancellation);
|
||||
message.EnsureSuccessStatusCode();
|
||||
var content = await message.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
|
||||
{
|
||||
var uri = GetFullUri(path);
|
||||
var request = new HttpRequestMessage(method, uri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
|
||||
return request;
|
||||
}
|
||||
|
||||
private string GetBase64Creds()
|
||||
{
|
||||
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
|
||||
}
|
||||
|
||||
private Uri GetFullUri(string partialUrl)
|
||||
{
|
||||
var uri = _Uri.AbsoluteUri;
|
||||
if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
uri += "/";
|
||||
return new Uri(uri + partialUrl);
|
||||
}
|
||||
}
|
||||
}
|
124
BTCPayServer/Payments/Lightning/CLightning/ChargeSession.cs
Normal file
124
BTCPayServer/Payments/Lightning/CLightning/ChargeSession.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class ChargeInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney MilliSatoshi { get; set; }
|
||||
[JsonProperty("paid_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
[JsonProperty("expires_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("payreq")]
|
||||
public string PaymentRequest { get; set; }
|
||||
}
|
||||
public class ChargeSession : IDisposable
|
||||
{
|
||||
private ClientWebSocket socket;
|
||||
|
||||
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
|
||||
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
|
||||
public ChargeSession(ClientWebSocket socket)
|
||||
{
|
||||
this.socket = socket;
|
||||
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
|
||||
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
ArraySegment<byte> _Buffer;
|
||||
public async Task<ChargeInvoice> NextEvent(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var buffer = _Buffer;
|
||||
var array = _Buffer.Array;
|
||||
var originalSize = _Buffer.Array.Length;
|
||||
var newSize = _Buffer.Array.Length;
|
||||
while (true)
|
||||
{
|
||||
var message = await socket.ReceiveAsync(buffer, cancellation);
|
||||
if (message.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
|
||||
break;
|
||||
}
|
||||
if (message.MessageType != WebSocketMessageType.Text)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
|
||||
break;
|
||||
}
|
||||
if (message.EndOfMessage)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
|
||||
try
|
||||
{
|
||||
var o = ParseMessage(buffer);
|
||||
if (newSize != originalSize)
|
||||
{
|
||||
Array.Resize(ref array, originalSize);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.Count - message.Count <= 0)
|
||||
{
|
||||
newSize *= 2;
|
||||
if (newSize > MAX_BUFFER_SIZE)
|
||||
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
|
||||
Array.Resize(ref array, newSize);
|
||||
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
|
||||
}
|
||||
|
||||
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Should never happen");
|
||||
}
|
||||
|
||||
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
|
||||
private ChargeInvoice ParseMessage(ArraySegment<byte> buffer)
|
||||
{
|
||||
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
|
||||
return JsonConvert.DeserializeObject<ChargeInvoice>(str, new JsonSerializerSettings());
|
||||
}
|
||||
|
||||
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
|
||||
{
|
||||
var array = _Buffer.Array;
|
||||
if (array.Length != ORIGINAL_BUFFER_SIZE)
|
||||
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
|
||||
await socket.CloseSocket(status, description, cancellation);
|
||||
throw new WebSocketException($"The socket has been closed ({status}: {description})");
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
await this.socket.CloseSocket();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await this.socket.CloseSocket();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
public LightMoney Amont { get; set; }
|
||||
public TimeSpan Expiry { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class CreateInvoiceResponse
|
||||
{
|
||||
public string PayReq { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
|
||||
public class GetInfoResponse
|
||||
{
|
||||
public class GetInfoAddress
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Port { get; set; }
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public int Port { get; set; }
|
||||
public GetInfoAddress[] Address { get; set; }
|
||||
public string Version { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string Network { get; set; }
|
||||
}
|
||||
}
|
260
BTCPayServer/Payments/Lightning/ChargeListener.cs
Normal file
260
BTCPayServer/Payments/Lightning/ChargeListener.cs
Normal file
@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class ChargeListener : IHostedService
|
||||
{
|
||||
class ListenedInvoice
|
||||
{
|
||||
public LightningLikePaymentMethodDetails PaymentMethodDetails { get; set; }
|
||||
public LightningSupportedPaymentMethod SupportedPaymentMethod { get; set; }
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public string Uri { get; internal set; }
|
||||
public BTCPayNetwork Network { get; internal set; }
|
||||
public string InvoiceId { get; internal set; }
|
||||
}
|
||||
|
||||
EventAggregator _Aggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
public ChargeListener(EventAggregator aggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_Aggregator = aggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
await EnsureListening(inv.InvoiceId, false);
|
||||
}
|
||||
}));
|
||||
|
||||
_ListenPoller = new Timer(async s =>
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(async invoiceId => await EnsureListening(invoiceId, true))
|
||||
.ToArray());
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task EnsureListening(string invoiceId, bool poll)
|
||||
{
|
||||
if (Listening(invoiceId))
|
||||
return;
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
{
|
||||
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
|
||||
if (lightningMethod == null)
|
||||
continue;
|
||||
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
|
||||
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
|
||||
if (lightningSupportedMethod == null)
|
||||
continue;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
|
||||
var listenedInvoice = new ListenedInvoice()
|
||||
{
|
||||
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
|
||||
PaymentMethodDetails = lightningMethod,
|
||||
SupportedPaymentMethod = lightningSupportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
Network = network,
|
||||
InvoiceId = invoice.Id
|
||||
};
|
||||
|
||||
if (poll)
|
||||
{
|
||||
var charge = GetChargeClient(lightningSupportedMethod, network);
|
||||
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||
if (chargeInvoice == null)
|
||||
continue;
|
||||
if(chargeInvoice.Status == "paid")
|
||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
||||
continue;
|
||||
}
|
||||
|
||||
StartListening(listenedInvoice);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_ListenPoller != null)
|
||||
{
|
||||
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
var charge = GetChargeClient(supportedPaymentMethod, network);
|
||||
var session = await charge.Listen(_Cts.Token);
|
||||
while (true)
|
||||
{
|
||||
var notification = await session.NextEvent(_Cts.Token);
|
||||
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
|
||||
if (listenedInvoice == null)
|
||||
continue;
|
||||
|
||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||
notification.PaymentRequest == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
{
|
||||
if (notification.Status == "paid" && notification.PaidAt.HasValue)
|
||||
{
|
||||
await AddPayment(network, notification, listenedInvoice);
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
if (notification.Status == "expired")
|
||||
{
|
||||
if (DoneListening(listenedInvoice))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
}
|
||||
|
||||
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
|
||||
{
|
||||
await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.PaymentRequest,
|
||||
Amount = notification.MilliSatoshi
|
||||
}, network.CryptoCode, accounted: true);
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
|
||||
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
List<Task> _ListeningLightning = new List<Task>();
|
||||
MultiValueDictionary<string, ListenedInvoice> _ListenedInvoiceByLightningUrl = new MultiValueDictionary<string, ListenedInvoice>();
|
||||
Dictionary<string, ListenedInvoice> _ListenedInvoiceByChargeInvoiceId = new Dictionary<string, ListenedInvoice>();
|
||||
HashSet<string> _InvoiceIds = new HashSet<string>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening an invoice
|
||||
/// </summary>
|
||||
/// <param name="listenedInvoice">The invoice to stop listening</param>
|
||||
/// <returns>true if still need to listen the lightning instance</returns>
|
||||
bool DoneListening(ListenedInvoice listenedInvoice)
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_ListenedInvoiceByLightningUrl.Remove(listenedInvoice.Uri, listenedInvoice);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Listening(string invoiceId)
|
||||
{
|
||||
lock(_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
return _InvoiceIds.Contains(invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
private ListenedInvoice GetListenedInvoice(string chargeInvoiceId)
|
||||
{
|
||||
ListenedInvoice listenedInvoice = null;
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.TryGetValue(chargeInvoiceId, out listenedInvoice);
|
||||
}
|
||||
return listenedInvoice;
|
||||
}
|
||||
|
||||
bool StartListening(ListenedInvoice listenedInvoice)
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
if (_InvoiceIds.Contains(listenedInvoice.InvoiceId))
|
||||
return false;
|
||||
if (!_ListenedInvoiceByLightningUrl.ContainsKey(listenedInvoice.Uri))
|
||||
{
|
||||
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
||||
_ListeningLightning.Add(listen);
|
||||
listen.ContinueWith(_ =>
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListeningLightning.Remove(listen);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
||||
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
||||
_InvoiceIds.Add(listenedInvoice.InvoiceId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
Task[] listening = null;
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
listening = _ListeningLightning.ToArray();
|
||||
}
|
||||
await Task.WhenAll(listening);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
{
|
||||
public class AllChannelResponse
|
||||
{
|
2
BTCPayServer/Eclair/ChannelResponse.cs → BTCPayServer/Payments/Lightning/Eclair/ChannelResponse.cs
2
BTCPayServer/Eclair/ChannelResponse.cs → BTCPayServer/Payments/Lightning/Eclair/ChannelResponse.cs
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
{
|
||||
public class ChannelResponse
|
||||
{
|
38
BTCPayServer/Eclair/EclairRPCClient.cs → BTCPayServer/Payments/Lightning/Eclair/EclairRPCClient.cs
38
BTCPayServer/Eclair/EclairRPCClient.cs → BTCPayServer/Payments/Lightning/Eclair/EclairRPCClient.cs
@ -9,8 +9,18 @@ using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBitcoin.RPC;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
{
|
||||
public class SendResponse
|
||||
{
|
||||
public string PaymentHash { get; set; }
|
||||
}
|
||||
public class ChannelInfo
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public string State { get; set; }
|
||||
}
|
||||
public class EclairRPCClient
|
||||
{
|
||||
public EclairRPCClient(Uri address, Network network)
|
||||
@ -21,8 +31,13 @@ namespace BTCPayServer.Eclair
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
Address = address;
|
||||
Network = network;
|
||||
if (string.IsNullOrEmpty(address.UserInfo))
|
||||
throw new ArgumentException(paramName: nameof(address), message: "User info in the url should be provided");
|
||||
Password = address.UserInfo;
|
||||
}
|
||||
|
||||
public string Password { get; set; }
|
||||
|
||||
public Network Network { get; private set; }
|
||||
|
||||
|
||||
@ -33,7 +48,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public Task<GetInfoResponse> GetInfoAsync()
|
||||
{
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", Array.Empty<object>()));
|
||||
}
|
||||
|
||||
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
|
||||
@ -104,17 +119,17 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public async Task<AllChannelResponse[]> AllChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] Channels()
|
||||
public ChannelInfo[] Channels()
|
||||
{
|
||||
return ChannelsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string[]> ChannelsAsync()
|
||||
public async Task<ChannelInfo[]> ChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<ChannelInfo[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Close(string channelId)
|
||||
@ -122,6 +137,11 @@ namespace BTCPayServer.Eclair
|
||||
CloseAsync(channelId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task SendAsync(string paymentRequest)
|
||||
{
|
||||
await SendCommandAsync<SendResponse>(new RPCRequest("send", new[] { paymentRequest })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CloseAsync(string channelId)
|
||||
{
|
||||
if (channelId == null)
|
||||
@ -155,7 +175,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public async Task<string[]> AllNodesAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Uri Address { get; private set; }
|
||||
@ -165,6 +185,8 @@ namespace BTCPayServer.Eclair
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
|
||||
webRequest.ContentType = "application/json";
|
||||
webRequest.Method = "POST";
|
||||
var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(Password));
|
||||
webRequest.Headers[HttpRequestHeader.Authorization] = $"Basic {auth}";
|
||||
return webRequest;
|
||||
}
|
||||
|
||||
@ -220,7 +242,7 @@ namespace BTCPayServer.Eclair
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
pushAmount = pushAmount ?? LightMoney.Zero;
|
||||
|
||||
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
||||
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
||||
|
||||
return result.ResultString;
|
||||
}
|
2
BTCPayServer/Eclair/GetInfoResponse.cs → BTCPayServer/Payments/Lightning/Eclair/GetInfoResponse.cs
2
BTCPayServer/Eclair/GetInfoResponse.cs → BTCPayServer/Payments/Lightning/Eclair/GetInfoResponse.cs
@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
{
|
||||
public class GetInfoResponse
|
||||
{
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user