diff --git a/.circleci/can-build.sh b/.circleci/can-build.sh new file mode 100755 index 000000000..3fe53e575 --- /dev/null +++ b/.circleci/can-build.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +echo "Checking if it is possible to build Bitcoin only..." +cd ../BTCPayServer.Tests +docker-compose -f "docker-compose.yml" build \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index 19669605e..bd0462823 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: - checkout - run: command: | - cd .circleci && ./run-tests.sh "Fast=Fast" + cd .circleci && ./run-tests.sh "Fast=Fast" && ./can-build.sh selenium_tests: machine: enabled: true @@ -49,8 +49,10 @@ jobs: LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile . + sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64 + sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 arm32v7: machine: @@ -63,8 +65,10 @@ jobs: LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile . + sudo docker build --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 + sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 arm64v8: machine: @@ -77,8 +81,10 @@ jobs: LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile . + sudo docker build --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 + sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 multiarch: machine: @@ -99,6 +105,13 @@ jobs: sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7 sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8 sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p + + + sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 + sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 --os linux --arch amd64 + sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 --os linux --arch arm --variant v7 + sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 --os linux --arch arm64 --variant v8 + sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG-altcoins -p workflows: version: 2 diff --git a/.circleci/run-tests.sh b/.circleci/run-tests.sh index ba34c262b..f31cc6674 100755 --- a/.circleci/run-tests.sh +++ b/.circleci/run-tests.sh @@ -3,7 +3,7 @@ set -e cd ../BTCPayServer.Tests docker-compose -v -docker-compose down --v -docker-compose pull -docker-compose build -docker-compose run -e "TEST_FILTERS=$1" tests +docker-compose -f "docker-compose.altcoins.yml" down --v +docker-compose -f "docker-compose.altcoins.yml" pull +docker-compose -f "docker-compose.altcoins.yml" build +docker-compose -f "docker-compose.altcoins.yml" run -e "TEST_FILTERS=$1" tests diff --git a/.gitignore b/.gitignore index fbf55eb42..1fb4de6cc 100644 --- a/.gitignore +++ b/.gitignore @@ -292,5 +292,9 @@ __pycache__/ BTCPayServer/wwwroot/bundles/* !BTCPayServer/wwwroot/bundles/.gitignore -.vscode +.vscode/* +!.vscode/launch.json +!.vscode/tasks.json +!.vscode/extensions.json BTCPayServer/testpwd +.DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..b6962761f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["ms-dotnettools.csharp"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..f79718485 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/BTCPayServer/bin/Debug/netcoreapp3.1/BTCPayServer.dll", + "args": [], + "cwd": "${workspaceFolder}/BTCPayServer", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bListening on\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + }, + "logging": { + "moduleLoad": false + } + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..0c09b3f3a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/BTCPayServer/BTCPayServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/BTCPayServer/BTCPayServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/BTCPayServer/BTCPayServer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index b3cd8c197..b136b2a06 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 diff --git a/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs b/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs deleted file mode 100644 index f3ec5c797..000000000 --- a/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.JsonConverters -{ - public class DecimalStringJsonConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return (objectType == typeof(decimal) || objectType == typeof(decimal?)); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, - JsonSerializer serializer) - { - JToken token = JToken.Load(reader); - switch (token.Type) - { - case JTokenType.Float: - case JTokenType.Integer: - case JTokenType.String: - return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture); - case JTokenType.Null when objectType == typeof(decimal?): - return null; - default: - throw new JsonSerializationException("Unexpected token type: " + - token.Type); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value != null) - writer.WriteValue(((decimal)value).ToString(CultureInfo.InvariantCulture)); - } - } -} diff --git a/BTCPayServer.Client/JsonConverters/NumericStringJsonConverter.cs b/BTCPayServer.Client/JsonConverters/NumericStringJsonConverter.cs new file mode 100644 index 000000000..459bd1e0b --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/NumericStringJsonConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.JsonConverters +{ + public class NumericStringJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(decimal) || + objectType == typeof(decimal?) || + objectType == typeof(double) || + objectType == typeof(double?)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Float: + case JTokenType.Integer: + case JTokenType.String: + if (objectType == typeof(decimal) || objectType == typeof(decimal?) ) + return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture); + if (objectType == typeof(double) || objectType == typeof(double?)) + return double.Parse(token.ToString(), CultureInfo.InvariantCulture); + + throw new JsonSerializationException("Unexpected object type: " + objectType); + case JTokenType.Null when objectType == typeof(decimal?) || objectType == typeof(double?): + return null; + default: + throw new JsonSerializationException("Unexpected token type: " + + token.Type); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch (value) + { + case null: + break; + case decimal x: + writer.WriteValue(x.ToString(CultureInfo.InvariantCulture)); + break; + case double x: + writer.WriteValue(x.ToString(CultureInfo.InvariantCulture)); + break; + } + } + } +} diff --git a/BTCPayServer.Client/Models/CreatePayoutRequest.cs b/BTCPayServer.Client/Models/CreatePayoutRequest.cs index d8b665b4b..cfc1e33b1 100644 --- a/BTCPayServer.Client/Models/CreatePayoutRequest.cs +++ b/BTCPayServer.Client/Models/CreatePayoutRequest.cs @@ -6,7 +6,7 @@ namespace BTCPayServer.Client.Models public class CreatePayoutRequest { public string Destination { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal? Amount { get; set; } public string PaymentMethod { get; set; } } diff --git a/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs b/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs index 474c35624..a314573ad 100644 --- a/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs +++ b/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs @@ -8,7 +8,7 @@ namespace BTCPayServer.Client.Models public class CreatePullPaymentRequest { public string Name { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } public string Currency { get; set; } [JsonConverter(typeof(TimeSpanJsonConverter))] diff --git a/BTCPayServer.Client/Models/PaymentRequestBaseData.cs b/BTCPayServer.Client/Models/PaymentRequestBaseData.cs index 4a61af99f..d573ef7f3 100644 --- a/BTCPayServer.Client/Models/PaymentRequestBaseData.cs +++ b/BTCPayServer.Client/Models/PaymentRequestBaseData.cs @@ -8,7 +8,7 @@ namespace BTCPayServer.Client.Models { public class PaymentRequestBaseData { - [JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } public string Currency { get; set; } public DateTime? ExpiryDate { get; set; } diff --git a/BTCPayServer.Client/Models/PayoutData.cs b/BTCPayServer.Client/Models/PayoutData.cs index ea8698bf1..71a74fa72 100644 --- a/BTCPayServer.Client/Models/PayoutData.cs +++ b/BTCPayServer.Client/Models/PayoutData.cs @@ -21,9 +21,9 @@ namespace BTCPayServer.Client.Models public string PullPaymentId { get; set; } public string Destination { get; set; } public string PaymentMethod { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal? PaymentMethodAmount { get; set; } [JsonConverter(typeof(StringEnumConverter))] public PayoutState State { get; set; } diff --git a/BTCPayServer.Client/Models/PullPaymentBaseData.cs b/BTCPayServer.Client/Models/PullPaymentBaseData.cs index e9256621e..d95b60e06 100644 --- a/BTCPayServer.Client/Models/PullPaymentBaseData.cs +++ b/BTCPayServer.Client/Models/PullPaymentBaseData.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Client.Models public string Id { get; set; } public string Name { get; set; } public string Currency { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } [JsonConverter(typeof(TimeSpanJsonConverter))] public TimeSpan? Period { get; set; } diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs index bef65cd3f..28537869c 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs @@ -1,3 +1,4 @@ +#if ALTCOINS using NBitcoin; using NBitcoin.Altcoins; using NBitcoin.Altcoins.Elements; @@ -34,3 +35,4 @@ namespace BTCPayServer } +#endif diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs index a14c49c2b..ac729caac 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs @@ -1,3 +1,4 @@ +#if ALTCOINS using NBitcoin; namespace BTCPayServer @@ -81,3 +82,4 @@ namespace BTCPayServer } +#endif diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index 47321e83f..711e718e4 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -1,3 +1,4 @@ +#if ALTCOINS using System.Collections.Generic; using System.Linq; using NBitcoin; @@ -58,3 +59,4 @@ namespace BTCPayServer } } } +#endif diff --git a/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs b/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs new file mode 100644 index 000000000..c6c23b4b4 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs @@ -0,0 +1,18 @@ +#if ALTCOINS +using System.Collections.Generic; +using System.Linq; + +namespace BTCPayServer +{ + public static class LiquidExtensions + { + public static IEnumerable GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider) + { + var elementsBased = networkProvider.GetAll().OfType(); + var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct(); + return networkProvider.GetAll().OfType() + .Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant()); + } + } +} +#endif diff --git a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs index 012c4bdd3..6ad4ceadb 100644 --- a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs +++ b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs @@ -20,7 +20,8 @@ namespace BTCPayServer "XMR_X = XMR_BTC * BTC_X", "XMR_BTC = kraken(XMR_BTC)" }, - CryptoImagePath = "/imlegacy/monero.svg" + CryptoImagePath = "/imlegacy/monero.svg", + UriScheme = "monero" }); } } diff --git a/BTCPayServer.Common/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs index 6cf4b769e..76f532989 100644 --- a/BTCPayServer.Common/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs @@ -3,5 +3,6 @@ namespace BTCPayServer public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase { public int MaxTrackedConfirmation = 10; + public string UriScheme { get; set; } } } diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcoin.cs similarity index 100% rename from BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Bitcoin.cs diff --git a/BTCPayServer.Common/BTCPayNetworkProvider.cs b/BTCPayServer.Common/BTCPayNetworkProvider.cs index bb044ab84..99552deed 100644 --- a/BTCPayServer.Common/BTCPayNetworkProvider.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.cs @@ -131,4 +131,4 @@ namespace BTCPayServer return network as T; } } -} +} \ No newline at end of file diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index 6293fed52..47ca8fd8e 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -1,4 +1,4 @@ - + @@ -6,4 +6,7 @@ + + + diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 7436b550d..f44139247 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -1,4 +1,4 @@ - + diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 9ceef4b02..608f6d7ea 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -1,4 +1,4 @@ - + diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs new file mode 100644 index 000000000..8d4cc2769 --- /dev/null +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -0,0 +1,1040 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; +using BTCPayServer.Controllers; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Models; +using BTCPayServer.Models.AccountViewModels; +using BTCPayServer.Models.AppViewModels; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Models.WalletViewModels; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Bitcoin; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Rating; +using BTCPayServer.Security.Bitpay; +using BTCPayServer.Services; +using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; +using BTCPayServer.Tests.Logging; +using BTCPayServer.U2F.Models; +using BTCPayServer.Validation; +using ExchangeSharp; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBitcoin.Payment; +using NBitpayClient; +using NBXplorer.DerivationStrategy; +using NBXplorer.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; + +namespace BTCPayServer.Tests +{ + public class AltcoinTests + { + public const int TestTimeout = 60_000; + public AltcoinTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] + [Trait("Lightning", "Lightning")] + public async Task CanAddDerivationSchemes() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLTC(); + tester.ActivateLightning(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + user.RegisterDerivationScheme("LTC"); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); + var btcNetwork = tester.PayTester.Networks.GetNetwork("BTC"); + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 1.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.Equal(3, invoice.CryptoInfo.Length); + + var controller = user.GetController(); + var lightningVM = + (LightningNodeViewModel)Assert.IsType(controller.AddLightningNode(user.StoreId, "BTC")) + .Model; + Assert.True(lightningVM.Enabled); + lightningVM.Enabled = false; + controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult(); + lightningVM = + (LightningNodeViewModel)Assert.IsType(controller.AddLightningNode(user.StoreId, "BTC")) + .Model; + Assert.False(lightningVM.Enabled); + + // Only Enabling/Disabling the payment method must redirect to store page + var derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + Assert.True(derivationVM.Enabled); + derivationVM.Enabled = false; + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + Assert.False(derivationVM.Enabled); + + // Clicking next without changing anything should send to the confirmation screen + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + + invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 1.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.Single(invoice.CryptoInfo); + Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); + + // Removing the derivation scheme, should redirect to store page + var oldScheme = derivationVM.DerivationScheme; + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.DerivationScheme = null; + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + // Setting it again should redirect to the confirmation page + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.DerivationScheme = oldScheme; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + + + //cobo vault file + var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}"; + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content); + derivationVM.Enabled = true; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + //wasabi wallet file + content = + "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}"; + + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content); + derivationVM.Enabled = true; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + + // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + content = + "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; + derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content); + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.False(derivationVM + .Confirmation); // Should fail, we are giving a mainnet file to a testnet network + + // And with a good file? (upub) + content = + "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; + derivationVM = (DerivationSchemeViewModel)Assert + .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content); + derivationVM.Enabled = true; + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller + .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); + Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") + .GetAwaiter().GetResult()); + + + // Now let's check that no data has been lost in the process + var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult(); + var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) +#pragma warning disable CS0618 // Type or member is obsolete + .OfType().First(o => o.PaymentId.IsBTCOnChain); +#pragma warning restore CS0618 // Type or member is obsolete + DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected); + Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); + + // Let's check that the root hdkey and account key path are taken into account when making a PSBT + invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 1.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + tester.ExplorerNode.Generate(1); + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address, + tester.ExplorerNode.Network); + tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m)); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal("paid", invoice.Status); + }); + var wallet = tester.PayTester.GetController(); + var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, + new WalletSendModel() + { + Outputs = new List() + { + new WalletSendModel.TransactionOutput() + { + Amount = 0.5m, + DestinationAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, btcNetwork.NBitcoinNetwork) + .ToString(), + } + }, + FeeSatoshiPerByte = 1 + }, default).GetAwaiter().GetResult(); + + Assert.NotNull(psbt); + + var root = new Mnemonic( + "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") + .DeriveExtKey().AsHDKeyCache(); + var account = root.Derive(new KeyPath("m/49'/0'/0'")); + Assert.All(psbt.PSBT.Inputs, input => + { + var keyPath = input.HDKeyPaths.Single(); + Assert.False(keyPath.Value.KeyPath.IsHardened); + Assert.Equal(account.Derive(keyPath.Value.KeyPath).GetPublicKey(), keyPath.Key); + Assert.Equal(keyPath.Value.MasterFingerprint, + onchainBTC.AccountKeySettings[0].AccountKey.GetPublicKey().GetHDFingerPrint()); + }); + } + } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] + [Trait("Lightning", "Lightning")] + public async Task CanCreateInvoiceWithSpecificPaymentMethods() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLightning(); + tester.ActivateLTC(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterLightningNode("BTC", LightningConnectionType.Charge); + user.RegisterDerivationScheme("BTC"); + user.RegisterDerivationScheme("LTC"); + + var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); + + + invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC") + { + SupportedTransactionCurrencies = new Dictionary() + { + {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} + } + }); + + Assert.Single(invoice.SupportedTransactionCurrencies); + } + } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] + public async Task CanHaveLTCOnlyStore() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLTC(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("LTC"); + + // First we try payment with a merchant having only BTC + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 500, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.Single(invoice.CryptoInfo); + Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); + 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); + cashCow.SendToAddress(invoiceAddress, firstPayment); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); + }); + + Assert.Single(invoice.CryptoInfo); // Only BTC should be presented + + var controller = tester.PayTester.GetController(null); + var checkout = + (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) + .GetAwaiter().GetResult()).Value; + Assert.Single(checkout.AvailableCryptos); + Assert.Equal("LTC", checkout.CryptoCode); + + ////////////////////// + + // Despite it is called BitcoinAddress it should be LTC because BTC is not available + Assert.Null(invoice.BitcoinAddress); + Assert.NotEqual(1.0m, invoice.Rate); + Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate + cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due); + + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal("paid", invoice.Status); + checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) + .GetAwaiter().GetResult()).Value; + Assert.Equal("paid", checkout.Status); + }); + } + } + + + [Fact] + [Trait("Selenium", "Selenium")] + [Trait("Altcoins", "Altcoins")] + public async Task CanCreateRefunds() + { + using (var s = SeleniumTester.Create()) + { + s.Server.ActivateLTC(); + await s.StartAsync(); + var user = s.Server.NewAccount(); + await user.GrantAccessAsync(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + user.RegisterDerivationScheme("BTC"); + await s.Server.ExplorerNode.GenerateAsync(1); + + foreach (var multiCurrency in new[] { false, true }) + { + if (multiCurrency) + user.RegisterDerivationScheme("LTC"); + foreach (var rateSelection in new[] { "FiatText", "CurrentRateText", "RateThenText" }) + await CanCreateRefundsCore(s, user, multiCurrency, rateSelection); + } + } + } + + private static async Task CanCreateRefundsCore(SeleniumTester s, TestAccount user, bool multiCurrency, string rateSelection) + { + s.GoToHome(); + s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m, 5100.0m)); + var invoice = await user.BitPay.CreateInvoiceAsync(new NBitpayClient.Invoice() + { + Currency = "USD", + Price = 5000.0m + }); + var info = invoice.CryptoInfo.First(o => o.CryptoCode == "BTC"); + var totalDue = decimal.Parse(info.TotalDue, CultureInfo.InvariantCulture); + var paid = totalDue + 0.1m; + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(info.Address, Network.RegTest), Money.Coins(paid)); + await s.Server.ExplorerNode.GenerateAsync(1); + await TestUtils.EventuallyAsync(async () => + { + invoice = await user.BitPay.GetInvoiceAsync(invoice.Id); + Assert.Equal("confirmed", invoice.Status); + }); + + // BTC crash by 50% + s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m / 2.0m, 5100.0m / 2.0m)); + s.GoToInvoice(invoice.Id); + s.Driver.FindElement(By.Id("refundlink")).Click(); + if (multiCurrency) + { + s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter); + s.Driver.FindElement(By.Id("ok")).Click(); + } + Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat + Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before + Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate + s.Driver.FindElement(By.Id(rateSelection)).Click(); + s.Driver.FindElement(By.Id("ok")).Click(); + Assert.Contains("pull-payments", s.Driver.Url); + if (rateSelection == "FiatText") + Assert.Contains("$5,500.00", s.Driver.PageSource); + if (rateSelection == "CurrentRateText") + Assert.Contains("2.20000000 ₿", s.Driver.PageSource); + if (rateSelection == "RateThenText") + Assert.Contains("1.10000000 ₿", s.Driver.PageSource); + s.GoToHome(); + s.GoToInvoices(); + s.GoToInvoice(invoice.Id); + s.Driver.FindElement(By.Id("refundlink")).Click(); + Assert.Contains("pull-payments", s.Driver.Url); + } + + [Fact(Timeout = TestTimeout)] + [Trait("Altcoins", "Altcoins")] + [Trait("Lightning", "Lightning")] + public async Task CanUsePaymentMethodDropdown() + { + using (var s = SeleniumTester.Create()) + { + s.Server.ActivateLTC(); + s.Server.ActivateLightning(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.AddDerivationScheme("BTC"); + + //check that there is no dropdown since only one payment method is set + var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); + s.GoToInvoiceCheckout(invoiceId); + s.Driver.FindElement(By.ClassName("payment__currencies_noborder")); + s.GoToHome(); + s.GoToStore(store.storeId); + s.AddDerivationScheme("LTC"); + s.AddLightningNode("BTC", LightningConnectionType.CLightning); + //there should be three now + invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); + s.GoToInvoiceCheckout(invoiceId); + var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + Assert.Contains("BTC", currencyDropdownButton.Text); + currencyDropdownButton.Click(); + + var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); + Assert.Equal(3, elements.Count); + elements.Single(element => element.Text.Contains("LTC")).Click(); + currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + Assert.Contains("LTC", currencyDropdownButton.Text); + currencyDropdownButton.Click(); + + elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); + elements.Single(element => element.Text.Contains("Lightning")).Click(); + + currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + Assert.Contains("Lightning", currencyDropdownButton.Text); + + s.Driver.Quit(); + } + } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] + public async Task CanPayWithTwoCurrencies() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLTC(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + var cashCow = tester.ExplorerNode; + cashCow.Generate(2); // get some money in case + var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + var firstPayment = Money.Coins(0.04m); + cashCow.SendToAddress(invoiceAddress, firstPayment); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); + + Assert.Single(invoice.CryptoInfo); // Only BTC should be presented + + var controller = tester.PayTester.GetController(null); + var checkout = + (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) + .GetAwaiter().GetResult()).Value; + 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.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + cashCow = tester.ExplorerNode; + invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + firstPayment = Money.Coins(0.04m); + cashCow.SendToAddress(invoiceAddress, firstPayment); + Logs.Tester.LogInformation("First payment sent to " + invoiceAddress); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); + + cashCow = tester.LTCExplorerNode; + var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); + Assert.NotNull(ltcCryptoInfo); + invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); + var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); + cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money... + cashCow.SendToAddress(invoiceAddress, secondPayment); + Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(Money.Zero, invoice.BtcDue); + var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); + Assert.Equal(Money.Zero, ltcPaid.Due); + Assert.Equal(secondPayment, ltcPaid.CryptoPaid); + Assert.Equal("paid", invoice.Status); + Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); + }); + + controller = tester.PayTester.GetController(null); + checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC") + .GetAwaiter().GetResult()).Value; + Assert.Equal(2, checkout.AvailableCryptos.Count); + Assert.Equal("LTC", checkout.CryptoCode); + + + 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")); + + + // Check if we can disable LTC + invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true, + SupportedTransactionCurrencies = new Dictionary() + { + {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} + } + }, Facade.Merchant); + + Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC")); + Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC")); + } + } + + [Fact] + [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] + public async Task CanUsePoSApp() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLTC(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + user.RegisterDerivationScheme("LTC"); + var apps = user.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); + vm.Name = "test"; + vm.SelectedAppType = AppType.PointOfSale.ToString(); + Assert.IsType(apps.CreateApp(vm).Result); + var appId = Assert.IsType(Assert.IsType(apps.ListApps().Result).Model) + .Apps[0].Id; + var vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = "CAD"; + vmpos.ButtonText = "{0} Purchase"; + vmpos.CustomButtonText = "Nicolas Sexy Hair"; + vmpos.CustomTipText = "Wanna tip?"; + vmpos.CustomTipPercentages = "15,18,20"; + vmpos.Template = @" +apple: + price: 5.0 + title: good apple +orange: + price: 10.0 +donation: + price: 1.02 + custom: true +"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + Assert.Equal("hello", vmpos.Title); + + var publicApps = user.GetController(); + var vmview = + Assert.IsType(Assert + .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); + Assert.Equal("hello", vmview.Title); + Assert.Equal(3, vmview.Items.Length); + Assert.Equal("good apple", vmview.Items[0].Title); + 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.Equal("{0} Purchase", vmview.ButtonText); + Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); + Assert.Equal("Wanna tip?", vmview.CustomTipText); + Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result); + + // + var invoices = user.BitPay.GetInvoices(); + var orangeInvoice = invoices.First(); + Assert.Equal(10.00m, orangeInvoice.Price); + Assert.Equal("CAD", orangeInvoice.Currency); + Assert.Equal("orange", orangeInvoice.ItemDesc); + + + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result); + + invoices = user.BitPay.GetInvoices(); + var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); + Assert.NotNull(appleInvoice); + Assert.Equal("good apple", appleInvoice.ItemDesc); + + + // testing custom amount + var action = Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); + Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName); + invoices = user.BitPay.GetInvoices(); + var donationInvoice = invoices.Single(i => i.Price == 6.6m); + Assert.NotNull(donationInvoice); + Assert.Equal("CAD", donationInvoice.Currency); + Assert.Equal("donation", donationInvoice.ItemDesc); + + foreach (var test in new[] + { + (Code: "EUR", ExpectedSymbol: "€", ExpectedDecimalSeparator: ",", ExpectedDivisibility: 2, + ExpectedThousandSeparator: "\xa0", ExpectedPrefixed: false, ExpectedSymbolSpace: true), + (Code: "INR", ExpectedSymbol: "₹", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 2, + ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true), + (Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0, + ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false), + (Code: "BTC", ExpectedSymbol: "₿", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8, + ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true), + }) + { + Logs.Tester.LogInformation($"Testing for {test.Code}"); + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = test.Item1; + vmpos.ButtonText = "{0} Purchase"; + vmpos.CustomButtonText = "Nicolas Sexy Hair"; + vmpos.CustomTipText = "Wanna tip?"; + vmpos.Template = @" +apple: + price: 1000.0 + title: good apple +orange: + price: 10.0 +donation: + price: 1.02 + custom: true +"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + publicApps = user.GetController(); + vmview = Assert.IsType(Assert + .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); + Assert.Equal(test.Code, vmview.CurrencyCode); + Assert.Equal(test.ExpectedSymbol, + vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); + Assert.Equal(test.ExpectedSymbol, + vmview.CurrencyInfo.CurrencySymbol + .Replace("¥", "¥")); // Hack so JPY test pass on linux as well); + Assert.Equal(test.ExpectedDecimalSeparator, vmview.CurrencyInfo.DecimalSeparator); + Assert.Equal(test.ExpectedThousandSeparator, vmview.CurrencyInfo.ThousandSeparator); + Assert.Equal(test.ExpectedPrefixed, vmview.CurrencyInfo.Prefixed); + Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility); + Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace); + } + + + //test inventory related features + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = "BTC"; + vmpos.Template = @" +inventoryitem: + price: 1.0 + title: good apple + inventory: 1 +noninventoryitem: + price: 10.0"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + + //inventoryitem has 1 item available + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); + //we already bought all available stock so this should fail + await Task.Delay(100); + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); + + //inventoryitem has unlimited items available + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); + + //verify invoices where created + invoices = user.BitPay.GetInvoices(); + Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem"))); + var inventoryItemInvoice = + Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem"))); + Assert.NotNull(inventoryItemInvoice); + + //let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + var appService = tester.PayTester.GetService(); + var eventAggregator = tester.PayTester.GetService(); + Assert.IsType(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid")); + //check that item is back in stock + TestUtils.Eventually(() => + { + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + Assert.Equal(1, + appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); + }, 10000); + + + //test payment methods option + + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = "BTC"; + vmpos.Template = @" +btconly: + price: 1.0 + title: good apple + payment_methods: + - BTC +normal: + price: 1.0"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result); + Assert.IsType(publicApps + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result); + invoices = user.BitPay.GetInvoices(); + var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); + var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); + Assert.Single(btcOnlyInvoice.CryptoInfo); + Assert.Equal("BTC", + btcOnlyInvoice.CryptoInfo.First().CryptoCode); + Assert.Equal(PaymentTypes.BTCLike.ToString(), + btcOnlyInvoice.CryptoInfo.First().PaymentType); + + Assert.Equal(2, normalInvoice.CryptoInfo.Length); + Assert.Contains( + normalInvoice.CryptoInfo, + s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains( + s.CryptoCode)); + } + } + + [Fact] + [Trait("Fast", "Fast")] + [Trait("Altcoins", "Altcoins")] + public void CanCalculateCryptoDue2() + { +#pragma warning disable CS0618 + var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); + var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); + var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] + { + new BitcoinLikePaymentHandler(null, networkProvider, null, null, null), + new LightningLikePaymentHandler(null, null, networkProvider, null), + }); + var networkBTC = networkProvider.GetNetwork("BTC"); + var networkLTC = networkProvider.GetNetwork("LTC"); + InvoiceEntity invoiceEntity = new InvoiceEntity(); + invoiceEntity.Networks = networkProvider; + invoiceEntity.Payments = new System.Collections.Generic.List(); + invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 }; + PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); + paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } + .SetPaymentMethodDetails( + new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() + { + NextNetworkFee = Money.Coins(0.00000100m), + DepositAddress = dummy + })); + paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m } + .SetPaymentMethodDetails( + new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() + { + NextNetworkFee = Money.Coins(0.00010000m), + DepositAddress = dummy + })); + invoiceEntity.SetPaymentMethods(paymentMethods); + + var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); + var accounting = btc.Calculate(); + + invoiceEntity.Payments.Add( + new PaymentEntity() + { + Accounted = true, + CryptoCode = "BTC", + NetworkFee = 0.00000100m, + Network = networkProvider.GetNetwork("BTC"), + } + .SetCryptoPaymentData(new BitcoinLikePaymentData() + { + Network = networkProvider.GetNetwork("BTC"), + Output = new TxOut() { Value = Money.Coins(0.00151263m) } + })); + accounting = btc.Calculate(); + invoiceEntity.Payments.Add( + new PaymentEntity() + { + Accounted = true, + CryptoCode = "BTC", + NetworkFee = 0.00000100m, + Network = networkProvider.GetNetwork("BTC") + } + .SetCryptoPaymentData(new BitcoinLikePaymentData() + { + Network = networkProvider.GetNetwork("BTC"), + Output = new TxOut() { Value = accounting.Due } + })); + accounting = btc.Calculate(); + Assert.Equal(Money.Zero, accounting.Due); + Assert.Equal(Money.Zero, accounting.DueUncapped); + + var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); + accounting = ltc.Calculate(); + + Assert.Equal(Money.Zero, accounting.Due); + // LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) + Assert.True(accounting.DueUncapped < Money.Zero); + + var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2); + Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode); +#pragma warning restore CS0618 + } + + [Fact] + [Trait("Fast", "Fast")] + [Trait("Altcoins", "Altcoins")] + public void CanParseDerivationScheme() + { + var testnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Testnet); + var regtestNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); + var mainnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Mainnet); + var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork("BTC")); + var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork("BTC")); + NBXplorer.DerivationStrategy.DerivationStrategyBase result; + // Passing electrum stuff + // Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit + result = testnetParser.Parse( + "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); + Assert.Equal( + "tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", + result.ToString()); + result = mainnetParser.Parse( + "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); + Assert.Equal( + "xpub68fZn8w5ZTP5X4zymr1B1vKsMtJUiudtN2DZHQzJJc87gW1tXh7S4SALCsQijUzXstg2reVyuZYFuPnTDKXNiNgDZNpNiC4BrVzaaGEaRHj", + result.ToString()); + // P2SH + result = testnetParser.Parse( + "upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu"); + Assert.Equal( + "tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", + result.ToString()); + + result = mainnetParser.Parse( + "ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5"); + Assert.Equal( + "xpub661MyMwAqRbcGiYMrHB74DtmLBrNPSsUU6PV7ALAtpYyFhkc6TrUuLhxhET4VgwgQPnPfvYvEAHojf7QmQRj8imudHFoC7hju4f9xxri8wR-[p2sh]", + result.ToString()); + + // if prefix not recognize, assume it is segwit + result = testnetParser.Parse( + "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); + Assert.Equal( + "tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", + result.ToString()); + //////////////// + + var tpub = + "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"; + + result = testnetParser.Parse(tpub); + Assert.Equal(tpub, result.ToString()); + testnetParser.HintScriptPubKey = BitcoinAddress + .Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); + Assert.Equal(tpub, result.ToString()); + + testnetParser.HintScriptPubKey = BitcoinAddress + .Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); + Assert.Equal($"{tpub}-[p2sh]", result.ToString()); + + testnetParser.HintScriptPubKey = BitcoinAddress + .Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); + Assert.Equal($"{tpub}-[legacy]", result.ToString()); + + testnetParser.HintScriptPubKey = BitcoinAddress + .Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse($"{tpub}-[legacy]"); + Assert.Equal($"{tpub}-[p2sh]", result.ToString()); + + result = testnetParser.Parse(tpub); + Assert.Equal($"{tpub}-[p2sh]", result.ToString()); + + var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC")); + var parsed = + regtestParser.Parse( + "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); + Assert.Equal( + "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", + parsed.ToString()); + + // Let's make sure we can't generate segwit with dogecoin + regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); + parsed = regtestParser.Parse( + "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); + Assert.Equal( + "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", + parsed.ToString()); + + regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); + parsed = regtestParser.Parse( + "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); + Assert.Equal( + "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", + parsed.ToString()); + } + } +} diff --git a/BTCPayServer.Tests/ElementsTests.cs b/BTCPayServer.Tests/AltcoinTests/ElementsTests.cs similarity index 99% rename from BTCPayServer.Tests/ElementsTests.cs rename to BTCPayServer.Tests/AltcoinTests/ElementsTests.cs index 035cba800..5edff1848 100644 --- a/BTCPayServer.Tests/ElementsTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/ElementsTests.cs @@ -66,6 +66,7 @@ namespace BTCPayServer.Tests Assert.NotNull(options.NetworkProvider.GetNetwork("USDT")); } + [Fact] [Trait("Altcoins", "Altcoins")] public async Task ElementsAssetsAreHandledCorrectly() diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index db839de1b..76ffc3d2d 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -1,11 +1,7 @@ - - + + - netcoreapp3.1 - $(TargetFrameworkOverride) false - NU1701,CA1816,CA1308,CA1810,CA2208 - 8.0 AB0AC1DD-9D26-485B-9416-56A33F268117 true @@ -21,6 +17,9 @@ $(DefineConstants);SHORT_TIMEOUT + + $(DefineConstants);ALTCOINS + @@ -33,11 +32,16 @@ runtime; build; native; contentfiles; analyzers - + + + Dockerfile + + Always + Always diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index 5eb52f8b7..e41104dab 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -103,53 +103,6 @@ namespace BTCPayServer.Tests } } - [Fact(Timeout = TestTimeout)] - [Trait("Altcoins", "Altcoins")] - [Trait("Lightning", "Lightning")] - public async Task CanUsePaymentMethodDropdown() - { - using (var s = SeleniumTester.Create()) - { - s.Server.ActivateLTC(); - s.Server.ActivateLightning(); - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(); - var store = s.CreateNewStore(); - s.AddDerivationScheme("BTC"); - - //check that there is no dropdown since only one payment method is set - var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); - s.GoToInvoiceCheckout(invoiceId); - s.Driver.FindElement(By.ClassName("payment__currencies_noborder")); - s.GoToHome(); - s.GoToStore(store.storeId); - s.AddDerivationScheme("LTC"); - s.AddLightningNode("BTC", LightningConnectionType.CLightning); - //there should be three now - invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); - s.GoToInvoiceCheckout(invoiceId); - var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); - Assert.Contains("BTC", currencyDropdownButton.Text); - currencyDropdownButton.Click(); - - var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); - Assert.Equal(3, elements.Count); - elements.Single(element => element.Text.Contains("LTC")).Click(); - currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); - Assert.Contains("LTC", currencyDropdownButton.Text); - currencyDropdownButton.Click(); - - elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); - elements.Single(element => element.Text.Contains("Lightning")).Click(); - - currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); - Assert.Contains("Lightning", currencyDropdownButton.Text); - - s.Driver.Quit(); - } - } - [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanUseLightningSatsFeature() diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index ea37b47b6..436a25906 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.202 AS builder RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \ && rm -rf /var/lib/apt/lists/* @@ -21,6 +21,9 @@ ENV SCREEN_HEIGHT 600 \ SCREEN_WIDTH 1200 COPY . . -RUN cd BTCPayServer.Tests && dotnet build /p:CI_TESTS=true /p:RazorCompileOnBuild=true + +ARG CONFIGURATION_NAME=Release +RUN cd BTCPayServer.Tests && dotnet build --configuration ${CONFIGURATION_NAME} /p:CI_TESTS=true /p:RazorCompileOnBuild=true WORKDIR /source/BTCPayServer.Tests +ENV CONFIGURATION_NAME=${CONFIGURATION_NAME} ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 2bd699e87..1262e55e6 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -734,17 +734,18 @@ namespace BTCPayServer.Tests [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] - public void DecimalStringJsonConverterTests() + public void NumericJsonConverterTests() { JsonReader Get(string val) { return new JsonTextReader(new StringReader(val)); } - var jsonConverter = new DecimalStringJsonConverter(); + var jsonConverter = new NumericStringJsonConverter(); Assert.True(jsonConverter.CanConvert(typeof(decimal))); Assert.True(jsonConverter.CanConvert(typeof(decimal?))); - Assert.False(jsonConverter.CanConvert(typeof(double))); + Assert.True(jsonConverter.CanConvert(typeof(double))); + Assert.True(jsonConverter.CanConvert(typeof(double?))); Assert.False(jsonConverter.CanConvert(typeof(float))); Assert.False(jsonConverter.CanConvert(typeof(int))); Assert.False(jsonConverter.CanConvert(typeof(string))); @@ -755,12 +756,20 @@ namespace BTCPayServer.Tests Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null)); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null)); Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null)); + Assert.Equal((double)1.0, jsonConverter.ReadJson(Get(numberJson), typeof(double), null, null)); + Assert.Equal((double)1.2, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(double), null, null)); + Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(double?), null, null)); Assert.Throws(() => { jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null); + });Assert.Throws(() => + { + jsonConverter.ReadJson(Get("null"), typeof(double), null, null); }); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null)); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null)); + Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null)); + Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double?), null, null)); } } } diff --git a/BTCPayServer.Tests/README.md b/BTCPayServer.Tests/README.md index 8ea5b0c25..16e31a81b 100644 --- a/BTCPayServer.Tests/README.md +++ b/BTCPayServer.Tests/README.md @@ -1,74 +1,52 @@ -# How to be started for development +# Tooling -BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured. -You can however use the `docker-compose.yml` of this folder to get it running. - -In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running. - -This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself. - -``` -docker-compose up dev -``` - -You can run the tests while it is running through your favorite IDE, or with - -``` -dotnet test -``` - -Once you want to stop - -``` -docker-compose down -``` - -If you want to stop, and remove all existing data - -``` -docker-compose down --v -``` - -You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`. +This README describe some useful tooling that you may need during development and testing. +To learn how to get started with your local development environment, read [our documentation](https://docs.btcpayserver.org/LocalDevelopment/). ## How to manually test payments ### Using the test bitcoin-cli -You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`: -``` +You can call bitcoin-cli inside the container with `docker exec`. +For example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`: + +```sh ./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090 ``` If you are using Powershell: + ```powershell .\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090 ``` You can also generate blocks: + ```powershell .\docker-bitcoin-generate.ps1 3 ``` ### Using the test litecoin-cli -Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead. +Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead. ### Using the test lightning-cli If you are using Linux: -``` + +```sh ./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh ``` If you are using Powershell: -``` + +```powershell .\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh ``` If you get this message: -``` +```json { "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } } ``` diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 87129ca07..d214abb64 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -12,6 +12,7 @@ using BTCPayServer.Views.Wallets; using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitcoin.Payment; +using NBitpayClient; using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; @@ -686,80 +687,6 @@ namespace BTCPayServer.Tests } } - [Fact] - [Trait("Selenium", "Selenium")] - [Trait("Altcoins", "Altcoins")] - public async Task CanCreateRefunds() - { - using (var s = SeleniumTester.Create()) - { - s.Server.ActivateLTC(); - await s.StartAsync(); - var user = s.Server.NewAccount(); - await user.GrantAccessAsync(); - s.GoToLogin(); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - user.RegisterDerivationScheme("BTC"); - await s.Server.ExplorerNode.GenerateAsync(1); - - foreach (var multiCurrency in new[] { false, true }) - { - if (multiCurrency) - user.RegisterDerivationScheme("LTC"); - foreach (var rateSelection in new[] { "FiatText", "CurrentRateText", "RateThenText" }) - await CanCreateRefundsCore(s, user, multiCurrency, rateSelection); - } - } - } - - private static async Task CanCreateRefundsCore(SeleniumTester s, TestAccount user, bool multiCurrency, string rateSelection) - { - s.GoToHome(); - s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m, 5100.0m)); - var invoice = await user.BitPay.CreateInvoiceAsync(new NBitpayClient.Invoice() - { - Currency = "USD", - Price = 5000.0m - }); - var info = invoice.CryptoInfo.First(o => o.CryptoCode == "BTC"); - var totalDue = decimal.Parse(info.TotalDue, CultureInfo.InvariantCulture); - var paid = totalDue + 0.1m; - await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(info.Address, Network.RegTest), Money.Coins(paid)); - await s.Server.ExplorerNode.GenerateAsync(1); - await TestUtils.EventuallyAsync(async () => - { - invoice = await user.BitPay.GetInvoiceAsync(invoice.Id); - Assert.Equal("confirmed", invoice.Status); - }); - - // BTC crash by 50% - s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m / 2.0m, 5100.0m / 2.0m)); - s.GoToInvoice(invoice.Id); - s.Driver.FindElement(By.Id("refundlink")).Click(); - if (multiCurrency) - { - s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter); - s.Driver.FindElement(By.Id("ok")).Click(); - } - Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat - Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before - Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate - s.Driver.FindElement(By.Id(rateSelection)).Click(); - s.Driver.FindElement(By.Id("ok")).Click(); - Assert.Contains("pull-payments", s.Driver.Url); - if (rateSelection == "FiatText") - Assert.Contains("$5,500.00", s.Driver.PageSource); - if (rateSelection == "CurrentRateText") - Assert.Contains("2.20000000 ₿", s.Driver.PageSource); - if (rateSelection == "RateThenText") - Assert.Contains("1.10000000 ₿", s.Driver.PageSource); - s.GoToHome(); - s.GoToInvoices(); - s.GoToInvoice(invoice.Id); - s.Driver.FindElement(By.Id("refundlink")).Click(); - Assert.Contains("pull-payments", s.Driver.Url); - } - [Fact] [Trait("Selenium", "Selenium")] public async Task CanUsePullPaymentsViaUI() diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 9cb775455..698122649 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -63,7 +63,7 @@ namespace BTCPayServer.Tests PayTester.SSHConnection = GetEnvironment("TESTS_SSHCONNECTION", "root@127.0.0.1:21622"); PayTester.SocksEndpoint = GetEnvironment("TESTS_SOCKSENDPOINT", "localhost:9050"); } - +#if ALTCOINS public void ActivateLTC() { LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); @@ -78,7 +78,7 @@ namespace BTCPayServer.Tests PayTester.Chains.Add("LBTC"); PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address; } - +#endif public void ActivateLightning() { var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork; @@ -170,20 +170,21 @@ namespace BTCPayServer.Tests { get; set; } - +#if ALTCOINS public RPCClient LTCExplorerNode { get; set; } public RPCClient LBTCExplorerNode { get; set; } + public ExplorerClient LTCExplorerClient { get; set; } + public ExplorerClient LBTCExplorerClient { get; set; } +#endif public ExplorerClient ExplorerClient { get; set; } - public ExplorerClient LTCExplorerClient { get; set; } - public ExplorerClient LBTCExplorerClient { get; set; } readonly HttpClient _Http = new HttpClient(); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6be6bdc52..248ad9d66 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -67,6 +67,90 @@ namespace BTCPayServer.Tests Logs.LogProvider = new XUnitLogProvider(helper); } + class DockerImage + { + public string User { get; private set; } + public string Name { get; private set; } + public string Tag { get; private set; } + + public string Source { get; set; } + + public static DockerImage Parse(string str) + { + //${BTCPAY_IMAGE: -btcpayserver / btcpayserver:1.0.3.21} + var variableMatch = Regex.Match(str, @"\$\{[^-]+-([^\}]+)\}"); + if (variableMatch.Success) + { + str = variableMatch.Groups[1].Value; + } + DockerImage img = new DockerImage(); + var match = Regex.Match(str, "([^/]*/)?([^:]+):?(.*)"); + if (!match.Success) + throw new FormatException(); + img.User = match.Groups[1].Length == 0 ? string.Empty : match.Groups[1].Value.Substring(0, match.Groups[1].Value.Length - 1); + img.Name = match.Groups[2].Value; + img.Tag = match.Groups[3].Value; + if (img.Tag == string.Empty) + img.Tag = "latest"; + return img; + } + public override string ToString() + { + return ToString(true); + } + public string ToString(bool includeTag) + { + StringBuilder builder = new StringBuilder(); + if (!String.IsNullOrWhiteSpace(User)) + builder.Append($"{User}/"); + builder.Append($"{Name}"); + if (includeTag) + { + if (!String.IsNullOrWhiteSpace(Tag)) + builder.Append($":{Tag}"); + } + return builder.ToString(); + } + } + + + /// + /// This test check that we don't forget to bump one image in both docker-compose.altcoins.yml and docker-compose.yml + /// + [Fact] + [Trait("Fast", "Fast")] + public void CheckDockerComposeUpToDate() + { + var compose1 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.yml")); + var compose2 = File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer.Tests", "docker-compose.altcoins.yml")); + + List GetImages(string content) + { + List images = new List(); + foreach (var line in content.Split(new[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) + { + var l = line.Trim(); + if (l.StartsWith("image:", StringComparison.OrdinalIgnoreCase)) + { + images.Add(DockerImage.Parse(l.Substring("image:".Length).Trim())); + } + } + return images; + } + + var img1 = GetImages(compose1); + var img2 = GetImages(compose2); + var groups = img1.Concat(img2).GroupBy(g => g.Name); + foreach (var g in groups) + { + var tags = new HashSet(g.Select(o => o.Tag)); + if (tags.Count != 1) + { + Assert.False(true, $"All docker images '{g.Key}' in docker-compose.yml and docker-compose.altcoins.yml should have the same tags. (Found {string.Join(',', tags)})"); + } + } + } + [Fact] [Trait("Fast", "Fast")] public async Task CheckNoDeadLink() @@ -203,87 +287,6 @@ namespace BTCPayServer.Tests Assert.False(attribute.IsValid("httpdsadsa.com")); } - [Fact] - [Trait("Fast", "Fast")] - public void CanCalculateCryptoDue2() - { -#pragma warning disable CS0618 - var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); - var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); - var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] - { - new BitcoinLikePaymentHandler(null, networkProvider, null, null, null), - new LightningLikePaymentHandler(null, null, networkProvider, null), - }); - var networkBTC = networkProvider.GetNetwork("BTC"); - var networkLTC = networkProvider.GetNetwork("LTC"); - InvoiceEntity invoiceEntity = new InvoiceEntity(); - invoiceEntity.Networks = networkProvider; - invoiceEntity.Payments = new System.Collections.Generic.List(); - invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 }; - PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); - paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } - .SetPaymentMethodDetails( - new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() - { - NextNetworkFee = Money.Coins(0.00000100m), - DepositAddress = dummy - })); - paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m } - .SetPaymentMethodDetails( - new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() - { - NextNetworkFee = Money.Coins(0.00010000m), - DepositAddress = dummy - })); - invoiceEntity.SetPaymentMethods(paymentMethods); - - var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); - var accounting = btc.Calculate(); - - invoiceEntity.Payments.Add( - new PaymentEntity() - { - Accounted = true, - CryptoCode = "BTC", - NetworkFee = 0.00000100m, - Network = networkProvider.GetNetwork("BTC"), - } - .SetCryptoPaymentData(new BitcoinLikePaymentData() - { - Network = networkProvider.GetNetwork("BTC"), - Output = new TxOut() { Value = Money.Coins(0.00151263m) } - })); - accounting = btc.Calculate(); - invoiceEntity.Payments.Add( - new PaymentEntity() - { - Accounted = true, - CryptoCode = "BTC", - NetworkFee = 0.00000100m, - Network = networkProvider.GetNetwork("BTC") - } - .SetCryptoPaymentData(new BitcoinLikePaymentData() - { - Network = networkProvider.GetNetwork("BTC"), - Output = new TxOut() { Value = accounting.Due } - })); - accounting = btc.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Zero, accounting.DueUncapped); - - var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); - accounting = ltc.Calculate(); - - Assert.Equal(Money.Zero, accounting.Due); - // LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) - Assert.True(accounting.DueUncapped < Money.Zero); - - var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2); - Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode); -#pragma warning restore CS0618 - } - [Fact] [Trait("Fast", "Fast")] public void CanParseTorrc() @@ -1354,7 +1357,7 @@ namespace BTCPayServer.Tests { var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController() - .ListInvoices(filter).Result).Model; + .ListInvoices(new InvoicesModel { SearchTerm = filter }).Result).Model; Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId)); } @@ -1816,76 +1819,6 @@ namespace BTCPayServer.Tests } } - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - [Trait("Altcoins", "Altcoins")] - public async Task CanHaveLTCOnlyStore() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLTC(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("LTC"); - - // First we try payment with a merchant having only BTC - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 500, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - Assert.Single(invoice.CryptoInfo); - Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); - 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); - cashCow.SendToAddress(invoiceAddress, firstPayment); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); - }); - - Assert.Single(invoice.CryptoInfo); // Only BTC should be presented - - var controller = tester.PayTester.GetController(null); - var checkout = - (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) - .GetAwaiter().GetResult()).Value; - Assert.Single(checkout.AvailableCryptos); - Assert.Equal("LTC", checkout.CryptoCode); - - ////////////////////// - - // Despite it is called BitcoinAddress it should be LTC because BTC is not available - Assert.Null(invoice.BitcoinAddress); - Assert.NotEqual(1.0m, invoice.Rate); - Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate - cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due); - - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal("paid", invoice.Status); - checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) - .GetAwaiter().GetResult()).Value; - Assert.Equal("paid", checkout.Status); - }); - } - } - [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanModifyRates() @@ -1953,145 +1886,6 @@ namespace BTCPayServer.Tests } } - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - [Trait("Altcoins", "Altcoins")] - public async Task CanPayWithTwoCurrencies() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLTC(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - // First we try payment with a merchant having only BTC - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - var cashCow = tester.ExplorerNode; - cashCow.Generate(2); // get some money in case - var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - var firstPayment = Money.Coins(0.04m); - cashCow.SendToAddress(invoiceAddress, firstPayment); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.True(invoice.BtcPaid == firstPayment); - }); - - Assert.Single(invoice.CryptoInfo); // Only BTC should be presented - - var controller = tester.PayTester.GetController(null); - var checkout = - (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) - .GetAwaiter().GetResult()).Value; - 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.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - cashCow = tester.ExplorerNode; - invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - firstPayment = Money.Coins(0.04m); - cashCow.SendToAddress(invoiceAddress, firstPayment); - Logs.Tester.LogInformation("First payment sent to " + invoiceAddress); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.True(invoice.BtcPaid == firstPayment); - }); - - cashCow = tester.LTCExplorerNode; - var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); - Assert.NotNull(ltcCryptoInfo); - invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); - var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); - cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money... - cashCow.SendToAddress(invoiceAddress, secondPayment); - Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(Money.Zero, invoice.BtcDue); - var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); - Assert.Equal(Money.Zero, ltcPaid.Due); - Assert.Equal(secondPayment, ltcPaid.CryptoPaid); - Assert.Equal("paid", invoice.Status); - Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); - }); - - controller = tester.PayTester.GetController(null); - checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC") - .GetAwaiter().GetResult()).Value; - Assert.Equal(2, checkout.AvailableCryptos.Count); - Assert.Equal("LTC", checkout.CryptoCode); - - - 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")); - - - // Check if we can disable LTC - invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true, - SupportedTransactionCurrencies = new Dictionary() - { - {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} - } - }, Facade.Merchant); - - Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC")); - Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC")); - } - } - [Fact] [Trait("Fast", "Fast")] public void HasCurrencyDataForNetworks() @@ -2127,305 +1921,6 @@ namespace BTCPayServer.Tests Assert.False(CurrencyValue.TryParse("1.501", out result)); } - [Fact] - [Trait("Fast", "Fast")] - public void CanParseDerivationScheme() - { - var testnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Testnet); - var regtestNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); - var mainnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Mainnet); - var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork("BTC")); - var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork("BTC")); - NBXplorer.DerivationStrategy.DerivationStrategyBase result; - // Passing electrum stuff - // Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit - result = testnetParser.Parse( - "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); - Assert.Equal( - "tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", - result.ToString()); - result = mainnetParser.Parse( - "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); - Assert.Equal( - "xpub68fZn8w5ZTP5X4zymr1B1vKsMtJUiudtN2DZHQzJJc87gW1tXh7S4SALCsQijUzXstg2reVyuZYFuPnTDKXNiNgDZNpNiC4BrVzaaGEaRHj", - result.ToString()); - // P2SH - result = testnetParser.Parse( - "upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu"); - Assert.Equal( - "tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", - result.ToString()); - - result = mainnetParser.Parse( - "ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5"); - Assert.Equal( - "xpub661MyMwAqRbcGiYMrHB74DtmLBrNPSsUU6PV7ALAtpYyFhkc6TrUuLhxhET4VgwgQPnPfvYvEAHojf7QmQRj8imudHFoC7hju4f9xxri8wR-[p2sh]", - result.ToString()); - - // if prefix not recognize, assume it is segwit - result = testnetParser.Parse( - "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); - Assert.Equal( - "tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", - result.ToString()); - //////////////// - - var tpub = - "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"; - - result = testnetParser.Parse(tpub); - Assert.Equal(tpub, result.ToString()); - testnetParser.HintScriptPubKey = BitcoinAddress - .Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey; - result = testnetParser.Parse(tpub); - Assert.Equal(tpub, result.ToString()); - - testnetParser.HintScriptPubKey = BitcoinAddress - .Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; - result = testnetParser.Parse(tpub); - Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - - testnetParser.HintScriptPubKey = BitcoinAddress - .Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey; - result = testnetParser.Parse(tpub); - Assert.Equal($"{tpub}-[legacy]", result.ToString()); - - testnetParser.HintScriptPubKey = BitcoinAddress - .Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; - result = testnetParser.Parse($"{tpub}-[legacy]"); - Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - - result = testnetParser.Parse(tpub); - Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - - var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC")); - var parsed = - regtestParser.Parse( - "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); - Assert.Equal( - "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", - parsed.ToString()); - - // Let's make sure we can't generate segwit with dogecoin - regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); - parsed = regtestParser.Parse( - "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); - Assert.Equal( - "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", - parsed.ToString()); - - regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); - parsed = regtestParser.Parse( - "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); - Assert.Equal( - "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", - parsed.ToString()); - } - - [Fact] - [Trait("Integration", "Integration")] - [Trait("Altcoins", "Altcoins")] - [Trait("Lightning", "Lightning")] - public async Task CanAddDerivationSchemes() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLTC(); - tester.ActivateLightning(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - user.RegisterDerivationScheme("LTC"); - user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); - var btcNetwork = tester.PayTester.Networks.GetNetwork("BTC"); - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 1.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - Assert.Equal(3, invoice.CryptoInfo.Length); - - var controller = user.GetController(); - var lightningVM = - (LightningNodeViewModel)Assert.IsType(controller.AddLightningNode(user.StoreId, "BTC")) - .Model; - Assert.True(lightningVM.Enabled); - lightningVM.Enabled = false; - controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult(); - lightningVM = - (LightningNodeViewModel)Assert.IsType(controller.AddLightningNode(user.StoreId, "BTC")) - .Model; - Assert.False(lightningVM.Enabled); - - // Only Enabling/Disabling the payment method must redirect to store page - var derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - Assert.True(derivationVM.Enabled); - derivationVM.Enabled = false; - Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") - .GetAwaiter().GetResult()); - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - Assert.False(derivationVM.Enabled); - - // Clicking next without changing anything should send to the confirmation screen - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.True(derivationVM.Confirmation); - - invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 1.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - Assert.Single(invoice.CryptoInfo); - Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); - - // Removing the derivation scheme, should redirect to store page - var oldScheme = derivationVM.DerivationScheme; - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.DerivationScheme = null; - Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") - .GetAwaiter().GetResult()); - - // Setting it again should redirect to the confirmation page - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.DerivationScheme = oldScheme; - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.True(derivationVM.Confirmation); - - - //cobo vault file - var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}"; - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content); - derivationVM.Enabled = true; - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.True(derivationVM.Confirmation); - Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") - .GetAwaiter().GetResult()); - - //wasabi wallet file - content = - "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}"; - - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content); - derivationVM.Enabled = true; - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.True(derivationVM.Confirmation); - Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") - .GetAwaiter().GetResult()); - - - // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - content = - "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; - derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content); - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.False(derivationVM - .Confirmation); // Should fail, we are giving a mainnet file to a testnet network - - // And with a good file? (upub) - content = - "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; - derivationVM = (DerivationSchemeViewModel)Assert - .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content); - derivationVM.Enabled = true; - derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller - .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.True(derivationVM.Confirmation); - Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") - .GetAwaiter().GetResult()); - - - // Now let's check that no data has been lost in the process - var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult(); - var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) - .OfType().First(o => o.PaymentId.IsBTCOnChain); - DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected); - Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); - - // Let's check that the root hdkey and account key path are taken into account when making a PSBT - invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 1.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - tester.ExplorerNode.Generate(1); - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address, - tester.ExplorerNode.Network); - tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m)); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal("paid", invoice.Status); - }); - var wallet = tester.PayTester.GetController(); - var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, - new WalletSendModel() - { - Outputs = new List() - { - new WalletSendModel.TransactionOutput() - { - Amount = 0.5m, - DestinationAddress = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork) - .ToString(), - } - }, - FeeSatoshiPerByte = 1 - }, default).GetAwaiter().GetResult(); - - Assert.NotNull(psbt); - - var root = new Mnemonic( - "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") - .DeriveExtKey().AsHDKeyCache(); - var account = root.Derive(new KeyPath("m/49'/0'/0'")); - Assert.All(psbt.PSBT.Inputs, input => - { - var keyPath = input.HDKeyPaths.Single(); - Assert.False(keyPath.Value.KeyPath.IsHardened); - Assert.Equal(account.Derive(keyPath.Value.KeyPath).GetPublicKey(), keyPath.Key); - Assert.Equal(keyPath.Value.MasterFingerprint, - onchainBTC.AccountKeySettings[0].AccountKey.GetPublicKey().GetHDFingerPrint()); - }); - } - } - [Fact] [Trait("Integration", "Integration")] public async Task CanSetPaymentMethodLimits() @@ -2493,228 +1988,6 @@ namespace BTCPayServer.Tests } } - [Fact] - [Trait("Integration", "Integration")] - [Trait("Altcoins", "Altcoins")] - public async Task CanUsePoSApp() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLTC(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - user.RegisterDerivationScheme("LTC"); - var apps = user.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); - vm.Name = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); - Assert.IsType(apps.CreateApp(vm).Result); - var appId = Assert.IsType(Assert.IsType(apps.ListApps().Result).Model) - .Apps[0].Id; - var vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - vmpos.Title = "hello"; - vmpos.Currency = "CAD"; - vmpos.ButtonText = "{0} Purchase"; - vmpos.CustomButtonText = "Nicolas Sexy Hair"; - vmpos.CustomTipText = "Wanna tip?"; - vmpos.CustomTipPercentages = "15,18,20"; - vmpos.Template = @" -apple: - price: 5.0 - title: good apple -orange: - price: 10.0 -donation: - price: 1.02 - custom: true -"; - Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - Assert.Equal("hello", vmpos.Title); - - var publicApps = user.GetController(); - var vmview = - Assert.IsType(Assert - .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); - Assert.Equal("hello", vmview.Title); - Assert.Equal(3, vmview.Items.Length); - Assert.Equal("good apple", vmview.Items[0].Title); - 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.Equal("{0} Purchase", vmview.ButtonText); - Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); - Assert.Equal("Wanna tip?", vmview.CustomTipText); - Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result); - - // - var invoices = user.BitPay.GetInvoices(); - var orangeInvoice = invoices.First(); - Assert.Equal(10.00m, orangeInvoice.Price); - Assert.Equal("CAD", orangeInvoice.Currency); - Assert.Equal("orange", orangeInvoice.ItemDesc); - - - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result); - - invoices = user.BitPay.GetInvoices(); - var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); - Assert.NotNull(appleInvoice); - Assert.Equal("good apple", appleInvoice.ItemDesc); - - - // testing custom amount - var action = Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); - Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName); - invoices = user.BitPay.GetInvoices(); - var donationInvoice = invoices.Single(i => i.Price == 6.6m); - Assert.NotNull(donationInvoice); - Assert.Equal("CAD", donationInvoice.Currency); - Assert.Equal("donation", donationInvoice.ItemDesc); - - foreach (var test in new[] - { - (Code: "EUR", ExpectedSymbol: "€", ExpectedDecimalSeparator: ",", ExpectedDivisibility: 2, - ExpectedThousandSeparator: "\xa0", ExpectedPrefixed: false, ExpectedSymbolSpace: true), - (Code: "INR", ExpectedSymbol: "₹", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 2, - ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true), - (Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0, - ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false), - (Code: "BTC", ExpectedSymbol: "₿", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8, - ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true), - }) - { - Logs.Tester.LogInformation($"Testing for {test.Code}"); - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - vmpos.Title = "hello"; - vmpos.Currency = test.Item1; - vmpos.ButtonText = "{0} Purchase"; - vmpos.CustomButtonText = "Nicolas Sexy Hair"; - vmpos.CustomTipText = "Wanna tip?"; - vmpos.Template = @" -apple: - price: 1000.0 - title: good apple -orange: - price: 10.0 -donation: - price: 1.02 - custom: true -"; - Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); - publicApps = user.GetController(); - vmview = Assert.IsType(Assert - .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); - Assert.Equal(test.Code, vmview.CurrencyCode); - Assert.Equal(test.ExpectedSymbol, - vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); - Assert.Equal(test.ExpectedSymbol, - vmview.CurrencyInfo.CurrencySymbol - .Replace("¥", "¥")); // Hack so JPY test pass on linux as well); - Assert.Equal(test.ExpectedDecimalSeparator, vmview.CurrencyInfo.DecimalSeparator); - Assert.Equal(test.ExpectedThousandSeparator, vmview.CurrencyInfo.ThousandSeparator); - Assert.Equal(test.ExpectedPrefixed, vmview.CurrencyInfo.Prefixed); - Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility); - Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace); - } - - - //test inventory related features - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - vmpos.Title = "hello"; - vmpos.Currency = "BTC"; - vmpos.Template = @" -inventoryitem: - price: 1.0 - title: good apple - inventory: 1 -noninventoryitem: - price: 10.0"; - Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); - - //inventoryitem has 1 item available - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); - //we already bought all available stock so this should fail - await Task.Delay(100); - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); - - //inventoryitem has unlimited items available - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); - - //verify invoices where created - invoices = user.BitPay.GetInvoices(); - Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem"))); - var inventoryItemInvoice = - Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem"))); - Assert.NotNull(inventoryItemInvoice); - - //let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - var appService = tester.PayTester.GetService(); - var eventAggregator = tester.PayTester.GetService(); - Assert.IsType(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid")); - //check that item is back in stock - TestUtils.Eventually(() => - { - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - Assert.Equal(1, - appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); - }, 10000); - - - //test payment methods option - - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(appId).Result).Model); - vmpos.Title = "hello"; - vmpos.Currency = "BTC"; - vmpos.Template = @" -btconly: - price: 1.0 - title: good apple - payment_methods: - - BTC -normal: - price: 1.0"; - Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result); - Assert.IsType(publicApps - .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result); - invoices = user.BitPay.GetInvoices(); - var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); - var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); - Assert.Single(btcOnlyInvoice.CryptoInfo); - Assert.Equal("BTC", - btcOnlyInvoice.CryptoInfo.First().CryptoCode); - Assert.Equal(PaymentTypes.BTCLike.ToString(), - btcOnlyInvoice.CryptoInfo.First().PaymentType); - - Assert.Equal(2, normalInvoice.CryptoInfo.Length); - Assert.Contains( - normalInvoice.CryptoInfo, - s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains( - s.CryptoCode)); - } - } - - [Fact] [Trait("Fast", "Fast")] public async Task CanScheduleBackgroundTasks() @@ -3089,11 +2362,10 @@ normal: user.GetController().Export("csv").GetAwaiter().GetResult(); var paidresult = Assert.IsType(exportResultPaid); Assert.Equal("application/csv", paidresult.ContentType); - Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content); - Assert.Contains($",\"On-Chain\",\"BTC\",\"0.0991\",\"0.0001\",\"5000.0\"", paidresult.Content); - Assert.Contains($",\"USD\",\"5.00", - paidresult.Content); // Seems hacky but some plateform does not render this decimal the same - Assert.Contains($"0\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", + Assert.Contains($",orderId,{invoice.Id},", paidresult.Content); + Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content); + Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same + Assert.Contains("0,,\"Some \"\", description\",new (paidPartial),new,paidPartial", paidresult.Content); }); } @@ -3719,40 +2991,6 @@ normal: Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); } - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - [Trait("Altcoins", "Altcoins")] - [Trait("Lightning", "Lightning")] - public async Task CanCreateInvoiceWithSpecificPaymentMethods() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLightning(); - tester.ActivateLTC(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterLightningNode("BTC", LightningConnectionType.Charge); - user.RegisterDerivationScheme("BTC"); - user.RegisterDerivationScheme("LTC"); - - var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")); - Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); - - - invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC") - { - SupportedTransactionCurrencies = new Dictionary() - { - {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} - } - }); - - Assert.Single(invoice.SupportedTransactionCurrencies); - } - } - [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] @@ -3865,5 +3103,61 @@ normal: .GetResult()) .Where(i => i.GetAddress() == h).Any(); } + + + class MockVersionFetcher : IVersionFetcher + { + public const string MOCK_NEW_VERSION = "9.9.9.9"; + public Task Fetch(CancellationToken cancellation) + { + return Task.FromResult(MOCK_NEW_VERSION); + } + } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanCheckForNewVersion() + { + using (var tester = ServerTester.Create(newDb: true)) + { + await tester.StartAsync(); + + var acc = tester.NewAccount(); + acc.GrantAccess(true); + + var settings = tester.PayTester.GetService(); + await settings.UpdateSetting(new PoliciesSettings() { CheckForNewVersions = true }); + + var mockEnv = tester.PayTester.GetService(); + var mockSender = tester.PayTester.GetService(); + + var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher()); + await svc.ProcessVersionCheck(); + + // since last version present in database was null, it should've been updated with version mock returned + var lastVersion = await settings.GetSettingAsync(); + Assert.Equal(MockVersionFetcher.MOCK_NEW_VERSION, lastVersion.LastVersion); + + // we should also have notification in UI + var ctrl = acc.GetController(); + var newVersion = MockVersionFetcher.MOCK_NEW_VERSION; + + var vm = Assert.IsType( + Assert.IsType(ctrl.Index()).Model); + + Assert.True(vm.Skip == 0); + Assert.True(vm.Count == 50); + Assert.True(vm.Total == 1); + Assert.True(vm.Items.Count == 1); + + var fn = vm.Items.First(); + var now = DateTimeOffset.UtcNow; + Assert.True(fn.Created >= now.AddSeconds(-3)); + Assert.True(fn.Created <= now); + Assert.Equal($"New version {newVersion} released!", fn.Body); + Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); + Assert.False(fn.Seen); + } + } } } diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml new file mode 100644 index 000000000..326b1e9f2 --- /dev/null +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -0,0 +1,419 @@ +version: "3" + +# Run `docker-compose up dev` for bootstrapping your development environment +# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run, +# The Visual Studio launch setting `Docker-regtest` is configured to use this environment. +services: + + tests: + build: + context: .. + dockerfile: BTCPayServer.Tests/Dockerfile + args: + CONFIGURATION_NAME: Altcoins-Release + environment: + TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3 + TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3 + TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/ + TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/ + TESTS_DB: "Postgres" + TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver + TESTS_HOSTNAME: tests + TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false} + TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none} + TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" + TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" + TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify" + TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/" + TESTS_INCONTAINER: "true" + TESTS_SSHCONNECTION: "root@sshd:22" + TESTS_SSHPASSWORD: "" + TESTS_SSHKEYFILE: "" + TESTS_SOCKSENDPOINT: "tor:9050" + expose: + - "80" + links: + - dev + extra_hosts: + - "tests:127.0.0.1" + volumes: + - "sshd_datadir:/root/.ssh" + - "customer_lightningd_datadir:/etc/customer_lightningd_datadir" + - "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir" + + # The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services + dev: + image: alpine:3.7 + command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ] + links: + - nbxplorer + - postgres + - customer_lightningd + - merchant_lightningd + - lightning-charged + - customer_lnd + - merchant_lnd + - sshd + - tor + - monero_wallet + + sshd: + build: + context: . + dockerfile: sshd.Dockerfile + ports: + - "21622:22" + expose: + - 22 + volumes: + - "sshd_datadir:/root/.ssh" + + devlnd: + image: btcpayserver/bitcoin:0.19.0.1 + environment: + BITCOIN_NETWORK: regtest + BITCOIN_EXTRA_ARGS: | + deprecatedrpc=signrawtransaction + connect=bitcoind:39388 + links: + - nbxplorer + - postgres + - customer_lnd + - merchant_lnd + nbxplorer: + image: nicolasdorier/nbxplorer:2.1.35 + restart: unless-stopped + ports: + - "32838:32838" + expose: + - "32838" + environment: + NBXPLORER_NETWORK: regtest + NBXPLORER_CHAINS: "btc,ltc,lbtc" + NBXPLORER_BTCRPCURL: http://bitcoind:43782/ + NBXPLORER_BTCNODEENDPOINT: bitcoind:39388 + NBXPLORER_BTCRPCUSER: ceiwHEbqWI83 + NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3 + NBXPLORER_LTCRPCURL: http://litecoind:43782/ + NBXPLORER_LTCNODEENDPOINT: litecoind:39388 + NBXPLORER_LTCRPCUSER: ceiwHEbqWI83 + NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3 + NBXPLORER_LBTCRPCURL: "http://elementsd-liquid:19332/" + NBXPLORER_LBTCNODEENDPOINT: "elementsd-liquid:19444" + NBXPLORER_LBTCRPCUSER: "liquid" + NBXPLORER_LBTCRPCPASSWORD: "liquid" + NBXPLORER_BIND: 0.0.0.0:32838 + NBXPLORER_MINGAPSIZE: 5 + NBXPLORER_MAXGAPSIZE: 10 + NBXPLORER_VERBOSE: 1 + NBXPLORER_NOAUTH: 1 + links: + - bitcoind + - litecoind + - elementsd-liquid + + + bitcoind: + restart: unless-stopped + image: btcpayserver/bitcoin:0.19.0.1 + environment: + BITCOIN_NETWORK: regtest + BITCOIN_EXTRA_ARGS: |- + rpcuser=ceiwHEbqWI83 + rpcpassword=DwubwWsoo3 + rpcport=43782 + rpcbind=0.0.0.0:43782 + port=39388 + whitelist=0.0.0.0/0 + zmqpubrawblock=tcp://0.0.0.0:28332 + zmqpubrawtx=tcp://0.0.0.0:28333 + deprecatedrpc=signrawtransaction + ports: + - "43782:43782" + - "39388:39388" + expose: + - "43782" # RPC + - "39388" # P2P + - "28332" # ZMQ + - "28333" # ZMQ + volumes: + - "bitcoin_datadir:/data" + + customer_lightningd: + image: btcpayserver/lightning:v0.8.2-dev + stop_signal: SIGKILL + restart: unless-stopped + environment: + EXPOSE_TCP: "true" + LIGHTNINGD_CHAIN: "btc" + LIGHTNINGD_NETWORK: "regtest" + LIGHTNINGD_OPT: | + bitcoin-datadir=/etc/bitcoin + bitcoin-rpcconnect=bitcoind + announce-addr=customer_lightningd + log-level=debug + funding-confirms=1 + dev-fast-gossip + dev-bitcoind-poll=1 + ports: + - "30992:9835" # api port + expose: + - "9735" # server port + - "9835" # api port + volumes: + - "bitcoin_datadir:/etc/bitcoin" + - "customer_lightningd_datadir:/root/.lightning" + links: + - bitcoind + + lightning-charged: + image: shesek/lightning-charge:0.4.19-standalone + restart: unless-stopped + environment: + NETWORK: regtest + API_TOKEN: foiewnccewuify + BITCOIND_RPCCONNECT: bitcoind + volumes: + - "bitcoin_datadir:/etc/bitcoin" + - "lightning_charge_datadir:/data" + - "merchant_lightningd_datadir:/etc/lightning" + expose: + - "9112" # Charge + - "9735" # Lightning + ports: + - "54938:9112" # Charge + links: + - bitcoind + - merchant_lightningd + + merchant_lightningd: + image: btcpayserver/lightning:v0.8.2-dev + stop_signal: SIGKILL + environment: + EXPOSE_TCP: "true" + LIGHTNINGD_CHAIN: "btc" + LIGHTNINGD_NETWORK: "regtest" + LIGHTNINGD_OPT: | + bitcoin-datadir=/etc/bitcoin + bitcoin-rpcconnect=bitcoind + announce-addr=merchant_lightningd + funding-confirms=1 + log-level=debug + dev-fast-gossip + dev-bitcoind-poll=1 + ports: + - "30993:9835" # api port + expose: + - "9735" # server port + - "9835" # api port + volumes: + - "bitcoin_datadir:/etc/bitcoin" + - "merchant_lightningd_datadir:/root/.lightning" + links: + - bitcoind + + litecoind: + restart: unless-stopped + image: nicolasdorier/docker-litecoin:0.16.3 + environment: + BITCOIN_EXTRA_ARGS: |- + rpcuser=ceiwHEbqWI83 + rpcpassword=DwubwWsoo3 + regtest=1 + rpcport=43782 + port=39388 + whitelist=0.0.0.0/0 + ports: + - "43783:43782" + expose: + - "43782" # RPC + - "39388" # P2P + + elementsd-liquid: + restart: always + container_name: btcpayserver_elementsd_liquid + image: btcpayserver/elements:0.18.1.7 + environment: + ELEMENTS_CHAIN: elementsregtest + ELEMENTS_EXTRA_ARGS: | + mainchainrpcport=43782 + mainchainrpchost=bitcoind + mainchainrpcuser=liquid + mainchainrpcpassword=liquid + rpcport=19332 + rpcbind=0.0.0.0:19332 + rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6 + port=19444 + whitelist=0.0.0.0/0 + validatepegin=0 + initialfreecoins=210000000000000 + con_dyna_deploy_start=99999999999 + expose: + - "19332" + - "19444" + ports: + - "19332:19332" + - "19444:19444" + volumes: + - "elementsd_liquid_datadir:/data" + + postgres: + image: postgres:9.6.5 + ports: + - "39372:5432" + expose: + - "5432" + + merchant_lnd: + image: btcpayserver/lnd:v0.10.2-beta + restart: unless-stopped + environment: + LND_CHAIN: "btc" + LND_ENVIRONMENT: "regtest" + LND_EXPLORERURL: "http://nbxplorer:32838/" + LND_EXTRA_ARGS: | + restlisten=0.0.0.0:8080 + rpclisten=127.0.0.1:10008 + rpclisten=0.0.0.0:10009 + bitcoin.node=bitcoind + bitcoind.rpchost=bitcoind:43782 + bitcoind.zmqpubrawblock=tcp://bitcoind:28332 + bitcoind.zmqpubrawtx=tcp://bitcoind:28333 + externalip=merchant_lnd:9735 + bitcoin.defaultchanconfs=1 + no-macaroons=1 + debuglevel=debug + trickledelay=1000 + ports: + - "35531:8080" + expose: + - "9735" + volumes: + - "merchant_lnd_datadir:/data" + - "bitcoin_datadir:/deps/.bitcoin" + links: + - bitcoind + + customer_lnd: + image: btcpayserver/lnd:v0.10.2-beta + restart: unless-stopped + environment: + LND_CHAIN: "btc" + LND_ENVIRONMENT: "regtest" + LND_EXPLORERURL: "http://nbxplorer:32838/" + LND_EXTRA_ARGS: | + restlisten=0.0.0.0:8080 + rpclisten=127.0.0.1:10008 + rpclisten=0.0.0.0:10009 + bitcoin.node=bitcoind + bitcoind.rpchost=bitcoind:43782 + bitcoind.zmqpubrawblock=tcp://bitcoind:28332 + bitcoind.zmqpubrawtx=tcp://bitcoind:28333 + externalip=customer_lnd:10009 + bitcoin.defaultchanconfs=1 + no-macaroons=1 + debuglevel=debug + trickledelay=1000 + ports: + - "35532:8080" + expose: + - "8080" + - "10009" + volumes: + - "customer_lnd_datadir:/root/.lnd" + - "bitcoin_datadir:/deps/.bitcoin" + links: + - bitcoind + + tor: + restart: unless-stopped + image: btcpayserver/tor:0.4.1.5 + container_name: tor + environment: + TOR_PASSWORD: btcpayserver + ports: + - "9050:9050" # SOCKS + - "9051:9051" # Tor Control + volumes: + - "tor_datadir:/home/tor/.tor" + - "torrcdir:/usr/local/etc/tor" + - "tor_servicesdir:/var/lib/tor/hidden_services" + monerod: + image: btcpayserver/monero:0.15.0.1-amd64 + restart: unless-stopped + container_name: xmr_monerod + entrypoint: sleep 999999 +# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline + volumes: + - "monero_data:/home/monero/.bitmonero" + ports: + - "18081:18081" + monero_wallet: + image: btcpayserver/monero:0.15.0.1-amd64 + restart: unless-stopped + container_name: xmr_wallet_rpc + entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" + ports: + - "18082:18082" + volumes: + - "./monero_wallet:/wallet" + depends_on: + - monerod + + litecoind: + restart: unless-stopped + image: nicolasdorier/docker-litecoin:0.16.3 + environment: + BITCOIN_EXTRA_ARGS: |- + rpcuser=ceiwHEbqWI83 + rpcpassword=DwubwWsoo3 + regtest=1 + rpcport=43782 + port=39388 + whitelist=0.0.0.0/0 + ports: + - "43783:43782" + expose: + - "43782" # RPC + - "39388" # P2P + + elementsd-liquid: + restart: always + container_name: btcpayserver_elementsd_liquid + image: btcpayserver/elements:0.18.1.7 + environment: + ELEMENTS_CHAIN: elementsregtest + ELEMENTS_EXTRA_ARGS: | + mainchainrpcport=43782 + mainchainrpchost=bitcoind + mainchainrpcuser=liquid + mainchainrpcpassword=liquid + rpcport=19332 + rpcbind=0.0.0.0:19332 + rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6 + port=19444 + whitelist=0.0.0.0/0 + validatepegin=0 + initialfreecoins=210000000000000 + con_dyna_deploy_start=99999999999 + expose: + - "19332" + - "19444" + ports: + - "19332:19332" + - "19444:19444" + volumes: + - "elementsd_liquid_datadir:/data" +volumes: + sshd_datadir: + bitcoin_datadir: + elementsd_liquid_datadir: + customer_lightningd_datadir: + merchant_lightningd_datadir: + lightning_charge_datadir: + customer_lnd_datadir: + merchant_lnd_datadir: + tor_datadir: + torrcdir: + tor_servicesdir: + monero_data: diff --git a/BTCPayServer.Tests/docker-compose.monero.yml b/BTCPayServer.Tests/docker-compose.monero.yml deleted file mode 100644 index 9baa859a7..000000000 --- a/BTCPayServer.Tests/docker-compose.monero.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "3" - -services: - - monerod: - image: btcpayserver/monero:0.15.0.1-amd64 - restart: unless-stopped - container_name: xmr_monerod - entrypoint: sleep 999999 -# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline - volumes: - - "monero_data:/home/monero/.bitmonero" - ports: - - "18081:18081" - monero_wallet: - image: btcpayserver/monero:0.15.0.1-amd64 - restart: unless-stopped - container_name: xmr_wallet_rpc - entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" - ports: - - "18082:18082" - volumes: - - "./monero_wallet:/wallet" - depends_on: - - monerod - -volumes: - monero_data: diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 28f0485f2..dc90f8358 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -9,11 +9,11 @@ services: build: context: .. dockerfile: BTCPayServer.Tests/Dockerfile + args: + CONFIGURATION_NAME: Release environment: TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3 - TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3 TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/ - TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/ TESTS_DB: "Postgres" TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver TESTS_HOSTNAME: tests @@ -78,7 +78,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.35 + image: nicolasdorier/nbxplorer:2.1.37 restart: unless-stopped ports: - "32838:32838" @@ -86,19 +86,11 @@ services: - "32838" environment: NBXPLORER_NETWORK: regtest - NBXPLORER_CHAINS: "btc,ltc,lbtc" + NBXPLORER_CHAINS: "btc" NBXPLORER_BTCRPCURL: http://bitcoind:43782/ NBXPLORER_BTCNODEENDPOINT: bitcoind:39388 NBXPLORER_BTCRPCUSER: ceiwHEbqWI83 NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3 - NBXPLORER_LTCRPCURL: http://litecoind:43782/ - NBXPLORER_LTCNODEENDPOINT: litecoind:39388 - NBXPLORER_LTCRPCUSER: ceiwHEbqWI83 - NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3 - NBXPLORER_LBTCRPCURL: "http://elementsd-liquid:19332/" - NBXPLORER_LBTCNODEENDPOINT: "elementsd-liquid:19444" - NBXPLORER_LBTCRPCUSER: "liquid" - NBXPLORER_LBTCRPCPASSWORD: "liquid" NBXPLORER_BIND: 0.0.0.0:32838 NBXPLORER_MINGAPSIZE: 5 NBXPLORER_MAXGAPSIZE: 10 @@ -106,8 +98,6 @@ services: NBXPLORER_NOAUTH: 1 links: - bitcoind - - litecoind - - elementsd-liquid bitcoind: @@ -209,51 +199,6 @@ services: links: - bitcoind - litecoind: - restart: unless-stopped - image: nicolasdorier/docker-litecoin:0.16.3 - environment: - BITCOIN_EXTRA_ARGS: |- - rpcuser=ceiwHEbqWI83 - rpcpassword=DwubwWsoo3 - regtest=1 - rpcport=43782 - port=39388 - whitelist=0.0.0.0/0 - ports: - - "43783:43782" - expose: - - "43782" # RPC - - "39388" # P2P - - elementsd-liquid: - restart: always - container_name: btcpayserver_elementsd_liquid - image: btcpayserver/elements:0.18.1.7 - environment: - ELEMENTS_CHAIN: elementsregtest - ELEMENTS_EXTRA_ARGS: | - mainchainrpcport=43782 - mainchainrpchost=bitcoind - mainchainrpcuser=liquid - mainchainrpcpassword=liquid - rpcport=19332 - rpcbind=0.0.0.0:19332 - rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6 - port=19444 - whitelist=0.0.0.0/0 - validatepegin=0 - initialfreecoins=210000000000000 - con_dyna_deploy_start=99999999999 - expose: - - "19332" - - "19444" - ports: - - "19332:19332" - - "19444:19444" - volumes: - - "elementsd_liquid_datadir:/data" - postgres: image: postgres:9.6.5 ports: @@ -336,7 +281,6 @@ services: - "torrcdir:/usr/local/etc/tor" - "tor_servicesdir:/var/lib/tor/hidden_services" - volumes: sshd_datadir: bitcoin_datadir: diff --git a/BTCPayServer.Tests/docker-entrypoint.sh b/BTCPayServer.Tests/docker-entrypoint.sh index 5f11b488c..a73e5f7be 100755 --- a/BTCPayServer.Tests/docker-entrypoint.sh +++ b/BTCPayServer.Tests/docker-entrypoint.sh @@ -6,4 +6,4 @@ if [ ! -z "$TEST_FILTERS" ]; then FILTERS="--filter $TEST_FILTERS" fi -dotnet test $FILTERS --no-build -v n < /dev/null +dotnet test -c ${CONFIGURATION_NAME} $FILTERS --no-build -v n < /dev/null diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 41730078f..0d0e4a041 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -1,10 +1,7 @@ - + - - false - $(DefineConstants);RAZOR_RUNTIME_COMPILE - + Exe @@ -21,6 +18,10 @@ + + true + PreserveNewest + @@ -28,12 +29,28 @@ + + + + + + + + + + + + + + + + diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index b596bb90d..3cd189a7b 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NBitcoin; using Serilog.Events; +using TwentyTwenty.Storage; namespace BTCPayServer.Configuration { @@ -90,11 +91,14 @@ namespace BTCPayServer.Configuration var networkProvider = new BTCPayNetworkProvider(NetworkType); var filtered = networkProvider.Filter(supportedChains.ToArray()); - var elementsBased = filtered.GetAll().OfType(); - var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct(); - var allSubChains = networkProvider.GetAll().OfType() - .Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant()); - supportedChains.AddRange(allSubChains); +#if ALTCOINS + supportedChains.AddRange(filtered.GetAllElementsSubChains()); +#endif +#if !ALTCOINS + var onlyBTC = supportedChains.Count == 1 && supportedChains.First() == "BTC"; + if (!onlyBTC) + throw new ConfigException($"This build of BTCPay Server does not support altcoins"); +#endif NetworkProvider = networkProvider.Filter(supportedChains.ToArray()); foreach (var chain in supportedChains) { @@ -172,6 +176,7 @@ namespace BTCPayServer.Configuration SocksEndpoint = endpoint; } + UpdateUrl = conf.GetOrDefault("updateurl", null); var sshSettings = ParseSSHConfiguration(conf); if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server)) @@ -297,5 +302,6 @@ namespace BTCPayServer.Configuration set; } public string TorrcFile { get; set; } + public Uri UpdateUrl { get; set; } } } diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index e24a31bd2..d16245d9a 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -38,6 +38,7 @@ namespace BTCPayServer.Configuration app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue); app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue); app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue); + app.Option("--updateurl", $"Url used for once a day new release version check. Check performed only if value is not empty (default: empty)", CommandOptionType.SingleValue); app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue); app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue); app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue); diff --git a/BTCPayServer/Contracts/ISyncSummaryProvider.cs b/BTCPayServer/Contracts/ISyncSummaryProvider.cs new file mode 100644 index 000000000..44cdc918c --- /dev/null +++ b/BTCPayServer/Contracts/ISyncSummaryProvider.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Contracts +{ + public interface ISyncSummaryProvider + { + bool AllAvailable(); + + string Partial { get; } + } + +} diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index b8c47a313..6d1b8d3c0 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -443,13 +443,8 @@ namespace BTCPayServer.Controllers var settings = await _SettingsRepository.GetSettingAsync(); settings.FirstRun = false; await _SettingsRepository.UpdateSetting(settings); - if (_Options.DisableRegistration) - { - // Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users). - Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)"); - policies.LockSubscription = true; - await _SettingsRepository.UpdateSetting(policies); - } + + await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration); RegisteredAdmin = true; } @@ -626,7 +621,7 @@ namespace BTCPayServer.Controllers private bool CanLoginOrRegister() { - return _btcPayServerEnvironment.IsDevelopping || _btcPayServerEnvironment.IsSecure; + return _btcPayServerEnvironment.IsDeveloping || _btcPayServerEnvironment.IsSecure; } private void SetInsecureFlags() diff --git a/BTCPayServer/Controllers/AppsController.cs b/BTCPayServer/Controllers/AppsController.cs index 45496f7ee..3a788e384 100644 --- a/BTCPayServer/Controllers/AppsController.cs +++ b/BTCPayServer/Controllers/AppsController.cs @@ -47,9 +47,42 @@ namespace BTCPayServer.Controllers public string CreatedAppId { get; set; } - public async Task ListApps() + public async Task ListApps( + string sortOrder = null, + string sortOrderColumn = null + ) { var apps = await _AppService.GetAllApps(GetUserId()); + + if (sortOrder != null && sortOrderColumn != null) + { + apps = apps.OrderByDescending(app => + { + switch (sortOrderColumn) + { + case nameof(app.AppName): + return app.AppName; + case nameof(app.StoreName): + return app.StoreName; + case nameof(app.AppType): + return app.AppType; + default: + return app.Id; + } + }).ToArray(); + + switch (sortOrder) + { + case "desc": + ViewData[$"{sortOrderColumn}SortOrder"] = "asc"; + break; + case "asc": + apps = apps.Reverse().ToArray(); + ViewData[$"{sortOrderColumn}SortOrder"] = "desc"; + break; + } + } + return View(new ListAppsViewModel() { Apps = apps diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs index 6979fcfff..5e10b25ab 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs @@ -288,7 +288,7 @@ namespace BTCPayServer.Controllers.GreenField protected bool CanUseInternalLightning(bool doingAdminThings) { - return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || + return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || (_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings)); } diff --git a/BTCPayServer/Controllers/GreenField/UsersController.cs b/BTCPayServer/Controllers/GreenField/UsersController.cs index 94adf201f..350b7cf02 100644 --- a/BTCPayServer/Controllers/GreenField/UsersController.cs +++ b/BTCPayServer/Controllers/GreenField/UsersController.cs @@ -148,13 +148,7 @@ namespace BTCPayServer.Controllers.GreenField await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); if (!anyAdmin) { - if (_options.DisableRegistration) - { - // automatically lock subscriptions now that we have our first admin - Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)"); - policies.LockSubscription = true; - await _settingsRepository.UpdateSetting(policies); - } + await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration); } } _eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true }); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 572cc248a..802b47cac 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -29,7 +29,6 @@ using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitpayClient; using NBXplorer; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StoreData = BTCPayServer.Data.StoreData; @@ -598,55 +597,23 @@ namespace BTCPayServer.Controllers return Ok("{}"); } - public class InvoicePreference - { - public int? TimezoneOffset { get; set; } - public string SearchTerm { get; set; } - } - [HttpGet] [Route("invoices")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [BitpayAPIConstraint(false)] - public async Task ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int? timezoneOffset = null) + public async Task ListInvoices(InvoicesModel model = null) { - // If the user enter an empty searchTerm, then the variable will be null and not empty string - // but we want searchTerm to be null only if the user is browsing the page via some link - // NOT if the user entered some empty search - searchTerm = searchTerm is string ? searchTerm : - this.Request.Query.ContainsKey(nameof(searchTerm)) ? string.Empty : - null; - if (searchTerm is null) - { - if (this.Request.Cookies.TryGetValue("ListInvoicePreferences", out var str)) - { - var preferences = JsonConvert.DeserializeObject(str); - searchTerm = preferences.SearchTerm; - timezoneOffset = timezoneOffset is int v ? v : preferences.TimezoneOffset; - } - } - else - { - var preferences = new InvoicePreference(); - preferences.SearchTerm = searchTerm; - preferences.TimezoneOffset = timezoneOffset; - this.Response.Cookies.Append("ListInvoicePreferences", JsonConvert.SerializeObject(preferences)); - } - var fs = new SearchString(searchTerm); + model = this.ParseListQuery(model ?? new InvoicesModel()); + + var fs = new SearchString(model.SearchTerm); var storeIds = fs.GetFilterArray("storeid") != null ? fs.GetFilterArray("storeid") : new List().ToArray(); - var model = new InvoicesModel - { - SearchTerm = searchTerm, - Skip = skip, - Count = count, - StoreIds = storeIds, - TimezoneOffset = timezoneOffset - }; - InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset ?? 0); + model.StoreIds = storeIds; + + InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0); var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery); - invoiceQuery.Count = count; - invoiceQuery.Skip = skip; + invoiceQuery.Count = model.Count; + invoiceQuery.Skip = model.Skip; var list = await _InvoiceRepository.GetInvoices(invoiceQuery); foreach (var invoice in list) diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index 077d567c1..a2491ea61 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -61,23 +61,22 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("")] [BitpayAPIConstraint(false)] - public async Task GetPaymentRequests(int skip = 0, int count = 50, bool includeArchived = false) + public async Task GetPaymentRequests(ListPaymentRequestsViewModel model = null) { + model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel()); + + var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true; var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery() { UserId = GetUserId(), - Skip = skip, - Count = count, + Skip = model.Skip, + Count = model.Count, IncludeArchived = includeArchived }); - return View(new ListPaymentRequestsViewModel() - { - IncludeArchived = includeArchived, - Skip = skip, - Count = count, - Total = result.Total, - Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList() - }); + + model.Total = result.Total; + model.Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList(); + return View(model); } [HttpGet] diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 46b892494..0abf1cc47 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -172,7 +172,7 @@ namespace BTCPayServer.Controllers private bool CanUseInternalLightning() { - return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll); + return (_BTCPayEnv.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll); } } } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index a55965bb5..99384dcde 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -520,7 +520,9 @@ namespace BTCPayServer.Controllers Value = value, WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode), Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null, +#if ALTCOINS Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value) +#endif }); break; case LightningPaymentType _: diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 79a21734f..5d73f04c4 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -259,7 +259,11 @@ namespace BTCPayServer.Controllers [Route("{walletId}/transactions")] public async Task WalletTransactions( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, string labelFilter = null) + WalletId walletId, + string labelFilter = null, + int skip = 0, + int count = 50 + ) { DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId); if (paymentMethod == null) @@ -271,7 +275,12 @@ namespace BTCPayServer.Controllers var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation); var walletBlob = await walletBlobAsync; var walletTransactionsInfo = await walletTransactionsInfoAsync; - var model = new ListTransactionsViewModel(); + var model = new ListTransactionsViewModel + { + Skip = skip, + Count = count, + Total = 0 + }; if (transactions == null) { TempData.SetStatusMessageModel(new StatusMessageModel() @@ -309,7 +318,8 @@ namespace BTCPayServer.Controllers model.Transactions.Add(vm); } - model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList(); + model.Total = model.Transactions.Count; + model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).Skip(skip).Take(count).ToList(); } return View(model); diff --git a/BTCPayServer/Data/PullPaymentsExtensions.cs b/BTCPayServer/Data/PullPaymentsExtensions.cs index 3165f3de5..b29e872b8 100644 --- a/BTCPayServer/Data/PullPaymentsExtensions.cs +++ b/BTCPayServer/Data/PullPaymentsExtensions.cs @@ -149,9 +149,9 @@ namespace BTCPayServer.Data } public class PayoutBlob { - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal? CryptoAmount { get; set; } public int MinimumConfirmation { get; set; } = 1; public IClaimDestination Destination { get; set; } @@ -190,9 +190,9 @@ namespace BTCPayServer.Data public string Name { get; set; } public string Currency { get; set; } public int Divisibility { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Limit { get; set; } - [JsonConverter(typeof(DecimalStringJsonConverter))] + [JsonConverter(typeof(NumericStringJsonConverter))] public decimal MinimumClaim { get; set; } public PullPaymentView View { get; set; } = new PullPaymentView(); [JsonConverter(typeof(TimeSpanJsonConverter))] diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index bcab2867d..20f67fe1f 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -64,7 +64,6 @@ namespace BTCPayServer.Data { if (storeData == null) throw new ArgumentNullException(nameof(storeData)); - networks = networks.UnfilteredNetworks; #pragma warning disable CS0618 bool btcReturned = false; diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 52f4b54d5..eb4ad0f44 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.WebSockets; +using System.Reflection; using System.Security.Claims; using System.Text; using System.Threading; @@ -449,5 +450,21 @@ namespace BTCPayServer }; return controller.View("PostRedirect", redirectVm); } + + public static string ToSql(this IQueryable query) where TEntity : class + { + var enumerator = query.Provider.Execute>(query.Expression).GetEnumerator(); + var relationalCommandCache = enumerator.Private("_relationalCommandCache"); + var selectExpression = relationalCommandCache.Private("_selectExpression"); + var factory = relationalCommandCache.Private("_querySqlGeneratorFactory"); + + var sqlGenerator = factory.Create(); + var command = sqlGenerator.GetCommand(selectExpression); + + string sql = command.CommandText; + return sql; + } + private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); + private static T Private(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); } } diff --git a/BTCPayServer/Extensions/ActionLogicExtensions.cs b/BTCPayServer/Extensions/ActionLogicExtensions.cs new file mode 100644 index 000000000..9e5d247f2 --- /dev/null +++ b/BTCPayServer/Extensions/ActionLogicExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Logging; +using BTCPayServer.Services; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer +{ + // All logic that would otherwise be duplicated across solution goes into this utility class + // ~If~ Once this starts growing out of control, begin extracting action logic classes out of here + // Also some of logic in here may be result of parallel development of Greenfield API + // It's much better that we extract those common methods then copy paste and maintain same code across codebase + internal static class ActionLogicExtensions + { + internal static async Task FirstAdminRegistered(this SettingsRepository settingsRepository, PoliciesSettings policies, + bool updateCheck, bool disableRegistrations) + { + if (updateCheck) + { + Logs.PayServer.LogInformation("First admin created, enabling checks for new versions"); + policies.CheckForNewVersions = updateCheck; + } + + if (disableRegistrations) + { + // Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users). + Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)"); + policies.LockSubscription = true; + } + + if (updateCheck || disableRegistrations) + await settingsRepository.UpdateSetting(policies); + } + } +} diff --git a/BTCPayServer/Extensions/ControllerBaseExtensions.cs b/BTCPayServer/Extensions/ControllerBaseExtensions.cs new file mode 100644 index 000000000..47520c950 --- /dev/null +++ b/BTCPayServer/Extensions/ControllerBaseExtensions.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using BTCPayServer.Models; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Models.PaymentRequestViewModels; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace BTCPayServer +{ + // Classes here remember users preferences on certain pages and store them in unified blob cookie "UserPreferCookie" + public static class ControllerBaseExtension + { + public static T ParseListQuery(this ControllerBase ctrl, T model) where T : BasePagingViewModel + { + PropertyInfo prop; + if (model is InvoicesModel) + prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.InvoicesQuery)); + else if (model is ListPaymentRequestsViewModel) + prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.PaymentRequestsQuery)); + else + throw new Exception("Unsupported BasePagingViewModel for cookie user preferences saving"); + + return ProcessParse(ctrl, model, prop); + } + + private static T ProcessParse(ControllerBase ctrl, T model, PropertyInfo prop) where T : BasePagingViewModel + { + var prefCookie = parsePrefCookie(ctrl); + + // If the user enter an empty searchTerm, then the variable will be null and not empty string + // but we want searchTerm to be null only if the user is browsing the page via some link + // NOT if the user entered some empty search + var searchTerm = model.SearchTerm; + searchTerm = searchTerm is string ? searchTerm : + ctrl.Request.Query.ContainsKey(nameof(searchTerm)) ? string.Empty : + null; + if (searchTerm is null) + { + var section = prop.GetValue(prefCookie) as ListQueryDataHolder; + if (section != null && !String.IsNullOrEmpty(section.SearchTerm)) + { + model.SearchTerm = section.SearchTerm; + model.TimezoneOffset = section.TimezoneOffset ?? 0; + } + } + else + { + prop.SetValue(prefCookie, new ListQueryDataHolder(model.SearchTerm, model.TimezoneOffset)); + ctrl.Response.Cookies.Append(nameof(UserPrefsCookie), JsonConvert.SerializeObject(prefCookie)); + } + + return model; + } + + private static UserPrefsCookie parsePrefCookie(ControllerBase ctrl) + { + var prefCookie = new UserPrefsCookie(); + ctrl.Request.Cookies.TryGetValue(nameof(UserPrefsCookie), out var strPrefCookie); + if (!String.IsNullOrEmpty(strPrefCookie)) + { + try + { + prefCookie = JsonConvert.DeserializeObject(strPrefCookie); + } + catch { /* ignore cookie deserialization failures */ } + } + + return prefCookie; + } + + class UserPrefsCookie + { + public ListQueryDataHolder InvoicesQuery { get; set; } + + public ListQueryDataHolder PaymentRequestsQuery { get; set; } + } + + class ListQueryDataHolder + { + public ListQueryDataHolder() { } + + public ListQueryDataHolder(string searchTerm, int? timezoneOffset) + { + SearchTerm = searchTerm; + TimezoneOffset = timezoneOffset; + } + + public int? TimezoneOffset { get; set; } + public string SearchTerm { get; set; } + } + } +} diff --git a/BTCPayServer/Extensions/MoneyExtensions.cs b/BTCPayServer/Extensions/MoneyExtensions.cs index 736975d12..a764774b3 100644 --- a/BTCPayServer/Extensions/MoneyExtensions.cs +++ b/BTCPayServer/Extensions/MoneyExtensions.cs @@ -16,6 +16,7 @@ namespace BTCPayServer return money.ToDecimal(MoneyUnit.BTC); case MoneyBag mb: return mb.Select(money => money.GetValue(network)).Sum(); +#if ALTCOINS case AssetMoney assetMoney: if (network is ElementsBTCPayNetwork elementsBTCPayNetwork) { @@ -24,6 +25,7 @@ namespace BTCPayServer : 0; } throw new NotSupportedException("IMoney type not supported"); +#endif default: throw new NotSupportedException("IMoney type not supported"); } diff --git a/BTCPayServer/HostedServices/BaseAsyncService.cs b/BTCPayServer/HostedServices/BaseAsyncService.cs index 52efbb80a..4512d1c5f 100644 --- a/BTCPayServer/HostedServices/BaseAsyncService.cs +++ b/BTCPayServer/HostedServices/BaseAsyncService.cs @@ -10,12 +10,11 @@ namespace BTCPayServer.HostedServices { public abstract class BaseAsyncService : IHostedService { - private CancellationTokenSource _Cts; + private CancellationTokenSource _Cts = new CancellationTokenSource(); protected Task[] _Tasks; public virtual Task StartAsync(CancellationToken cancellationToken) { - _Cts = new CancellationTokenSource(); _Tasks = InitializeTasks(); return Task.CompletedTask; } diff --git a/BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs b/BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs new file mode 100644 index 000000000..c7dd75637 --- /dev/null +++ b/BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs @@ -0,0 +1,121 @@ +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Logging; +using BTCPayServer.Services; +using BTCPayServer.Services.Notifications; +using BTCPayServer.Services.Notifications.Blobs; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.HostedServices +{ + public class NewVersionCheckerHostedService : BaseAsyncService + { + private readonly SettingsRepository _settingsRepository; + private readonly BTCPayServerEnvironment _env; + private readonly NotificationSender _notificationSender; + private readonly IVersionFetcher _versionFetcher; + + public NewVersionCheckerHostedService(SettingsRepository settingsRepository, BTCPayServerEnvironment env, + NotificationSender notificationSender, IVersionFetcher versionFetcher) + { + _settingsRepository = settingsRepository; + _env = env; + _notificationSender = notificationSender; + _versionFetcher = versionFetcher; + } + + internal override Task[] InitializeTasks() + { + return new Task[] { CreateLoopTask(LoopVersionCheck) }; + } + + protected async Task LoopVersionCheck() + { + try + { + await ProcessVersionCheck(); + } + catch (Exception ex) + { + Logs.Events.LogError(ex, "Error while performing new version check"); + } + await Task.Delay(TimeSpan.FromDays(1), Cancellation); + } + + public async Task ProcessVersionCheck() + { + var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); + if (policies.CheckForNewVersions) + { + var tag = await _versionFetcher.Fetch(Cancellation); + if (tag != null && tag != _env.Version) + { + var dh = await _settingsRepository.GetSettingAsync() ?? new NewVersionCheckerDataHolder(); + if (dh.LastVersion != tag) + { + await _notificationSender.SendNotification(new AdminScope(), new NewVersionNotification(tag)); + + dh.LastVersion = tag; + await _settingsRepository.UpdateSetting(dh); + } + } + } + } + } + + public class NewVersionCheckerDataHolder + { + public string LastVersion { get; set; } + } + + public interface IVersionFetcher + { + Task Fetch(CancellationToken cancellation); + } + + public class GithubVersionFetcher : IVersionFetcher + { + private readonly HttpClient _httpClient; + private readonly Uri _updateurl; + public GithubVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options) + { + _httpClient = httpClientFactory.CreateClient(nameof(GithubVersionFetcher)); + _httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "BTCPayServer/NewVersionChecker"); + + _updateurl = options.UpdateUrl; + } + + private static readonly Regex _releaseVersionTag = new Regex("^(v[1-9]+(\\.[0-9]+)*(-[0-9]+)?)$"); + public async Task Fetch(CancellationToken cancellation) + { + if (_updateurl == null) + return null; + + using (var resp = await _httpClient.GetAsync(_updateurl, cancellation)) + { + var strResp = await resp.Content.ReadAsStringAsync(); + if (resp.IsSuccessStatusCode) + { + var jobj = JObject.Parse(strResp); + var tag = jobj["tag_name"].ToString(); + + var isReleaseVersionTag = _releaseVersionTag.IsMatch(tag); + return isReleaseVersionTag ? tag : null; + } + else + { + Logs.Events.LogWarning($"Unsuccessful status code returned during new version check. " + + $"Url: {_updateurl}, HTTP Code: {resp.StatusCode}, Response Body: {strResp}"); + } + } + + return null; + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 99bed5819..1cadb52b0 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using BTCPayServer.Configuration; +using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.HostedServices; @@ -16,7 +17,6 @@ using BTCPayServer.Security; using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.GreenField; using BTCPayServer.Services; -using BTCPayServer.Services.Altcoins.Monero; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Fees; using BTCPayServer.Services.Invoices; @@ -47,7 +47,9 @@ using NBXplorer.DerivationStrategy; using Newtonsoft.Json; using NicolasDorier.RateLimits; using Serilog; - +#if ALTCOINS +using BTCPayServer.Services.Altcoins.Monero; +#endif namespace BTCPayServer.Hosting { public static class BTCPayServerServices @@ -75,7 +77,9 @@ namespace BTCPayServer.Hosting services.RegisterJsonConverter(n => new ClaimDestinationJsonConverter(n)); services.AddPayJoinServices(); +#if ALTCOINS services.AddMoneroLike(); +#endif services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -168,6 +172,7 @@ namespace BTCPayServer.Hosting htmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; }; htmlSanitizer.AllowedAttributes.Add("class"); htmlSanitizer.AllowedTags.Add("iframe"); + htmlSanitizer.AllowedTags.Add("style"); htmlSanitizer.AllowedTags.Remove("img"); htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen"); htmlSanitizer.AllowedAttributes.Add("allowfullscreen"); @@ -177,6 +182,7 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -233,7 +239,11 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddScoped(); services.AddScoped(); + + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -278,7 +288,7 @@ namespace BTCPayServer.Hosting { var btcPayEnv = provider.GetService(); var rateLimits = new RateLimitService(); - if (btcPayEnv.IsDevelopping) + if (btcPayEnv.IsDeveloping) { rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay"); rateLimits.SetZone($"zone={ZoneLimits.Register} rate=1000r/min burst=100 nodelay"); diff --git a/BTCPayServer/Models/BasePagingViewModel.cs b/BTCPayServer/Models/BasePagingViewModel.cs new file mode 100644 index 000000000..3df9a4424 --- /dev/null +++ b/BTCPayServer/Models/BasePagingViewModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Models +{ + public abstract class BasePagingViewModel + { + public int Skip { get; set; } = 0; + public int Count { get; set; } = 50; + public int Total { get; set; } + [DisplayFormat(ConvertEmptyStringToNull = false)] + public string SearchTerm { get; set; } + public int? TimezoneOffset { get; set; } + } +} diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index ca0404d3f..ef2d03fd2 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -4,13 +4,8 @@ using BTCPayServer.Services.Invoices; namespace BTCPayServer.Models.InvoicingModels { - public class InvoicesModel + public class InvoicesModel : BasePagingViewModel { - public int Skip { get; set; } - public int Count { get; set; } - public int Total { get; set; } - public string SearchTerm { get; set; } - public int? TimezoneOffset { get; set; } public List Invoices { get; set; } = new List(); public string[] StoreIds { get; set; } } diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index 0abe7c75c..a45521817 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -8,15 +8,10 @@ using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; namespace BTCPayServer.Models.PaymentRequestViewModels { - public class ListPaymentRequestsViewModel + public class ListPaymentRequestsViewModel : BasePagingViewModel { - public int Skip { get; set; } - public int Count { get; set; } - public List Items { get; set; } - public int Total { get; set; } - public bool IncludeArchived { get; set; } } public class UpdatePaymentRequestViewModel diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 9d7215dd0..21372d30a 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -11,6 +11,8 @@ namespace BTCPayServer.Models.StoreViewModels public DerivationSchemeViewModel() { } + + [Display(Name = "Derivation scheme")] public string DerivationScheme { get; set; @@ -31,7 +33,7 @@ namespace BTCPayServer.Models.StoreViewModels public KeyPath RootKeyPath { get; set; } - [Display(Name = "Electrum/Hardware Wallet File")] + [Display(Name = "Wallet File")] public IFormFile WalletFile { get; set; } public string Config { get; set; } public string Source { get; set; } diff --git a/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs b/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs index d14788c0a..d63640ddd 100644 --- a/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs @@ -19,5 +19,8 @@ namespace BTCPayServer.Models.WalletViewModels } public HashSet