Compare commits

...

166 Commits

Author SHA1 Message Date
273cf1adc9 Fix checkout if only one currency is present 2018-06-24 00:53:56 +09:00
5feb520843 Add support for groestlcoin 2018-06-24 00:45:57 +09:00
17e914778d Make sure that lightning payments events are using the state of the invoice when they got issued () 2018-06-21 14:15:36 +09:00
db24ab792f update clightning 2018-06-18 23:07:55 +09:00
448cc06a11 Merge pull request from ChekaZ/master
Support UFO
2018-06-12 10:43:53 +09:00
0780df4fd7 Support UFO 2018-06-09 17:25:45 +02:00
04174b7431 Fix authentication 2018-06-06 16:02:37 +09:00
b7c58c2083 Fix bug of authentication caused by previous refactoring on authentication 2018-06-06 14:46:41 +09:00
cd75fd6842 bump 2018-06-05 12:53:55 +09:00
370951a3bd make sure postgres DB is created with C locale 2018-06-05 12:51:37 +09:00
2c08b0137b Update NBitpayClient 2018-06-05 12:29:45 +09:00
1eee31e9f1 Fix rate bug: Sometimes coinaverage is sending null bid and ask 2018-06-04 12:01:30 +09:00
01cf579530 Use proper custom authentication handler for bitpay 2018-06-04 12:00:03 +09:00
f72705935a Merge pull request from ChekaZ/master
Support Feathercoin
2018-06-04 01:45:07 +09:00
a29ab6b6b0 Support Feathercoin 2018-06-03 14:30:43 +02:00
4784518235 update link 2018-06-01 13:21:56 +09:00
0697b8bf86 update images 2018-05-31 23:54:03 +09:00
5050b59014 bump 2018-05-31 18:41:33 +09:00
665cf4c3b1 Updating BTCPayServer to .NET Core 2.1 2018-05-31 18:41:03 +09:00
98e81ab0fd Merge branch 'rockstardev-master' 2018-05-29 12:27:55 +09:00
6ce70237fc Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-05-29 12:27:45 +09:00
4f23fc99a1 Renaming method to better communicate function 2018-05-29 12:27:04 +09:00
d7fccae452 Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-29 12:27:04 +09:00
d7a5021ed2 Updating Invoice details to show local date time 2018-05-29 12:27:03 +09:00
63ec832667 Support for localizing datetimes base on browser's timezone 2018-05-29 12:27:03 +09:00
8d95b9fa04 Renaming method to better communicate function 2018-05-28 09:36:49 -05:00
b497d1871e Merge pull request from andres-pinilla/patch-3
Updated es.js
2018-05-28 10:50:30 +09:00
c7cd029482 Update es.js
Various strings updated.
2018-05-27 20:36:32 -05:00
68f2cba60d Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-26 09:42:55 -05:00
5c4200b036 Updating Invoice details to show local date time 2018-05-26 09:35:42 -05:00
bc06114023 Support for localizing datetimes base on browser's timezone 2018-05-26 09:32:20 -05:00
556082c4c9 fix json serialization 2018-05-26 18:29:57 +09:00
6a46d02fc6 Add dummy policies field 2018-05-26 18:26:02 +09:00
d75e5b8b12 Merge pull request from cronos-polis/master
Polis Integration
2018-05-26 14:50:32 +09:00
a97ef2eee8 MinerFee matching Bitpay API 2018-05-25 22:49:49 +09:00
be33ebc168 fix typo 2018-05-25 17:35:01 +09:00
789193a0c8 fix typo 2018-05-25 12:55:29 +09:00
01792cf299 Merge pull request from yashbhutwala/fix_typo
Fix spelling of lightning
2018-05-25 11:41:46 +09:00
ff9265f721 Fix spelling of lightning 2018-05-24 16:26:01 -04:00
8d61314852 Use FullNotification and improve instructions 2018-05-25 00:02:24 +09:00
1ce6ae8727 fix typo 2018-05-24 23:58:24 +09:00
dec5dbc0d2 Ability to pass fields to POS app 2018-05-24 23:54:48 +09:00
4e32dad1ea Can solve inverses at exchange level 2018-05-23 19:29:01 +09:00
127ca7582f Make sure inverse rules have priority 2018-05-23 19:13:12 +09:00
b98993f84b fix typo 2018-05-23 11:16:19 +09:00
e35f074b66 Better label for Rate Multiplier 2018-05-23 02:43:22 +09:00
ba3d13d56c Make sure the rate of the merchant is using the ask of a divided exchange 2018-05-23 02:18:38 +09:00
ead67887ab Enable SSL was ignored 2018-05-23 00:59:50 +09:00
437f27f107 Merge pull request from andres-pinilla/master
Better spanish translation (tu)
2018-05-22 14:11:25 +09:00
8d41a8e98d Better spanish translation (tú) 2018-05-21 23:15:04 -05:00
7e6ab015a6 Fix bug on Error page 2018-05-22 01:05:45 +09:00
f8bc3a5081 bump 2018-05-21 20:47:07 +09:00
dd1a93ee0e Revert "Remove unused Error.cshtml"
This reverts commit 7b2ef9aec2c5f5afbecf1b639995b4a645390928.
2018-05-21 20:44:03 +09:00
3cf3aa63f6 CurrencyNameTable can use fallback 2018-05-20 23:37:18 +09:00
011dd5574f Add a fallback currency format info 2018-05-20 23:22:20 +09:00
365911286b bump 2018-05-20 17:04:03 +09:00
fe5347aa86 Maintaining BitPay compatibility
Ref: https://github.com/btcpayserver/btcpayserver/issues/180
2018-05-20 17:00:54 +09:00
f22c8a72cd Rebase 2018-05-16 11:21:57 -05:00
eeb522fe7d Remove special case for showing crypto currency 2018-05-16 21:19:48 +09:00
f9e40b209a Merge pull request from rockstardev/fiat
Showing exchange rate for cryptos
2018-05-16 21:14:23 +09:00
20635ea3d6 Showing exchange rate for cryptos
Ref: https://github.com/btcpayserver/btcpayserver/pull/170#issuecomment-389462155
2018-05-16 05:46:11 -05:00
7062705d6f bump 2018-05-16 10:40:48 +09:00
58b994e043 fix tests 2018-05-16 10:40:25 +09:00
640ff36fa2 fix build 2018-05-16 10:26:45 +09:00
39ec5242d7 Bump NBitpayClient 2018-05-16 10:22:43 +09:00
1c50210e61 fix build 2018-05-16 10:22:42 +09:00
a1ffda0151 Merge pull request from Kukks/feature/extended-invoice
[WIP] Feature/extended invoice
2018-05-16 10:21:52 +09:00
fd15348551 Merge pull request from monaco-ex/pr-support-monacoin
Support Monacoin.
2018-05-16 10:21:07 +09:00
989c99c550 Merge pull request from rockstardev/fiat
Display fiat value of invoice during checkout
2018-05-16 10:18:24 +09:00
bcf97b1474 Hiding display of payment-tabs now that we have flex line-items 2018-05-15 15:17:59 -05:00
67abbed66a Removing display of exchange if invoice source amount is in crypto 2018-05-15 15:17:58 -05:00
eb01e91e13 Only show Order Amount in Fiat if Invoice is created with fiat value 2018-05-15 15:17:58 -05:00
12ceb9e0bc Simplifying CSS styles for line-items box
Now that we'll have different box sizes it's not possible to rely on exact height specification
2018-05-15 15:17:57 -05:00
ecf03f90aa Fix UriAttribute bug, and currency formatting crash 2018-05-16 02:24:59 +09:00
1747414a57 update clightning of docker compose 2018-05-16 01:37:20 +09:00
3a02f16c6e Fix bug where exchange name in rate rules were uncorrectly considered a currency 2018-05-16 01:27:15 +09:00
a6ee337ed0 more coverage 2018-05-15 16:25:43 +02:00
559f535257 add some coverage for bitpay fields 2018-05-15 16:18:37 +02:00
2952ccc7fd Merge remote-tracking branch 'origin/master' into feature/extended-invoice 2018-05-15 15:44:51 +02:00
f81ca1888d Merge remote-tracking branch 'upstream/master' 2018-05-14 11:23:54 -05:00
ed02e0f4d6 Merge pull request from zeusthealmighty/patch-1
KeyPath Updated
2018-05-14 11:23:08 -05:00
0a83f21af5 KeyPath Updated 2018-05-14 10:04:12 -05:00
23a3c145ed fix run.sh 2018-05-14 22:08:35 +09:00
4184c6c208 Convert in UriAttribute use invariant culture 2018-05-14 21:28:33 +09:00
29c28b1841 Merge pull request from Kukks/bugfix/real-url-validation
use alternative uri validation
2018-05-14 17:31:56 +09:00
de48fb4077 add direct file test cases 2018-05-14 09:34:19 +02:00
bcd79c5882 use alternative uri validation 2018-05-14 09:32:04 +02:00
b8c513aa2b Support Monacoin. 2018-05-14 05:44:17 +00:00
ad67f4ef18 update to use longs 2018-05-13 09:47:42 +02:00
2c0bcfc0ec Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-13 08:34:36 +02:00
0ba1072d54 bump 2018-05-13 15:09:38 +09:00
f7fe855274 Do not roundup rates 2018-05-13 15:09:17 +09:00
449738414b Add cryptopia 2018-05-12 19:37:32 +09:00
a34842585d bump 2018-05-12 19:19:40 +09:00
eb882c2c46 Update package 2018-05-12 19:15:54 +09:00
ca65c6bd8f fix 2018-05-12 18:38:43 +09:00
3770adb7d3 Displaying fiat value of invoice's order amount in details 2018-05-11 12:15:26 -05:00
7fdf19ca22 Remove cryptopia from CoinAverage 2018-05-11 11:07:42 -05:00
4e776adb03 Merge remote-tracking branch 'upstream/master' 2018-05-11 11:06:51 -05:00
26db946392 BTCPayRateProviderFactory is responsible for getting the supported exchange list 2018-05-12 00:54:17 +09:00
d102c142b9 Typo 2018-05-11 10:46:49 -05:00
f7989541b9 Change Polis Rates - Add Cryptopia 2018-05-11 10:26:08 -05:00
b7f0ce18b3 Make sure Lightning charge can't hang out the payment 2018-05-12 00:23:25 +09:00
e1dfbfe3b0 Rebase 2018-05-11 10:20:23 -05:00
786d129452 Make sure to not freeze if ligthning does not respond 2018-05-12 00:14:39 +09:00
a37a8e8fcd Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 16:46:38 +02:00
355989c278 bump 2018-05-11 23:34:42 +09:00
af3dee95de round up rates sent back by the RateProviderFactory 2018-05-11 23:31:50 +09:00
70a6bd6a01 bump 2018-05-11 22:42:29 +09:00
4afb0acc84 does not generate antiforgery 2018-05-11 22:41:11 +09:00
9afc143801 Use decimals and fix invoices 2018-05-11 22:38:31 +09:00
8e4943df65 low-medium speed policy 2018-05-11 22:12:45 +09:00
9b3bd8343d bump nuget packages 2018-05-11 21:33:46 +09:00
ee4f83ddba small fixes 2018-05-11 12:21:25 +02:00
c326998381 bump nbitpayclient dependency to .20 2018-05-11 11:42:13 +02:00
239a011e60 add new properties and change types to decimal 2018-05-11 11:31:21 +02:00
5ffe118159 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 11:24:32 +02:00
6f07849e1d Use policies security for controlling access to bitpay api 2018-05-11 17:16:18 +09:00
dbe5c62d11 Merge remote-tracking branch 'btcpayserver/master' into feature/extended-invoice 2018-05-11 10:08:29 +02:00
199db01eaf No need of authentication for GetInvoice API () 2018-05-11 17:05:08 +09:00
a3c46c8f67 Use hangfire in-memory provider until the postgres binding to hangfire get updated. 2018-05-11 15:06:11 +09:00
66a68d6180 Rename LockSubscription, remove the link if not available 2018-05-10 16:02:49 +09:00
be1128a886 Support for displaying fiat value of invoice 2018-05-09 22:39:13 -05:00
d41a5a65a2 Update posgres and clightning in tests 2018-05-10 11:56:46 +09:00
d5cab938ee bump 2018-05-09 14:10:06 +09:00
9dddfb65f0 Add globalization to the alpine package 2018-05-09 14:09:41 +09:00
6bd5976d90 Update SQLite package 2018-05-08 19:17:06 +09:00
b3385bf901 update tests image 2018-05-08 18:09:12 +09:00
bba268b5e2 Upgrade to .NET Core 2.1 2018-05-08 17:57:53 +09:00
70c98b6901 use the theme manager for ViewPointOfSale 2018-05-08 00:19:28 +09:00
2d3b7fea2e update packages 2018-05-07 23:02:55 +09:00
3bdf1c9a00 Merge pull request from Vutov/master
Added BTG Support
2018-05-07 14:16:31 +09:00
a52665ea80 bump 2018-05-07 12:29:56 +09:00
3d943d49e6 Make sure if the default crypto is no available, we don't get error 404 2018-05-07 12:25:50 +09:00
6ca8ba9231 Allow signing on non segwit transactions via the ledger 2018-05-07 12:17:46 +09:00
75d685ae6c fix grammar 2018-05-07 00:24:23 +09:00
7b2ef9aec2 Remove unused Error.cshtml 2018-05-06 22:49:00 +09:00
efe666b284 Fix call to Rates via bitpay API 2018-05-06 22:41:38 +09:00
ca8af5047a Changed DefaultRateRules 2018-05-06 14:59:49 +03:00
cdc0b0d628 Fix crash when creating a token 2018-05-06 19:03:30 +09:00
87e28b70fd cap MinimumTotalDue to 1 satoshi 2018-05-06 13:55:03 +09:00
b96f464e39 Add "unusual:" filter to invoice list 2018-05-06 13:16:39 +09:00
bca68986f3 Added BTG Support 2018-05-05 23:06:09 +03:00
272ac49872 try to better respect event ordering 2018-05-06 02:06:07 +09:00
5f05ca5ac6 bump 2018-05-06 00:43:05 +09:00
7872b3ec55 Add a new invoice event: expiredPaidPartial and fix some corner case for tolerance 2018-05-06 00:40:44 +09:00
27a0aebd12 Merge pull request from Kukks/feature/order-tolerance
Payment Tolerance
2018-05-06 00:06:39 +09:00
366490516e Can filter with "exceptionstatus:", show the exception status on invoice list page 2018-05-05 23:25:09 +09:00
9a92646d4d add test and refactor for PR 2018-05-05 16:07:22 +02:00
b002c49dac Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 16:04:59 +02:00
3f4ec9ba80 simplify currency parsing if _ is forgotten and there is 6 letters 2018-05-05 22:59:53 +09:00
0290a5eacd update clightning 2018-05-05 22:46:07 +09:00
744734a6a1 Returns fallback feerate for coins not supporting fee rate query in NBXplorer 2018-05-05 22:19:36 +09:00
29f662f87c bump NBXplorer 2018-05-05 22:05:22 +09:00
af21f9f10c Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-05 08:49:16 +02:00
efdc99b9d1 Do not spam the logs about failed mail 2018-05-05 01:42:42 +09:00
4458e63c1a Break default DOGE rules in two, add some documentation about inverses 2018-05-05 01:34:08 +09:00
01069ed583 Remove unnecessary branching 2018-05-04 17:50:05 +02:00
0fc770bbb1 extract logic of accounting to accounting and remove bitpay breaking changes 2018-05-04 17:47:33 +02:00
dfb79ef96e Merge remote-tracking branch 'btcpayserver/master' into feature/order-tolerance 2018-05-04 17:46:39 +02:00
7c3ddf904c Merge remote-tracking branch 'upstream/master' 2018-05-04 09:47:03 -05:00
c3d73236e0 start work on payment tolerance feature 2018-05-04 16:15:34 +02:00
25208915eb Merge remote-tracking branch 'upstream/master' 2018-04-30 10:29:30 -05:00
52e0845fc5 Merge remote-tracking branch 'upstream/master' 2018-04-21 09:59:46 -05:00
daf1a0a4bc Revert to origin csproj 2018-04-17 11:19:28 -05:00
bc8978182e Add Polis 2018-04-17 11:13:50 -05:00
131 changed files with 3213 additions and 985 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetworkProvider.BitcoinGold.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Feathercoin.csBTCPayNetworkProvider.Groestlcoin.csBTCPayNetworkProvider.Monacoin.csBTCPayNetworkProvider.Polis.csBTCPayNetworkProvider.Ufo.csBTCPayNetworkProvider.csBTCPayServer.csproj
Controllers
CurrencyValue.cs
Data
DerivationSchemeParser.cs
Events
Extensions.cs
HostedServices
Hosting
Logging
Migrations
Models
Payments
Program.cs
Rating
Security
Services
Validation
Views
bundleconfig.json
wwwroot
DockerfileNuget.ConfigREADME.mdrun.sh

@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

@ -129,19 +129,19 @@ namespace BTCPayServer.Tests
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
Value = 5000m
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
Value = 4500m
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
Value = 500m
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
}

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
namespace BTCPayServer.Tests
{
@ -35,9 +36,9 @@ namespace BTCPayServer.Tests
rules.ToString());
var tests = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
};
@ -81,9 +82,9 @@ namespace BTCPayServer.Tests
var tests2 = new[]
{
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
};
@ -94,12 +95,12 @@ namespace BTCPayServer.Tests
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
}
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
rule2.Reevaluate();
Assert.True(rule2.HasError);
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
@ -116,7 +117,7 @@ namespace BTCPayServer.Tests
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
@ -124,11 +125,55 @@ namespace BTCPayServer.Tests
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
////////
// Make sure kraken is not converted to CurrencyPair
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
// Make sure can handle pairs
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.Value.Value);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.Value.Value);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();
builder.AppendLine("EUR_CDNT = 10");
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / 10", rule2.ToString(false));
// Make sure an inverse can be solved on an exchange
builder = new StringBuilder();
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
}
}
}

@ -185,99 +185,6 @@ namespace BTCPayServer.Tests
HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest
{
Uri serverUri;
public MockHttpRequest(Uri serverUri)
{
this.serverUri = serverUri;
}
public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Scheme
{
get => serverUri.Scheme;
set => throw new NotImplementedException();
}
public override bool IsHttps
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override HostString Host
{
get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException();
}
public override PathString PathBase
{
get => "";
set => throw new NotImplementedException();
}
public override PathString Path
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override QueryString QueryString
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IQueryCollection Query
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Protocol
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override long? ContentLength
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ContentType
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Stream Body
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
public BTCPayServerTester PayTester
{
get; set;

@ -36,6 +36,7 @@ using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
namespace BTCPayServer.Tests
@ -48,6 +49,27 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanHandleUriValidation()
{
var attribute = new UriAttribute();
Assert.True(attribute.IsValid("http://localhost"));
Assert.True(attribute.IsValid("http://localhost:1234"));
Assert.True(attribute.IsValid("https://localhost"));
Assert.True(attribute.IsValid("https://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1"));
Assert.True(attribute.IsValid("http://127.0.0.1:1234"));
Assert.True(attribute.IsValid("http://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com"));
Assert.True(attribute.IsValid("https://gozo.com:1234"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.css"));
Assert.True(attribute.IsValid("https://gozo.com:1234/test.png"));
Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e"));
Assert.False(attribute.IsValid(2));
Assert.False(attribute.IsValid("http://"));
Assert.False(attribute.IsValid("httpdsadsa.com"));
}
[Fact]
public void CanCalculateCryptoDue2()
{
@ -229,6 +251,89 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
[Fact]
public void CanAcceptInvoiceWithTolerance()
{
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.PaymentTolerance = 0;
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
entity.PaymentTolerance = 10;
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
entity.PaymentTolerance = 100;
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
}
[Fact]
public void CanAcceptInvoiceWithTolerance2()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(stores.UpdateStore()).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
// Pays 75%
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
});
}
}
[Fact]
public void RoundupCurrenciesCorrectly()
{
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)"),
(0.001m, "$0.001 (USD)"),
(0.01m, "$0.01 (USD)"),
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
Assert.Equal(test.Item2, actual);
}
}
[Fact]
public void CanPayUsingBIP70()
{
@ -238,10 +343,11 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -373,7 +479,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Price = 0.01m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -406,7 +512,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Price = 0.01m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -434,7 +540,7 @@ namespace BTCPayServer.Tests
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01,
Price = 0.01m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -487,7 +593,7 @@ namespace BTCPayServer.Tests
acc.RegisterDerivationScheme("BTC");
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
Price = 5.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -531,6 +637,53 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanListInvoices()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = acc.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}");
AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah");
AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}");
AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial");
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
}
}
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
{
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
}
[Fact]
public void CanRBFPayment()
{
@ -542,7 +695,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD"
}, Facade.Merchant);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
@ -618,8 +771,30 @@ namespace BTCPayServer.Tests
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
// Test request pairing code client side
var storeController = user.GetController<StoresController>();
storeController.CreateToken(new CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "test2",
StoreId = user.StoreId
}).GetAwaiter().GetResult();
Assert.NotNull(storeController.GeneratedPairingCode);
var k = new Key();
var bitpay = new Bitpay(k, tester.PayTester.ServerUri);
bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait();
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
// Same with new instance
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
@ -642,7 +817,7 @@ namespace BTCPayServer.Tests
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
var invoice = new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD"
};
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
@ -684,7 +859,7 @@ namespace BTCPayServer.Tests
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -708,7 +883,7 @@ namespace BTCPayServer.Tests
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -726,7 +901,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -761,6 +936,11 @@ namespace BTCPayServer.Tests
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
@ -782,7 +962,7 @@ namespace BTCPayServer.Tests
// 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(1.0m, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
@ -869,7 +1049,7 @@ namespace BTCPayServer.Tests
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -895,13 +1075,23 @@ namespace BTCPayServer.Tests
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("BTC", checkout.CryptoCode);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -943,6 +1133,18 @@ namespace BTCPayServer.Tests
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
}
}
@ -1023,7 +1225,7 @@ namespace BTCPayServer.Tests
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5,
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1036,7 +1238,7 @@ namespace BTCPayServer.Tests
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5,
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1083,9 +1285,9 @@ namespace BTCPayServer.Tests
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price);
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
}
@ -1136,7 +1338,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1146,6 +1348,8 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -1241,12 +1445,12 @@ namespace BTCPayServer.Tests
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
Assert.NotEqual(0.0, localInvoice.Rate);
Assert.NotEqual(0.0m, localInvoice.Rate);
});
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1285,10 +1489,10 @@ namespace BTCPayServer.Tests
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
@ -1297,7 +1501,7 @@ namespace BTCPayServer.Tests
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
@ -1309,11 +1513,11 @@ namespace BTCPayServer.Tests
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => ( e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
&& e.Value > 1.0m // 1BTC will always be more than 1USD
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}
@ -1356,7 +1560,6 @@ namespace BTCPayServer.Tests
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.DirectProviders.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(10);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();

@ -46,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.2
image: nicolasdorier/nbxplorer:1.0.2.8
ports:
- "32838:32838"
expose:
@ -89,14 +89,15 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.11-dev
image: nicolasdorier/clightning:0.0.0.21-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
bind-addr=0.0.0.0
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
ports:
@ -111,7 +112,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.9
image: shesek/lightning-charge:0.3.12
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -130,13 +131,14 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.11-dev
image: nicolasdorier/clightning:0.0.0.21-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
bind-addr=0.0.0.0
announce-addr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000

@ -0,0 +1,29 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinGold()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoingold",
DefaultRateRules = new[]
{
"BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = bitfinex(BTG_BTC)",
},
CryptoImagePath = "imlegacy/btg-symbol.svg",
LightningImagePath = "imlegacy/btg-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
});
}
}
}

@ -20,7 +20,11 @@ namespace BTCPayServer
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" },
DefaultRateRules = new[]
{
"DOGE_X = DOGE_BTC * BTC_X",
"DOGE_BTC = bittrex(DOGE_BTC)"
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")

@ -0,0 +1,34 @@
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 InitFeathercoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",
"FTC_BTC = bittrex(FTC_BTC)"
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitGroestlcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,35 @@
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 InitMonacoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = zaif(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,34 @@
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 InitPolis()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,34 @@
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 InitUfo()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]
{
"UFO_X = UFO_BTC * BTC_X",
"UFO_BTC = coinexchange(UFO_BTC)"
},
CryptoImagePath = "imlegacy/ufo.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
});
}
}
}

@ -48,6 +48,12 @@ namespace BTCPayServer
InitBitcoin();
InitLitecoin();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitUfo();
}
/// <summary>

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.2.4</Version>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.36</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -30,38 +30,35 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.4" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="NBitcoin" Version="4.1.1.18" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
<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.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.11" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>

@ -12,7 +12,8 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("tokens")]
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
PairingCodeEntity pairingEntity = null;
@ -53,7 +55,7 @@ namespace BTCPayServer.Controllers
else
{
var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
@ -77,6 +79,7 @@ namespace BTCPayServer.Controllers
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,

@ -17,6 +17,8 @@ using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
namespace BTCPayServer.Controllers
{
@ -57,15 +59,56 @@ namespace BTCPayServer.Controllers
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
var items = Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency) == null)
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
@ -102,8 +145,9 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency);
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
@ -162,7 +206,14 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
[IgnoreAntiforgeryToken]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -172,12 +223,12 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
double price = 0.0;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
@ -185,20 +236,26 @@ namespace BTCPayServer.Controllers
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
price = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}

@ -176,24 +176,19 @@ namespace BTCPayServer.Controllers
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Select(us => new
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
Apps = us.StoreData.Apps
})
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.IsOwner,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id,
StoreId = us.StoreId,
StoreName = us.StoreName
}))
.ToArrayAsync();
.Where(us => us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}

@ -13,11 +13,14 @@ using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;
@ -43,9 +46,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{id}")]
[AllowAnonymous]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
var invoice = await _InvoiceRepository.GetInvoice(null, id);
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
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..."));

@ -51,14 +51,17 @@ namespace BTCPayServer.Controllers
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",
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
"low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.Rate = ExchangeRate(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
@ -201,6 +204,12 @@ namespace BTCPayServer.Controllers
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
@ -210,6 +219,7 @@ namespace BTCPayServer.Controllers
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -232,15 +242,16 @@ namespace BTCPayServer.Controllers
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
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 = FormatCurrency(paymentMethod),
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
@ -278,14 +289,45 @@ namespace BTCPayServer.Controllers
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
}
private string FormatCurrency(PaymentMethod paymentMethod)
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
{
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if (cryptoCode == productInformation.Currency)
return null;
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public string FormatCurrency(decimal price, string currency)
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
}
[HttpGet]
@ -314,7 +356,7 @@ namespace BTCPayServer.Controllers
{
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)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -370,15 +412,20 @@ namespace BTCPayServer.Controllers
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "new",
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
@ -415,7 +462,7 @@ namespace BTCPayServer.Controllers
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if(store == null)
if (store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}
@ -488,8 +535,11 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return NotFound();
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}

@ -98,6 +98,7 @@ namespace BTCPayServer.Controllers
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
@ -108,6 +109,9 @@ namespace BTCPayServer.Controllers
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
@ -205,7 +209,7 @@ namespace BTCPayServer.Controllers
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -276,6 +280,7 @@ namespace BTCPayServer.Controllers
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))

@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
return Json(new DataWrapper<Rate[]>(rates));
}
[Route("api/rates")]
@ -54,8 +55,9 @@ namespace BTCPayServer.Controllers
return result;
}
var store = await _StoreRepo.FindStore(storeId);
var store = this.HttpContext.GetStoreData();
if(store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
@ -86,7 +88,8 @@ namespace BTCPayServer.Controllers
{
CryptoCode = r.Pair.Left,
Code = r.Pair.Right,
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
CurrencyPair = r.Pair.ToString(),
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
Value = r.Value.Value
}).Where(n => n.Name != null).ToArray());
}
@ -106,6 +109,14 @@ namespace BTCPayServer.Controllers
get;
set;
}
[JsonProperty(PropertyName = "currencyPair")]
public string CurrencyPair
{
get;
set;
}
[JsonProperty(PropertyName = "code")]
public string Code
{

@ -243,10 +243,7 @@ namespace BTCPayServer.Controllers
{
try
{
if(string.IsNullOrWhiteSpace(model.Settings.From)
|| string.IsNullOrWhiteSpace(model.TestEmail)
|| string.IsNullOrWhiteSpace(model.Settings.Login)
|| string.IsNullOrWhiteSpace(model.Settings.Server))
if(!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);

@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy))
if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers
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, summary.Status.BitcoinStatus.MinRelayTxFee);
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
builder.SendEstimatedFees(feeRateValue);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if(!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach(var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if(tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}

@ -53,8 +53,7 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env,
CoinAverageSettings coinAverage)
IHostingEnvironment env)
{
_RateFactory = rateFactory;
_Dashboard = dashboard;
@ -72,9 +71,7 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_CoinAverage = coinAverage;
}
CoinAverageSettings _CoinAverage;
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -431,6 +428,7 @@ namespace BTCPayServer.Controllers
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
return View(vm);
}
@ -496,6 +494,7 @@ namespace BTCPayServer.Controllers
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
blob.PaymentTolerance = model.PaymentTolerance;
if (StoreData.SetStoreBlob(blob))
{
@ -516,7 +515,7 @@ namespace BTCPayServer.Controllers
private CoinAverageExchange[] GetSupportedExchanges()
{
return _CoinAverage.AvailableExchanges
return _RateFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -644,11 +643,11 @@ namespace BTCPayServer.Controllers
{
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
if (model.Stores.Count() == 0)
{
StatusMessage = "Error: You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
if (model.Stores.Count() == 0)
{
StatusMessage = "Error: You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
}
return View(model);
}

@ -21,7 +21,7 @@ namespace BTCPayServer
return false;
var currency = match.Groups.Last().Value.ToUpperInvariant();
var currencyData = _CurrencyTable.GetCurrencyData(currency);
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);

@ -6,6 +6,11 @@ using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Metadata;
namespace BTCPayServer.Data
{
@ -31,20 +36,66 @@ namespace BTCPayServer.Data
return new ApplicationDbContext(builder.Options);
}
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
{
}
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
{
builder
.Append("CREATE DATABASE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
builder
.Append(" TEMPLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
builder
.Append(" LC_CTYPE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" LC_COLLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" ENCODING ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
if (operation.Tablespace != null)
{
builder
.Append(" TABLESPACE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
}
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
EndStatement(builder, suppressTransaction: true);
}
}
public void ConfigureBuilder(DbContextOptionsBuilder builder)
{
if (_Type == DatabaseType.Sqlite)
builder.UseSqlite(_ConnectionString);
else if (_Type == DatabaseType.Postgres)
builder.UseNpgsql(_ConnectionString);
builder
.UseNpgsql(_ConnectionString)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
if (_Type == DatabaseType.Sqlite)
builder.UseMemoryStorage(); //Sql provider does not support multiple workers
else if (_Type == DatabaseType.Postgres)
builder.UsePostgreSqlStorage(_ConnectionString);
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

@ -247,6 +247,7 @@ namespace BTCPayServer.Data
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
PaymentTolerance = 0;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
@ -326,6 +327,10 @@ namespace BTCPayServer.Data
}
}
[DefaultValue(0)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public double PaymentTolerance { get; set; }
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
{
if (!RateScripting ||

@ -72,7 +72,7 @@ namespace BTCPayServer
}
try
{
var data = Encoders.Base58Check.DecodeData(parts[i]);
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
@ -80,7 +80,7 @@ namespace BTCPayServer
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{

@ -8,24 +8,20 @@ namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
{
}
public InvoiceEvent(string invoiceId, int code, string name)
{
InvoiceId = invoiceId;
Invoice = invoice;
EventCode = code;
Name = name;
}
public string InvoiceId { get; set; }
public Models.InvoiceResponse Invoice { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
}
}
}

@ -6,21 +6,6 @@ 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)

@ -36,28 +36,6 @@ namespace BTCPayServer
{
public static class Extensions
{
public static string Prettify(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();

@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices
{
get { return _creativeStartUri; }
}
public bool ShowRegister { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
}
}
public class CssThemeManagerHostedService : BaseAsyncService
@ -58,10 +65,19 @@ namespace BTCPayServer.HostedServices
{
return new[]
{
CreateLoopTask(ListenForThemeChanges)
CreateLoopTask(ListenForThemeChanges),
CreateLoopTask(ListenForPoliciesChanges),
};
}
async Task ListenForPoliciesChanges()
{
await new SynchronizationContextRemover();
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
_CssThemeManager.Update(data);
await _SettingsRepository.WaitSettingsChanged<PoliciesSettings>(Cancellation);
}
async Task ListenForThemeChanges()
{
await new SynchronizationContextRemover();

@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
@ -207,7 +211,7 @@ namespace BTCPayServer.HostedServices
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
@ -304,8 +308,11 @@ namespace BTCPayServer.HostedServices
{
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
await SaveEvent(invoice.Id, e);
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
List<Task> tasks = new List<Task>();
// Awaiting this later help make sure invoices should arrive in order
tasks.Add(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)
@ -315,20 +322,22 @@ namespace BTCPayServer.HostedServices
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed"
e.Name == "invoice_completed" ||
e.Name == "invoice_expiredPaidPartial"
)
await Notify(invoice);
tasks.Add(Notify(invoice));
}
if (e.Name == "invoice_confirmed")
{
await Notify(invoice);
tasks.Add(Notify(invoice));
}
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
tasks.Add(Notify(invoice, e.EventCode, e.Name));
}
await Task.WhenAll(tasks.ToArray());
}));

@ -66,8 +66,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
invoice.Status = "expired";
if(invoice.ExceptionStatus == "paidPartial")
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -78,11 +80,11 @@ namespace BTCPayServer.HostedServices
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
if (invoice.Status == "new" || invoice.Status == "expired")
{
if (accounting.Paid >= accounting.TotalDue)
if (accounting.Paid >= accounting.MinimumTotalDue)
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -91,22 +93,22 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
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")
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
{
invoice.ExceptionStatus = null;
context.MarkDirty();
@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
}
if (accounting.Paid < accounting.TotalDue)
if (accounting.Paid < accounting.MinimumTotalDue)
{
invoice.Status = "new";
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
@ -134,17 +136,17 @@ namespace BTCPayServer.HostedServices
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(confirmedAccounting.Paid < accounting.TotalDue))
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.TotalDue)
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
@ -153,9 +155,9 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == "confirmed")
{
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.TotalDue)
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
@ -247,13 +249,13 @@ namespace BTCPayServer.HostedServices
{
if (b.Name == "invoice_created")
{
Watch(b.InvoiceId);
await Wait(b.InvoiceId);
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == "invoice_receivedPayment")
{
Watch(b.InvoiceId);
Watch(b.Invoice.Id);
}
}));
return Task.CompletedTask;
@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)

@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
{
State = NBXplorerState.NotConnected;
status = null;
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`");
}
_Dashboard.Publish(_Network, State, status, error);

@ -12,7 +12,6 @@ using NBitcoin;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using System.IO;
using Microsoft.Data.Sqlite;
using NBXplorer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
@ -118,7 +117,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
@ -138,6 +136,7 @@ namespace BTCPayServer.Hosting
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>

@ -79,12 +79,13 @@ namespace BTCPayServer.Hosting
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
isJson)
return true;
if (
@ -94,9 +95,9 @@ namespace BTCPayServer.Hosting
return true;
if (
bitpayAuth &&
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&

@ -35,7 +35,6 @@ using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
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;
@ -104,10 +103,6 @@ namespace BTCPayServer.Hosting
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
@ -146,12 +141,8 @@ namespace BTCPayServer.Hosting
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();

@ -1,13 +1,14 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions.Internal;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
namespace BTCPayServer.Logging
{
@ -20,19 +21,18 @@ namespace BTCPayServer.Logging
}
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor);
}
public void Dispose()
{
}
}
/// <summary>
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
/// </summary>
public class CustomConsoleLogger : ILogger
public class CustomerConsoleLogger : ILogger
{
private static readonly string _loglevelPadding = ": ";
private static readonly string _messagePadding;
@ -47,19 +47,33 @@ namespace BTCPayServer.Logging
[ThreadStatic]
private static StringBuilder _logBuilder;
static CustomConsoleLogger()
static CustomerConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Filter = filter ?? ((category, logLevel) => true);
IncludeScopes = includeScopes;
}
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
{
}
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Filter = filter ?? ((category, logLevel) => true);
ScopeProvider = scopeProvider;
_queueProcessor = loggerProcessor;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -80,7 +94,12 @@ namespace BTCPayServer.Logging
}
set
{
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_queueProcessor.Console = value;
}
}
@ -92,13 +111,13 @@ namespace BTCPayServer.Logging
}
set
{
_filter = value ?? throw new ArgumentNullException(nameof(value));
}
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
public bool IncludeScopes
{
get; set;
_filter = value;
}
}
public string Name
@ -106,6 +125,16 @@ namespace BTCPayServer.Logging
get;
}
internal IExternalScopeProvider ScopeProvider
{
get; set;
}
public bool DisableColors
{
get; set;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
@ -154,10 +183,7 @@ namespace BTCPayServer.Logging
while (lenAfter++ < 18)
logBuilder.Append(" ");
// scope information
if (IncludeScopes)
{
GetScopeInformation(logBuilder);
}
GetScopeInformation(logBuilder);
if (!string.IsNullOrEmpty(message))
{
@ -202,18 +228,15 @@ namespace BTCPayServer.Logging
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel == LogLevel.None)
{
return false;
}
return Filter(Name, logLevel);
}
public IDisposable BeginScope<TState>(TState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
return ConsoleLogScope.Push(Name, state);
}
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
private static string GetLogLevelString(LogLevel logLevel)
{
@ -238,6 +261,11 @@ namespace BTCPayServer.Logging
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
if (DisableColors)
{
return new ConsoleColors(null, null);
}
// We must explicitly set the background color if we are setting the foreground color,
// since just setting one can look bad on the users console.
switch (logLevel)
@ -259,30 +287,25 @@ namespace BTCPayServer.Logging
}
}
private void GetScopeInformation(StringBuilder builder)
private void GetScopeInformation(StringBuilder stringBuilder)
{
var current = ConsoleLogScope.Current;
string scopeLog = string.Empty;
var length = builder.Length;
while (current != null)
var scopeProvider = ScopeProvider;
if (scopeProvider != null)
{
if (length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
{
scopeLog = $"=> {current} ";
}
var initialLength = stringBuilder.Length;
builder.Insert(length, scopeLog);
current = current.Parent;
}
if (builder.Length > length)
{
builder.Insert(length, _messagePadding);
builder.AppendLine();
scopeProvider.ForEachScope((scope, state) =>
{
var (builder, length) = state;
var first = length == builder.Length;
builder.Append(first ? "=> " : " => ").Append(scope);
}, (stringBuilder, initialLength));
if (stringBuilder.Length > initialLength)
{
stringBuilder.Insert(initialLength, _messagePadding);
stringBuilder.AppendLine();
}
}
}
@ -333,9 +356,9 @@ namespace BTCPayServer.Logging
// Start Console message queue processor
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
default(CancellationToken),
TaskCreationOptions.LongRunning, TaskScheduler.Default);
state: this,
cancellationToken: default(CancellationToken),
creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default);
}
public virtual void EnqueueMessage(LogMessageEntry message)

@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
Id = table.Column<string>(nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
},
constraints: table =>
{
@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Id = table.Column<string>(nullable: false),
AccessFailedCount = table.Column<int>(nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false),
LockoutEnabled = table.Column<bool>(nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(nullable: true),
PhoneNumber = table.Column<string>(nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(nullable: true),
TwoFactorEnabled = table.Column<bool>(nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
UserName = table.Column<string>(maxLength: 256, nullable: true)
},
constraints: table =>
{
@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations
name: "Stores",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
Id = table.Column<string>(nullable: false),
DerivationStrategy = table.Column<string>(nullable: true),
SpeedPolicy = table.Column<int>(nullable: false),
StoreCertificate = table.Column<byte[]>(nullable: true),
StoreName = table.Column<string>(type: "TEXT", nullable: true),
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
StoreName = table.Column<string>(nullable: true),
StoreWebsite = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
RoleId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
LoginProvider = table.Column<string>(nullable: false),
ProviderKey = table.Column<string>(nullable: false),
ProviderDisplayName = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
UserId = table.Column<string>(nullable: false),
RoleId = table.Column<string>(nullable: false)
},
constraints: table =>
{
@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
UserId = table.Column<string>(nullable: false),
LoginProvider = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: false),
Value = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations
name: "Invoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
OrderId = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
CustomerEmail = table.Column<string>(nullable: true),
ExceptionStatus = table.Column<string>(nullable: true),
ItemCode = table.Column<string>(nullable: true),
OrderId = table.Column<string>(nullable: true),
Status = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations
name: "UserStore",
columns: table => new
{
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
Role = table.Column<string>(type: "TEXT", nullable: true)
ApplicationUserId = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: false),
Role = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations
name: "Payments",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations
name: "RefundAddresses",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
name: "Settings",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
Id = table.Column<string>(nullable: false),
Value = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
name: "AddressInvoices",
columns: table => new
{
Address = table.Column<string>(type: "TEXT", nullable: false),
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
Address = table.Column<string>(nullable: false),
InvoiceDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations
name: "PairedSINData",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Id = table.Column<string>(nullable: false),
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
PairingTime = table.Column<DateTimeOffset>(nullable: false),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations
name: "PairingCodes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(nullable: false),
DateCreated = table.Column<DateTime>(nullable: false),
Expiration = table.Column<DateTimeOffset>(nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true),
TokenValue = table.Column<string>(nullable: true)
},
constraints: table =>
{

@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
name: "PendingInvoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false)
Id = table.Column<string>(nullable: false)
},
constraints: table =>
{

@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations
name: "HistoricalAddressInvoices",
columns: table => new
{
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
Address = table.Column<string>(type: "TEXT", nullable: false),
InvoiceDataId = table.Column<string>(nullable: false),
Address = table.Column<string>(nullable: false),
Assigned = table.Column<DateTimeOffset>(nullable: false),
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
},

@ -20,5 +20,9 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
}
}

@ -79,7 +79,7 @@ namespace BTCPayServer.Models
//"price":5
[JsonProperty("price")]
public double Price
public decimal Price
{
get; set;
}
@ -94,7 +94,7 @@ namespace BTCPayServer.Models
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
[Obsolete("Use CryptoInfo.ExRates instead")]
public Dictionary<string, double> ExRates
public Dictionary<string, decimal> ExRates
{
get; set;
}
@ -224,6 +224,29 @@ namespace BTCPayServer.Models
{
get; set;
}
[JsonProperty("paymentSubtotals")]
public Dictionary<string, long> PaymentSubtotals { get; set; }
[JsonProperty("paymentTotals")]
public Dictionary<string, long> PaymentTotals { get; set; }
[JsonProperty("amountPaid", DefaultValueHandling = DefaultValueHandling.Include)]
public long AmountPaid { get; set; }
[JsonProperty("minerFees")]
public Dictionary<string, NBitpayClient.MinerFeeInfo> MinerFees { get; set; }
[JsonProperty("exchangeRates")]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
[JsonProperty("supportedTransactionCurrencies")]
public Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
[JsonProperty("addresses")]
public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
}
public class Flags
{
@ -233,4 +256,5 @@ namespace BTCPayServer.Models
get; set;
}
}
}

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.InvoicingModels
{
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
Currency = "USD";
}
[Required]
public double? Amount
public decimal? Amount
{
get; set;
}
@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[Url]
[Uri]
public string NotificationUrl
{
get; set;

@ -33,10 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels
public class InvoiceModel
{
public string Date
{
get; set;
}
public DateTimeOffset Date { get; set; }
public string OrderId { get; set; }
public string RedirectUrl { get; set; }
@ -49,6 +46,8 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public bool ShowCheckout { get; set; }
public string ExceptionStatus { get; set; }
public string AmountCurrency
{
get; set;

@ -37,6 +37,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string TimeLeft { get; set; }
public string Rate { get; set; }
public string OrderAmount { get; set; }
public string OrderAmountFiat { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string InvoiceBitcoinUrlQR { get; set; }
public int TxCount { get; set; }

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
@ -42,10 +43,10 @@ namespace BTCPayServer.Models.StoreViewModels
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
[Uri]
public string CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
[Uri]
public string CustomLogo { get; set; }
[Display(Name = "Custom HTML title to display on Checkout page")]

@ -44,7 +44,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by ...")]
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
{

@ -1,6 +1,7 @@
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validation;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -34,7 +35,7 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Url]
[Uri]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite
@ -85,5 +86,13 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
} = new List<LightningNode>();
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Range(0, 100)]
public double PaymentTolerance
{
get;
set;
}
}
}

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
@ -44,6 +45,9 @@ namespace BTCPayServer.Models
public class PairingCodeResponse
{
[JsonProperty(PropertyName = "policies")]
public JArray Policies { get; set; }
[JsonProperty(PropertyName = "pairingCode")]
public string PairingCode
{

@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin
{
return ConfirmationCount >= 1;
}
else if (speedPolicy == SpeedPolicy.LowMediumSpeed)
{
return ConfirmationCount >= 2;
}
else if (speedPolicy == SpeedPolicy.LowSpeed)
{
return ConfirmationCount >= 6;

@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
EventAggregator aggregator, IApplicationLifetime lifetime)
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallets = wallets;
@ -161,7 +161,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
if(payment != null)
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
}
else
{
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null)
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
totalPayment++;
}
}
@ -346,10 +346,10 @@ namespace BTCPayServer.Payments.Bitcoin
.FirstOrDefault();
}
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, InvoiceEntity invoice, PaymentEntity payment, DerivationStrategyBase strategy)
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
@ -358,13 +358,13 @@ namespace BTCPayServer.Payments.Bitcoin
{
var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address.ToString();
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

@ -156,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}

@ -36,17 +36,25 @@ namespace BTCPayServer.Payments.Lightning
expiry = TimeSpan.FromSeconds(1);
LightningInvoice lightningInvoice = null;
try
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
using (var cts = new CancellationTokenSource(5000))
{
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
try
{
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
}
var nodeInfo = await test;
return new LightningLikePaymentMethodDetails()
@ -62,34 +70,36 @@ namespace BTCPayServer.Payments.Lightning
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
using (var cts = new CancellationTokenSource(5000))
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if (info.Address == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
if (info.Address == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
}
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)

@ -46,7 +46,7 @@ namespace BTCPayServer.Payments.Lightning
{
if (inv.Name == "invoice_created")
{
await EnsureListening(inv.InvoiceId, false);
await EnsureListening(inv.Invoice.Id, false);
}
}));
@ -189,8 +189,12 @@ namespace BTCPayServer.Payments.Lightning
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
if(invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
}
}
List<Task> _ListeningLightning = new List<Task>();

@ -40,7 +40,6 @@ namespace BTCPayServer
.UseIISIntegration()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseConfiguration(conf)
.UseApplicationInsights()
.ConfigureLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);

@ -45,6 +45,11 @@ namespace BTCPayServer.Rating
var currencyPair = splitted[0];
if (currencyPair.Length < 6 || currencyPair.Length > 10)
return false;
if (currencyPair.Length == 6)
{
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
return true;
}
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);

@ -41,9 +41,9 @@ namespace BTCPayServer.Rating
}
else
{
if (rate.Value.HasValue)
if (rate.BidAsk != null)
{
_AllRates[key].Value = rate.Value;
_AllRates[key].BidAsk = rate.BidAsk;
}
}
}
@ -58,39 +58,175 @@ namespace BTCPayServer.Rating
return GetEnumerator();
}
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
public void SetRate(string exchangeName, CurrencyPair currencyPair, BidAsk bidAsk)
{
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
rate.Value = value;
if(rate != null)
{
rate.BidAsk = bidAsk;
}
var invPair = currencyPair.Inverse();
var invRate = rates.FirstOrDefault(r => r.CurrencyPair == invPair);
if (invRate != null)
{
invRate.BidAsk = bidAsk?.Inverse();
}
}
}
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
public BidAsk GetRate(string exchangeName, CurrencyPair currencyPair)
{
if (currencyPair.Left == currencyPair.Right)
return 1.0m;
return BidAsk.One;
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
return rate.Value;
return rate.BidAsk;
}
return null;
}
}
public class BidAsk
{
private readonly static BidAsk _One = new BidAsk(1.0m);
public static BidAsk One
{
get
{
return _One;
}
}
private readonly static BidAsk _Zero = new BidAsk(0.0m);
public static BidAsk Zero
{
get
{
return _Zero;
}
}
public BidAsk(decimal bid, decimal ask)
{
if (bid > ask)
throw new ArgumentException("the bid should be lower than ask", nameof(bid));
_Ask = ask;
_Bid = bid;
}
public BidAsk(decimal v) : this(v, v)
{
}
private readonly decimal _Bid;
public decimal Bid
{
get
{
return _Bid;
}
}
private readonly decimal _Ask;
public decimal Ask
{
get
{
return _Ask;
}
}
public BidAsk Inverse()
{
return new BidAsk(1.0m / Ask, 1.0m / Bid);
}
public static BidAsk operator +(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid + b.Bid, a.Ask + b.Ask);
}
public static BidAsk operator +(BidAsk a)
{
return new BidAsk(a.Bid, a.Ask);
}
public static BidAsk operator -(BidAsk a)
{
return new BidAsk(-a.Bid, -a.Ask);
}
public static BidAsk operator *(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid * b.Bid, a.Ask * b.Ask);
}
public static BidAsk operator /(BidAsk a, BidAsk b)
{
// This one is tricky.
// BTC_EUR = (6000, 6100)
// Implicit rule give
// EUR_BTC = 1 / BTC_EUR
// Or
// EUR_BTC = (1, 1) / BTC_EUR
// Naive calculation would give us ( 1/6000, 1/6100) = (0.000166, 0.000163)
// However, this is an invalid BidAsk!!! because 0.000166 > 0.000163
// So instead, we need to calculate (1/6100, 1/6000)
return new BidAsk(a.Bid / b.Ask, a.Ask / b.Bid);
}
public static BidAsk operator -(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid - b.Bid, a.Ask - b.Ask);
}
public override bool Equals(object obj)
{
BidAsk item = obj as BidAsk;
if (item == null)
return false;
return Bid == item.Bid && Ask == item.Ask;
}
public static bool operator ==(BidAsk a, BidAsk b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.Bid == b.Bid && a.Ask == b.Ask;
}
public static bool operator !=(BidAsk a, BidAsk b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode(StringComparison.InvariantCulture);
}
public override string ToString()
{
if (Bid == Ask)
return Bid.ToString(CultureInfo.InvariantCulture);
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
}
}
public class ExchangeRate
{
public string Exchange { get; set; }
public CurrencyPair CurrencyPair { get; set; }
public decimal? Value { get; set; }
public BidAsk BidAsk { get; set; }
public override string ToString()
{
if (Value == null)
if (BidAsk == null)
return $"{Exchange}({CurrencyPair})";
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
return $"{Exchange}({CurrencyPair}) == {BidAsk.ToString()}";
}
}
}

@ -18,6 +18,7 @@ namespace BTCPayServer.Rating
UnsupportedOperator,
MissingArgument,
DivideByZero,
InvalidNegative,
PreprocessError,
RateUnavailable,
InvalidExchangeName,
@ -139,7 +140,7 @@ namespace BTCPayServer.Rating
}
return new RateRule(this, currencyPair, candidate);
}
public ExpressionSyntax FindBestCandidate(CurrencyPair p)
{
var invP = p.Inverse();
@ -147,9 +148,9 @@ namespace BTCPayServer.Rating
foreach (var pair in new[]
{
(Pair: p, Priority: 0, Inverse: false),
(Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false),
(Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false),
(Pair: invP, Priority: 2, Inverse: true),
(Pair: invP, Priority: 1, Inverse: true),
(Pair: new CurrencyPair(p.Left, "X"), Priority: 2, Inverse: false),
(Pair: new CurrencyPair("X", p.Right), Priority: 2, Inverse: false),
(Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true),
(Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true),
(Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false)
@ -216,8 +217,7 @@ namespace BTCPayServer.Rating
}
else
{
var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture));
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token);
return RateRules.CreateExpression(rate.ToString());
}
}
}
@ -225,7 +225,7 @@ namespace BTCPayServer.Rating
class CalculateWalker : CSharpSyntaxWalker
{
public Stack<decimal> Values = new Stack<decimal>();
public Stack<BidAsk> Values = new Stack<BidAsk>();
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node)
@ -254,7 +254,15 @@ namespace BTCPayServer.Rating
switch (node.Kind())
{
case SyntaxKind.UnaryMinusExpression:
Values.Push(-Values.Pop());
var v = Values.Pop();
if(v.Bid == v.Ask)
{
Values.Push(-v);
}
else
{
Errors.Add(RateRulesErrors.InvalidNegative);
}
break;
case SyntaxKind.UnaryPlusExpression:
Values.Push(+Values.Pop());
@ -299,7 +307,7 @@ namespace BTCPayServer.Rating
Values.Push(a * b);
break;
case SyntaxKind.DivideExpression:
if (b == decimal.Zero)
if (a.Ask == decimal.Zero || b.Ask == decimal.Zero)
{
Errors.Add(RateRulesErrors.DivideByZero);
}
@ -309,19 +317,48 @@ namespace BTCPayServer.Rating
}
break;
case SyntaxKind.SubtractExpression:
Values.Push(a - b);
if (b.Bid == b.Ask)
{
Values.Push(a - b);
}
else
{
Errors.Add(RateRulesErrors.InvalidNegative);
}
break;
default:
throw new NotSupportedException("Should never happen");
}
}
Stack<decimal> _TupleValues = null;
public override void VisitTupleExpression(TupleExpressionSyntax node)
{
_TupleValues = new Stack<decimal>();
base.VisitTupleExpression(node);
if(_TupleValues.Count != 2)
{
Errors.Add(RateRulesErrors.MissingArgument);
}
else
{
var ask = _TupleValues.Pop();
var bid = _TupleValues.Pop();
Values.Push(new BidAsk(bid, ask));
}
_TupleValues = null;
}
public override void VisitLiteralExpression(LiteralExpressionSyntax node)
{
switch (node.Kind())
{
case SyntaxKind.NumericLiteralExpression:
Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture));
var v = decimal.Parse(node.ToString(), CultureInfo.InvariantCulture);
if (_TupleValues == null)
Values.Push(new BidAsk(v));
else
_TupleValues.Push(v);
break;
}
}
@ -347,17 +384,23 @@ namespace BTCPayServer.Rating
class FlattenExpressionRewriter : CSharpSyntaxRewriter
{
RateRules parent;
CurrencyPair pair;
int nested = 0;
public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair)
{
Context.Push(pair);
this.pair = pair;
this.parent = parent;
}
public ExchangeRates ExchangeRates = new ExchangeRates();
public Stack<CurrencyPair> Context { get; set; } = new Stack<CurrencyPair>();
bool IsInvocation;
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (IsInvocation)
{
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})");
}
IsInvocation = true;
_ExchangeName = node.Expression.ToString();
var result = base.VisitInvocationExpression(node);
@ -365,18 +408,27 @@ namespace BTCPayServer.Rating
return result;
}
bool IsArgumentList;
public override SyntaxNode VisitArgumentList(ArgumentListSyntax node)
{
IsArgumentList = true;
var result = base.VisitArgumentList(node);
IsArgumentList = false;
return result;
}
string _ExchangeName = null;
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
const int MaxNestedCount = 8;
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
{
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
if (
(!IsInvocation || IsArgumentList) &&
CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
{
var ctx = Context.Peek();
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left,
right: currentPair.Right == "X" ? ctx.Right : currentPair.Right);
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left,
right: currentPair.Right == "X" ? pair.Right : currentPair.Right);
if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD)
{
ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName });
@ -385,13 +437,13 @@ namespace BTCPayServer.Rating
else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD
{
var bestCandidate = parent.FindBestCandidate(replacedPair);
if (Context.Count > MaxNestedCount)
if (nested > MaxNestedCount)
{
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
}
Context.Push(replacedPair);
var replaced = Visit(bestCandidate);
var innerFlatten = CreateNewContext(replacedPair);
var replaced = innerFlatten.Visit(bestCandidate);
if (replaced is ExpressionSyntax expression)
{
var hasBinaryOps = new HasBinaryOperations();
@ -401,7 +453,6 @@ namespace BTCPayServer.Rating
replaced = SyntaxFactory.ParenthesizedExpression(expression);
}
}
Context.Pop();
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
{
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
@ -411,16 +462,37 @@ namespace BTCPayServer.Rating
}
return base.VisitIdentifierName(node);
}
private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair)
{
return new FlattenExpressionRewriter(parent, pair)
{
Errors = Errors,
nested = nested + 1,
ExchangeRates = ExchangeRates,
};
}
}
private SyntaxNode expression;
FlattenExpressionRewriter flatten;
public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate)
{
_CurrencyPair = currencyPair;
flatten = new FlattenExpressionRewriter(parent, currencyPair);
this.expression = flatten.Visit(candidate);
}
private readonly CurrencyPair _CurrencyPair;
public CurrencyPair CurrencyPair
{
get
{
return _CurrencyPair;
}
}
public ExchangeRates ExchangeRates
{
get
@ -452,7 +524,7 @@ namespace BTCPayServer.Rating
Errors.AddRange(calculate.Errors);
return false;
}
_Value = calculate.Values.Pop();
_Value = calculate.Values.Pop().Bid;
_EvaluatedNode = result;
return true;
}

@ -0,0 +1,247 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Authentication;
using System.Text.Encodings.Web;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Security
{
public class BitpayAuthentication
{
public class BitpayAuthOptions : AuthenticationSchemeOptions
{
}
class BitpayAuthHandler : AuthenticationHandler<BitpayAuthOptions>
{
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayAuthHandler(
TokenRepository tokenRepository,
StoreRepository storeRepository,
IOptionsMonitor<BitpayAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
_TokenRepository = tokenRepository;
_StoreRepository = storeRepository;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.HttpContext.GetIsBitpayAPI())
{
List<Claim> claims = new List<Claim>();
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
string storeId = null;
// Careful, those are not the opposite. failedAuth says if a the tentative failed.
// successAuth, ensure that at least one succeed.
var failedAuth = false;
var successAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
storeId = result.StoreId;
failedAuth = !result.SuccessAuth;
successAuth = result.SuccessAuth;
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
successAuth = storeId != null;
}
if (failedAuth)
{
return AuthenticateResult.Fail("Invalid credentials");
}
if (successAuth)
{
if (storeId != null)
{
claims.Add(new Claim(Policies.CanUseStore.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
}
return AuthenticateResult.NoResult();
}
private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List<Claim> claims)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
claims.Add(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return (null, false);
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return (storeId, true);
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if (expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "GET")
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) => { });
services.AddAuthentication().AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
}
}
}

@ -1,196 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
namespace BTCPayServer.Security
{
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayClaimsFilter(
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
_TokenRepository = tokenRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BitpayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (context.HttpContext.GetIsBitpayAPI())
{
var bitpayAuth = context.HttpContext.GetBitpayAuth();
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
var store = await _StoreRepository.FindStore(storeId);
context.HttpContext.SetStoreData(store);
}
else if (failedAuth)
{
throw new BitpayHttpException(401, "Can't access to store");
}
}
}
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return null;
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return storeId;
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if (expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
}
}

@ -8,6 +8,7 @@ namespace BTCPayServer.Security
{
public static class Policies
{
public const string BitpayAuthentication = "Bitpay.Auth";
public const string CookieAuthentication = "Identity.Application";
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{

@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees
ExplorerClient _ExplorerClient;
public async Task<FeeRate> GetFeeRateAsync()
{
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
return _Factory.Fallback;
try
{
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;

@ -118,18 +118,7 @@ namespace BTCPayServer.Services
}
}
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (!strategy.Segwit)
return false;
return await GetKeyPath(_Ledger, network, strategy) != null;
}
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
@ -143,7 +132,7 @@ namespace BTCPayServer.Services
{
try
{
var extpubkey = await GetExtPubKey(ledger, network, account, true);
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{
foundKeyPath = account;
@ -159,79 +148,12 @@ namespace BTCPayServer.Services
return foundKeyPath;
}
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
ReceivedCoin[] coins, BTCPayNetwork network,
(IDestination destination, Money amount, bool substractFees)[] send,
FeeRate feeRate,
IDestination changeAddress,
KeyPath changeKeyPath,
FeeRate minTxRelayFee)
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
Transaction unsigned,
KeyPath changeKeyPath)
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (network == null)
throw new ArgumentNullException(nameof(network));
if (feeRate == null)
throw new ArgumentNullException(nameof(feeRate));
if (changeAddress == null)
throw new ArgumentNullException(nameof(changeAddress));
if (feeRate.FeePerK <= Money.Zero)
{
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
}
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress);
builder.SendEstimatedFees(feeRate);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach(var c in coins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
_Transport.Timeout = TimeSpan.FromMinutes(5);
var fullySigned = await Ledger.SignTransactionAsync(
usedCoins.Select(c => new SignatureRequest
{
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
return fullySigned;
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
}
}

@ -12,6 +12,8 @@ using NBXplorer.Models;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using NBitpayClient;
using BTCPayServer.Payments.Bitcoin;
namespace BTCPayServer.Services.Invoices
{
@ -100,7 +102,8 @@ namespace BTCPayServer.Services.Invoices
{
HighSpeed = 0,
MediumSpeed = 1,
LowSpeed = 2
LowSpeed = 2,
LowMediumSpeed = 3
}
public class InvoiceEntity
{
@ -314,6 +317,7 @@ namespace BTCPayServer.Services.Invoices
}
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public double PaymentTolerance { get; set; }
public bool IsExpired()
{
@ -334,19 +338,34 @@ namespace BTCPayServer.Services.Invoices
ExpirationTime = ExpirationTime,
Status = Status,
Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable }
Flags = new Flags() { Refundable = Refundable },
PaymentSubtotals = new Dictionary<string, long>(),
PaymentTotals = new Dictionary<string, long>(),
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(),
Addresses = new Dictionary<string, string>(),
PaymentCodes = new Dictionary<string, InvoicePaymentUrls>(),
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
};
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
foreach (var info in this.GetPaymentMethods(networkProvider))
{
var accounting = info.Calculate();
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
var subtotalPrice = accounting.TotalDue - accounting.NetworkFee;
var cryptoCode = info.GetId().CryptoCode;
var address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
var exrates = new Dictionary<string, decimal>
{
{ ProductInformation.Currency, cryptoInfo.Rate }
};
cryptoInfo.CryptoCode = cryptoCode;
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString();
cryptoInfo.Price = subtotalPrice.ToString();
cryptoInfo.Due = accounting.Due.ToString();
cryptoInfo.Paid = accounting.Paid.ToString();
@ -355,17 +374,19 @@ namespace BTCPayServer.Services.Invoices
cryptoInfo.TxCount = accounting.TxCount;
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
cryptoInfo.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
};
cryptoInfo.Address = address;
cryptoInfo.ExRates = exrates;
var paymentId = info.GetId();
var scheme = info.Network.UriScheme;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
if (paymentId.PaymentType == PaymentTypes.BTCLike)
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo);
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
@ -375,7 +396,7 @@ namespace BTCPayServer.Services.Invoices
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
if (paymentId.PaymentType == PaymentTypes.LightningLike)
{
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
@ -394,10 +415,23 @@ namespace BTCPayServer.Services.Invoices
dto.BTCDue = cryptoInfo.Due;
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
dto.CryptoInfo.Add(cryptoInfo);
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
{
Enabled = true
});
dto.Addresses.Add(paymentId.ToString(), address);
dto.ExchangeRates.TryAdd(cryptoCode, exrates);
}
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
Populate(ProductInformation, dto);
Populate(BuyerInformation, dto);
@ -523,6 +557,10 @@ namespace BTCPayServer.Services.Invoices
/// Total amount of network fee to pay to the invoice
/// </summary>
public Money NetworkFee { get; set; }
/// <summary>
/// Minimum required to be paid in order to accept invocie as paid
/// </summary>
public Money MinimumTotalDue { get; set; }
}
public class PaymentMethod
@ -671,6 +709,7 @@ namespace BTCPayServer.Services.Invoices
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))));
return accounting;
}

@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices
invoice.StoreId = storeId;
using (var context = _ContextFactory.CreateContext())
{
context.Invoices.Add(new InvoiceData()
context.Invoices.Add(new Data.InvoiceData()
{
StoreDataId = storeId,
Id = invoice.Id,
@ -267,7 +267,7 @@ namespace BTCPayServer.Services.Invoices
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
@ -307,7 +307,7 @@ namespace BTCPayServer.Services.Invoices
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
invoiceData.Status = status;
@ -320,7 +320,7 @@ namespace BTCPayServer.Services.Invoices
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData?.Status != "paid")
return;
invoiceData.Status = "invalid";
@ -331,7 +331,7 @@ namespace BTCPayServer.Services.Invoices
{
using (var context = _ContextFactory.CreateContext())
{
IQueryable<InvoiceData> query =
IQueryable<Data.InvoiceData> query =
context
.Invoices
.Include(o => o.Payments)
@ -351,7 +351,7 @@ namespace BTCPayServer.Services.Invoices
}
}
private InvoiceEntity ToEntity(InvoiceData invoice)
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
#pragma warning disable CS0618
@ -386,7 +386,7 @@ namespace BTCPayServer.Services.Invoices
{
using (var context = _ContextFactory.CreateContext())
{
IQueryable<InvoiceData> query = context
IQueryable<Data.InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
@ -436,6 +436,18 @@ namespace BTCPayServer.Services.Invoices
query = query.Where(i => statusSet.Contains(i.Status));
}
if(queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
@ -451,6 +463,29 @@ namespace BTCPayServer.Services.Invoices
}
private string NormalizeExceptionStatus(string status)
{
status = status.ToLowerInvariant();
switch (status)
{
case "paidover":
case "over":
case "overpaid":
status = "paidOver";
break;
case "paidlate":
case "late":
status = "paidLate";
break;
case "paidpartial":
case "underpaid":
case "partial":
status = "paidPartial";
break;
}
return status;
}
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
{
if (outputs.Length == 0)
@ -614,10 +649,18 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public bool? Unusual { get; set; }
public string[] Status
{
get; set;
}
public string[] ExceptionStatus
{
get; set;
}
public string InvoiceId
{
get;

@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails
}
public async Task SendEmailAsync(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>();
if (settings == null)
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails
public async Task SendMailCore(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>();
if (settings == null)
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);

@ -40,10 +40,22 @@ namespace BTCPayServer.Services.Mails
get; set;
}
public bool IsComplete()
{
SmtpClient smtp = null;
try
{
smtp = CreateSmtpClient();
return true;
}
catch { }
return false;
}
public SmtpClient CreateSmtpClient()
{
SmtpClient client = new SmtpClient(Server, Port.Value);
client.EnableSsl = true;
client.EnableSsl = EnableSSL;
client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(Login, Password);
client.DeliveryMethod = SmtpDeliveryMethod.Network;

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
@ -8,12 +9,14 @@ namespace BTCPayServer.Services
{
public class PoliciesSettings
{
[Display(Name = "Requires a confirmation mail for registering")]
public bool RequiresConfirmedEmail
{
get; set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[Display(Name = "Disable registration")]
public bool LockSubscription { get; set; }
}
}

@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -35,7 +36,6 @@ namespace BTCPayServer.Services.Rates
}
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
@ -68,10 +68,12 @@ namespace BTCPayServer.Services.Rates
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings });
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
@ -82,6 +84,20 @@ namespace BTCPayServer.Services.Rates
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
}
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders

@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates
{
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), BidAsk = new BidAsk(r.Value) })
.ToList());
}
}

@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Rates
public const string CoinAverageName = "coinaverage";
public CoinAverageRateProvider()
{
}
static HttpClient _Client = new HttpClient();
@ -69,10 +69,31 @@ namespace BTCPayServer.Services.Rates
public ICoinAverageAuthenticator Authenticator { get; set; }
private bool TryToDecimal(JProperty p, out decimal v)
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
{
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
bidAsk = null;
if (Exchange == CoinAverageName)
{
JToken last = p.Value["last"];
if (!decimal.TryParse(last.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) ||
v <= 0)
return false;
bidAsk = new BidAsk(v);
return true;
}
else
{
JToken bid = p.Value["bid"];
JToken ask = p.Value["ask"];
if (bid == null || ask == null ||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 > v2 ||
v1 <= 0 || v2 <= 0)
return false;
bidAsk = new BidAsk(v1, v2);
return true;
}
}
public async Task<ExchangeRates> GetRatesAsync()
@ -108,10 +129,10 @@ namespace BTCPayServer.Services.Rates
{
ExchangeRate exchangeRate = new ExchangeRate();
exchangeRate.Exchange = Exchange;
if (!TryToDecimal(prop, out decimal value))
if (!TryToBidAsk(prop, out var value))
continue;
exchangeRate.Value = value;
if(CurrencyPair.TryParse(prop.Name, out var pair))
exchangeRate.BidAsk = value;
if (CurrencyPair.TryParse(prop.Name, out var pair))
{
exchangeRate.CurrencyPair = pair;
exchangeRates.Add(exchangeRate);

@ -43,12 +43,11 @@ namespace BTCPayServer.Services.Rates
{
public CoinAverageExchanges()
{
Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
}
public void Add(CoinAverageExchange exchange)
{
Add(exchange.Name, exchange);
TryAdd(exchange.Name, exchange);
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator

@ -31,6 +31,7 @@ namespace BTCPayServer.Services.Rates
get;
internal set;
}
public bool Crypto { get; set; }
}
public class CurrencyNameTable
{
@ -40,6 +41,27 @@ namespace BTCPayServer.Services.Rates
}
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{
var data = GetCurrencyProvider(currency);
if (data is NumberFormatInfo nfi)
return nfi;
if (data is CultureInfo ci)
return ci.NumberFormat;
if (!useFallback)
return null;
return CreateFallbackCurrencyFormatInfo(currency);
}
private NumberFormatInfo CreateFallbackCurrencyFormatInfo(string currency)
{
var usd = GetNumberFormatInfo("USD", false);
var currencyInfo = (NumberFormatInfo)usd.Clone();
currencyInfo.CurrencySymbol = currency;
return currencyInfo;
}
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
@ -54,7 +76,11 @@ namespace BTCPayServer.Services.Rates
}
catch { }
}
AddCurrency(_CurrencyProviders, "BTC", 8, "BTC");
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
}
}
return _CurrencyProviders.TryGet(currency);
}
@ -106,13 +132,38 @@ namespace BTCPayServer.Services.Rates
info.Symbol = splitted[3];
}
}
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = 8,
Name = network.CryptoCode,
Crypto = true
});
}
return dico.Values.ToArray();
}
public CurrencyData GetCurrencyData(string currency)
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
CurrencyData result;
_Currencies.TryGetValue(currency.ToUpperInvariant(), out result);
if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
{
if(useFallback)
{
var usd = GetCurrencyData("USD", false);
result = new CurrencyData()
{
Code = currency,
Crypto = true,
Name = currency,
Divisibility = usd.Divisibility
};
}
}
return result;
}

@ -61,7 +61,7 @@ namespace BTCPayServer.Services.Rates
var rate = new ExchangeRate();
rate.CurrencyPair = pair;
rate.Exchange = _ExchangeName;
rate.Value = ticker.Value.Bid;
rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask);
return rate;
}
catch (ArgumentException)

@ -14,13 +14,19 @@ namespace BTCPayServer.Services.Rates
public const string QuadrigacxName = "quadrigacx";
static HttpClient _Client = new HttpClient();
private bool TryToDecimal(JObject p, out decimal v)
private bool TryToBidAsk(JObject p, out BidAsk v)
{
v = 0.0m;
JToken token = p.Property("bid")?.Value;
if (token == null)
v = null;
JToken bid = p.Property("bid")?.Value;
JToken ask = p.Property("ask")?.Value;
if (bid == null || ask == null)
return false;
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 <= 0m || v2 <= 0m || v1 > v2)
return false;
v = new BidAsk(v1, v2);
return true;
}
public async Task<ExchangeRates> GetRatesAsync()
@ -37,9 +43,9 @@ namespace BTCPayServer.Services.Rates
continue;
rate.CurrencyPair = pair;
rate.Exchange = QuadrigacxName;
if (!TryToDecimal((JObject)prop.Value, out var v))
if (!TryToBidAsk((JObject)prop.Value, out var v))
continue;
rate.Value = v;
rate.BidAsk = v;
exchangeRates.Add(rate);
}
return exchangeRates;

@ -0,0 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace BTCPayServer.Validation
{
//from https://stackoverflow.com/a/47196738/275504
public class UriAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture);
Uri uri;
bool valid = string.IsNullOrWhiteSpace(str) || Uri.TryCreate(str, UriKind.Absolute, out uri);
if (!valid)
{
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
}

@ -7,7 +7,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", "Thank you for confirming your email.")
<partial name="_StatusMessage" model="@("Thank you for confirming your email.")" />
</div>
</div>
</div>

@ -7,7 +7,7 @@
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
</div>
<div class="row">

@ -7,7 +7,7 @@
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
</div>
<div class="row">

@ -8,7 +8,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>

@ -12,7 +12,7 @@
</div>
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
@ -38,6 +38,31 @@
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if(Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if(Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li><strong>Build the invoice's url by yourself</strong> do not trust the <code>url</code> field, this can be spoofed to use attacker's server.</li>
<li>Send a <code>GET</code> request to the invoice's url with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" />
</div>
@ -47,3 +72,10 @@
</div>
</div>
</section>
@section Scripts {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
}

@ -1,4 +1,6 @@
@model ViewPointOfSaleViewModel
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
@ -11,13 +13,13 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="~/vendor/bootstrap4/css/bootstrap.css" />
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
</head>
<body class="h-100">
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<form method="post">
<form method="post" asp-antiforgery="false">
<div class="row">
@for(int i = 0; i < Model.Items.Length; i++)
{
@ -34,7 +36,7 @@
{
<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<form method="post" data-buy>
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
<button class="btn btn-primary" type="submit">Pay</button>

@ -16,7 +16,7 @@
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<div class="timer-row__spinner">
@Html.Partial("Checkout-Spinner")
<partial name="Checkout-Spinner" />
</div>
<div class="timer-row__message">
<span v-if="srvModel.status === 'expired' || srvModel.status === 'invalid'">
@ -51,7 +51,7 @@
}
</div>
<div class="payment__spinner">
@Html.Partial("Checkout-Spinner")
<partial name="Checkout-Spinner" />
</div>
</div>
</div>
@ -73,7 +73,7 @@
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<div class="single-item-order__right__ex-rate">
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
</div>
@ -87,6 +87,12 @@
<div class="line-items__item__label">{{$t("Order Amount")}}</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items_fiatvalue" v-if="srvModel.orderAmountFiat">
<div class="line-items__item__label">&nbsp;</div>
<div class="line-items__item__value single-item-order__right__ex-rate">
{{srvModel.orderAmountFiat}}
</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span>{{$t("Network Cost")}}</span>
@ -133,7 +139,7 @@
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="payment-box">
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
@ -151,7 +157,7 @@
<button type="submit" class="action-button" style="margin-top: 15px;">
<span class="button-text">{{$t("Continue")}}</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>
@ -323,7 +329,7 @@
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>
@ -430,7 +436,7 @@
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>

@ -57,7 +57,7 @@
<div class="modal-content long">
<div class="content">
<div class="invoice">
@Html.Partial("Checkout-Body")
<partial name="Checkout-Body" />
</div>
</div>
</div>

@ -28,7 +28,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
@ -53,15 +53,15 @@
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate</td>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate</td>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate</td>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Transaction speed</th>
@ -289,7 +289,7 @@
@foreach(var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>
<td>@evt.Timestamp.ToBrowserDate()</td>
<td>@evt.Message</td>
</tr>
}

@ -8,7 +8,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
@ -20,14 +20,16 @@
<div id="help" class="collapse text-left">
<p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.<br />
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters
You can also apply filters to your search by searching for <code>filtername:value</code>, here is a list of supported filters
</p>
<ul>
<li><b>storeid:id</b> for filtering a specific store</li>
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
</ul>
<p>
If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`.
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed status:complete</code>.
</p>
</div>
<div class="form-group">
@ -64,7 +66,7 @@
@foreach(var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
@if(invoice.RedirectUrl != string.Empty)
{
@ -95,7 +97,7 @@
}
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.Status == "new")
@if(invoice.ShowCheckout)
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>

@ -4,7 +4,7 @@
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form method="post">

@ -3,7 +3,7 @@
ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins");
}
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
@if (Model.CurrentLogins?.Count > 0)
{
<h4>Registered Logins</h4>

@ -4,7 +4,7 @@
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">

@ -4,7 +4,7 @@
}
<h4>Set your password</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
<p class="text-info">
You do not have a local username/password for this site. Add a local
account so you can log in without an external login.

@ -5,7 +5,7 @@
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">

@ -5,7 +5,7 @@
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<partial name="_StatusMessage" for="StatusMessage" />
<table class="table table-sm table-responsive-md">

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