diff --git a/.circleci/config.yml b/.circleci/config.yml index f1051b460..f9e05df5a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,29 +1,41 @@ version: 2 jobs: - build: - machine: - docker_layer_caching: true - steps: - - checkout - - test: + fast_tests: machine: docker_layer_caching: true steps: - checkout - run: command: | - cd BTCPayServer.Tests - docker-compose down --v - docker-compose build - TESTS_RUN_EXTERNAL_INTEGRATION="true" - if [ -n "$CIRCLE_PR_NUMBER" ]; then - TESTS_RUN_EXTERNAL_INTEGRATION="false" - fi - docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests + cd .circleci && ./run-tests.sh "Fast=Fast" + selenium_tests: + machine: + docker_layer_caching: true + steps: + - checkout + - run: + command: | + cd .circleci && ./run-tests.sh "Selenium=Selenium" + integration_tests: + machine: + docker_layer_caching: true + steps: + - checkout + - run: + command: | + cd .circleci && ./run-tests.sh "Integration=Integration" + external_tests: + machine: + docker_layer_caching: true + steps: + - checkout + - run: + command: | + cd .circleci && ./run-tests.sh "ExternalIntegration=ExternalIntegration" + # publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined - publish_docker_linuxamd64: + amd64: machine: docker_layer_caching: true steps: @@ -32,11 +44,11 @@ jobs: command: | LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # - sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 . + sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64 - publish_docker_linuxarm: + arm32v7: machine: docker_layer_caching: true steps: @@ -46,11 +58,11 @@ jobs: sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # - sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 . + sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 - publish_docker_multiarch: + multiarch: machine: enabled: true image: circleci/classic:201808-01 @@ -73,11 +85,17 @@ workflows: version: 2 build_and_test: jobs: - - test + - fast_tests + - selenium_tests + - integration_tests + - external_tests: + filters: + branches: + only: master publish: jobs: - - publish_docker_linuxamd64: + - amd64: filters: # ignore any commit on any branch by default branches: @@ -85,16 +103,16 @@ workflows: # only act on version tags tags: only: /v[1-9]+(\.[0-9]+)*/ - - publish_docker_linuxarm: + - arm32v7: filters: branches: ignore: /.*/ tags: only: /v[1-9]+(\.[0-9]+)*/ - - publish_docker_multiarch: + - multiarch: requires: - - publish_docker_linuxamd64 - - publish_docker_linuxarm + - amd64 + - arm32v7 filters: branches: ignore: /.*/ diff --git a/.circleci/run-tests.sh b/.circleci/run-tests.sh new file mode 100755 index 000000000..1e0ed4754 --- /dev/null +++ b/.circleci/run-tests.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +cd ../BTCPayServer.Tests +docker-compose -v +docker-compose down --v +docker-compose build +docker-compose run -e "TEST_FILTERS=$1" tests diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs similarity index 58% rename from BTCPayServer/BTCPayNetwork.cs rename to BTCPayServer.Common/BTCPayNetwork.cs index 685215e6b..0733ee98c 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -3,13 +3,18 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; +using Newtonsoft.Json; namespace BTCPayServer { + public enum DerivationType + { + Legacy, + SegwitP2SH, + Segwit + } public class BTCPayDefaultSettings { static BTCPayDefaultSettings() @@ -38,13 +43,69 @@ namespace BTCPayServer public string DefaultConfigurationFile { get; set; } public int DefaultPort { get; set; } } - public class BTCPayNetwork + + public class BTCPayNetwork:BTCPayNetworkBase { public Network NBitcoinNetwork { get; set; } + public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; } + public bool SupportRBF { get; internal set; } + public string LightningImagePath { get; set; } + public BTCPayDefaultSettings DefaultSettings { get; set; } + public KeyPath CoinType { get; internal set; } + public Dictionary ElectrumMapping = new Dictionary(); + + + public KeyPath GetRootKeyPath(DerivationType type) + { + KeyPath baseKey; + if (!NBitcoinNetwork.Consensus.SupportSegwit) + { + baseKey = new KeyPath("44'"); + } + else + { + switch (type) + { + case DerivationType.Legacy: + baseKey = new KeyPath("44'"); + break; + case DerivationType.SegwitP2SH: + baseKey = new KeyPath("49'"); + break; + case DerivationType.Segwit: + baseKey = new KeyPath("84'"); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + return baseKey + .Derive(CoinType); + } + + public KeyPath GetRootKeyPath() + { + return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'") + .Derive(CoinType); + } + + public override T ToObject(string json) + { + return NBXplorerNetwork.Serializer.ToObject(json); + } + + public override string ToString(T obj) + { + return NBXplorerNetwork.Serializer.ToString(obj); + } + } + + public abstract class BTCPayNetworkBase + { + public string CryptoCode { get; internal set; } public string BlockExplorerLink { get; internal set; } public string UriScheme { get; internal set; } - public Money MinFee { get; internal set; } public string DisplayName { get; set; } [Obsolete("Should not be needed")] @@ -57,23 +118,22 @@ namespace BTCPayServer } public string CryptoImagePath { get; set; } - public string LightningImagePath { get; set; } - public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; } - public BTCPayDefaultSettings DefaultSettings { get; set; } - public KeyPath CoinType { get; internal set; } public int MaxTrackedConfirmation { get; internal set; } = 6; public string[] DefaultRateRules { get; internal set; } = Array.Empty(); - public override string ToString() { return CryptoCode; } - internal KeyPath GetRootKeyPath() + public virtual T ToObject(string json) { - return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'") - .Derive(CoinType); + return JsonConvert.DeserializeObject(json); + } + + public virtual string ToString(T obj) + { + return JsonConvert.SerializeObject(obj); } } } diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcoin.cs similarity index 51% rename from BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Bitcoin.cs index b6752d0e2..cb6155a0b 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcoin.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; -using NBitpayClient; using NBXplorer; namespace BTCPayServer @@ -18,14 +16,29 @@ namespace BTCPayServer { CryptoCode = nbxplorerNetwork.CryptoCode, DisplayName = "Bitcoin", - BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}", + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/tx/{0}" : "https://blockstream.info/testnet/tx/{0}", NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "bitcoin", CryptoImagePath = "imlegacy/bitcoin.svg", LightningImagePath = "imlegacy/bitcoin-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), - CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'") + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"), + SupportRBF = true, + //https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py + ElectrumMapping = NetworkType == NetworkType.Mainnet + ? new Dictionary() + { + {0x0488b21eU, DerivationType.Legacy }, // xpub + {0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub + {0x4b24746U, DerivationType.Segwit }, //zpub + } + : new Dictionary() + { + {0x043587cfU, DerivationType.Legacy}, + {0x044a5262U, DerivationType.SegwitP2SH}, + {0x045f1cf6U, DerivationType.Segwit} + } }); } } diff --git a/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs b/BTCPayServer.Common/BTCPayNetworkProvider.BitcoinGold.cs similarity index 100% rename from BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.BitcoinGold.cs diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcoinplus.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Bitcoinplus.cs index d7b472993..7a9b01b7c 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcoinplus.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcore.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Bitcore.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Bitcore.cs index fad2bb54f..8f2d85825 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcore.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Bitcore.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Dash.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Dash.cs similarity index 94% rename from BTCPayServer/BTCPayNetworkProvider.Dash.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Dash.cs index 62e95b4fb..0794cb460 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Dash.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Dash.cs @@ -27,8 +27,7 @@ namespace BTCPayServer DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), //https://github.com/satoshilabs/slips/blob/master/slip-0044.md CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'") - : new KeyPath("1'"), - MinFee = Money.Satoshis(1m) + : new KeyPath("1'") }); } } diff --git a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Dogecoin.cs similarity index 91% rename from BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Dogecoin.cs index d583c20e2..ba70d681c 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Dogecoin.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; @@ -28,8 +27,7 @@ namespace BTCPayServer }, CryptoImagePath = "imlegacy/dogecoin.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), - CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"), - MinFee = Money.Coins(1m) + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'") }); } } diff --git a/BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Feathercoin.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Feathercoin.cs index 8f9642e2f..327ac5d5d 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Feathercoin.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Groestlcoin.cs similarity index 81% rename from BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Groestlcoin.cs index ec63bdfc1..c20bd80bd 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Groestlcoin.cs @@ -10,20 +10,21 @@ namespace BTCPayServer { public void InitGroestlcoin() { - var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS"); Add(new BTCPayNetwork() { CryptoCode = nbxplorerNetwork.CryptoCode, DisplayName = "Groestlcoin", - BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm", + BlockExplorerLink = NetworkType == NetworkType.Mainnet + ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" + : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm", NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "groestlcoin", DefaultRateRules = new[] { - "GRS_X = GRS_BTC * BTC_X", - "GRS_BTC = bittrex(GRS_BTC)" + "GRS_X = GRS_BTC * BTC_X", + "GRS_BTC = bittrex(GRS_BTC)" }, CryptoImagePath = "imlegacy/groestlcoin.png", LightningImagePath = "imlegacy/groestlcoin-lightning.svg", diff --git a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Litecoin.cs similarity index 51% rename from BTCPayServer/BTCPayNetworkProvider.Litecoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Litecoin.cs index c09692f49..fd3f386fe 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Litecoin.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; @@ -17,14 +16,30 @@ namespace BTCPayServer { CryptoCode = nbxplorerNetwork.CryptoCode, DisplayName = "Litecoin", - BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}", + BlockExplorerLink = NetworkType == NetworkType.Mainnet + ? "https://live.blockcypher.com/ltc/tx/{0}/" + : "http://explorer.litecointools.com/tx/{0}", NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "litecoin", CryptoImagePath = "imlegacy/litecoin.svg", LightningImagePath = "imlegacy/litecoin-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), - CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'") + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"), + //https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py + ElectrumMapping = NetworkType == NetworkType.Mainnet + ? new Dictionary() + { + {0x0488b21eU, DerivationType.Legacy }, + {0x049d7cb2U, DerivationType.SegwitP2SH }, + {0x04b24746U, DerivationType.Segwit }, + } + : new Dictionary() + { + {0x043587cfU, DerivationType.Legacy }, + {0x044a5262U, DerivationType.SegwitP2SH }, + {0x045f1cf6U, DerivationType.Segwit } + } }); } } diff --git a/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Monacoin.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Monacoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Monacoin.cs index cb491f417..4c9f056d2 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Monacoin.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Polis.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Polis.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Polis.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Polis.cs index 5379477c6..9ae9a8dbc 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Polis.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Polis.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Ufo.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Ufo.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Ufo.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Ufo.cs index 801bd7194..01b2b3b83 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Ufo.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Ufo.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.Viacoin.cs b/BTCPayServer.Common/BTCPayNetworkProvider.Viacoin.cs similarity index 97% rename from BTCPayServer/BTCPayNetworkProvider.Viacoin.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.Viacoin.cs index 58103a7cc..b7260d4ad 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Viacoin.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.Viacoin.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer.Common/BTCPayNetworkProvider.cs similarity index 56% rename from BTCPayServer/BTCPayNetworkProvider.cs rename to BTCPayServer.Common/BTCPayNetworkProvider.cs index be0c4970d..066178a9d 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.cs @@ -3,17 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Services.Rates; -using Microsoft.Extensions.Caching.Memory; using NBitcoin; -using NBitpayClient; using NBXplorer; namespace BTCPayServer { public partial class BTCPayNetworkProvider { - Dictionary _Networks = new Dictionary(); + Dictionary _Networks = new Dictionary(); private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider; @@ -25,13 +22,14 @@ namespace BTCPayServer } } - BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes) + BTCPayNetworkProvider(BTCPayNetworkProvider unfiltered, string[] cryptoCodes) { - NetworkType = filtered.NetworkType; - _NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType); - _Networks = new Dictionary(); + UnfilteredNetworks = unfiltered.UnfilteredNetworks ?? unfiltered; + NetworkType = unfiltered.NetworkType; + _NBXplorerNetworkProvider = new NBXplorerNetworkProvider(unfiltered.NetworkType); + _Networks = new Dictionary(); cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray(); - foreach (var network in filtered._Networks) + foreach (var network in unfiltered._Networks) { if(cryptoCodes.Contains(network.Key)) { @@ -40,9 +38,12 @@ namespace BTCPayServer } } + public BTCPayNetworkProvider UnfilteredNetworks { get; } + public NetworkType NetworkType { get; private set; } public BTCPayNetworkProvider(NetworkType networkType) { + UnfilteredNetworks = this; _NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType); NetworkType = networkType; InitBitcoin(); @@ -56,6 +57,22 @@ namespace BTCPayServer InitGroestlcoin(); InitViacoin(); + // Assume that electrum mappings are same as BTC if not specified + foreach (var network in _Networks.Values.OfType()) + { + if(network.ElectrumMapping.Count == 0) + { + network.ElectrumMapping = GetNetwork("BTC").ElectrumMapping; + if (!network.NBitcoinNetwork.Consensus.SupportSegwit) + { + network.ElectrumMapping = + network.ElectrumMapping + .Where(kv => kv.Value == DerivationType.Legacy) + .ToDictionary(k => k.Key, k => k.Value); + } + } + } + // Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586 //InitPolis(); //InitBitcoinplus(); @@ -73,20 +90,14 @@ namespace BTCPayServer } [Obsolete("To use only for legacy stuff")] - public BTCPayNetwork BTC - { - get - { - return GetNetwork("BTC"); - } - } + public BTCPayNetwork BTC => GetNetwork("BTC"); - public void Add(BTCPayNetwork network) + public void Add(BTCPayNetworkBase network) { _Networks.Add(network.CryptoCode.ToUpperInvariant(), network); } - public IEnumerable GetAll() + public IEnumerable GetAll() { return _Networks.Values.ToArray(); } @@ -95,15 +106,20 @@ namespace BTCPayServer { return _Networks.ContainsKey(cryptoCode.ToUpperInvariant()); } - - public BTCPayNetwork GetNetwork(string cryptoCode) + public BTCPayNetworkBase GetNetwork(string cryptoCode) { - if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network)) + return GetNetwork(cryptoCode); + } + public T GetNetwork(string cryptoCode) where T: BTCPayNetworkBase + { + if (cryptoCode == null) + throw new ArgumentNullException(nameof(cryptoCode)); + if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetworkBase network)) { if (cryptoCode == "XBT") - return GetNetwork("BTC"); + return GetNetwork("BTC"); } - return network; + return network as T; } } } diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj new file mode 100644 index 000000000..488f0f888 --- /dev/null +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/BTCPayServer/CustomThreadPool.cs b/BTCPayServer.Common/CustomThreadPool.cs similarity index 98% rename from BTCPayServer/CustomThreadPool.cs rename to BTCPayServer.Common/CustomThreadPool.cs index 72792624f..cfa09a52a 100644 --- a/BTCPayServer/CustomThreadPool.cs +++ b/BTCPayServer.Common/CustomThreadPool.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace BTCPayServer { - class CustomThreadPool : IDisposable + public class CustomThreadPool : IDisposable { CancellationTokenSource _Cancel = new CancellationTokenSource(); TaskCompletionSource _Exited; diff --git a/BTCPayServer.Common/Extensions.cs b/BTCPayServer.Common/Extensions.cs new file mode 100644 index 000000000..6fd3b0025 --- /dev/null +++ b/BTCPayServer.Common/Extensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BTCPayServer +{ + public static class UtilitiesExtensions + { + public static void AddRange(this HashSet hashSet, IEnumerable items) + { + foreach (var item in items) + { + hashSet.Add(item); + } + } + } +} diff --git a/BTCPayServer/Logging/ConsoleLogger.cs b/BTCPayServer.Common/Logging/ConsoleLogger.cs similarity index 100% rename from BTCPayServer/Logging/ConsoleLogger.cs rename to BTCPayServer.Common/Logging/ConsoleLogger.cs diff --git a/BTCPayServer/Logging/Logs.cs b/BTCPayServer.Common/Logging/Logs.cs similarity index 100% rename from BTCPayServer/Logging/Logs.cs rename to BTCPayServer.Common/Logging/Logs.cs diff --git a/BTCPayServer/MultiValueDictionary.cs b/BTCPayServer.Common/MultiValueDictionary.cs similarity index 100% rename from BTCPayServer/MultiValueDictionary.cs rename to BTCPayServer.Common/MultiValueDictionary.cs diff --git a/BTCPayServer/SynchronizationContextRemover.cs b/BTCPayServer.Common/SynchronizationContextRemover.cs similarity index 100% rename from BTCPayServer/SynchronizationContextRemover.cs rename to BTCPayServer.Common/SynchronizationContextRemover.cs diff --git a/BTCPayServer/ZipUtils.cs b/BTCPayServer.Common/ZipUtils.cs similarity index 97% rename from BTCPayServer/ZipUtils.cs rename to BTCPayServer.Common/ZipUtils.cs index 6e89ec9aa..90eb42b5a 100644 --- a/BTCPayServer/ZipUtils.cs +++ b/BTCPayServer.Common/ZipUtils.cs @@ -6,7 +6,7 @@ using System.Text; namespace BTCPayServer { - class ZipUtils + public class ZipUtils { public static byte[] Zip(string unzipped) { diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj new file mode 100644 index 000000000..68b99d6bd --- /dev/null +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -0,0 +1,24 @@ + + + + + netcoreapp2.1 + 7.3 + + + + + + + + + + + + + + + + + + diff --git a/BTCPayServer/Rating/CurrencyPair.cs b/BTCPayServer.Rating/CurrencyPair.cs similarity index 97% rename from BTCPayServer/Rating/CurrencyPair.cs rename to BTCPayServer.Rating/CurrencyPair.cs index adeacd603..af4d3ab96 100644 --- a/BTCPayServer/Rating/CurrencyPair.cs +++ b/BTCPayServer.Rating/CurrencyPair.cs @@ -53,7 +53,7 @@ namespace BTCPayServer.Rating for (int i = 3; i < 5; i++) { var potentialCryptoName = currencyPair.Substring(0, i); - var network = _NetworkProvider.GetNetwork(potentialCryptoName); + var network = _NetworkProvider.GetNetwork(potentialCryptoName); if (network != null) { value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i)); diff --git a/BTCPayServer/Rating/ExchangeRates.cs b/BTCPayServer.Rating/ExchangeRates.cs similarity index 100% rename from BTCPayServer/Rating/ExchangeRates.cs rename to BTCPayServer.Rating/ExchangeRates.cs diff --git a/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs similarity index 99% rename from BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs rename to BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs index a88a8a167..b923e4839 100644 --- a/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.ExceptionServices; using System.Threading.Tasks; -using BTCPayServer.Data; -using BTCPayServer.Logging; using BTCPayServer.Rating; using System.Threading; +using Microsoft.Extensions.Logging.Abstractions; +using BTCPayServer.Logging; namespace BTCPayServer.Services.Rates { @@ -39,6 +39,7 @@ namespace BTCPayServer.Services.Rates } IRateProvider _Inner; + public BackgroundFetcherRateProvider(IRateProvider inner) { if (inner == null) diff --git a/BTCPayServer/Services/Rates/BitbankRateProvider.cs b/BTCPayServer.Rating/Providers/BitbankRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/BitbankRateProvider.cs rename to BTCPayServer.Rating/Providers/BitbankRateProvider.cs diff --git a/BTCPayServer/Services/Rates/BitpayRateProvider.cs b/BTCPayServer.Rating/Providers/BitpayRateProvider.cs similarity index 98% rename from BTCPayServer/Services/Rates/BitpayRateProvider.cs rename to BTCPayServer.Rating/Providers/BitpayRateProvider.cs index bb8ead359..15d67dca4 100644 --- a/BTCPayServer/Services/Rates/BitpayRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BitpayRateProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using NBitcoin; using BTCPayServer.Rating; using System.Threading; diff --git a/BTCPayServer/Services/Rates/ByllsRateProvider.cs b/BTCPayServer.Rating/Providers/ByllsRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/ByllsRateProvider.cs rename to BTCPayServer.Rating/Providers/ByllsRateProvider.cs diff --git a/BTCPayServer/Services/Rates/CachedRateProvider.cs b/BTCPayServer.Rating/Providers/CachedRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/CachedRateProvider.cs rename to BTCPayServer.Rating/Providers/CachedRateProvider.cs diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/CoinAverageRateProvider.cs rename to BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer.Rating/Providers/CoinAverageSettings.cs similarity index 100% rename from BTCPayServer/Services/Rates/CoinAverageSettings.cs rename to BTCPayServer.Rating/Providers/CoinAverageSettings.cs diff --git a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs b/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs rename to BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs diff --git a/BTCPayServer/Services/Rates/FallbackRateProvider.cs b/BTCPayServer.Rating/Providers/FallbackRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/FallbackRateProvider.cs rename to BTCPayServer.Rating/Providers/FallbackRateProvider.cs diff --git a/BTCPayServer/Services/Rates/IHasExchangeName.cs b/BTCPayServer.Rating/Providers/IHasExchangeName.cs similarity index 100% rename from BTCPayServer/Services/Rates/IHasExchangeName.cs rename to BTCPayServer.Rating/Providers/IHasExchangeName.cs diff --git a/BTCPayServer/Services/Rates/IRateProvider.cs b/BTCPayServer.Rating/Providers/IRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/IRateProvider.cs rename to BTCPayServer.Rating/Providers/IRateProvider.cs diff --git a/BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs b/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs rename to BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs diff --git a/BTCPayServer/Services/Rates/NullRateProvider.cs b/BTCPayServer.Rating/Providers/NullRateProvider.cs similarity index 100% rename from BTCPayServer/Services/Rates/NullRateProvider.cs rename to BTCPayServer.Rating/Providers/NullRateProvider.cs diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer.Rating/RateRules.cs similarity index 100% rename from BTCPayServer/Rating/RateRules.cs rename to BTCPayServer.Rating/RateRules.cs diff --git a/BTCPayServer/Services/Rates/RateFetcher.cs b/BTCPayServer.Rating/Services/RateFetcher.cs similarity index 100% rename from BTCPayServer/Services/Rates/RateFetcher.cs rename to BTCPayServer.Rating/Services/RateFetcher.cs diff --git a/BTCPayServer/Services/Rates/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs similarity index 99% rename from BTCPayServer/Services/Rates/RateProviderFactory.cs rename to BTCPayServer.Rating/Services/RateProviderFactory.cs index 5a57314ed..f23ed52ab 100644 --- a/BTCPayServer/Services/Rates/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using BTCPayServer.Rating; using ExchangeSharp; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BTCPayServer.Services.Rates diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 7da4b0661..3ff6203eb 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -11,6 +11,8 @@ + + all @@ -31,6 +33,7 @@ + diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 3eb198a8a..3eb559a31 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -33,9 +33,12 @@ using System.Security.Claims; using System.Security.Principal; using System.Text; using System.Threading; +using AspNet.Security.OpenIdConnect.Primitives; using Xunit; using BTCPayServer.Services; using System.Net.Http; +using Microsoft.AspNetCore.Hosting.Server.Features; +using System.Threading.Tasks; namespace BTCPayServer.Tests { @@ -102,12 +105,16 @@ namespace BTCPayServer.Tests StringBuilder config = new StringBuilder(); config.AppendLine($"{chain.ToLowerInvariant()}=1"); + if (InContainer) + { + config.AppendLine($"bind=0.0.0.0"); + } config.AppendLine($"port={Port}"); config.AppendLine($"chains=btc,ltc"); config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}"); config.AppendLine($"btc.explorer.cookiefile=0"); - + config.AppendLine("allow-admin-registration=1"); config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}"); config.AppendLine($"ltc.explorer.cookiefile=0"); config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}"); @@ -142,13 +149,17 @@ namespace BTCPayServer.Tests .UseStartup() .Build(); _Host.Start(); + + var urls = _Host.ServerFeatures.Get().Addresses; + foreach (var url in urls) + { + Logs.Tester.LogInformation("Listening on " + url); + } + Logs.Tester.LogInformation("Server URI " + ServerUri); + InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository)); - var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard)); - while(!dashBoard.IsFullySynched()) - { - Thread.Sleep(10); - } + Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider)); if (MockRates) { @@ -209,6 +220,35 @@ namespace BTCPayServer.Tests }); rateProvider.Providers.Add("bittrex", bittrex); } + + + + WaitSiteIsOperational().GetAwaiter().GetResult(); + } + + private async Task WaitSiteIsOperational() + { + var synching = WaitIsFullySynched(); + var accessingHomepage = WaitCanAccessHomepage(); + await Task.WhenAll(synching, accessingHomepage).ConfigureAwait(false); + } + + private async Task WaitCanAccessHomepage() + { + var resp = await HttpClient.GetAsync("/").ConfigureAwait(false); + while (resp.StatusCode != HttpStatusCode.OK) + { + await Task.Delay(10).ConfigureAwait(false); + } + } + + private async Task WaitIsFullySynched() + { + var dashBoard = GetService(); + while (!dashBoard.IsFullySynched()) + { + await Task.Delay(10).ConfigureAwait(false); + } } private string FindBTCPayServerDirectory() @@ -226,6 +266,7 @@ namespace BTCPayServer.Tests } public InvoiceRepository InvoiceRepository { get; private set; } public StoreRepository StoreRepository { get; private set; } + public BTCPayNetworkProvider Networks { get; private set; } public Uri IntegratedLightning { get; internal set; } public bool InContainer { get; internal set; } @@ -234,6 +275,8 @@ namespace BTCPayServer.Tests return _Host.Services.GetRequiredService(); } + public IServiceProvider ServiceProvider => _Host.Services; + public T GetController(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller { var context = new DefaultHttpContext(); @@ -243,7 +286,7 @@ namespace BTCPayServer.Tests if (userId != null) { List claims = new List(); - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId)); + claims.Add(new Claim(OpenIdConnectConstants.Claims.Subject, userId)); if (additionalClaims != null) claims.AddRange(additionalClaims); context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication)); diff --git a/BTCPayServer.Tests/ChangellyTests.cs b/BTCPayServer.Tests/ChangellyTests.cs index 94fbe6bed..ef94711c3 100644 --- a/BTCPayServer.Tests/ChangellyTests.cs +++ b/BTCPayServer.Tests/ChangellyTests.cs @@ -27,7 +27,7 @@ namespace BTCPayServer.Tests Logs.LogProvider = new XUnitLogProvider(helper); } - [Fact] + [Fact(Timeout = 60000)] [Trait("Integration", "Integration")] public async void CanSetChangellyPaymentMethod() { diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index 4be53427c..0def4b5a7 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -5,10 +5,24 @@ ENV LC_ALL en_US.UTF-8 ENV LANG en_US.UTF-8 WORKDIR /source +COPY Common.csproj Common.csproj COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj +COPY BTCPayServer.Common/BTCPayServer.Common.csproj BTCPayServer.Common/BTCPayServer.Common.csproj +COPY BTCPayServer.Rating/BTCPayServer.Rating.csproj BTCPayServer.Rating/BTCPayServer.Rating.csproj COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj + +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false + +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 + +RUN apk add --no-cache chromium chromium-chromedriver icu-libs + +ENV SCREEN_HEIGHT 600 \ + SCREEN_WIDTH 1200 + COPY . . -RUN dotnet build +RUN cd BTCPayServer.Tests && dotnet build WORKDIR /source/BTCPayServer.Tests ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/BTCPayServer.Tests/Extensions.cs b/BTCPayServer.Tests/Extensions.cs new file mode 100644 index 000000000..508719434 --- /dev/null +++ b/BTCPayServer.Tests/Extensions.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Tests.Logging; +using Microsoft.AspNetCore.Mvc; +using OpenQA.Selenium; +using Xunit; + +namespace BTCPayServer.Tests +{ + public static class Extensions + { + public static void ScrollTo(this IWebDriver driver, By by) + { + var element = driver.FindElement(by); + ((IJavaScriptExecutor)driver).ExecuteScript($"window.scrollBy({element.Location.X},{element.Location.Y});"); + } + /// + /// Sometimes the chrome driver is fucked up and we need some magic to click on the element. + /// + /// + public static void ForceClick(this IWebElement element) + { + element.SendKeys(Keys.Return); + } + public static void AssertNoError(this IWebDriver driver) + { + try + { + Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand"))); + } + catch + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine(); + foreach (var logKind in new []{ LogType.Browser, LogType.Client, LogType.Driver, LogType.Server }) + { + try + { + var logs = driver.Manage().Logs.GetLog(logKind); + builder.AppendLine($"Selenium [{logKind}]:"); + foreach (var entry in logs) + { + builder.AppendLine($"[{entry.Level}]: {entry.Message}"); + } + } + catch { } + builder.AppendLine($"---------"); + } + Logs.Tester.LogInformation(builder.ToString()); + builder = new StringBuilder(); + builder.AppendLine($"Selenium [Sources]:"); + builder.AppendLine(driver.PageSource); + builder.AppendLine($"---------"); + Logs.Tester.LogInformation(builder.ToString()); + throw; + } + } + public static T AssertViewModel(this IActionResult result) + { + Assert.NotNull(result); + var vr = Assert.IsType(result); + return Assert.IsType(vr.Model); + } + public static async Task AssertViewModelAsync(this Task task) + { + var result = await task; + Assert.NotNull(result); + var vr = Assert.IsType(result); + return Assert.IsType(vr.Model); + } + } +} diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs new file mode 100644 index 000000000..604b2bee1 --- /dev/null +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Controllers; +using BTCPayServer.Models.WalletViewModels; +using BTCPayServer.Tests.Logging; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitpayClient; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class PSBTTests + { + public PSBTTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; + Logs.LogProvider = new XUnitLogProvider(helper); + } + [Fact] + [Trait("Integration", "Integration")] + public async Task CanPlayWithPSBT() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 10, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some \", description", + FullNotifications = true + }, Facade.Merchant); + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m)); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal("paid", invoice.Status); + }); + + var walletController = tester.PayTester.GetController(user.UserId); + var walletId = new WalletId(user.StoreId, "BTC"); + var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(); + var sendModel = new WalletSendModel() + { + Outputs = new List() + { + new WalletSendModel.TransactionOutput() + { + DestinationAddress = sendDestination, + Amount = 0.1m, + } + }, + FeeSatoshiPerByte = 1, + CurrentBalance = 1.5m + }; + var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync(); + PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork); + BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork); + Assert.NotNull(vmLedger.SuccessPath); + Assert.NotNull(vmLedger.WebsocketPath); + + var redirectedPSBT = (string)Assert.IsType(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"]; + var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync(); + var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); + Assert.NotNull(vmPSBT.Decoded); + + var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt")); + PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); + + await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync(); + var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); + Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT); + + var signedPSBT = unsignedPSBT.Clone(); + signedPSBT.SignAll(user.DerivationScheme, user.ExtKey); + vmPSBT.PSBT = signedPSBT.ToBase64(); + var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination + Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); + Assert.Contains(psbtReady.Destinations, d => d.Positive); + var redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast")); + Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); + + vmPSBT.PSBT = unsignedPSBT.ToBase64(); + var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync(); + Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT); + combineVM.PSBT = signedPSBT.ToBase64(); + vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync(); + + var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); + Assert.True(signedPSBT.TryFinalize(out _)); + Assert.True(signedPSBT2.TryFinalize(out _)); + Assert.Equal(signedPSBT, signedPSBT2); + + // Can use uploaded file? + combineVM.PSBT = null; + combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); + vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync(); + signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); + Assert.True(signedPSBT.TryFinalize(out _)); + Assert.True(signedPSBT2.TryFinalize(out _)); + Assert.Equal(signedPSBT, signedPSBT2); + + var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel(); + Assert.Equal(signedPSBT.ToBase64(), ready.PSBT); + redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt")); + Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]); + redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); + Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); + } + } + } +} diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs index 36329cc10..651a9e4ac 100644 --- a/BTCPayServer.Tests/PaymentRequestTests.cs +++ b/BTCPayServer.Tests/PaymentRequestTests.cs @@ -153,5 +153,64 @@ namespace BTCPayServer.Tests } } + + [Fact(Timeout = 60 * 2 * 1000)] + [Trait("Integration", "Integration")] + public async Task CanCancelPaymentWhenPossible() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + + var paymentRequestController = user.GetController(); + + + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false)); + + + var request = new UpdatePaymentRequestViewModel() + { + Title = "original juice", + Currency = "BTC", + Amount = 1, + StoreId = user.StoreId, + Description = "description" + }; + var response = Assert + .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) + .RouteValues.First(); + + var paymentRequestId = response.Value.ToString(); + + var invoiceId = Assert + .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value + .ToString(); + + var actionResult = Assert + .IsType(await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + + Assert.Equal("Checkout", actionResult.ActionName); + Assert.Equal("Invoice", actionResult.ControllerName); + Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); + + var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); +Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status); + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); + + invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); + Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status); + + + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); + + + } + } } } diff --git a/BTCPayServer.Tests/README.md b/BTCPayServer.Tests/README.md index 29521580e..8ea5b0c25 100644 --- a/BTCPayServer.Tests/README.md +++ b/BTCPayServer.Tests/README.md @@ -41,10 +41,15 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i ``` 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. diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs new file mode 100644 index 000000000..ac2343a0c --- /dev/null +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -0,0 +1,143 @@ +using System; +using BTCPayServer; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using NBitcoin; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using Xunit; +using System.IO; +using BTCPayServer.Tests.Logging; +using System.Threading; + +namespace BTCPayServer.Tests +{ + public class SeleniumTester : IDisposable + { + public IWebDriver Driver { get; set; } + public ServerTester Server { get; set; } + + public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null) + { + var server = ServerTester.Create(scope); + return new SeleniumTester() + { + Server = server + }; + } + + public void Start() + { + Server.Start(); + ChromeOptions options = new ChromeOptions(); + options.AddArguments("headless"); // Comment to view browser + options.AddArguments("window-size=1200x600"); // Comment to view browser + options.AddArgument("shm-size=2g"); + if (Server.PayTester.InContainer) + { + options.AddArgument("no-sandbox"); + } + Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options); + Logs.Tester.LogInformation("Selenium: Using chrome driver"); + Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri); + Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}"); + Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); + Driver.Navigate().GoToUrl(Server.PayTester.ServerUri); + Driver.AssertNoError(); + } + + public string Link(string relativeLink) + { + return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash(); + } + + public string RegisterNewUser(bool isAdmin = false) + { + var usr = RandomUtils.GetUInt256().ToString() + "@a.com"; + Driver.FindElement(By.Id("Register")).Click(); + Driver.FindElement(By.Id("Email")).SendKeys(usr); + Driver.FindElement(By.Id("Password")).SendKeys("123456"); + Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); + if (isAdmin) + Driver.FindElement(By.Id("IsAdmin")).Click(); + Driver.FindElement(By.Id("RegisterButton")).Click(); + Driver.AssertNoError(); + return usr; + } + + public string CreateNewStore() + { + var usr = "Store" + RandomUtils.GetUInt64().ToString(); + Driver.FindElement(By.Id("Stores")).Click(); + Driver.FindElement(By.Id("CreateStore")).Click(); + Driver.FindElement(By.Id("Name")).SendKeys(usr); + Driver.FindElement(By.Id("Create")).Click(); + return usr; + } + + public void AddDerivationScheme(string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]") + { + Driver.FindElement(By.Id("ModifyBTC")).ForceClick(); + Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme); + Driver.FindElement(By.Id("Continue")).ForceClick(); + Driver.FindElement(By.Id("Confirm")).ForceClick(); + Driver.FindElement(By.Id("Save")).ForceClick(); + return; + } + + public void ClickOnAllSideMenus() + { + var links = Driver.FindElements(By.CssSelector(".nav-pills .nav-link")).Select(c => c.GetAttribute("href")).ToList(); + Driver.AssertNoError(); + Assert.NotEmpty(links); + foreach (var l in links) + { + Driver.Navigate().GoToUrl(l); + Driver.AssertNoError(); + } + } + + public void CreateInvoice(string random) + { + Driver.FindElement(By.Id("Invoices")).Click(); + Driver.FindElement(By.Id("CreateNewInvoice")).Click(); + Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100"); + Driver.FindElement(By.Name("StoreId")).SendKeys("Deriv" + random + Keys.Enter); + Driver.FindElement(By.Id("Create")).Click(); + return; + } + + + public void Dispose() + { + if (Driver != null) + { + try + { + Driver.Close(); + } + catch { } + Driver.Dispose(); + } + if (Server != null) + Server.Dispose(); + } + + internal void AssertNotFound() + { + Assert.Contains("Status Code: 404; Not Found", Driver.PageSource); + } + + internal void GoToHome() + { + Driver.Navigate().GoToUrl(Server.PayTester.ServerUri); + } + + internal void Logout() + { + Driver.FindElement(By.Id("Logout")).Click(); + } + } +} diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs new file mode 100644 index 000000000..9f347d890 --- /dev/null +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -0,0 +1,323 @@ +using System; +using Xunit; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using BTCPayServer.Tests.Logging; +using Xunit.Abstractions; +using OpenQA.Selenium.Interactions; +using System.Linq; +using NBitcoin; + +namespace BTCPayServer.Tests +{ + [Trait("Selenium", "Selenium")] + public class ChromeTests + { + public ChromeTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + public void CanNavigateServerSettings() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(true); + s.Driver.FindElement(By.Id("ServerSettings")).Click(); + s.Driver.AssertNoError(); + s.ClickOnAllSideMenus(); + s.Driver.Quit(); + } + } + + [Fact] + public void NewUserLogin() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + //Register & Log Out + var email = s.RegisterNewUser(); + s.Driver.FindElement(By.Id("Logout")).Click(); + s.Driver.AssertNoError(); + s.Driver.FindElement(By.Id("Login")).Click(); + s.Driver.AssertNoError(); + + s.Driver.Navigate().GoToUrl(s.Link("/invoices")); + Assert.Contains("ReturnUrl=%2Finvoices", s.Driver.Url); + + // We should be redirected to login + //Same User Can Log Back In + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); + + // We should be redirected to invoice + Assert.EndsWith("/invoices", s.Driver.Url); + + // Should not be able to reach server settings + s.Driver.Navigate().GoToUrl(s.Link("/server/users")); + Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url); + + //Change Password & Log Out + s.Driver.FindElement(By.Id("MySettings")).Click(); + s.Driver.FindElement(By.Id("ChangePassword")).Click(); + s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456"); + s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("UpdatePassword")).Click(); + s.Driver.FindElement(By.Id("Logout")).Click(); + s.Driver.AssertNoError(); + + //Log In With New Password + s.Driver.FindElement(By.Id("Login")).Click(); + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); + Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores"); + + s.Driver.FindElement(By.Id("MySettings")).Click(); + s.ClickOnAllSideMenus(); + + s.Driver.Quit(); + } + } + + private static void LogIn(SeleniumTester s, string email) + { + s.Driver.FindElement(By.Id("Login")).Click(); + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); + s.Driver.AssertNoError(); + } + + [Fact] + public void CanCreateStores() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + var alice = s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.AddDerivationScheme(); + s.Driver.AssertNoError(); + Assert.Contains(store, s.Driver.PageSource); + var storeUrl = s.Driver.Url; + s.ClickOnAllSideMenus(); + + CreateInvoice(s, store); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + var invoiceUrl = s.Driver.Url; + + // When logout we should not be able to access store and invoice details + s.Driver.FindElement(By.Id("Logout")).Click(); + s.Driver.Navigate().GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.Driver.Navigate().GoToUrl(invoiceUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + + // When logged we should not be able to access store and invoice details + var bob = s.RegisterNewUser(); + s.Driver.Navigate().GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.Driver.Navigate().GoToUrl(invoiceUrl); + s.AssertNotFound(); + s.GoToHome(); + s.Logout(); + + // Let's add Bob as a guest to alice's store + LogIn(s, alice); + s.Driver.Navigate().GoToUrl(storeUrl + "/users"); + s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter); + Assert.Contains("User added successfully", s.Driver.PageSource); + s.Logout(); + + // Bob should not have access to store, but should have access to invoice + LogIn(s, bob); + s.Driver.Navigate().GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.Driver.Navigate().GoToUrl(invoiceUrl); + s.Driver.AssertNoError(); + } + } + + [Fact] + public void CanCreateInvoice() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.AddDerivationScheme(); + + CreateInvoice(s, store); + + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + s.Driver.Quit(); + } + } + + private static void CreateInvoice(SeleniumTester s, string store) + { + s.Driver.FindElement(By.Id("Invoices")).Click(); + s.Driver.FindElement(By.Id("CreateNewInvoice")).Click(); + s.Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100"); + s.Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter); + s.Driver.FindElement(By.Id("Create")).Click(); + Assert.True(s.Driver.PageSource.Contains("just created!"), "Unable to create Invoice"); + } + + [Fact] + public void CanCreateAppPoS() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + + s.Driver.FindElement(By.Id("Apps")).Click(); + s.Driver.FindElement(By.Id("CreateNewApp")).Click(); + s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + store); + s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("PointOfSale" + Keys.Enter); + s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter); + s.Driver.FindElement(By.Id("Create")).Click(); + s.Driver.FindElement(By.CssSelector("input#EnableShoppingCart.form-check")).Click(); + s.Driver.FindElement(By.Id("SaveSettings")).ForceClick(); + Assert.True(s.Driver.PageSource.Contains("App updated"), "Unable to create PoS"); + s.Driver.Quit(); + } + } + + [Fact] + public void CanCreateAppCF() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.AddDerivationScheme(); + + s.Driver.FindElement(By.Id("Apps")).Click(); + s.Driver.FindElement(By.Id("CreateNewApp")).Click(); + s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + store); + s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("Crowdfund" + Keys.Enter); + s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter); + s.Driver.FindElement(By.Id("Create")).Click(); + s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter"); + s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC"); + s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY"); + s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700"); + s.Driver.FindElement(By.Id("SaveSettings")).Submit(); + s.Driver.FindElement(By.Id("ViewApp")).ForceClick(); + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF"); + s.Driver.Quit(); + } + } + + [Fact] + public void CanCreatePayRequest() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme(); + + s.Driver.FindElement(By.Id("PaymentRequests")).Click(); + s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click(); + s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123"); + s.Driver.FindElement(By.Id("Amount")).SendKeys("700"); + s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); + s.Driver.FindElement(By.Id("SaveButton")).Submit(); + s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return); + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request"); + s.Driver.Quit(); + } + } + + [Fact] + public void CanManageWallet() + { + using (var s = SeleniumTester.Create()) + { + s.Start(); + s.RegisterNewUser(); + s.CreateNewStore(); + + // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed + // to sign the transaction + var 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"; + var root = new Mnemonic(mnemonic).DeriveExtKey(); + s.AddDerivationScheme("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD"); + var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m)); + s.Server.ExplorerNode.Generate(1); + + s.Driver.FindElement(By.Id("Wallets")).Click(); + s.Driver.FindElement(By.LinkText("Manage")).Click(); + + s.ClickOnAllSideMenus(); + + // We setup the fingerprint and the account key path + s.Driver.FindElement(By.Id("WalletSettings")).ForceClick(); + s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160"); + s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter); + + // Check the tx sent earlier arrived + s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick(); + var walletTransactionLink = s.Driver.Url; + Assert.Contains(tx.ToString(), s.Driver.PageSource); + + + void SignWith(string signingSource) + { + // Send to bob + s.Driver.FindElement(By.Id("WalletSend")).Click(); + var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(0, bob, 1); + s.Driver.ScrollTo(By.Id("SendMenu")); + s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click(); + + // Input the seed + s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter); + + // Broadcast + Assert.Contains(bob.ToString(), s.Driver.PageSource); + Assert.Contains("1.00000000", s.Driver.PageSource); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + Assert.Equal(walletTransactionLink, s.Driver.Url); + } + + void SetTransactionOutput(int index, BitcoinAddress dest, decimal amount, bool subtract = false) + { + s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString()); + var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount")); + amountElement.Clear(); + amountElement.SendKeys(amount.ToString()); + var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput")); + if (checkboxElement.Selected != subtract) + { + checkboxElement.Click(); + } + } + SignWith(mnemonic); + var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString(); + SignWith(accountKey); + } + } + } +} diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 0b75db473..433625dcb 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -44,13 +44,14 @@ namespace BTCPayServer.Tests Directory.CreateDirectory(_Directory); NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); - ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); - LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); + ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); + ExplorerNode.ScanRPCCapabilities(); + LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); - ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/"))); - LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/"))); + ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/"))); + LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/"))); - var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork; + var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork; CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc); MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc); diff --git a/BTCPayServer.Tests/StorageTests.cs b/BTCPayServer.Tests/StorageTests.cs index 6360a7834..b2f07dc7f 100644 --- a/BTCPayServer.Tests/StorageTests.cs +++ b/BTCPayServer.Tests/StorageTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using BTCPayServer.Controllers; @@ -9,6 +10,7 @@ using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; using BTCPayServer.Storage.ViewModels; using BTCPayServer.Tests.Logging; +using DBriize.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc; @@ -38,15 +40,6 @@ namespace BTCPayServer.Tests user.GrantAccess(); var controller = tester.PayTester.GetController(user.UserId, user.StoreId); -// //For some reason, the tests cache something on circleci and this is set by default -// //Initially, there is no configuration, make sure we display the choices available to configure -// Assert.IsType(Assert.IsType(await controller.Storage()).Model); -// -// //the file list should tell us it's not configured: -// var viewFilesViewModelInitial = -// Assert.IsType(Assert.IsType(await controller.Files()).Model); -// Assert.False(viewFilesViewModelInitial.StorageConfigured); - //Once we select a provider, redirect to its view var localResult = Assert @@ -190,25 +183,13 @@ namespace BTCPayServer.Tests private async Task CanUploadRemoveFiles(ServerController controller) { - var filename = "uploadtestfile.txt"; var fileContent = "content"; - File.WriteAllText(filename, fileContent); - - var fileInfo = new FileInfo(filename); - var formFile = new FormFile( - new FileStream(filename, FileMode.OpenOrCreate), - 0, - fileInfo.Length, fileInfo.Name, fileInfo.Name) - { - Headers = new HeaderDictionary() - }; - formFile.ContentType = "text/plain"; - formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\""; - var uploadFormFileResult = Assert.IsType(await controller.CreateFile(formFile)); + var uploadFormFileResult = Assert.IsType(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent))); Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId")); var fileId = uploadFormFileResult.RouteValues["fileId"].ToString(); Assert.Equal("Files", uploadFormFileResult.ActionName); + //check if file was uploaded and saved in db var viewFilesViewModel = Assert.IsType(Assert.IsType(await controller.Files(fileId)).Model); @@ -216,21 +197,48 @@ namespace BTCPayServer.Tests Assert.Equal(fileId, viewFilesViewModel.SelectedFileId); Assert.NotEmpty(viewFilesViewModel.DirectFileUrl); - + + //verify file is available and the same var net = new System.Net.WebClient(); var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl)); Assert.Equal(fileContent, data); - + + //create a temporary link to file + var tmpLinkGenerate = Assert.IsType(await controller.CreateTemporaryFileUrl(fileId, + new ServerController.CreateTemporaryFileUrlViewModel() + { + IsDownload = true, + TimeAmount = 1, + TimeType = ServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes + })); + Assert.True(tmpLinkGenerate.RouteValues.ContainsKey("StatusMessage")); + var statusMessageModel = new StatusMessageModel(tmpLinkGenerate.RouteValues["StatusMessage"].ToString()); + Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity); + var index = statusMessageModel.Html.IndexOf("target='_blank'>"); + var url = statusMessageModel.Html.Substring(index).ReplaceMultiple(new Dictionary() + { + {"", string.Empty}, {"target='_blank'>", string.Empty} + }); + //verify tmpfile is available and the same + data = await net.DownloadStringTaskAsync(new Uri(url)); + Assert.Equal(fileContent, data); + + + //delete file Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert .IsType(await controller.DeleteFile(fileId)) .RouteValues["statusMessage"].ToString()).Severity); - + + //attempt to fetch deleted file viewFilesViewModel = Assert.IsType(Assert.IsType(await controller.Files(fileId)).Model); Assert.Null(viewFilesViewModel.DirectFileUrl); Assert.Null(viewFilesViewModel.SelectedFileId); } + + + diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 5ef6b0b08..e93a18b3f 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -95,7 +95,7 @@ namespace BTCPayServer.Tests } public async Task RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false) { - SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); + SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); var store = parent.PayTester.GetController(UserId, StoreId); ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]")); @@ -113,15 +113,18 @@ namespace BTCPayServer.Tests private async Task RegisterAsync() { var account = parent.PayTester.GetController(); - await account.Register(new RegisterViewModel() + RegisterDetails = new RegisterViewModel() { Email = Guid.NewGuid() + "@toto.com", ConfirmPassword = "Kitten0@", Password = "Kitten0@", - }); + }; + await account.Register(RegisterDetails); UserId = account.RegisteredUserId; } + public RegisterViewModel RegisterDetails{ get; set; } + public Bitpay BitPay { get; set; diff --git a/BTCPayServer.Tests/TestUtils.cs b/BTCPayServer.Tests/TestUtils.cs new file mode 100644 index 000000000..800865f83 --- /dev/null +++ b/BTCPayServer.Tests/TestUtils.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Xunit.Sdk; + +namespace BTCPayServer.Tests +{ + public static class TestUtils + { + public static FormFile GetFormFile(string filename, string content) + { + File.WriteAllText(filename, content); + + var fileInfo = new FileInfo(filename); + FormFile formFile = new FormFile( + new FileStream(filename, FileMode.OpenOrCreate), + 0, + fileInfo.Length, fileInfo.Name, fileInfo.Name) + { + Headers = new HeaderDictionary() + }; + formFile.ContentType = "text/plain"; + formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\""; + return formFile; + } + public static FormFile GetFormFile(string filename, byte[] content) + { + File.WriteAllBytes(filename, content); + + var fileInfo = new FileInfo(filename); + FormFile formFile = new FormFile( + new FileStream(filename, FileMode.OpenOrCreate), + 0, + fileInfo.Length, fileInfo.Name, fileInfo.Name) + { + Headers = new HeaderDictionary() + }; + formFile.ContentType = "application/octet-stream"; + formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\""; + return formFile; + } + public static void Eventually(Action act) + { + CancellationTokenSource cts = new CancellationTokenSource(20000); + while (true) + { + try + { + act(); + break; + } + catch (XunitException) when (!cts.Token.IsCancellationRequested) + { + cts.Token.WaitHandle.WaitOne(500); + } + } + } + + public static async Task EventuallyAsync(Func act) + { + CancellationTokenSource cts = new CancellationTokenSource(20000); + while (true) + { + try + { + await act(); + break; + } + catch (XunitException) when (!cts.Token.IsCancellationRequested) + { + await Task.Delay(500); + } + } + } + } +} diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 3bf6ee678..6e778061d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -56,6 +56,12 @@ using BTCPayServer.Configuration; using System.Security; using System.Runtime.CompilerServices; using System.Net; +using BTCPayServer.Models.AccountViewModels; +using BTCPayServer.Services.U2F.Models; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NBXplorer.DerivationStrategy; namespace BTCPayServer.Tests { @@ -93,56 +99,68 @@ namespace BTCPayServer.Tests [Trait("Fast", "Fast")] public void CanCalculateCryptoDue2() { - var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString(); #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), + new LightningLikePaymentHandler(null, null, networkProvider, null), + }); InvoiceEntity invoiceEntity = new InvoiceEntity(); invoiceEntity.Payments = new System.Collections.Generic.List(); - invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 }; + invoiceEntity.ProductInformation = new ProductInformation() {Price = 100}; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); - paymentMethods.Add(new PaymentMethod() - { - CryptoCode = "BTC", - Rate = 10513.44m, - }.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() - { - NextNetworkFee = Money.Coins(0.00000100m), - DepositAddress = dummy - })); - paymentMethods.Add(new PaymentMethod() - { - CryptoCode = "LTC", - Rate = 216.79m - }.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() - { - NextNetworkFee = Money.Coins(0.00010000m), - DepositAddress = dummy - })); + paymentMethods.Add(new PaymentMethod() {CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails( + new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() + { + NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy + })); + paymentMethods.Add(new PaymentMethod() {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), null); + 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 }.SetCryptoPaymentData(new BitcoinLikePaymentData() - { - Output = new TxOut() { Value = Money.Coins(0.00151263m) } - })); + invoiceEntity.Payments.Add( + new PaymentEntity() + { + Accounted = true, + CryptoCode = "BTC", + NetworkFee = 0.00000100m + } + .SetCryptoPaymentData(new BitcoinLikePaymentData() + { + Output = new TxOut() {Value = Money.Coins(0.00151263m)} + })); accounting = btc.Calculate(); - invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData() - { - Output = new TxOut() { Value = accounting.Due } - })); + invoiceEntity.Payments.Add( + new PaymentEntity() + { + Accounted = true, + CryptoCode = "BTC", + NetworkFee = 0.00000100m + } + .SetCryptoPaymentData(new BitcoinLikePaymentData() + { + Output = new TxOut() {Value = accounting.Due} + })); accounting = btc.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Zero, accounting.DueUncapped); - var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null); + 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, null); + var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2); Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode); #pragma warning restore CS0618 } @@ -190,70 +208,97 @@ namespace BTCPayServer.Tests [Trait("Fast", "Fast")] public void CanCalculateCryptoDue() { + var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); + var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] + { + new BitcoinLikePaymentHandler(null, networkProvider, null, null), + new LightningLikePaymentHandler(null, null, networkProvider, null), + }); var entity = new InvoiceEntity(); #pragma warning disable CS0618 entity.Payments = new System.Collections.Generic.List(); - entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.SetPaymentMethod(new PaymentMethod() + { + CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) + }); + entity.ProductInformation = new ProductInformation() {Price = 5000}; - var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); + var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true, NetworkFee = 0.1m }); + entity.Payments.Add(new PaymentEntity() + { + Output = new TxOut(Money.Coins(0.5m), new Key()), + Accounted = true, + NetworkFee = 0.1m + }); accounting = paymentMethod.Calculate(); //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 Assert.Equal(Money.Coins(0.7m), accounting.Due); Assert.Equal(Money.Coins(1.2m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m }); + entity.Payments.Add(new PaymentEntity() + { + Output = new TxOut(Money.Coins(0.2m), new Key()), + Accounted = true, + NetworkFee = 0.1m + }); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(0.6m), accounting.Due); Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m }); + entity.Payments.Add(new PaymentEntity() + { + Output = new TxOut(Money.Coins(0.6m), new Key()), + Accounted = true, + NetworkFee = 0.1m + }); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() + { + Output = new TxOut(Money.Coins(0.2m), new Key()), + Accounted = true + }); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); entity = new InvoiceEntity(); - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.ProductInformation = new ProductInformation() {Price = 5000}; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); - paymentMethods.Add(new PaymentMethod() - { - CryptoCode = "BTC", - Rate = 1000, - NextNetworkFee = Money.Coins(0.1m) - }); - paymentMethods.Add(new PaymentMethod() - { - CryptoCode = "LTC", - Rate = 500, - NextNetworkFee = Money.Coins(0.01m) - }); + paymentMethods.Add( + new PaymentMethod() {CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m)}); + paymentMethods.Add( + new PaymentMethod() {CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m)}); entity.SetPaymentMethods(paymentMethods); entity.Payments = new List(); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(5.1m), accounting.Due); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m }); + entity.Payments.Add(new PaymentEntity() + { + CryptoCode = "BTC", + Output = new TxOut(Money.Coins(1.0m), new Key()), + Accounted = true, + NetworkFee = 0.1m + }); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(4.2m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -261,17 +306,22 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); Assert.Equal(2, accounting.TxRequired); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due); Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid); Assert.Equal(Money.Coins(2.0m), accounting.Paid); Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m }); + entity.Payments.Add(new PaymentEntity() + { + CryptoCode = "LTC", + Output = new TxOut(Money.Coins(1.0m), new Key()), + Accounted = true, + NetworkFee = 0.01m + }); - - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -279,7 +329,7 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added Assert.Equal(2, accounting.TxRequired); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -288,9 +338,15 @@ namespace BTCPayServer.Tests Assert.Equal(2, accounting.TxRequired); var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true, NetworkFee = 0.1m }); + entity.Payments.Add(new PaymentEntity() + { + CryptoCode = "BTC", + Output = new TxOut(remaining, new Key()), + Accounted = true, + NetworkFee = 0.1m + }); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); @@ -299,13 +355,14 @@ namespace BTCPayServer.Tests Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(2, accounting.TxRequired); - paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null); + paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid); // Paying 2 BTC fee, LTC fee removed because fully paid - Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue); + Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), + accounting.TotalDue); Assert.Equal(1, accounting.TxRequired); Assert.Equal(accounting.Paid, accounting.TotalDue); #pragma warning restore CS0618 @@ -313,29 +370,74 @@ namespace BTCPayServer.Tests [Fact] [Trait("Integration", "Integration")] + public async Task GetRedirectedToLoginPathOnChallenge() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var client = tester.PayTester.HttpClient; + //Wallets endpoint is protected + var response = await client.GetAsync("wallets"); + var urlPath = response.RequestMessage.RequestUri.ToString() + .Replace(tester.PayTester.ServerUri.ToString(), ""); + //Cookie Challenge redirects you to login page + Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase); + + var queryString = response.RequestMessage.RequestUri.ParseQueryString(); + + Assert.NotNull(queryString["ReturnUrl"]); + Assert.Equal("/wallets", queryString["ReturnUrl"]); + } + } + + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanUseTestWebsiteUI() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var http = new HttpClient(); + var response = await http.GetAsync(tester.PayTester.ServerUri); + Assert.True(response.IsSuccessStatusCode); + } + } + + [Fact] + [Trait("Fast", "Fast")] public void CanAcceptInvoiceWithTolerance() { + var networkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); + var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] + { + new BitcoinLikePaymentHandler(null, networkProvider, null, null), + new LightningLikePaymentHandler(null, null, networkProvider, null), + }); var entity = new InvoiceEntity(); #pragma warning disable CS0618 entity.Payments = new List(); - entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.SetPaymentMethod(new PaymentMethod() + { + CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) + }); + entity.ProductInformation = new ProductInformation() {Price = 5000}; entity.PaymentTolerance = 0; - var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); + var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); - entity.PaymentTolerance = 10; - accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); + entity.PaymentTolerance = 10; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); - entity.PaymentTolerance = 100; - accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); + entity.PaymentTolerance = 100; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); } @@ -1455,59 +1557,70 @@ namespace BTCPayServer.Tests [Trait("Fast", "Fast")] public void CanParseDerivationScheme() { - var parser = new DerivationSchemeParser(Network.TestNet); + 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 - // Native - result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); + // 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 = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5"); + result = testnetParser.Parse("upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu"); Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString()); - result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); - Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", 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 = parser.Parse(tpub); + result = testnetParser.Parse(tpub); Assert.Equal(tpub, result.ToString()); - parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey; - result = parser.Parse(tpub); + testnetParser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); Assert.Equal(tpub, result.ToString()); - parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey; - result = parser.Parse(tpub); + testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey; - result = parser.Parse(tpub); + testnetParser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse(tpub); Assert.Equal($"{tpub}-[legacy]", result.ToString()); - parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey; - result = parser.Parse($"{tpub}-[legacy]"); + testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey; + result = testnetParser.Parse($"{tpub}-[legacy]"); Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - result = parser.Parse(tpub); + result = testnetParser.Parse(tpub); Assert.Equal($"{tpub}-[p2sh]", result.ToString()); - parser = new DerivationSchemeParser(Network.RegTest); - var parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); + 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 - parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest); - parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); + regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); + parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); - parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest); - parsed = parser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); + regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); + parsed = regtestParser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); } [Fact] [Trait("Integration", "Integration")] - public void CanDisablePaymentMethods() + public void CanAddDerivationSchemes() { using (var tester = ServerTester.Create()) { @@ -1517,7 +1630,7 @@ namespace BTCPayServer.Tests 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, @@ -1538,16 +1651,18 @@ namespace BTCPayServer.Tests 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(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(controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - // Confirmation - controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult(); Assert.False(derivationVM.Enabled); + + // Clicking next without changing anything should send to the confirmation screen derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - Assert.False(derivationVM.Enabled); + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; + Assert.True(derivationVM.Confirmation); invoice = user.BitPay.CreateInvoice(new Invoice() { @@ -1561,6 +1676,85 @@ namespace BTCPayServer.Tests 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(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(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); + + // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) + derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + string 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.ColdcardPublicFile = 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(controller.AddDerivationScheme(user.StoreId, "BTC")).Model; + derivationVM.ColdcardPublicFile = 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.TryParseFromColdcard(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()); + }); } } @@ -1990,8 +2184,8 @@ donation: var firstPayment = productPartDue - missingMoney; cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment)); - TestUtils.Eventually(() => - { + TestUtils.Eventually(() => + { invoice = user.BitPay.GetInvoice(invoice.Id); // Check that for the second payment, network fee are included due = Money.Parse(invoice.CryptoInfo[0].Due); @@ -2049,14 +2243,13 @@ donation: var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m); cashCow.SendToAddress(invoiceAddress, firstPayment); - TestUtils.Eventually(() => { var exportResultPaid = 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($",\"OnChain\",\"BTC\",\"0.0991\",\"0.0001\",\"5000.0\"", 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)\"", paidresult.Content); }); @@ -2464,27 +2657,28 @@ donation: { var unusedUri = new Uri("https://toto.com"); Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error)); - var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge); + var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet); Assert.Equal(new Uri("https://toto.com/test"), expanded.Server); - expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge); + expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge, NetworkType.Mainnet); Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server); - await Assert.ThrowsAsync(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge)); + await Assert.ThrowsAsync(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet)); + await connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Testnet); // Make sure absolute paths are not expanded Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error)); - expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge); + expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet); Assert.Equal(new Uri("https://tow/test"), expanded.Server); // Error if directory not exists Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error)); - await Assert.ThrowsAsync(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC)); - await Assert.ThrowsAsync(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest)); - await connStr.Expand(unusedUri, ExternalServiceTypes.Charge); + await Assert.ThrowsAsync(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet)); + await Assert.ThrowsAsync(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet)); + await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet); var macaroonDirectory = CreateDirectory(); Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error)); - await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC); - expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest); + await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet); + expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet); Assert.NotNull(expanded.Macaroons); Assert.Null(expanded.MacaroonFilePath); Assert.Null(expanded.Macaroons.AdminMacaroon); @@ -2494,7 +2688,7 @@ donation: File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa }); File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab }); File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac }); - expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest); + expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet); Assert.NotNull(expanded.Macaroons.AdminMacaroon); Assert.NotNull(expanded.Macaroons.InvoiceMacaroon); Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex); @@ -2503,7 +2697,7 @@ donation: Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error)); File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken"); - expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge); + expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet); Assert.Equal("apitoken", expanded.APIToken); } @@ -2572,6 +2766,35 @@ donation: Assert.Throws(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult()); } + [Fact] + [Trait("Fast", "Fast")] + public void ParseDerivationSchemeSettings() + { + var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork("BTC"); + 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(); + Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"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}", mainnet, out var settings)); + Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint); + Assert.Equal(settings.AccountKeySettings[0].RootFingerprint, HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default); + Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label); + Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString()); + Assert.Equal("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD", settings.AccountOriginal); + Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, settings.AccountDerivation.Derive(new KeyPath()).ScriptPubKey); + + var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork("BTC"); + + // Should be legacy + Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit); + + // Should be segwit p2sh + Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); + Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && p.Inner is DirectDerivationStrategy s2 && s2.Segwit); + + // Should be segwit + Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); + } + [Fact] [Trait("Fast", "Fast")] public void CheckParseStatusMessageModel() @@ -2607,48 +2830,117 @@ donation: Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity); } + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanCreateInvoiceWithSpecificPaymentMethods() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + 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] + [Trait("Integration", "Integration")] + public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + var accountController = tester.PayTester.GetController(); + + //no 2fa or u2f enabled, login should work + Assert.Equal(nameof(HomeController.Index), Assert.IsType(await accountController.Login(new LoginViewModel() + { + Email = user.RegisterDetails.Email, + Password = user.RegisterDetails.Password + })).ActionName); + + var manageController = user.GetController(); + + //by default no u2f devices available + Assert.Empty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); + var addRequest = Assert.IsType(Assert.IsType(manageController.AddU2FDevice("label")).Model); + //name should match the one provided in beginning + Assert.Equal("label",addRequest.Name); + + //sending an invalid response model back to server, should error out + var statusMessage = Assert + .IsType(await manageController.AddU2FDevice(addRequest)) + .RouteValues["StatusMessage"].ToString(); + Assert.NotNull(statusMessage); + Assert.Equal(StatusMessageModel.StatusSeverity.Error, new StatusMessageModel(statusMessage).Severity); + + var contextFactory = tester.PayTester.GetService(); + + //add a fake u2f device in db directly since emulating a u2f device is hard and annoying + using (var context = contextFactory.CreateContext()) + { + var newDevice = new U2FDevice() + { + Name = "fake", + Counter = 0, + KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"), + PublicKey= UTF8Encoding.UTF8.GetBytes("fake"), + AttestationCert= UTF8Encoding.UTF8.GetBytes("fake"), + ApplicationUserId= user.UserId + }; + await context.U2FDevices.AddAsync(newDevice); + await context.SaveChangesAsync(); + + Assert.NotNull(newDevice.Id); + Assert.NotEmpty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); + + } + + //check if we are showing the u2f login screen now + var secondLoginResult = Assert.IsType(await accountController.Login(new LoginViewModel() + { + Email = user.RegisterDetails.Email, + Password = user.RegisterDetails.Password + })); + + Assert.Equal("SecondaryLogin", secondLoginResult.ViewName); + var vm = Assert.IsType(secondLoginResult.Model); + //2fa was never enabled for user so this should be empty + Assert.Null(vm.LoginWith2FaViewModel); + Assert.NotNull(vm.LoginWithU2FViewModel); + } + } + private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) { var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString(); return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null; } - - public static class TestUtils - { - public static void Eventually(Action act) - { - CancellationTokenSource cts = new CancellationTokenSource(20000); - while (true) - { - try - { - act(); - break; - } - catch (XunitException) when (!cts.Token.IsCancellationRequested) - { - cts.Token.WaitHandle.WaitOne(500); - } - } - } - - public static async Task EventuallyAsync(Func act) - { - CancellationTokenSource cts = new CancellationTokenSource(20000); - while (true) - { - try - { - await act(); - break; - } - catch (XunitException) when (!cts.Token.IsCancellationRequested) - { - await Task.Delay(500); - } - } - } - } } } diff --git a/BTCPayServer.Tests/docker-bitcoin-generate.ps1 b/BTCPayServer.Tests/docker-bitcoin-generate.ps1 new file mode 100644 index 000000000..63ee3819f --- /dev/null +++ b/BTCPayServer.Tests/docker-bitcoin-generate.ps1 @@ -0,0 +1,3 @@ +$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind) +$address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress) +docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress $args $address diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 6cc03a346..d0eb8545d 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -38,7 +38,7 @@ services: # The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services dev: - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -55,7 +55,7 @@ services: - merchant_lnd devlnd: - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -71,7 +71,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:2.0.0.34 + image: nicolasdorier/nbxplorer:2.0.0.48 restart: unless-stopped ports: - "32838:32838" @@ -98,13 +98,14 @@ services: bitcoind: restart: unless-stopped - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 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 @@ -133,6 +134,7 @@ services: bind-addr=0.0.0.0 announce-addr=customer_lightningd log-level=debug + funding-confirms=1 dev-broadcast-interval=1000 dev-bitcoind-poll=1 ports: @@ -176,6 +178,7 @@ services: bitcoin-rpcconnect=bitcoind bind-addr=0.0.0.0 announce-addr=merchant_lightningd + funding-confirms=1 network=regtest log-level=debug dev-broadcast-interval=1000 @@ -239,6 +242,7 @@ services: bitcoind.zmqpubrawblock=tcp://bitcoind:28332 bitcoind.zmqpubrawtx=tcp://bitcoind:28333 externalip=merchant_lnd:9735 + bitcoin.defaultchanconfs=1 no-macaroons=1 debuglevel=debug noseedbackup=1 @@ -269,6 +273,7 @@ services: bitcoind.zmqpubrawblock=tcp://bitcoind:28332 bitcoind.zmqpubrawtx=tcp://bitcoind:28333 externalip=customer_lnd:10009 + bitcoin.defaultchanconfs=1 no-macaroons=1 debuglevel=debug noseedbackup=1 diff --git a/BTCPayServer.Tests/docker-entrypoint.sh b/BTCPayServer.Tests/docker-entrypoint.sh index 1cd229031..afc061905 100755 --- a/BTCPayServer.Tests/docker-entrypoint.sh +++ b/BTCPayServer.Tests/docker-entrypoint.sh @@ -1,8 +1,9 @@ #!/bin/sh set -e -dotnet test --filter Fast=Fast --no-build -dotnet test --filter Integration=Integration --no-build -v n -if [[ "$TESTS_RUN_EXTERNAL_INTEGRATION" == "true" ]]; then - dotnet test --filter ExternalIntegration=ExternalIntegration --no-build -v n +FILTERS=" " +if [[ "$TEST_FILTERS" ]]; then +FILTERS="--filter $TEST_FILTERS" fi + +dotnet test $FILTERS --no-build -v n diff --git a/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdAuthorization.cs b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdAuthorization.cs new file mode 100644 index 000000000..09dbf3691 --- /dev/null +++ b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdAuthorization.cs @@ -0,0 +1,6 @@ +using OpenIddict.EntityFrameworkCore.Models; + +namespace BTCPayServer.Authentication.OpenId.Models +{ + public class BTCPayOpenIdAuthorization : OpenIddictAuthorization { } +} \ No newline at end of file diff --git a/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdClient.cs b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdClient.cs new file mode 100644 index 000000000..bf2abb797 --- /dev/null +++ b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdClient.cs @@ -0,0 +1,11 @@ +using BTCPayServer.Models; +using OpenIddict.EntityFrameworkCore.Models; + +namespace BTCPayServer.Authentication.OpenId.Models +{ + public class BTCPayOpenIdClient: OpenIddictApplication + { + public string ApplicationUserId { get; set; } + public ApplicationUser ApplicationUser { get; set; } + } +} diff --git a/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdToken.cs b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdToken.cs new file mode 100644 index 000000000..00714240a --- /dev/null +++ b/BTCPayServer/Authentication/OpenId/Models/BTCPayOpenIdToken.cs @@ -0,0 +1,6 @@ +using OpenIddict.EntityFrameworkCore.Models; + +namespace BTCPayServer.Authentication.OpenId.Models +{ + public class BTCPayOpenIdToken : OpenIddictToken { } +} \ No newline at end of file diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 78f17aa48..228f98b46 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -1,12 +1,8 @@  + + Exe - netcoreapp2.1 - 1.0.3.94 - NU1701,CA1816,CA1308,CA1810,CA2208 - - - 7.3 @@ -34,11 +30,10 @@ - + - @@ -47,15 +42,16 @@ all runtime; build; native; contentfiles; analyzers - - + + + @@ -69,11 +65,12 @@ - - - - - + + + + + + @@ -129,9 +126,16 @@ + + + + + + + @@ -148,6 +152,9 @@ $(IncludeRazorContentInPack) + + $(IncludeRazorContentInPack) + $(IncludeRazorContentInPack) @@ -175,6 +182,15 @@ $(IncludeRazorContentInPack) + + $(IncludeRazorContentInPack) + + + $(IncludeRazorContentInPack) + + + $(IncludeRazorContentInPack) + $(IncludeRazorContentInPack) diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 7069f959d..36796f41a 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -6,13 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; -using System.Text; -using StandardConfiguration; using Microsoft.Extensions.Configuration; -using NBXplorer; -using BTCPayServer.Payments.Lightning; -using Renci.SshNet; -using NBitcoin.DataEncoders; using BTCPayServer.SSH; using BTCPayServer.Lightning; using Serilog.Events; @@ -49,7 +43,7 @@ namespace BTCPayServer.Configuration private set; } public EndPoint SocksEndpoint { get; set; } - + public List NBXplorerConnectionSettings { get; @@ -75,22 +69,24 @@ namespace BTCPayServer.Configuration public void LoadArgs(IConfiguration conf) { NetworkType = DefaultConfiguration.GetNetworkType(conf); - var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType); - DataDir = conf.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory); + DataDir = conf.GetDataDir(NetworkType); Logs.Configuration.LogInformation("Network: " + NetworkType.ToString()); + if (conf.GetOrDefault("launchsettings", false) && NetworkType != NetworkType.Regtest) + throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script"); + var supportedChains = conf.GetOrDefault("chains", "btc") .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(t => t.ToUpperInvariant()); NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray()); foreach (var chain in supportedChains) { - if (NetworkProvider.GetNetwork(chain) == null) + if (NetworkProvider.GetNetwork(chain) == null) throw new ConfigException($"Invalid chains \"{chain}\""); } var validChains = new List(); - foreach (var net in NetworkProvider.GetAll()) + foreach (var net in NetworkProvider.GetAll().OfType()) { NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting(); setting.CryptoCode = net.CryptoCode; @@ -109,6 +105,8 @@ namespace BTCPayServer.Configuration $"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine + $"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine + $" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine + + $"If you have an eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393;bitcoin-auth=bitcoinrpcuser:bitcoinrpcpassword" + Environment.NewLine + + $" eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393" + Environment.NewLine + $"Error: {error}" + Environment.NewLine + "This service will not be exposed through BTCPay Server"); } @@ -145,6 +143,7 @@ namespace BTCPayServer.Configuration PostgresConnectionString = conf.GetOrDefault("postgres", null); MySQLConnectionString = conf.GetOrDefault("mysql", null); BundleJsCss = conf.GetOrDefault("bundlejscss", true); + AllowAdminRegistration = conf.GetOrDefault("allow-admin-registration", false); TorrcFile = conf.GetOrDefault("torrcfile", null); var socksEndpointString = conf.GetOrDefault("socksendpoint", null); @@ -273,6 +272,7 @@ namespace BTCPayServer.Configuration get; set; } + public bool AllowAdminRegistration { get; set; } public List TrustedFingerprints { get; set; } = new List(); public SSHSettings SSHSettings { diff --git a/BTCPayServer/Configuration/ConfigurationExtensions.cs b/BTCPayServer/Configuration/ConfigurationExtensions.cs index 4788bc0c3..d14ff012c 100644 --- a/BTCPayServer/Configuration/ConfigurationExtensions.cs +++ b/BTCPayServer/Configuration/ConfigurationExtensions.cs @@ -58,5 +58,17 @@ namespace BTCPayServer.Configuration throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name); } } + + public static string GetDataDir(this IConfiguration configuration) + { + var networkType = DefaultConfiguration.GetNetworkType(configuration); + return GetDataDir(configuration, networkType); + } + + public static string GetDataDir(this IConfiguration configuration, NetworkType networkType) + { + var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType); + return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory); + } } } diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index 62f99d69e..1276cdaa0 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -29,6 +29,7 @@ namespace BTCPayServer.Configuration app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue); app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue); app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue); + app.Option("--allow-admin-registration", $"For debug only, will show a checkbox when a new user register to add himself as admin. (default: false)", CommandOptionType.BoolValue); app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue); app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue); app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue); @@ -45,13 +46,15 @@ namespace BTCPayServer.Configuration 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); - foreach (var network in provider.GetAll()) + foreach (var network in provider.GetAll().OfType()) { var crypto = network.CryptoCode.ToLowerInvariant(); app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue); app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue); app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue); - app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue); + app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from an external wallet (default: empty)", CommandOptionType.SingleValue); + app.Option($"--{crypto}externallndrest", $"The LND REST configuration BTCPay will expose to easily connect to the internal lnd wallet from an external wallet (default: empty)", CommandOptionType.SingleValue); + app.Option($"--{crypto}externalrtl", $"The Ride the Lightning configuration so BTCPay will expose to easily open it in server settings (default: empty)", CommandOptionType.SingleValue); app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue); app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue); } @@ -120,7 +123,7 @@ namespace BTCPayServer.Configuration builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;"); builder.AppendLine(); builder.AppendLine("### NBXplorer settings ###"); - foreach (var n in new BTCPayNetworkProvider(networkType).GetAll()) + foreach (var n in new BTCPayNetworkProvider(networkType).GetAll().OfType()) { builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}"); builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}"); diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs index fe4e7990c..e99b9ceed 100644 --- a/BTCPayServer/Configuration/ExternalConnectionString.cs +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -3,11 +3,20 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Controllers; +using NBitcoin; namespace BTCPayServer.Configuration { public class ExternalConnectionString { + public ExternalConnectionString() + { + + } + public ExternalConnectionString(Uri server) + { + Server = server; + } public Uri Server { get; set; } public byte[] Macaroon { get; set; } public Macaroons Macaroons { get; set; } @@ -22,13 +31,16 @@ namespace BTCPayServer.Configuration /// Return a connectionString which does not depends on external resources or information like relative path or file path /// /// - public async Task Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType) + public async Task Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType, NetworkType network) { var connectionString = this.Clone(); // Transform relative URI into absolute URI var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString()); - if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) && - !serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase)) + var isSecure = network != NetworkType.Mainnet || + serviceUri.Scheme == "https" || + serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) || + Extensions.IsLocalNetwork(serviceUri.DnsSafeHost); + if (!isSecure) { throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR"); } diff --git a/BTCPayServer/Configuration/ExternalService.cs b/BTCPayServer/Configuration/ExternalService.cs index bfd3688b4..0e56af98d 100644 --- a/BTCPayServer/Configuration/ExternalService.cs +++ b/BTCPayServer/Configuration/ExternalService.cs @@ -75,6 +75,7 @@ namespace BTCPayServer.Configuration LNDGRPC, Spark, RTL, - Charge + Charge, + P2P } } diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 94cc11c42..33bad508e 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -18,6 +18,9 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Logging; using BTCPayServer.Security; using System.Globalization; +using BTCPayServer.Services.U2F; +using BTCPayServer.Services.U2F.Models; +using Newtonsoft.Json; using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers @@ -33,6 +36,8 @@ namespace BTCPayServer.Controllers RoleManager _RoleManager; SettingsRepository _SettingsRepository; Configuration.BTCPayServerOptions _Options; + private readonly BTCPayServerEnvironment _btcPayServerEnvironment; + private readonly U2FService _u2FService; ILogger _logger; public AccountController( @@ -42,7 +47,9 @@ namespace BTCPayServer.Controllers SignInManager signInManager, EmailSenderFactory emailSenderFactory, SettingsRepository settingsRepository, - Configuration.BTCPayServerOptions options) + Configuration.BTCPayServerOptions options, + BTCPayServerEnvironment btcPayServerEnvironment, + U2FService u2FService) { this.storeRepository = storeRepository; _userManager = userManager; @@ -51,6 +58,8 @@ namespace BTCPayServer.Controllers _RoleManager = roleManager; _SettingsRepository = settingsRepository; _Options = options; + _btcPayServerEnvironment = btcPayServerEnvironment; + _u2FService = u2FService; _logger = Logs.PayServer; } @@ -91,8 +100,44 @@ namespace BTCPayServer.Controllers return View(model); } } - // This doesn't count login failures towards account lockout - // To enable password failures to trigger account lockout, set lockoutOnFailure: true + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + + if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id)) + { + if (await _userManager.CheckPasswordAsync(user, model.Password)) + { + LoginWith2faViewModel twoFModel = null; + + if (user.TwoFactorEnabled) + { + // we need to do an actual sign in attempt so that 2fa can function in next step + await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); + twoFModel = new LoginWith2faViewModel + { + RememberMe = model.RememberMe + }; + } + + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = twoFModel, + LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user) + }); + } + else + { + var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user); + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + + } + } + + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { @@ -101,10 +146,12 @@ namespace BTCPayServer.Controllers } if (result.RequiresTwoFactor) { - return RedirectToAction(nameof(LoginWith2fa), new + return View("SecondaryLogin", new SecondaryLoginViewModel() { - returnUrl, - model.RememberMe + LoginWith2FaViewModel = new LoginWith2faViewModel() + { + RememberMe = model.RememberMe + } }); } if (result.IsLockedOut) @@ -123,6 +170,71 @@ namespace BTCPayServer.Controllers return View(model); } + private async Task BuildU2FViewModel(bool rememberMe, ApplicationUser user) + { + if (_btcPayServerEnvironment.IsSecure) + { + var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id, + Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/')); + + return new LoginWithU2FViewModel() + { + Version = u2fChallenge[0].version, + Challenge = u2fChallenge[0].challenge, + Challenges = JsonConvert.SerializeObject(u2fChallenge), + AppId = u2fChallenge[0].appId, + UserId = user.Id, + RememberMe = rememberMe + }; + } + + return null; + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + var user = await _userManager.FindByIdAsync(viewModel.UserId); + + if (user == null) + { + return NotFound(); + } + + var errorMessage = string.Empty; + try + { + if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse)) + { + await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F"); + _logger.LogInformation("User logged in."); + return RedirectToLocal(returnUrl); + } + + errorMessage = "Invalid login attempt."; + } + catch (Exception e) + { + + errorMessage = e.Message; + } + + ModelState.AddModelError(string.Empty, errorMessage); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWithU2FViewModel = viewModel, + LoginWith2FaViewModel = !user.TwoFactorEnabled + ? null + : new LoginWith2faViewModel() + { + RememberMe = viewModel.RememberMe + } + }); + } + [HttpGet] [AllowAnonymous] public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) @@ -135,10 +247,13 @@ namespace BTCPayServer.Controllers throw new ApplicationException($"Unable to load two-factor authentication user."); } - var model = new LoginWith2faViewModel { RememberMe = rememberMe }; ViewData["ReturnUrl"] = returnUrl; - return View(model); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe }, + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + }); } [HttpPost] @@ -175,7 +290,11 @@ namespace BTCPayServer.Controllers { _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); ModelState.AddModelError(string.Empty, "Invalid authenticator code."); - return View(); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = model, + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + }); } } @@ -249,6 +368,7 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(HomeController.Index), "Home"); ViewData["ReturnUrl"] = returnUrl; ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration; return View(); } @@ -259,6 +379,7 @@ namespace BTCPayServer.Controllers { ViewData["ReturnUrl"] = returnUrl; ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration; var policies = await _SettingsRepository.GetSettingAsync() ?? new PoliciesSettings(); if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin)) return RedirectToAction(nameof(HomeController.Index), "Home"); @@ -270,7 +391,7 @@ namespace BTCPayServer.Controllers { var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin); Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}"); - if (admin.Count == 0) + if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration)) { await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); diff --git a/BTCPayServer/Controllers/AppsController.Crowdfund.cs b/BTCPayServer/Controllers/AppsController.Crowdfund.cs index 1ee719267..bfa3e5ffc 100644 --- a/BTCPayServer/Controllers/AppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/AppsController.Crowdfund.cs @@ -129,10 +129,10 @@ namespace BTCPayServer.Controllers Title = vm.Title, Enabled = vm.Enabled, EnforceTargetAmount = vm.EnforceTargetAmount, - StartDate = vm.StartDate, + StartDate = vm.StartDate?.ToUniversalTime(), TargetCurrency = vm.TargetCurrency, Description = _htmlSanitizer.Sanitize( vm.Description), - EndDate = vm.EndDate, + EndDate = vm.EndDate?.ToUniversalTime(), TargetAmount = vm.TargetAmount, CustomCSSLink = vm.CustomCSSLink, MainImageUrl = vm.MainImageUrl, diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 75c33388b..880ae8d0b 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -91,6 +91,77 @@ namespace BTCPayServer.Controllers }); } + [HttpPost] + [Route("/apps/{appId}/pos")] + [XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)] + [IgnoreAntiforgeryToken] + [EnableCors(CorsPolicies.All)] + public async Task ViewPointOfSale(string appId, + [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, + string email, + string orderId, + string notificationUrl, + string redirectUrl, + string choiceKey, + string posData = null, CancellationToken cancellationToken = default) + { + var app = await _AppService.GetApp(appId, AppType.PointOfSale); + if (string.IsNullOrEmpty(choiceKey) && amount <= 0) + { + return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); + } + if (app == null) + return NotFound(); + var settings = app.GetSettings(); + if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart) + { + return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); + } + string title = null; + var price = 0.0m; + ViewPointOfSaleViewModel.Item choice = null; + if (!string.IsNullOrEmpty(choiceKey)) + { + var choices = _AppService.Parse(settings.Template, settings.Currency); + choice = choices.FirstOrDefault(c => c.Id == choiceKey); + if (choice == null) + return NotFound(); + title = choice.Title; + price = choice.Price.Value; + if (amount > price) + price = amount; + } + else + { + if (!settings.ShowCustomAmount && !settings.EnableShoppingCart) + return NotFound(); + price = amount; + title = settings.Title; + } + var store = await _AppService.GetStore(app); + store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id)); + var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() + { + ItemCode = choice?.Id, + ItemDesc = title, + Currency = settings.Currency, + Price = price, + BuyerEmail = email, + OrderId = orderId, + NotificationURL = + string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl, + NotificationEmail = settings.NotificationEmail, + RedirectURL = redirectUrl ?? Request.GetDisplayUrl(), + FullNotifications = true, + ExtendedNotifications = true, + PosData = string.IsNullOrEmpty(posData) ? null : posData, + RedirectAutomatically = settings.RedirectAutomatically, + }, store, HttpContext.Request.GetAbsoluteRoot(), + new List() { AppService.GetAppInternalTag(appId) }, + cancellationToken); + return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id }); + } + [HttpGet] [Route("/apps/{appId}/crowdfund")] @@ -215,77 +286,6 @@ namespace BTCPayServer.Controllers } - [HttpPost] - [Route("/apps/{appId}/pos")] - [XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)] - [IgnoreAntiforgeryToken] - [EnableCors(CorsPolicies.All)] - public async Task ViewPointOfSale(string appId, - [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, - string email, - string orderId, - string notificationUrl, - string redirectUrl, - string choiceKey, - string posData = null, CancellationToken cancellationToken = default) - { - var app = await _AppService.GetApp(appId, AppType.PointOfSale); - if (string.IsNullOrEmpty(choiceKey) && amount <= 0) - { - return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); - } - if (app == null) - return NotFound(); - var settings = app.GetSettings(); - if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart) - { - return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); - } - string title = null; - var price = 0.0m; - ViewPointOfSaleViewModel.Item choice = null; - if (!string.IsNullOrEmpty(choiceKey)) - { - var choices = _AppService.Parse(settings.Template, settings.Currency); - choice = choices.FirstOrDefault(c => c.Id == choiceKey); - if (choice == null) - return NotFound(); - title = choice.Title; - price = choice.Price.Value; - if (amount > price) - price = amount; - } - else - { - if (!settings.ShowCustomAmount && !settings.EnableShoppingCart) - return NotFound(); - price = amount; - title = settings.Title; - } - var store = await _AppService.GetStore(app); - store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id)); - var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() - { - ItemCode = choice?.Id, - ItemDesc = title, - Currency = settings.Currency, - Price = price, - BuyerEmail = email, - OrderId = orderId, - NotificationURL = - string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl, - NotificationEmail = settings.NotificationEmail, - RedirectURL = redirectUrl ?? Request.GetDisplayUrl(), - FullNotifications = true, - ExtendedNotifications = true, - PosData = string.IsNullOrEmpty(posData) ? null : posData, - RedirectAutomatically = settings.RedirectAutomatically, - }, store, HttpContext.Request.GetAbsoluteRoot(), - new List() { AppService.GetAppInternalTag(appId) }, - cancellationToken); - return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id }); - } - private string GetUserId() { diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index f48b20611..c05345aff 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers HttpClientFactory = httpClientFactory; _cachedServerSettings = cachedServerSettings; } - + public async Task Index() { if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund) @@ -40,6 +40,20 @@ namespace BTCPayServer.Controllers return res; // return } } + else if (_cachedServerSettings.RootAppType is Services.Apps.AppType.PointOfSale) + { + var serviceProvider = HttpContext.RequestServices; + var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController)); + controller.Url = Url; + controller.ControllerContext = ControllerContext; + var res = await controller.ViewPointOfSale(_cachedServerSettings.RootAppId) as ViewResult; + if (res != null) + { + res.ViewName = "/Views/AppsPublic/ViewPointOfSale.cshtml"; + return res; // return + } + + } return View("Home"); } diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index e211cd791..c949f8e26 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -7,9 +7,7 @@ using BTCPayServer.Models; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; -using NBitpayClient; namespace BTCPayServer.Controllers { @@ -19,15 +17,12 @@ namespace BTCPayServer.Controllers { private InvoiceController _InvoiceController; private InvoiceRepository _InvoiceRepository; - private BTCPayNetworkProvider _NetworkProvider; public InvoiceControllerAPI(InvoiceController invoiceController, - InvoiceRepository invoceRepository, - BTCPayNetworkProvider networkProvider) + InvoiceRepository invoceRepository) { - this._InvoiceController = invoiceController; - this._InvoiceRepository = invoceRepository; - this._NetworkProvider = networkProvider; + _InvoiceController = invoiceController; + _InvoiceRepository = invoceRepository; } [HttpPost] @@ -51,8 +46,7 @@ namespace BTCPayServer.Controllers })).FirstOrDefault(); if (invoice == null) throw new BitpayHttpException(404, "Object not found"); - var resp = invoice.EntityToDTO(_NetworkProvider); - return new DataWrapper(resp); + return new DataWrapper(invoice.EntityToDTO()); } [HttpGet] [Route("invoices")] @@ -82,7 +76,7 @@ namespace BTCPayServer.Controllers }; var entities = (await _InvoiceRepository.GetInvoices(query)) - .Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray(); + .Select((o) => o.EntityToDTO()).ToArray(); return DataWrapper.Create(entities); } diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index fb8b99ff2..e84b66aa9 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -19,10 +19,8 @@ using BTCPayServer.Security; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices.Export; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.EntityFrameworkCore.Internal; using NBitcoin; using NBitpayClient; using NBXplorer; @@ -47,9 +45,9 @@ namespace BTCPayServer.Controllers if (invoice == null) return NotFound(); - var dto = invoice.EntityToDTO(_NetworkProvider); + var prodInfo = invoice.ProductInformation; var store = await _StoreRepository.FindStore(invoice.StoreId); - InvoiceDetailsModel model = new InvoiceDetailsModel() + var model = new InvoiceDetailsModel() { StoreName = store.StoreName, StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), @@ -65,115 +63,89 @@ namespace BTCPayServer.Controllers MonitoringDate = invoice.MonitoringExpiration, OrderId = invoice.OrderId, BuyerInformation = invoice.BuyerInformation, - Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency), - TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency), + Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency), + TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency), NotificationEmail = invoice.NotificationEmail, NotificationUrl = invoice.NotificationURL, RedirectUrl = invoice.RedirectURL, ProductInformation = invoice.ProductInformation, StatusException = invoice.ExceptionStatus, Events = invoice.Events, - PosData = PosDataParser.ParsePosData(dto.PosData) + PosData = PosDataParser.ParsePosData(invoice.PosData), + StatusMessage = StatusMessage }; - foreach (var data in invoice.GetPaymentMethods(null)) + model.Addresses = invoice.HistoricalAddresses.Select(h => + new InvoiceDetailsModel.AddressModel + { + Destination = h.GetAddress(), + PaymentMethod = h.GetPaymentMethodId().ToPrettyString(), + Current = !h.UnAssigned.HasValue + }).ToArray(); + + var details = InvoicePopulatePayments(invoice); + model.CryptoPayments = details.CryptoPayments; + model.OnChainPayments = details.OnChainPayments; + model.OffChainPayments = details.OffChainPayments; + + return View(model); + } + private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice) + { + var model = new InvoiceDetailsModel(); + + foreach (var data in invoice.GetPaymentMethods()) { - var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId()); var accounting = data.Calculate(); var paymentMethodId = data.GetId(); var cryptoPayment = new InvoiceDetailsModel.CryptoPayment(); - cryptoPayment.PaymentMethod = ToString(paymentMethodId); + cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString(); cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); - - var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; - if (onchainMethod != null) - { - cryptoPayment.Address = onchainMethod.DepositAddress; - } + var paymentMethodDetails = data.GetPaymentMethodDetails(); + cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination(); cryptoPayment.Rate = ExchangeRate(data); - cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21; model.CryptoPayments.Add(cryptoPayment); } - var onChainPayments = invoice - .GetPayments() - .Select>(async payment => + foreach (var payment in invoice.GetPayments()) + { + var paymentData = payment.GetCryptoPaymentData(); + //TODO: abstract + if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData) { - var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode()); - var paymentData = payment.GetCryptoPaymentData(); - if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData) + var m = new InvoiceDetailsModel.Payment(); + m.Crypto = payment.GetPaymentMethodId().CryptoCode; + m.DepositAddress = onChainPaymentData.GetDestination(); + + int confirmationCount = onChainPaymentData.ConfirmationCount; + if (confirmationCount >= payment.Network.MaxTrackedConfirmation) { - var m = new InvoiceDetailsModel.Payment(); - m.Crypto = payment.GetPaymentMethodId().CryptoCode; - m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork); - - int confirmationCount = 0; - if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted) - && (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date - { - confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0; - onChainPaymentData.ConfirmationCount = confirmationCount; - payment.SetCryptoPaymentData(onChainPaymentData); - await _InvoiceRepository.UpdatePayments(new List { payment }); - } - else - { - confirmationCount = onChainPaymentData.ConfirmationCount; - } - if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation) - { - m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation); - } - else - { - m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture); - } - - m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString(); - m.ReceivedTime = payment.ReceivedTime; - m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId); - m.Replaced = !payment.Accounted; - return m; + m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation); } else { - var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData; - return new InvoiceDetailsModel.OffChainPayment() - { - Crypto = paymentNetwork.CryptoCode, - BOLT11 = lightningPaymentData.BOLT11 - }; + m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture); } - }) - .ToArray(); - await Task.WhenAll(onChainPayments); - model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel - { - Destination = h.GetAddress(), - PaymentMethod = ToString(h.GetPaymentMethodId()), - Current = !h.UnAssigned.HasValue - }).ToArray(); - model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType().ToList(); - model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType().ToList(); - model.StatusMessage = StatusMessage; - return View(model); - } - private string ToString(PaymentMethodId paymentMethodId) - { - var type = paymentMethodId.PaymentType.ToString(); - switch (paymentMethodId.PaymentType) - { - case PaymentTypes.BTCLike: - type = "On-Chain"; - break; - case PaymentTypes.LightningLike: - type = "Off-Chain"; - break; + m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString(); + m.ReceivedTime = payment.ReceivedTime; + m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); + m.Replaced = !payment.Accounted; + model.OnChainPayments.Add(m); + } + else + { + var lightningPaymentData = (LightningLikePaymentData)paymentData; + model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment() + { + Crypto = payment.Network.CryptoCode, + BOLT11 = lightningPaymentData.BOLT11 + }); + } } - return $"{paymentMethodId.CryptoCode} ({type})"; + return model; } [HttpGet] @@ -230,7 +202,7 @@ namespace BTCPayServer.Controllers return View(model); } - + //TODO: abstract private async Task GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId) { var invoice = await _InvoiceRepository.GetInvoice(invoiceId); @@ -243,10 +215,11 @@ namespace BTCPayServer.Controllers paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider); isDefaultPaymentId = true; } - var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + BTCPayNetworkBase network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); if (network == null && isDefaultPaymentId) { - network = _NetworkProvider.GetAll().FirstOrDefault(); + //TODO: need to look into a better way for this as it does not scale + network = _NetworkProvider.GetAll().OfType().FirstOrDefault(); paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); } if (invoice == null || network == null) @@ -255,18 +228,18 @@ namespace BTCPayServer.Controllers { if (!isDefaultPaymentId) return null; - var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider) + var paymentMethodTemp = invoice.GetPaymentMethods() .Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode) .FirstOrDefault(); if (paymentMethodTemp == null) - paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First(); + paymentMethodTemp = invoice.GetPaymentMethods().First(); network = paymentMethodTemp.Network; paymentMethodId = paymentMethodTemp.GetId(); } - var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider); + var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); - var dto = invoice.EntityToDTO(_NetworkProvider); + var dto = invoice.EntityToDTO(); var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var storeBlob = store.GetStoreBlob(); var currency = invoice.ProductInformation.Currency; @@ -288,20 +261,19 @@ namespace BTCPayServer.Controllers (1m + (changelly.AmountMarkupPercentage / 100m))) : (decimal?)null; + + var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId]; var model = new PaymentModel() { CryptoCode = network.CryptoCode, RootPath = this.Request.PathBase.Value.WithTrailingSlash(), - PaymentMethodId = paymentMethodId.ToString(), - PaymentMethodName = GetDisplayName(paymentMethodId, network), - CryptoImage = GetImage(paymentMethodId, network), - IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike, OrderId = invoice.OrderId, InvoiceId = invoice.Id, DefaultLang = storeBlob.DefaultLang ?? "en", HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice", CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri, CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri, + CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ToString(), @@ -317,13 +289,7 @@ namespace BTCPayServer.Controllers MerchantRefLink = invoice.RedirectURL ?? "/", RedirectAutomatically = invoice.RedirectAutomatically, StoreName = store.StoreName, - InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 : - paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 : - throw new NotSupportedException(), PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo, - InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 : - paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() : - throw new NotSupportedException(), TxCount = accounting.TxRequired, BtcPaid = accounting.Paid.ToString(), #pragma warning disable CS0618 // Type or member is obsolete @@ -339,38 +305,39 @@ namespace BTCPayServer.Controllers CoinSwitchMerchantId = coinswitch?.MerchantId, CoinSwitchMode = coinswitch?.Mode, StoreId = store.Id, - AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider) + AvailableCryptos = invoice.GetPaymentMethods() .Where(i => i.Network != null) - .Select(kv => new PaymentModel.AvailableCrypto() + .Select(kv => { - PaymentMethodId = kv.GetId().ToString(), - CryptoCode = kv.GetId().CryptoCode, - PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network), - IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike, - CryptoImage = GetImage(kv.GetId(), kv.Network), - Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() }) + var availableCryptoPaymentMethodId = kv.GetId(); + var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId]; + return new PaymentModel.AvailableCrypto() + { + PaymentMethodId = kv.GetId().ToString(), + CryptoCode = kv.GetId().CryptoCode, + PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId), + IsLightning = + kv.GetId().PaymentType == PaymentTypes.LightningLike, + CryptoImage = Request.GetRelativePathOrAbsolute(availableCryptoHandler.GetCryptoImage(availableCryptoPaymentMethodId)), + Link = Url.Action(nameof(Checkout), + new + { + invoiceId = invoiceId, + paymentMethodId = kv.GetId().ToString() + }) + }; }).Where(c => c.CryptoImage != "/") .OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0) .ToList() }; + paymentMethodHandler.PreparePaymentModel(model, dto); + model.PaymentMethodId = paymentMethodId.ToString(); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); model.TimeLeft = expiration.PrettyPrint(); return model; } - private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network) - { - return paymentMethodId.PaymentType == PaymentTypes.BTCLike ? - network.DisplayName : network.DisplayName + " (Lightning)"; - } - - private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network) - { - return paymentMethodId.PaymentType == PaymentTypes.BTCLike ? - this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath); - } - private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation) { // if invoice source currency is the same as currently display currency, no need for "order amount from invoice" @@ -486,7 +453,8 @@ namespace BTCPayServer.Controllers var state = invoice.GetInvoiceState(); model.Invoices.Add(new InvoiceModel() { - Status = state.ToString(), + Status = invoice.Status, + StatusString = state.ToString(), ShowCheckout = invoice.Status == InvoiceStatus.New, Date = invoice.InvoiceTime, InvoiceId = invoice.Id, @@ -494,7 +462,8 @@ namespace BTCPayServer.Controllers RedirectUrl = invoice.RedirectURL ?? string.Empty, AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency), CanMarkInvalid = state.CanMarkInvalid(), - CanMarkComplete = state.CanMarkComplete() + CanMarkComplete = state.CanMarkComplete(), + Details = InvoicePopulatePayments(invoice) }); } model.Total = await counting; @@ -525,7 +494,7 @@ namespace BTCPayServer.Controllers [BitpayAPIConstraint(false)] public async Task Export(string format, string searchTerm = null, int timezoneOffset = 0) { - var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable); + var model = new InvoiceExport(_CurrencyNameTable); InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset); invoiceQuery.Skip = 0; @@ -544,6 +513,14 @@ namespace BTCPayServer.Controllers } + private SelectList GetPaymentMethodsSelectList() + { + return new SelectList(_paymentMethodHandlerDictionary.Distinct().SelectMany(handler => + handler.GetSupportedPaymentMethods() + .Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))), + nameof(SelectListItem.Value), + nameof(SelectListItem.Text)); + } [HttpGet] [Route("invoices/create")] @@ -557,7 +534,8 @@ namespace BTCPayServer.Controllers StatusMessage = "Error: You need to create at least one store before creating a transaction"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); } - return View(new CreateInvoiceModel() { Stores = stores }); + + return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = GetPaymentMethodsSelectList() }); } [HttpPost] @@ -568,6 +546,9 @@ namespace BTCPayServer.Controllers { var stores = await _StoreRepository.GetStoresByUserId(GetUserId()); model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId); + + model.AvailablePaymentMethods = GetPaymentMethodsSelectList(); + var store = stores.FirstOrDefault(s => s.Id == model.StoreId); if (store == null) { @@ -590,6 +571,7 @@ namespace BTCPayServer.Controllers return View(model); } + if (StatusMessage != null) { return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new @@ -612,6 +594,10 @@ namespace BTCPayServer.Controllers ItemDesc = model.ItemDesc, FullNotifications = true, BuyerEmail = model.BuyerEmail, + SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency() + { + Enabled = true + }) }, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken); StatusMessage = $"Invoice {result.Data.Id} just created!"; @@ -624,61 +610,46 @@ namespace BTCPayServer.Controllers } } - [HttpGet] - [Route("invoices/{invoiceId}/changestate/{newState}")] - [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] - [BitpayAPIConstraint(false)] - public IActionResult ChangeInvoiceState(string invoiceId, string newState) - { - if (newState == "invalid") - { - return View("Confirm", new ConfirmModel() - { - Action = "Make invoice invalid", - Title = "Change invoice state", - Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?", - }); - } - else if (newState == "complete") - { - return View("Confirm", new ConfirmModel() - { - Action = "Make invoice complete", - Title = "Change invoice state", - Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?", - ButtonClass = "btn-primary" - }); - } - else - return NotFound(); - } - [HttpPost] [Route("invoices/{invoiceId}/changestate/{newState}")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] - public async Task ChangeInvoiceStateConfirm(string invoiceId, string newState) + public async Task ChangeInvoiceState(string invoiceId, string newState) { var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() { InvoiceId = invoiceId, UserId = GetUserId() })).FirstOrDefault(); + + var model = new InvoiceStateChangeModel(); if (invoice == null) - return NotFound(); + { + model.NotFound = true; + return NotFound(model); + } + + if (newState == "invalid") { await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); _EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid)); - StatusMessage = "Invoice marked invalid"; + model.StatusString = new InvoiceState("invalid", "marked").ToString(); } else if (newState == "complete") { await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId); _EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted)); - StatusMessage = "Invoice marked complete"; + model.StatusString = new InvoiceState("complete", "marked").ToString(); } - return RedirectToAction(nameof(ListInvoices)); + + return Json(model); + } + + public class InvoiceStateChangeModel + { + public bool NotFound { get; set; } + public string StatusString { get; set; } } [TempData] diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 64be2a1c5..a66147df1 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers private CurrencyNameTable _CurrencyNameTable; EventAggregator _EventAggregator; BTCPayNetworkProvider _NetworkProvider; - private readonly BTCPayWalletProvider _WalletProvider; + private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; IServiceProvider _ServiceProvider; public InvoiceController( IServiceProvider serviceProvider, @@ -46,9 +46,9 @@ namespace BTCPayServer.Controllers RateFetcher rateProvider, StoreRepository storeRepository, EventAggregator eventAggregator, - BTCPayWalletProvider walletProvider, ContentSecurityPolicies csp, - BTCPayNetworkProvider networkProvider) + BTCPayNetworkProvider networkProvider, + PaymentMethodHandlerDictionary paymentMethodHandlerDictionary) { _ServiceProvider = serviceProvider; _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); @@ -58,7 +58,7 @@ namespace BTCPayServer.Controllers _UserManager = userManager; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; - _WalletProvider = walletProvider; + _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _CSP = csp; } @@ -67,13 +67,10 @@ namespace BTCPayServer.Controllers { if (!store.HasClaim(Policies.CanCreateInvoice.Key)) throw new UnauthorizedAccessException(); + invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; InvoiceLogs logs = new InvoiceLogs(); logs.Write("Creation of invoice starting"); - var entity = new InvoiceEntity - { - Version = InvoiceEntity.Lastest_Version, - InvoiceTime = DateTimeOffset.UtcNow - }; + var entity = _InvoiceRepository.CreateNewInvoice(); var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); var storeBlob = store.GetStoreBlob(); @@ -148,7 +145,7 @@ namespace BTCPayServer.Controllers foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId)) - .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) + .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) .Where(c => c != null)) { currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency)); @@ -160,14 +157,13 @@ namespace BTCPayServer.Controllers var rateRules = storeBlob.GetRateRules(_NetworkProvider); var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); - var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) - .Where(s => !excludeFilter.Match(s.PaymentId)) + .Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId)) .Select(c => - (Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())), + (Handler: _paymentMethodHandlerDictionary[c.PaymentId], SupportedPaymentMethod: c, - Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))) + Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))) .Where(c => c.Network != null) .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, @@ -206,7 +202,7 @@ namespace BTCPayServer.Controllers using (logs.Measure("Saving invoice")) { - entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider); + entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); } _ = Task.Run(async () => { @@ -221,7 +217,7 @@ namespace BTCPayServer.Controllers await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs); }); _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created)); - var resp = entity.EntityToDTO(_NetworkProvider); + var resp = entity.EntityToDTO(); return new DataWrapper(resp) { Facade = "pos/invoice" }; } @@ -244,11 +240,11 @@ namespace BTCPayServer.Controllers }).ToArray()); } - private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) + private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) { try { - var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:"; + var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var storeBlob = store.GetStoreBlob(); var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; @@ -269,38 +265,15 @@ namespace BTCPayServer.Controllers paymentMethod.SetPaymentMethodDetails(paymentDetails); } - Func compare = null; - CurrencyValue limitValue = null; - string errorMessage = null; - if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && - storeBlob.LightningMaxValue != null) + var errorMessage = await + handler + .IsPaymentMethodAllowedBasedOnInvoiceAmount(storeBlob, fetchingByCurrencyPair, + paymentMethod.Calculate().Due, supportedPaymentMethod.PaymentId); + if (errorMessage != null) { - compare = (a, b) => a > b; - limitValue = storeBlob.LightningMaxValue; - errorMessage = "The amount of the invoice is too high to be paid with lightning"; + logs.Write($"{logPrefix} {errorMessage}"); + return null; } - else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike && - storeBlob.OnChainMinValue != null) - { - compare = (a, b) => a < b; - limitValue = storeBlob.OnChainMinValue; - errorMessage = "The amount of the invoice is too low to be paid on chain"; - } - - if (compare != null) - { - var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)]; - if (limitValueRate.BidAsk != null) - { - var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid); - if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) - { - logs.Write($"{logPrefix} {errorMessage}"); - return null; - } - } - } - /////////////// #pragma warning disable CS0618 diff --git a/BTCPayServer/Controllers/ManageController.2FA.cs b/BTCPayServer/Controllers/ManageController.2FA.cs new file mode 100644 index 000000000..effc013ee --- /dev/null +++ b/BTCPayServer/Controllers/ManageController.2FA.cs @@ -0,0 +1,205 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Models.ManageViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Controllers +{ + public partial class ManageController + { + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + [HttpGet] + public async Task TwoFactorAuthentication() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new TwoFactorAuthenticationViewModel + { + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, + Is2faEnabled = user.TwoFactorEnabled, + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), + }; + + return View(model); + } + + [HttpGet] + public async Task Disable2faWarning() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException( + $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); + } + + return View(nameof(Disable2fa)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disable2fa() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new ApplicationException( + $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); + } + + _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + [HttpGet] + public async Task EnableAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + var model = new EnableAuthenticatorViewModel + { + SharedKey = FormatKey(unformattedKey), + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) + }; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + // Strip spaces and hypens + var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture) + .Replace("-", string.Empty, StringComparison.InvariantCulture); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError(nameof(model.Code), "Verification code is invalid."); + return View(model); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); + return RedirectToAction(nameof(GenerateRecoveryCodes)); + } + + [HttpGet] + public IActionResult ResetAuthenticatorWarning() + { + return View(nameof(ResetAuthenticator)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ResetAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); + + return RedirectToAction(nameof(EnableAuthenticator)); + } + + [HttpGet] + public async Task GenerateRecoveryCodes() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException( + $"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()}; + + _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); + + return View(model); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format(CultureInfo.InvariantCulture, + AuthenicatorUriFormat, + _urlEncoder.Encode("BTCPayServer"), + _urlEncoder.Encode(email), + unformattedKey); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.U2F.cs b/BTCPayServer/Controllers/ManageController.U2F.cs new file mode 100644 index 000000000..be4e9bcb1 --- /dev/null +++ b/BTCPayServer/Controllers/ManageController.U2F.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Services.U2F.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class ManageController + { + [HttpGet] + public async Task U2FAuthentication(string statusMessage = null) + { + return View(new U2FAuthenticationViewModel() + { + StatusMessage = statusMessage, + Devices = await _u2FService.GetDevices(_userManager.GetUserId(User)) + }); + } + + [HttpGet] + public async Task RemoveU2FDevice(string id) + { + await _u2FService.RemoveDevice(id, _userManager.GetUserId(User)); + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = "Device removed" + }); + } + + [HttpGet] + public IActionResult AddU2FDevice(string name) + { + if (!_btcPayServerEnvironment.IsSecure) + { + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Cannot register U2F device while not on https or tor" + } + }); + } + + var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User), + Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/')); + + return View(new AddU2FDeviceViewModel() + { + AppId = serverRegisterResponse.AppId, + Challenge = serverRegisterResponse.Challenge, + Version = serverRegisterResponse.Version, + Name = name + }); + } + + [HttpPost] + public async Task AddU2FDevice(AddU2FDeviceViewModel viewModel) + { + var errorMessage = string.Empty; + try + { + if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse, + string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name)) + { + return RedirectToAction("U2FAuthentication", new + + { + StatusMessage = "Device added!" + }); + } + } + catch (Exception e) + { + errorMessage = e.Message; + } + + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage + } + }); + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 681f5c8df..ee69eee44 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Encodings.Web; @@ -9,25 +8,23 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using BTCPayServer.Models; using BTCPayServer.Models.ManageViewModels; using BTCPayServer.Services; using BTCPayServer.Authentication; using Microsoft.AspNetCore.Hosting; -using NBitpayClient; -using NBitcoin; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Mails; using System.Globalization; using BTCPayServer.Security; +using BTCPayServer.Services.U2F; namespace BTCPayServer.Controllers { [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Route("[controller]/[action]")] - public class ManageController : Controller + public partial class ManageController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -36,10 +33,11 @@ namespace BTCPayServer.Controllers private readonly UrlEncoder _urlEncoder; TokenRepository _TokenRepository; IHostingEnvironment _Env; + private readonly U2FService _u2FService; + private readonly BTCPayServerEnvironment _btcPayServerEnvironment; StoreRepository _StoreRepository; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public ManageController( UserManager userManager, @@ -50,7 +48,9 @@ namespace BTCPayServer.Controllers TokenRepository tokenRepository, BTCPayWalletProvider walletProvider, StoreRepository storeRepository, - IHostingEnvironment env) + IHostingEnvironment env, + U2FService u2FService, + BTCPayServerEnvironment btcPayServerEnvironment) { _userManager = userManager; _signInManager = signInManager; @@ -59,6 +59,8 @@ namespace BTCPayServer.Controllers _urlEncoder = urlEncoder; _TokenRepository = tokenRepository; _Env = env; + _u2FService = u2FService; + _btcPayServerEnvironment = btcPayServerEnvironment; _StoreRepository = storeRepository; } @@ -339,163 +341,6 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ExternalLogins)); } - [HttpGet] - public async Task TwoFactorAuthentication() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var model = new TwoFactorAuthenticationViewModel - { - HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, - Is2faEnabled = user.TwoFactorEnabled, - RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), - }; - - return View(model); - } - - [HttpGet] - public async Task Disable2faWarning() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - if (!user.TwoFactorEnabled) - { - throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); - } - - return View(nameof(Disable2fa)); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Disable2fa() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); - if (!disable2faResult.Succeeded) - { - throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); - } - - _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); - return RedirectToAction(nameof(TwoFactorAuthentication)); - } - - [HttpGet] - public async Task EnableAuthenticator() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); - if (string.IsNullOrEmpty(unformattedKey)) - { - await _userManager.ResetAuthenticatorKeyAsync(user); - unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); - } - - var model = new EnableAuthenticatorViewModel - { - SharedKey = FormatKey(unformattedKey), - AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) - }; - - return View(model); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - // Strip spaces and hypens - var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture); - - var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( - user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); - - if (!is2faTokenValid) - { - ModelState.AddModelError(nameof(model.Code), "Verification code is invalid."); - return View(model); - } - - await _userManager.SetTwoFactorEnabledAsync(user, true); - _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); - return RedirectToAction(nameof(GenerateRecoveryCodes)); - } - - [HttpGet] - public IActionResult ResetAuthenticatorWarning() - { - return View(nameof(ResetAuthenticator)); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task ResetAuthenticator() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - await _userManager.SetTwoFactorEnabledAsync(user, false); - await _userManager.ResetAuthenticatorKeyAsync(user); - _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); - - return RedirectToAction(nameof(EnableAuthenticator)); - } - - [HttpGet] - public async Task GenerateRecoveryCodes() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - if (!user.TwoFactorEnabled) - { - throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); - } - - var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); - var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; - - _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); - - return View(model); - } #region Helpers @@ -506,33 +351,6 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(string.Empty, error.Description); } } - - private string FormatKey(string unformattedKey) - { - var result = new StringBuilder(); - int currentPosition = 0; - while (currentPosition + 4 < unformattedKey.Length) - { - result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); - currentPosition += 4; - } - if (currentPosition < unformattedKey.Length) - { - result.Append(unformattedKey.Substring(currentPosition)); - } - - return result.ToString().ToLowerInvariant(); - } - - private string GenerateQrCodeUri(string email, string unformattedKey) - { - return string.Format(CultureInfo.InvariantCulture, - AuthenicatorUriFormat, - _urlEncoder.Encode("BTCPayServer"), - _urlEncoder.Encode(email), - unformattedKey); - } - #endregion } } diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index 587359638..2281cb2b3 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -6,6 +6,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Events; using BTCPayServer.Filters; using BTCPayServer.Models; using BTCPayServer.Models.PaymentRequestViewModels; @@ -41,6 +42,7 @@ namespace BTCPayServer.Controllers private readonly EventAggregator _EventAggregator; private readonly CurrencyNameTable _Currencies; private readonly HtmlSanitizer _htmlSanitizer; + private readonly InvoiceRepository _InvoiceRepository; public PaymentRequestController( InvoiceController invoiceController, @@ -50,7 +52,8 @@ namespace BTCPayServer.Controllers PaymentRequestService paymentRequestService, EventAggregator eventAggregator, CurrencyNameTable currencies, - HtmlSanitizer htmlSanitizer) + HtmlSanitizer htmlSanitizer, + InvoiceRepository invoiceRepository) { _InvoiceController = invoiceController; _UserManager = userManager; @@ -60,6 +63,7 @@ namespace BTCPayServer.Controllers _EventAggregator = eventAggregator; _Currencies = currencies; _htmlSanitizer = htmlSanitizer; + _InvoiceRepository = invoiceRepository; } [HttpGet] @@ -150,7 +154,7 @@ namespace BTCPayServer.Controllers blob.Email = viewModel.Email; blob.Description = _htmlSanitizer.Sanitize(viewModel.Description); blob.Amount = viewModel.Amount; - blob.ExpiryDate = viewModel.ExpiryDate; + blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime(); blob.Currency = viewModel.Currency; blob.EmbeddedCSS = viewModel.EmbeddedCSS; blob.CustomCSSLink = viewModel.CustomCSSLink; @@ -212,7 +216,7 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{id}")] [AllowAnonymous] - public async Task ViewPaymentRequest(string id) + public async Task ViewPaymentRequest(string id, string statusMessage = null) { var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId()); if (result == null) @@ -220,6 +224,8 @@ namespace BTCPayServer.Controllers return NotFound(); } result.HubPath = PaymentRequestHub.GetHubPath(this.Request); + result.StatusMessage = statusMessage; + return View(result); } @@ -313,7 +319,39 @@ namespace BTCPayServer.Controllers } } + [HttpGet] + [Route("{id}/cancel")] + public async Task CancelUnpaidPendingInvoice(string id, bool redirect = true) + { + var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId()); + if (result == null ) + { + return NotFound(); + } + var invoice = result.Invoices.SingleOrDefault(requestInvoice => + requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New),StringComparison.InvariantCulture) && !requestInvoice.Payments.Any()); + + if (invoice == null ) + { + return BadRequest("No unpaid pending invoice to cancel"); + } + + await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id); + _EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, InvoiceEvent.MarkedInvalid)); + + if (redirect) + { + return RedirectToAction(nameof(ViewPaymentRequest), new + { + Id = id, + StatusMessage = "Payment cancelled" + }); + } + + return Ok("Payment cancelled"); + } + private string GetUserId() { return _UserManager.GetUserId(User); diff --git a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs index 33111d947..3095bc507 100644 --- a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs +++ b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers try { var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); - var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode); + var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode); var nodeInfo = await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails, network); diff --git a/BTCPayServer/Controllers/ServerController.Storage.cs b/BTCPayServer/Controllers/ServerController.Storage.cs index b0288bf9e..5d8e04f67 100644 --- a/BTCPayServer/Controllers/ServerController.Storage.cs +++ b/BTCPayServer/Controllers/ServerController.Storage.cs @@ -14,6 +14,7 @@ using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage; using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration; using BTCPayServer.Storage.Services.Providers.Models; using BTCPayServer.Storage.ViewModels; +using BTCPayServer.Views; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; @@ -26,7 +27,7 @@ namespace BTCPayServer.Controllers public async Task Files(string fileId = null, string statusMessage = null) { TempData["StatusMessage"] = statusMessage; - var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId); + var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId); return View(new ViewFilesViewModel() { @@ -96,7 +97,7 @@ namespace BTCPayServer.Controllers return NotFound(); } - var expiry = DateTimeOffset.Now; + var expiry = DateTimeOffset.UtcNow; switch (viewModel.TimeType) { case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds: @@ -115,14 +116,15 @@ namespace BTCPayServer.Controllers throw new ArgumentOutOfRangeException(); } - var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload); + var url = await _FileService.GetTemporaryFileUrl(Request.GetAbsoluteRootUri(), fileId, expiry, viewModel.IsDownload); return RedirectToAction(nameof(Files), new { StatusMessage = new StatusMessageModel() { + Severity = StatusMessageModel.StatusSeverity.Success, Html = - $"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. {url}" + $"Generated Temporary Url for file {file.FileName} which expires at {expiry.ToBrowserDate()}. {url}" }.ToString(), fileId, }); diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index d9d01cb43..e9ecb20c6 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -505,7 +505,7 @@ namespace BTCPayServer.Controllers public async Task Services() { var result = new ServicesViewModel(); - result.ExternalServices = _Options.ExternalServices; + result.ExternalServices = _Options.ExternalServices.ToList(); foreach (var externalService in _Options.OtherExternalServices) { result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService() @@ -532,6 +532,10 @@ namespace BTCPayServer.Controllers Link = $"http://{torService.OnionHost}" }); } + else if (TryParseAsExternalService(torService, out var externalService)) + { + result.ExternalServices.Add(externalService); + } else { result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService() @@ -551,6 +555,32 @@ namespace BTCPayServer.Controllers return View(result); } + private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService) + { + externalService = null; + if (torService.ServiceType == TorServiceType.P2P) + { + externalService = new ExternalService() + { + CryptoCode = torService.Network.CryptoCode, + DisplayName = "Full node P2P", + Type = ExternalServiceTypes.P2P, + ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)), + ServiceName = torService.Name, + }; + } + return externalService != null; + } + + private ExternalService GetService(string serviceName, string cryptoCode) + { + var result = _Options.ExternalServices.GetService(serviceName, cryptoCode); + if (result != null) + return result; + _torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result)); + return result; + } + [Route("server/services/{serviceName}/{cryptoCode}")] public async Task Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null) { @@ -559,13 +589,22 @@ namespace BTCPayServer.Controllers StatusMessage = $"Error: {cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + var service = GetService(serviceName, cryptoCode); if (service == null) return NotFound(); try { - var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); + if (service.Type == ExternalServiceTypes.P2P) + { + return View("P2PService", new LightningWalletServices() + { + ShowQR = showQR, + WalletName = service.ServiceName, + ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash() + }); + } + var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType); switch (service.Type) { case ExternalServiceTypes.Charge: @@ -674,14 +713,14 @@ namespace BTCPayServer.Controllers StatusMessage = $"Error: {cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + var service = GetService(serviceName, cryptoCode); if (service == null) return NotFound(); ExternalConnectionString connectionString = null; try { - connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); + connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType); } catch (Exception ex) { @@ -836,14 +875,16 @@ namespace BTCPayServer.Controllers .ToList(); vm.LogFileOffset = offset; - if (string.IsNullOrEmpty(file)) + if (string.IsNullOrEmpty(file) || !file.EndsWith(fileExtension, StringComparison.Ordinal)) return View("Logs", vm); vm.Log = ""; - var path = Path.Combine(di.FullName, file); + var fi = vm.LogFiles.FirstOrDefault(o => o.Name == file); + if (fi == null) + return NotFound(); try { using (var fileStream = new FileStream( - path, + fi.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) diff --git a/BTCPayServer/Controllers/StorageController.cs b/BTCPayServer/Controllers/StorageController.cs index 4bac3beac..8c2a77978 100644 --- a/BTCPayServer/Controllers/StorageController.cs +++ b/BTCPayServer/Controllers/StorageController.cs @@ -1,23 +1,28 @@ +using System; using System.Threading.Tasks; +using BTCPayServer.Configuration; using BTCPayServer.Storage.Services; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Storage { [Route("Storage")] - public class StorageController + public class StorageController : Controller { private readonly FileService _FileService; + private string _dir; - public StorageController(FileService fileService) + public StorageController(FileService fileService, BTCPayServerOptions serverOptions) { _FileService = fileService; + _dir =FileSystemFileProviderService.GetTempStorageDir(serverOptions); } [HttpGet("{fileId}")] public async Task GetFile(string fileId) { - var url = await _FileService.GetFileUrl(fileId); + var url = await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId); return new RedirectResult(url); } } diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 05eea079c..98ea11f86 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Services; using LedgerWallet; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBXplorer.DerivationStrategy; @@ -36,10 +39,19 @@ namespace BTCPayServer.Controllers DerivationSchemeViewModel vm = new DerivationSchemeViewModel(); vm.CryptoCode = cryptoCode; vm.RootKeyPath = network.GetRootKeyPath(); + vm.Network = network; SetExistingValues(store, vm); return View(vm); } + class GetXPubs + { + public BitcoinExtPubKey ExtPubKey { get; set; } + public DerivationStrategyBase DerivationScheme { get; set; } + public HDFingerprint RootFingerprint { get; set; } + public string Source { get; set; } + } + [HttpGet] [Route("{storeId}/derivations/{cryptoCode}/ledger/ws")] public async Task AddDerivationSchemeLedger( @@ -52,9 +64,9 @@ namespace BTCPayServer.Controllers return NotFound(); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var hw = new HardwareWalletService(webSocket); + var hw = new LedgerHardwareWalletService(webSocket); object result = null; - var network = _NetworkProvider.GetNetwork(cryptoCode); + var network = _NetworkProvider.GetNetwork(cryptoCode); using (var normalOperationTimeout = new CancellationTokenSource()) { @@ -70,7 +82,18 @@ namespace BTCPayServer.Controllers var k = KeyPath.Parse(keyPath); if (k.Indexes.Length == 0) throw new FormatException("Invalid key path"); - var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token); + + var getxpubResult = new GetXPubs(); + getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token); + var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit; + var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions() + { + P2SH = segwit, + Legacy = !segwit + }); + getxpubResult.DerivationScheme = derivation; + getxpubResult.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).ExtPubKey.PubKey.GetHDFingerPrint(); + getxpubResult.Source = hw.Device; result = getxpubResult; } } @@ -84,7 +107,7 @@ namespace BTCPayServer.Controllers if (result != null) { UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); - var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings)); + var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings)); await webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); } } @@ -97,24 +120,34 @@ namespace BTCPayServer.Controllers return new EmptyResult(); } + + private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm) { - vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString(); + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + if (derivation != null) + { + vm.DerivationScheme = derivation.AccountDerivation.ToString(); + vm.Config = derivation.ToJson(); + } vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike)); } - private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store) + private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store) { var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); var existing = store.GetSupportedPaymentMethods(_NetworkProvider) - .OfType() + .OfType() .FirstOrDefault(d => d.PaymentId == id); return existing; } + + [HttpPost] [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode) + public async Task AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, + string cryptoCode) { vm.CryptoCode = cryptoCode; var store = HttpContext.GetStoreData(); @@ -126,45 +159,100 @@ namespace BTCPayServer.Controllers { return NotFound(); } + + vm.Network = network; vm.RootKeyPath = network.GetRootKeyPath(); + DerivationSchemeSettings strategy = null; + var wallet = _WalletProvider.GetWallet(network); if (wallet == null) { return NotFound(); } - PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); - var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider) - .Where(c => c.PaymentId == paymentMethodId) - .OfType() - .Select(c => c.DerivationStrategyBase.ToString()) - .FirstOrDefault(); - DerivationStrategy strategy = null; - try + if (!string.IsNullOrEmpty(vm.Config)) { - if (!string.IsNullOrEmpty(vm.DerivationScheme)) + if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy)) { - strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network); - vm.DerivationScheme = strategy.ToString(); + vm.StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Config file was not in the correct format" + }.ToString(); + vm.Confirmation = false; + return View(vm); } } - catch + + if (vm.ColdcardPublicFile != null) { - ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); - vm.Confirmation = false; - return View(vm); + if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy)) + { + vm.StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Coldcard public file was not in the correct format" + }.ToString(); + vm.Confirmation = false; + return View(vm); + } } + else + { + try + { + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network); + if (newStrategy.AccountDerivation != strategy?.AccountDerivation) + { + var accountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork); + if (accountKey != null) + { + var accountSettings = newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey); + if (accountSettings != null) + { + accountSettings.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); + accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint)); + } + } + strategy = newStrategy; + strategy.Source = vm.Source; + vm.DerivationScheme = strategy.AccountDerivation.ToString(); + } + } + else + { + strategy = null; + } + } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); + vm.Confirmation = false; + return View(vm); + } + } + + var oldConfig = vm.Config; + vm.Config = strategy == null ? null : strategy.ToJson(); + + PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); + var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider) + .Where(c => c.PaymentId == paymentMethodId) + .OfType() + .FirstOrDefault(); var storeBlob = store.GetStoreBlob(); var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId); var willBeExcluded = !vm.Enabled; var showAddress = // Show addresses if: - // - If the user is testing the hint address in confirmation screen - (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || - // - The user is setting a new derivation scheme - (!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) || - // - The user is clicking on continue without changing anything - (!vm.Confirmation && willBeExcluded == wasExcluded); + // - If the user is testing the hint address in confirmation screen + (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || + // - The user is clicking on continue after changing the config + (!vm.Confirmation && oldConfig != vm.Config) || + // - The user is clickingon continue without changing config nor enabling/disabling + (!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded); showAddress = showAddress && strategy != null; if (!showAddress) @@ -172,10 +260,9 @@ namespace BTCPayServer.Controllers try { if (strategy != null) - await wallet.TrackAsync(strategy.DerivationStrategyBase); + await wallet.TrackAsync(strategy.AccountDerivation); store.SetSupportedPaymentMethod(paymentMethodId, strategy); - storeBlob.SetExcluded(paymentMethodId, willBeExcluded); - storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath)); + storeBlob.SetExcluded(paymentMethodId, willBeExcluded); store.SetStoreBlob(storeBlob); } catch @@ -185,8 +272,14 @@ namespace BTCPayServer.Controllers } await _Repo.UpdateStore(store); - StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified."; - return RedirectToAction(nameof(UpdateStore), new { storeId = storeId }); + if (oldConfig != vm.Config) + StatusMessage = $"Derivation settings for {network.CryptoCode} has been modified."; + if (willBeExcluded != wasExcluded) + { + var label = willBeExcluded ? "disabled" : "enabled"; + StatusMessage = $"On-Chain payments for {network.CryptoCode} has been {label}."; + } + return RedirectToAction(nameof(UpdateStore), new {storeId = storeId}); } else if (!string.IsNullOrEmpty(vm.HintAddress)) { @@ -203,27 +296,43 @@ namespace BTCPayServer.Controllers try { - strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network); + var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network); + if (newStrategy.AccountDerivation != strategy.AccountDerivation) + { + strategy.AccountDerivation = newStrategy.AccountDerivation; + strategy.AccountOriginal = null; + } } catch { ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address"); return ShowAddresses(vm, strategy); } + vm.HintAddress = ""; - vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\""; + vm.StatusMessage = + "Address successfully found, please verify that the rest is correct and click on \"Confirm\""; ModelState.Remove(nameof(vm.HintAddress)); ModelState.Remove(nameof(vm.DerivationScheme)); } + return ShowAddresses(vm, strategy); } - private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy) + private async Task ReadAllText(IFormFile file) { - vm.DerivationScheme = strategy.DerivationStrategyBase.ToString(); + using (var stream = new StreamReader(file.OpenReadStream())) + { + return await stream.ReadToEndAsync(); + } + } + + private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy) + { + vm.DerivationScheme = strategy.AccountDerivation.ToString(); if (!string.IsNullOrEmpty(vm.DerivationScheme)) { - var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit); + var line = strategy.AccountDerivation.GetLineFor(DerivationFeature.Deposit); for (int i = 0; i < 10; i++) { @@ -232,6 +341,7 @@ namespace BTCPayServer.Controllers } } vm.Confirmation = true; + ModelState.Remove(nameof(vm.Config)); // Remove the cached value return View(vm); } } diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index f37c10b14..a5f998c8f 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -152,7 +152,7 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter"); return View(vm); case "test": - var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService>(); + var handler = _ServiceProvider.GetRequiredService(); try { var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 2bc0e40ae..9c00e5d4e 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning; using BTCPayServer.Rating; using BTCPayServer.Security; using BTCPayServer.Services; +using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; @@ -34,7 +35,7 @@ namespace BTCPayServer.Controllers { [Route("stores")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key)] + [Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { @@ -56,7 +57,8 @@ namespace BTCPayServer.Controllers LanguageService langService, ChangellyClientProvider changellyClientProvider, IOptions mvcJsonOptions, - IHostingEnvironment env, IHttpClientFactory httpClientFactory) + IHostingEnvironment env, IHttpClientFactory httpClientFactory, + PaymentMethodHandlerDictionary paymentMethodHandlerDictionary) { _RateFactory = rateFactory; _Repo = repo; @@ -69,6 +71,7 @@ namespace BTCPayServer.Controllers _WalletProvider = walletProvider; _Env = env; _httpClientFactory = httpClientFactory; + _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; _FeeRateProvider = feeRateProvider; @@ -91,6 +94,7 @@ namespace BTCPayServer.Controllers private readonly ChangellyClientProvider _changellyClientProvider; IHostingEnvironment _Env; private IHttpClientFactory _httpClientFactory; + private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; [TempData] public string StatusMessage @@ -362,7 +366,7 @@ namespace BTCPayServer.Controllers void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData) { var choices = storeData.GetEnabledPaymentIds(_NetworkProvider) - .Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray(); + .Select(o => new CheckoutExperienceViewModel.Format() { Name = o.ToPrettyString(), Value = o.ToString(), PaymentId = o }).ToArray(); var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider); var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId); @@ -370,13 +374,6 @@ namespace BTCPayServer.Controllers vm.DefaultPaymentMethod = chosen?.Value; } - private string GetDisplayName(PaymentMethodId paymentMethodId) - { - var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode; - return paymentMethodId.PaymentType == PaymentTypes.BTCLike ? - display : $"{display} (Lightning)"; - } - [HttpPost] [Route("{storeId}/checkout")] public async Task CheckoutExperience(CheckoutExperienceViewModel model) @@ -470,38 +467,40 @@ namespace BTCPayServer.Controllers var derivationByCryptoCode = store .GetSupportedPaymentMethods(_NetworkProvider) - .OfType() + .OfType() .ToDictionary(c => c.Network.CryptoCode); - foreach (var network in _NetworkProvider.GetAll()) - { - var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); - vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() - { - Crypto = network.CryptoCode, - Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty, - WalletId = new WalletId(store.Id, network.CryptoCode), - Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike)) - }); - } var lightningByCryptoCode = store - .GetSupportedPaymentMethods(_NetworkProvider) - .OfType() - .ToDictionary(c => c.CryptoCode); + .GetSupportedPaymentMethods(_NetworkProvider) + .OfType() + .ToDictionary(c => c.CryptoCode); - foreach (var network in _NetworkProvider.GetAll()) + foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods())) { - var lightning = lightningByCryptoCode.TryGet(network.CryptoCode); - var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike); - vm.LightningNodes.Add(new StoreViewModel.LightningNode() + switch (paymentMethodId.PaymentType) { - CryptoCode = network.CryptoCode, - Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty, - Enabled = !excludeFilters.Match(paymentId) - }); + case BitcoinPaymentType _: + var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode); + vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() + { + Crypto = paymentMethodId.CryptoCode, + Value = strategy?.ToPrettyString() ?? string.Empty, + WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode), + Enabled = !excludeFilters.Match(paymentMethodId) + }); + break; + case LightningPaymentType _: + var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode); + vm.LightningNodes.Add(new StoreViewModel.LightningNode() + { + CryptoCode = paymentMethodId.CryptoCode, + Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty, + Enabled = !excludeFilters.Match(paymentMethodId) + }); + break; + } } - var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled; vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod() { @@ -596,11 +595,11 @@ namespace BTCPayServer.Controllers .ToArray(); } - private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) + private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) { - var parser = new DerivationSchemeParser(network.NBitcoinNetwork); + var parser = new DerivationSchemeParser(network); parser.HintScriptPubKey = hint; - return new DerivationStrategy(parser.Parse(derivationScheme), network); + return new DerivationSchemeSettings(parser.Parse(derivationScheme), network); } [HttpGet] diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs new file mode 100644 index 000000000..fd0b88e6f --- /dev/null +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.ModelBinders; +using BTCPayServer.Models.WalletViewModels; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBXplorer.Models; + +namespace BTCPayServer.Controllers +{ + public partial class WalletsController + { + + [NonAction] + public async Task CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken) + { + var nbx = ExplorerClientProvider.GetExplorerClient(network); + CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); + + foreach (var transactionOutput in sendModel.Outputs) + { + var psbtDestination = new CreatePSBTDestination(); + psbtRequest.Destinations.Add(psbtDestination); + psbtDestination.Destination = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork); + psbtDestination.Amount = Money.Coins(transactionOutput.Amount.Value); + psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput; + } + + if (network.SupportRBF) + { + psbtRequest.RBF = !sendModel.DisableRBF; + } + + psbtRequest.FeePreference = new FeePreference(); + psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1); + if (sendModel.NoChange) + { + psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().Destination; + } + + var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken)); + if (psbt == null) + throw new NotSupportedException("You need to update your version of NBXplorer"); + return psbt; + } + + [HttpGet] + [Route("{walletId}/psbt")] + public async Task WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletPSBTViewModel vm) + { + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt) + { + vm.Decoded = psbt.ToString(); + vm.PSBT = psbt.ToBase64(); + } + return View(vm ?? new WalletPSBTViewModel()); + } + [HttpPost] + [Route("{walletId}/psbt")] + public async Task WalletPSBT( + [ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, + WalletPSBTViewModel vm, string command = null) + { + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + var psbt = await vm.GetPSBT(network.NBitcoinNetwork); + if (psbt == null) + { + ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT"); + return View(vm); + } + switch (command) + { + case null: + vm.Decoded = psbt.ToString(); + ModelState.Remove(nameof(vm.PSBT)); + ModelState.Remove(nameof(vm.FileName)); + ModelState.Remove(nameof(vm.UploadedPSBTFile)); + vm.PSBT = psbt.ToBase64(); + vm.FileName = vm.UploadedPSBTFile?.FileName; + return View(vm); + case "ledger": + return ViewWalletSendLedger(psbt); + case "update": + var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); + psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network); + if (psbt == null) + { + ModelState.AddModelError(nameof(vm.PSBT), "You need to update your version of NBXplorer"); + return View(vm); + } + StatusMessage = "PSBT updated!"; + return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64(), FileName = vm.FileName }); + case "seed": + return SignWithSeed(walletId, psbt.ToBase64()); + case "broadcast": + { + return await WalletPSBTReady(walletId, psbt.ToBase64()); + } + case "combine": + ModelState.Remove(nameof(vm.PSBT)); + return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() }); + case "save-psbt": + return FilePSBT(psbt, vm.FileName); + default: + return View(vm); + } + } + + private async Task UpdatePSBT(DerivationSchemeSettings derivationSchemeSettings, PSBT psbt, BTCPayNetwork network) + { + var result = await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(new UpdatePSBTRequest() + { + PSBT = psbt, + DerivationScheme = derivationSchemeSettings.AccountDerivation, + }); + if (result == null) + return null; + derivationSchemeSettings.RebaseKeyPaths(result.PSBT); + return result.PSBT; + } + + [HttpGet] + [Route("{walletId}/psbt/ready")] + public async Task WalletPSBTReady( + [ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, string psbt = null, + string signingKey = null, + string signingKeyPath = null) + { + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + var vm = new WalletPSBTReadyViewModel() { PSBT = psbt }; + vm.SigningKey = signingKey; + vm.SigningKeyPath = signingKeyPath; + + var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); + if (derivationSchemeSettings == null) + return NotFound(); + await FetchTransactionDetails(derivationSchemeSettings, vm, network); + return View(nameof(WalletPSBTReady), vm); + } + + private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network) + { + var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); + psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? psbtObject; + IHDKey signingKey = null; + RootedKeyPath signingKeyPath = null; + try + { + signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork); + } + catch { } + try + { + signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork); + } + catch { } + + try + { + signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath); + } + catch { } + + if (signingKey == null || signingKeyPath == null) + { + var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings(); + if (signingKey == null) + { + signingKey = signingKeySettings.AccountKey; + vm.SigningKey = signingKey.ToString(); + } + if (vm.SigningKeyPath == null) + { + signingKeyPath = signingKeySettings.GetRootedKeyPath(); + vm.SigningKeyPath = signingKeyPath?.ToString(); + } + } + + if (psbtObject.IsAllFinalized()) + { + vm.CanCalculateBalance = false; + } + else + { + var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath); + vm.BalanceChange = ValueToString(balanceChange, network); + vm.CanCalculateBalance = true; + vm.Positive = balanceChange >= Money.Zero; + } + + foreach (var input in psbtObject.Inputs) + { + var inputVm = new WalletPSBTReadyViewModel.InputViewModel(); + vm.Inputs.Add(inputVm); + var mine = input.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any(); + var balanceChange2 = input.GetTxOut()?.Value ?? Money.Zero; + if (mine) + balanceChange2 = -balanceChange2; + inputVm.BalanceChange = ValueToString(balanceChange2, network); + inputVm.Positive = balanceChange2 >= Money.Zero; + inputVm.Index = (int)input.Index; + } + + foreach (var output in psbtObject.Outputs) + { + var dest = new WalletPSBTReadyViewModel.DestinationViewModel(); + vm.Destinations.Add(dest); + var mine = output.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any(); + var balanceChange2 = output.Value; + if (!mine) + balanceChange2 = -balanceChange2; + dest.Balance = ValueToString(balanceChange2, network); + dest.Positive = balanceChange2 >= Money.Zero; + dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString(); + } + + if (psbtObject.TryGetFee(out var fee)) + { + vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel() + { + Positive = false, + Balance = ValueToString(- fee, network), + Destination = "Mining fees" + }); + } + if (psbtObject.TryGetEstimatedFeeRate(out var feeRate)) + { + vm.FeeRate = feeRate.ToString(); + } + + var sanityErrors = psbtObject.CheckSanity(); + if (sanityErrors.Count != 0) + { + vm.SetErrors(sanityErrors); + } + else if (!psbtObject.IsAllFinalized() && !psbtObject.TryFinalize(out var errors)) + { + vm.SetErrors(errors); + } + } + + [HttpPost] + [Route("{walletId}/psbt/ready")] + public async Task WalletPSBTReady( + [ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletPSBTReadyViewModel vm, string command = null) + { + PSBT psbt = null; + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + try + { + psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); + var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); + if (derivationSchemeSettings == null) + return NotFound(); + await FetchTransactionDetails(derivationSchemeSettings, vm, network); + } + catch + { + vm.GlobalError = "Invalid PSBT"; + return View(vm); + } + if (command == "broadcast") + { + if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) + { + vm.SetErrors(errors); + return View(vm); + } + var transaction = psbt.ExtractTransaction(); + try + { + var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); + if (!broadcastResult.Success) + { + vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; + return View(vm); + } + } + catch (Exception ex) + { + vm.GlobalError = "Error while broadcasting: " + ex.Message; + return View(vm); + } + return await RedirectToWalletTransaction(walletId, transaction); + } + else if (command == "analyze-psbt") + { + return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() }); + } + else + { + vm.GlobalError = "Unknown command"; + return View(vm); + } + } + + private IActionResult ViewPSBT(PSBT psbt, IEnumerable errors = null) + { + return ViewPSBT(psbt, null, errors?.Select(e => e.ToString()).ToList()); + } + private IActionResult ViewPSBT(PSBT psbt, IEnumerable errors = null) + { + return ViewPSBT(psbt, null, errors); + } + private IActionResult ViewPSBT(PSBT psbt, string fileName, IEnumerable errors = null) + { + ModelState.Remove(nameof(WalletPSBTViewModel.PSBT)); + ModelState.Remove(nameof(WalletPSBTViewModel.FileName)); + return View(nameof(WalletPSBT), new WalletPSBTViewModel() + { + Decoded = psbt.ToString(), + FileName = fileName ?? string.Empty, + PSBT = psbt.ToBase64(), + Errors = errors?.ToList() + }); + } + + private IActionResult FilePSBT(PSBT psbt, string fileName) + { + return File(psbt.ToBytes(), "application/octet-stream", fileName); + } + + [HttpPost] + [Route("{walletId}/psbt/combine")] + public async Task WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletPSBTCombineViewModel vm) + { + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + var psbt = await vm.GetPSBT(network.NBitcoinNetwork); + if (psbt == null) + { + ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT"); + return View(vm); + } + var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork); + if (sourcePSBT == null) + { + ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT"); + return View(vm); + } + sourcePSBT = sourcePSBT.Combine(psbt); + StatusMessage = "PSBT Successfully combined!"; + return ViewPSBT(sourcePSBT); + } + } +} diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index b61038826..89ea8a7e0 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.WebSockets; @@ -21,8 +22,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Options; using NBitcoin; +using NBitcoin.DataEncoders; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; @@ -80,9 +83,9 @@ namespace BTCPayServer.Controllers var onChainWallets = stores .SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider) - .OfType() + .OfType() .Select(d => ((Wallet: _walletProvider.GetWallet(d.Network), - DerivationStrategy: d.DerivationStrategyBase, + DerivationStrategy: d.AccountDerivation, Network: d.Network))) .Where(_ => _.Wallet != null) .Select(_ => (Wallet: _.Wallet, @@ -117,13 +120,12 @@ namespace BTCPayServer.Controllers [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId) { - var store = await Repository.FindStore(walletId.StoreId, GetUserId()); - DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store); + DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId); if (paymentMethod == null) return NotFound(); var wallet = _walletProvider.GetWallet(paymentMethod.Network); - var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase); + var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation); var model = new ListTransactionsViewModel(); foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions)) @@ -146,36 +148,43 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false) + WalletId walletId, string defaultDestination = null, string defaultAmount = null) { if (walletId?.StoreId == null) return NotFound(); var store = await Repository.FindStore(walletId.StoreId, GetUserId()); - DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store); + DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId, store); if (paymentMethod == null) return NotFound(); - var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); + var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) return NotFound(); var storeData = store.GetStoreBlob(); var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider); rateRules.Spread = 0.0m; var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD"); - WalletSendModel model = new WalletSendModel() + double.TryParse(defaultAmount, out var amount); + var model = new WalletSendModel() { - Destination = defaultDestination, + Outputs = new List() + { + new WalletSendModel.TransactionOutput() + { + Amount = Convert.ToDecimal(amount), + DestinationAddress = defaultDestination + } + }, CryptoCode = walletId.CryptoCode }; - if (double.TryParse(defaultAmount, out var amount)) - model.Amount = (decimal)amount; + var feeProvider = _feeRateProvider.CreateFeeProvider(network); var recommendedFees = feeProvider.GetFeeRateAsync(); - var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase); + var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation); model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC); model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi; model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte; - + model.SupportRBF = network.SupportRBF; using (CancellationTokenSource cts = new CancellationTokenSource()) { try @@ -195,7 +204,6 @@ namespace BTCPayServer.Controllers } catch (Exception ex) { model.RateError = ex.Message; } } - model.AdvancedMode = advancedMode; return View(model); } @@ -203,64 +211,211 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletSendModel vm, string command = null) + WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default) { if (walletId?.StoreId == null) return NotFound(); var store = await Repository.FindStore(walletId.StoreId, GetUserId()); if (store == null) return NotFound(); - var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); + var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) return NotFound(); - - if (command == "noob" || command == "expert") + vm.SupportRBF = network.SupportRBF; + decimal transactionAmountSum = 0; + + if (command == "add-output") { ModelState.Clear(); - vm.AdvancedMode = command == "expert"; + vm.Outputs.Add(new WalletSendModel.TransactionOutput()); + return View(vm); + } + if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase)) + { + ModelState.Clear(); + var index = int.Parse(command.Substring(command.IndexOf(":",StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture); + vm.Outputs.RemoveAt(index); + return View(vm); + } + + + if (!vm.Outputs.Any()) + { + ModelState.AddModelError(string.Empty, + "Please add at least one transaction output"); return View(vm); } - var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork); - if (destination == null) - ModelState.AddModelError(nameof(vm.Destination), "Invalid address"); - - if (vm.Amount.HasValue) + var subtractFeesOutputsCount = new List(); + var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput); + for (var i = 0; i < vm.Outputs.Count; i++) { - if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees) - ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees"); - if (vm.CurrentBalance < vm.Amount.Value) - ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own"); + var transactionOutput = vm.Outputs[i]; + if (transactionOutput.SubtractFeesFromOutput) + { + subtractFeesOutputsCount.Add(i); + } + var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork); + if (destination == null) + ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Invalid address"); + + if (transactionOutput.Amount.HasValue) + { + transactionAmountSum += transactionOutput.Amount.Value; + + if (vm.CurrentBalance == transactionOutput.Amount.Value && + !transactionOutput.SubtractFeesFromOutput) + vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput, + "You are sending your entire balance to the same destination, you should subtract the fees", + ModelState); + } } + + if (subtractFeesOutputsCount.Count > 1) + { + foreach (var subtractFeesOutput in subtractFeesOutputsCount) + { + vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput, + "You can only subtract fees from one output", ModelState); + } + }else if (vm.CurrentBalance == transactionAmountSum && !substractFees) + { + ModelState.AddModelError(string.Empty, + "You are sending your entire balance, you should subtract the fees from an output"); + } + + if (vm.CurrentBalance < transactionAmountSum) + { + for (var i = 0; i < vm.Outputs.Count; i++) + { + vm.AddModelError(model => model.Outputs[i].Amount, + "You are sending more than what you own", ModelState); + } + } + if (!ModelState.IsValid) return View(vm); - return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel() + DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId); + + CreatePSBTResponse psbt = null; + try { - Destination = vm.Destination, - Amount = vm.Amount.Value, - SubstractFees = vm.SubstractFees, - FeeSatoshiPerByte = vm.FeeSatoshiPerByte, - NoChange = vm.NoChange + psbt = await CreatePSBT(network, derivationScheme, vm, cancellation); + } + catch (NBXplorerException ex) + { + ModelState.AddModelError(string.Empty, ex.Error.Message); + return View(vm); + } + catch (NotSupportedException) + { + ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer"); + return View(vm); + } + derivationScheme.RebaseKeyPaths(psbt.PSBT); + + switch (command) + { + case "ledger": + return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress); + case "seed": + return SignWithSeed(walletId, psbt.PSBT.ToBase64()); + case "analyze-psbt": + var name = + $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; + return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName = name }); + default: + return View(vm); + } + + } + + private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null) + { + return View("WalletSendLedger", new WalletSendLedgerModel() + { + PSBT = psbt.ToBase64(), + HintChange = hintChange?.ToString(), + WebsocketPath = this.Url.Action(nameof(LedgerConnection)), + SuccessPath = this.Url.Action(nameof(WalletPSBTReady)) + }); + } + + [HttpGet("{walletId}/psbt/seed")] + public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId,string psbt) + { + return View(nameof(SignWithSeed), new SignWithSeedViewModel() + { + PSBT = psbt }); } - [HttpGet] - [Route("{walletId}/send/ledger")] - public async Task WalletSendLedger( - [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletSendLedgerModel vm) + [HttpPost("{walletId}/psbt/seed")] + public async Task SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, SignWithSeedViewModel viewModel) { - if (walletId?.StoreId == null) - return NotFound(); - var store = await Repository.FindStore(walletId.StoreId, GetUserId()); - DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store); - if (paymentMethod == null) - return NotFound(); - var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); + if (!ModelState.IsValid) + { + return View(viewModel); + } + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); if (network == null) - return NotFound(); - return View(vm); + throw new FormatException("Invalid value for crypto code"); + + ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork); + + if (extKey == null) + { + ModelState.AddModelError(nameof(viewModel.SeedOrKey), + "Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv"); + } + + var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork); + + if (!psbt.IsReadyToSign()) + { + ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed"); + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + ExtKey signingKey = null; + var settings = (await GetDerivationSchemeSettings(walletId)); + var signingKeySettings = settings.GetSigningAccountKeySettings(); + if (signingKeySettings.RootFingerprint is null) + signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint(); + + RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath(); + // The user gave the root key, let's try to rebase the PSBT, and derive the account private key + if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) + { + psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath); + signingKey = extKey.Derive(rootedKeyPath.KeyPath); + } + // The user maybe gave the account key, let's try to sign with it + else + { + signingKey = extKey; + } + var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath); + if (balanceChange == Money.Zero) + { + ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings."); + return View(viewModel); + } + psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath); + ModelState.Remove(nameof(viewModel.PSBT)); + return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString()); + } + + private string ValueToString(Money v, BTCPayNetworkBase network) + { + return v.ToString() + " " + network.CryptoCode; } private IDestination[] ParseDestination(string destination, Network network) @@ -276,6 +431,19 @@ namespace BTCPayServer.Controllers } } + private async Task RedirectToWalletTransaction(WalletId walletId, Transaction transaction) + { + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + if (transaction != null) + { + var wallet = _walletProvider.GetWallet(network); + var derivationSettings = await GetDerivationSchemeSettings(walletId); + wallet.InvalidateCache(derivationSettings.AccountDerivation); + StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})"; + } + return RedirectToAction(nameof(WalletTransactions)); + } + [HttpGet] [Route("{walletId}/rescan")] public async Task WalletRescan( @@ -284,8 +452,7 @@ namespace BTCPayServer.Controllers { if (walletId?.StoreId == null) return NotFound(); - var store = await Repository.FindStore(walletId.StoreId, GetUserId()); - DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store); + DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId); if (paymentMethod == null) return NotFound(); @@ -294,7 +461,7 @@ namespace BTCPayServer.Controllers vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true"); vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true; var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode); - var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase); + var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation); if (scanProgress != null) { vm.PreviousError = scanProgress.Error; @@ -321,21 +488,20 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{walletId}/rescan")] - [Authorize(Policy = Policies.CanModifyServerSettings.Key)] + [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)] public async Task WalletRescan( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, RescanWalletModel vm) { if (walletId?.StoreId == null) return NotFound(); - var store = await Repository.FindStore(walletId.StoreId, GetUserId()); - DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store); + DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId); if (paymentMethod == null) return NotFound(); var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode); try { - await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex); + await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex); } catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress") { @@ -357,18 +523,24 @@ namespace BTCPayServer.Controllers return null; } - private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store) + private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId, StoreData store) { if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key)) return null; var paymentMethod = store .GetSupportedPaymentMethods(NetworkProvider) - .OfType() + .OfType() .FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode); return paymentMethod; } + private async Task GetDerivationSchemeSettings(WalletId walletId) + { + var store = (await Repository.FindStore(walletId.StoreId, GetUserId())); + return GetDerivationSchemeSettings(walletId, store); + } + private static async Task GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy) { using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) @@ -389,17 +561,6 @@ namespace BTCPayServer.Controllers return _userManager.GetUserId(User); } - [HttpGet] - [Route("{walletId}/send/ledger/success")] - public IActionResult WalletSendLedgerSuccess( - [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, - string txid) - { - StatusMessage = $"Transaction broadcasted ({txid})"; - return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() }); - } - [HttpGet] [Route("{walletId}/send/ledger/ws")] public async Task LedgerConnection( @@ -410,16 +571,18 @@ namespace BTCPayServer.Controllers // getxpub int account = 0, // sendtoaddress - bool noChange = false, - string destination = null, string amount = null, string feeRate = null, string substractFees = null + string psbt = null, + string hintChange = null ) { if (!HttpContext.WebSockets.IsWebSocketRequest) return NotFound(); - var cryptoCode = walletId.CryptoCode; + var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + if (network == null) + throw new FormatException("Invalid value for crypto code"); var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); - var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase; + var derivationSettings = GetDerivationSchemeSettings(walletId, storeData); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); @@ -427,63 +590,11 @@ namespace BTCPayServer.Controllers using (var signTimeout = new CancellationTokenSource()) { normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30)); - var hw = new HardwareWalletService(webSocket); + var hw = new LedgerHardwareWalletService(webSocket); + var model = new WalletSendLedgerModel(); object result = null; try { - BTCPayNetwork network = null; - if (cryptoCode != null) - { - network = NetworkProvider.GetNetwork(cryptoCode); - if (network == null) - throw new FormatException("Invalid value for crypto code"); - } - - BitcoinAddress destinationAddress = null; - if (destination != null) - { - try - { - destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork); - } - catch { } - if (destinationAddress == null) - throw new FormatException("Invalid value for destination"); - } - - FeeRate feeRateValue = null; - if (feeRate != null) - { - try - { - feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1); - } - catch { } - if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero) - throw new FormatException("Invalid value for fee rate"); - } - - Money amountBTC = null; - if (amount != null) - { - try - { - amountBTC = Money.Parse(amount); - } - catch { } - if (amountBTC == null || amountBTC <= Money.Zero) - throw new FormatException("Invalid value for amount"); - } - - bool subsctractFeesValue = false; - if (substractFees != null) - { - try - { - subsctractFeesValue = bool.Parse(substractFees); - } - catch { throw new FormatException("Invalid value for subtract fees"); } - } if (command == "test") { result = await hw.Test(normalOperationTimeout.Token); @@ -492,127 +603,60 @@ namespace BTCPayServer.Controllers { if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new Exception($"{network.CryptoCode}: not started or fully synched"); - var strategy = GetDirectDerivationStrategy(derivationScheme); - var wallet = _walletProvider.GetWallet(network); - var change = wallet.GetChangeAddressAsync(derivationScheme); - var keypaths = new Dictionary(); - List availableCoins = new List(); - foreach (var c in await wallet.GetUnspentCoins(derivationScheme)) - { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); - availableCoins.Add(c.Coin); - } - var changeAddress = await change; - - var storeBlob = storeData.GetStoreBlob(); - var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); - var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId); - // Some deployment have the wallet root key path saved in the store blob - // If it does, we only have to make 1 call to the hw to check if it can sign the given strategy, - if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token)) + var accountKey = derivationSettings.GetSigningAccountKeySettings(); + // Some deployment does not have the AccountKeyPath set, let's fix this... + if (accountKey.AccountKeyPath == null) { // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy - foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token); - if (foundKeyPath == null) - throw new HardwareWalletException($"This store is not configured to use this ledger"); - storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath); - storeData.SetStoreBlob(storeBlob); + var foundKeyPath = await hw.FindKeyPathFromDerivation(network, + derivationSettings.AccountDerivation, + normalOperationTimeout.Token); + accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger"); + storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } -retry: - var send = new[] { ( - destination: destinationAddress as IDestination, - amount: amountBTC, - substractFees: subsctractFeesValue) }; - - foreach (var element in send) - { - if (element.destination == null) - throw new ArgumentNullException(nameof(element.destination)); - if (element.amount == null) - throw new ArgumentNullException(nameof(element.amount)); - if (element.amount <= Money.Zero) - throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); - } - - TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder(); - builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; - builder.AddCoins(availableCoins); - - foreach (var element in send) - { - builder.Send(element.destination, element.amount); - if (element.substractFees) - builder.SubtractFees(); - } - - builder.SetChange(changeAddress.Item1); - - if (network.MinFee == null) - { - builder.SendEstimatedFees(feeRateValue); - } + // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger else { - var estimatedFee = builder.EstimateFees(feeRateValue); - if (network.MinFee > estimatedFee) - builder.SendFees(network.MinFee); - else - builder.SendEstimatedFees(feeRateValue); - } - var unsigned = builder.BuildTransaction(false); - - var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey); - if (noChange && hasChange) - { - availableCoins = builder.FindSpentCoins(unsigned).Cast().ToList(); - amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum(); - subsctractFeesValue = true; - goto retry; - } - - var usedCoins = builder.FindSpentCoins(unsigned); - - Dictionary parentTransactions = new Dictionary(); - - if (!strategy.Segwit) - { - var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet(); - var explorer = ExplorerClientProvider.GetExplorerClient(network); - var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList(); - foreach (var getTransactionAsync in getTransactionAsyncs) + // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, + // but some deployment does not have it, so let's use AccountKeyPath instead + if (accountKey.RootFingerprint == null) { - var tx = (await getTransactionAsync.Op); - if (tx == null) - throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found"); - parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction); + + var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token); + if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey())) + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + // We have the root fingerprint, we can check the root from it + else + { + var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token); + if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value) + throw new HardwareWalletException($"This store is not configured to use this ledger"); } } + // Some deployment does not have the RootFingerprint set, let's fix this... + if (accountKey.RootFingerprint == null) + { + accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint(); + storeData.SetSupportedPaymentMethod(derivationSettings); + await Repository.UpdateStore(storeData); + } + + var psbtResponse = new CreatePSBTResponse() + { + PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork), + ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork) + }; + + + derivationSettings.RebaseKeyPaths(psbtResponse.PSBT); signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); - var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest - { - InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash), - InputCoin = c, - KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), - PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey - }).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token); - try - { - var broadcastResult = await wallet.BroadcastTransactionsAsync(new List() { transaction }); - if (!broadcastResult[0].Success) - { - throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}"); - } - } - catch (Exception ex) - { - throw new Exception("Error while broadcasting: " + ex.Message); - } - wallet.InvalidateCache(derivationScheme); - result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() }; + psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token); + result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() }; } } catch (OperationCanceledException) @@ -638,14 +682,57 @@ retry: return new EmptyResult(); } - private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy) + [Route("{walletId}/settings")] + public async Task WalletSettings( + [ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId) { - if (strategy == null) - throw new Exception("The derivation scheme is not provided"); - var directStrategy = strategy as DirectDerivationStrategy; - if (directStrategy == null) - directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; - return directStrategy; + var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); + if (derivationSchemeSettings == null) + return NotFound(); + var store = (await Repository.FindStore(walletId.StoreId, GetUserId())); + var vm = new WalletSettingsViewModel() + { + Label = derivationSchemeSettings.Label, + DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(), + DerivationSchemeInput = derivationSchemeSettings.AccountOriginal, + SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString() + }; + vm.AccountKeys = derivationSchemeSettings.AccountKeySettings + .Select(e => new WalletSettingsAccountKeyViewModel() + { + AccountKey = e.AccountKey.ToString(), + MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null, + AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" + }).ToList(); + return View(vm); + } + + [Route("{walletId}/settings")] + [HttpPost] + public async Task WalletSettings( + [ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletSettingsViewModel vm) + { + if (!ModelState.IsValid) + return View(vm); + var derivationScheme = await GetDerivationSchemeSettings(walletId); + if (derivationScheme == null) + return NotFound(); + derivationScheme.Label = vm.Label; + derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork); + for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++) + { + derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null + : new KeyPath(vm.AccountKeys[i].AccountKeyPath); + derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null + : new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint)); + } + var store = (await Repository.FindStore(walletId.StoreId, GetUserId())); + store.SetSupportedPaymentMethod(derivationScheme); + await Repository.UpdateStore(store); + StatusMessage = "Wallet settings updated"; + return RedirectToAction(nameof(WalletSettings)); } } @@ -656,6 +743,7 @@ retry: public class SendToAddressResult { - public string TransactionId { get; set; } + [JsonProperty("psbt")] + public string PSBT { get; set; } } } diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index 169129606..964dd1a9d 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -1,10 +1,13 @@ using System.Linq; +using BTCPayServer.Authentication.OpenId.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using BTCPayServer.Models; using BTCPayServer.Services.PaymentRequests; +using BTCPayServer.Services.U2F.Models; using BTCPayServer.Storage.Models; using Microsoft.EntityFrameworkCore.Infrastructure; +using OpenIddict.EntityFrameworkCore.Models; namespace BTCPayServer.Data { @@ -98,7 +101,10 @@ namespace BTCPayServer.Data { get; set; } + + public DbSet U2FDevices { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -221,6 +227,10 @@ namespace BTCPayServer.Data builder.Entity() .HasIndex(o => o.Status); + + builder.UseOpenIddict, BTCPayOpenIdToken, string>(); + } } + } diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index ba94e1c42..1c5397c1f 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -64,17 +64,15 @@ namespace BTCPayServer.Data } public IEnumerable GetSupportedPaymentMethods(BTCPayNetworkProvider networks) { + networks = networks.UnfilteredNetworks; #pragma warning disable CS0618 bool btcReturned = false; // Legacy stuff which should go away if (!string.IsNullOrEmpty(DerivationStrategy)) { - if (networks.BTC != null) - { - btcReturned = true; - yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC); - } + btcReturned = true; + yield return DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC); } @@ -84,20 +82,26 @@ namespace BTCPayServer.Data foreach (var strat in strategies.Properties()) { var paymentMethodId = PaymentMethodId.Parse(strat.Name); - var network = networks.GetNetwork(paymentMethodId.CryptoCode); + var network = networks.GetNetwork(paymentMethodId.CryptoCode); if (network != null) { if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned) continue; if (strat.Value.Type == JTokenType.Null) continue; - yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network); + yield return + paymentMethodId.PaymentType.DeserializeSupportedPaymentMethod(network, strat.Value); } } } #pragma warning restore CS0618 } + public void SetSupportedPaymentMethod(ISupportedPaymentMethod supportedPaymentMethod) + { + SetSupportedPaymentMethod(null, supportedPaymentMethod); + } + /// /// Set or remove a new supported payment method for the store /// @@ -105,8 +109,16 @@ namespace BTCPayServer.Data /// The payment method, or null to remove public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod) { - if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId) - throw new InvalidOperationException("Argument mismatch"); + if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId) + { + throw new InvalidOperationException("Incoherent arguments, this should never happen"); + } + if (supportedPaymentMethod == null && paymentMethodId == null) + throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified"); + if (supportedPaymentMethod != null && paymentMethodId == null) + { + paymentMethodId = supportedPaymentMethod.PaymentId; + } #pragma warning disable CS0618 JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies); @@ -134,7 +146,7 @@ namespace BTCPayServer.Data } } - if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) + if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain) { DerivationStrategy = null; } @@ -269,7 +281,7 @@ namespace BTCPayServer.Data public double Multiplier { get; set; } - public decimal Apply(BTCPayNetwork network, decimal rate) + public decimal Apply(BTCPayNetworkBase network, decimal rate) { return rate * (decimal)Multiplier; } @@ -430,23 +442,9 @@ namespace BTCPayServer.Data [Obsolete("Use GetExcludedPaymentMethods instead")] public string[] ExcludedPaymentMethods { get; set; } -#pragma warning disable CS0618 // Type or member is obsolete - public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath) - { - if (keyPath == null) - WalletKeyPathRoots.Remove(paymentMethodId.ToString()); - else - WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString()); - } - public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId) - { - if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k)) - return KeyPath.Parse(k); - return null; - } -#pragma warning restore CS0618 // Type or member is obsolete - [Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")] - public Dictionary WalletKeyPathRoots { get; set; } = new Dictionary(); + + [Obsolete("Use DerivationSchemeSettings instead")] + public Dictionary WalletKeyPathRoots { get; set; } public EmailSettings EmailSettings { get; set; } public bool RedirectAutomatically { get; set; } diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs index a340f108f..d801aa2a6 100644 --- a/BTCPayServer/DerivationSchemeParser.cs +++ b/BTCPayServer/DerivationSchemeParser.cs @@ -12,14 +12,51 @@ namespace BTCPayServer { public class DerivationSchemeParser { - public Network Network { get; set; } + public BTCPayNetwork BtcPayNetwork { get; } + + public Network Network => BtcPayNetwork.NBitcoinNetwork; + public Script HintScriptPubKey { get; set; } - public DerivationSchemeParser(Network expectedNetwork) + Dictionary ElectrumMapping = new Dictionary(); + + public DerivationSchemeParser(BTCPayNetwork expectedNetwork) { - Network = expectedNetwork; + if (expectedNetwork == null) + throw new ArgumentNullException(nameof(expectedNetwork)); + BtcPayNetwork = expectedNetwork; } + + public DerivationStrategyBase ParseElectrum(string str) + { + + if (str == null) + throw new ArgumentNullException(nameof(str)); + str = str.Trim(); + var data = Network.GetBase58CheckEncoder().DecodeData(str); + if (data.Length < 4) + throw new FormatException(); + var prefix = Utils.ToUInt32(data, false); + + var standardPrefix = Utils.ToBytes(0x0488b21eU, false); + for (int ii = 0; ii < 4; ii++) + data[ii] = standardPrefix[ii]; + var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network); + if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) + { + throw new FormatException(); + } + if (type == DerivationType.Segwit) + return new DirectDerivationStrategy(extPubKey) { Segwit = true }; + if (type == DerivationType.Legacy) + return new DirectDerivationStrategy(extPubKey) { Segwit = false }; + if (type == DerivationType.SegwitP2SH) + return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]"); + throw new FormatException(); + } + + public DerivationStrategyBase Parse(string str) { if (str == null) @@ -41,7 +78,7 @@ namespace BTCPayServer } } - if(!Network.Consensus.SupportSegwit) + if (!Network.Consensus.SupportSegwit) hintedLabels.Add("legacy"); try @@ -53,15 +90,6 @@ namespace BTCPayServer { } - Dictionary electrumMapping = new Dictionary(); - //Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py - var standard = 0x0488b21eU; - electrumMapping.Add(standard, new[] { "legacy" }); - var p2wpkh_p2sh = 0x049d7cb2U; - electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" }); - var p2wpkh = 0x4b24746U; - electrumMapping.Add(p2wpkh, Array.Empty()); - var parts = str.Split('-'); bool hasLabel = false; for (int i = 0; i < parts.Length; i++) @@ -84,17 +112,22 @@ namespace BTCPayServer if (data.Length < 4) continue; var prefix = Utils.ToUInt32(data, false); - var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false); + + var standardPrefix = Utils.ToBytes(0x0488b21eU, false); for (int ii = 0; ii < 4; ii++) data[ii] = standardPrefix[ii]; + var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString(); - var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString(); - electrumMapping.TryGetValue(prefix, out string[] labels); - if (labels != null) + if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) { - foreach (var label in labels) + switch (type) { - hintedLabels.Add(label.ToLowerInvariant()); + case DerivationType.Legacy: + hintedLabels.Add("legacy"); + break; + case DerivationType.SegwitP2SH: + hintedLabels.Add("p2sh"); + break; } } parts[i] = derivationScheme; @@ -136,7 +169,7 @@ namespace BTCPayServer resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p))); foreach (var labels in ItemCombinations(hintLabels.ToList())) { - var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray())); + var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray())); if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey) return hinted; } @@ -149,20 +182,20 @@ namespace BTCPayServer } /// - /// Method to create lists containing possible combinations of an input list of items. This is - /// basically copied from code by user "jaolho" on this thread: - /// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values - /// - /// type of the items on the input list - /// list of items - /// minimum number of items wanted in the generated combinations, - /// if zero the empty combination is included, - /// default is one - /// maximum number of items wanted in the generated combinations, - /// default is no maximum limit - /// list of lists for possible combinations of the input items - public static List> ItemCombinations(List inputList, int minimumItems = 1, - int maximumItems = int.MaxValue) + /// Method to create lists containing possible combinations of an input list of items. This is + /// basically copied from code by user "jaolho" on this thread: + /// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values + /// + /// type of the items on the input list + /// list of items + /// minimum number of items wanted in the generated combinations, + /// if zero the empty combination is included, + /// default is one + /// maximum number of items wanted in the generated combinations, + /// default is no maximum limit + /// list of lists for possible combinations of the input items + public static List> ItemCombinations(List inputList, int minimumItems = 1, + int maximumItems = int.MaxValue) { int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1; List> listOfLists = new List>(nonEmptyCombinations + 1); diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs new file mode 100644 index 000000000..e34994e69 --- /dev/null +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Payments; +using NBitcoin; +using NBXplorer.DerivationStrategy; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer +{ + public class DerivationSchemeSettings : ISupportedPaymentMethod + { + public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (derivationStrategy == null) + throw new ArgumentNullException(nameof(derivationStrategy)); + var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy); + return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() }; + } + + public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (config == null) + throw new ArgumentNullException(nameof(config)); + strategy = null; + try + { + strategy = network.NBXplorerNetwork.Serializer.ToObject(config); + strategy.Network = network; + } + catch { } + return strategy != null; + } + + public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings) + { + settings = null; + if (coldcardExport == null) + throw new ArgumentNullException(nameof(coldcardExport)); + if (network == null) + throw new ArgumentNullException(nameof(network)); + var result = new DerivationSchemeSettings(); + result.Source = "Coldcard"; + var derivationSchemeParser = new DerivationSchemeParser(network); + JObject jobj = null; + try + { + jobj = JObject.Parse(coldcardExport); + jobj = (JObject)jobj["keystore"]; + } + catch + { + return false; + } + + if (jobj.ContainsKey("xpub")) + { + try + { + result.AccountOriginal = jobj["xpub"].Value().Trim(); + result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal); + result.AccountKeySettings = new AccountKeySettings[1]; + result.AccountKeySettings[0] = new AccountKeySettings(); + result.AccountKeySettings[0].AccountKey = result.AccountDerivation.GetExtPubKeys().Single().GetWif(network.NBitcoinNetwork); + if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit) + result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation + } + catch + { + return false; + } + } + else + { + return false; + } + + if (jobj.ContainsKey("label")) + { + try + { + result.Label = jobj["label"].Value(); + } + catch { return false; } + } + + if (jobj.ContainsKey("ckcc_xfp")) + { + try + { + result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value()); + } + catch { return false; } + } + + if (jobj.ContainsKey("derivation")) + { + try + { + result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value()); + } + catch { return false; } + } + settings = result; + settings.Network = network; + return true; + } + + public DerivationSchemeSettings() + { + + } + + public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (derivationStrategy == null) + throw new ArgumentNullException(nameof(derivationStrategy)); + AccountDerivation = derivationStrategy; + Network = network; + AccountKeySettings = derivationStrategy.GetExtPubKeys().Select(c => new AccountKeySettings() + { + AccountKey = c.GetWif(network.NBitcoinNetwork) + }).ToArray(); + } + + + BitcoinExtPubKey _SigningKey; + public BitcoinExtPubKey SigningKey + { + get + { + return _SigningKey ?? AccountKeySettings?.Select(k => k.AccountKey).FirstOrDefault(); + } + set + { + _SigningKey = value; + } + } + + [JsonIgnore] + public BTCPayNetwork Network { get; set; } + public string Source { get; set; } + + [Obsolete("Use GetAccountKeySettings().AccountKeyPath instead")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public KeyPath AccountKeyPath { get; set; } + + public DerivationStrategyBase AccountDerivation { get; set; } + public string AccountOriginal { get; set; } + + [Obsolete("Use GetAccountKeySettings().RootFingerprint instead")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public HDFingerprint? RootFingerprint { get; set; } + + [Obsolete("Use GetAccountKeySettings().AccountKey instead")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public BitcoinExtPubKey ExplicitAccountKey { get; set; } + + [JsonIgnore] + [Obsolete("Use GetAccountKeySettings().AccountKey instead")] + public BitcoinExtPubKey AccountKey + { + get + { + return ExplicitAccountKey ?? new BitcoinExtPubKey(AccountDerivation.GetExtPubKeys().First(), Network.NBitcoinNetwork); + } + } + + public AccountKeySettings GetSigningAccountKeySettings() + { + return AccountKeySettings.Single(a => a.AccountKey == SigningKey); + } + + AccountKeySettings[] _AccountKeySettings; + public AccountKeySettings[] AccountKeySettings + { + get + { + // Legacy + if (_AccountKeySettings == null) + { + if (this.Network == null) + return null; + _AccountKeySettings = AccountDerivation.GetExtPubKeys().Select(e => new AccountKeySettings() + { + AccountKey = e.GetWif(this.Network.NBitcoinNetwork), + }).ToArray(); +#pragma warning disable CS0618 // Type or member is obsolete + _AccountKeySettings[0].AccountKeyPath = AccountKeyPath; + _AccountKeySettings[0].RootFingerprint = RootFingerprint; + ExplicitAccountKey = null; + AccountKeyPath = null; + RootFingerprint = null; +#pragma warning restore CS0618 // Type or member is obsolete + } + return _AccountKeySettings; + } + set + { + _AccountKeySettings = value; + } + } + + public IEnumerable GetPSBTRebaseKeyRules() + { + foreach (var accountKey in AccountKeySettings) + { + if (accountKey.AccountKeyPath != null && accountKey.RootFingerprint is HDFingerprint fp) + { + yield return new NBXplorer.Models.PSBTRebaseKeyRules() + { + AccountKey = accountKey.AccountKey, + AccountKeyPath = accountKey.AccountKeyPath, + MasterFingerprint = fp + }; + } + } + } + + public string Label { get; set; } + + [JsonIgnore] + public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike); + + public override string ToString() + { + return AccountDerivation.ToString(); + } + public string ToPrettyString() + { + return !string.IsNullOrEmpty(Label) ? Label : + !String.IsNullOrEmpty(AccountOriginal) ? AccountOriginal : + ToString(); + } + + public string ToJson() + { + return Network.NBXplorerNetwork.Serializer.ToString(this); + } + + public void RebaseKeyPaths(PSBT psbt) + { + foreach (var rebase in GetPSBTRebaseKeyRules()) + { + psbt.RebaseKeyPaths(rebase.AccountKey, rebase.GetRootedKeyPath()); + } + } + } + public class AccountKeySettings + { + public HDFingerprint? RootFingerprint { get; set; } + public KeyPath AccountKeyPath { get; set; } + + public RootedKeyPath GetRootedKeyPath() + { + if (RootFingerprint is HDFingerprint fp && AccountKeyPath != null) + return new RootedKeyPath(fp, AccountKeyPath); + return null; + } + public BitcoinExtPubKey AccountKey { get; set; } + public bool IsFullySetup() + { + return AccountKeyPath != null && RootFingerprint is HDFingerprint; + } + } +} diff --git a/BTCPayServer/DerivationStrategy.cs b/BTCPayServer/DerivationStrategy.cs deleted file mode 100644 index 463cb4a75..000000000 --- a/BTCPayServer/DerivationStrategy.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Payments; -using BTCPayServer.Services.Invoices; -using NBitcoin; -using NBXplorer.DerivationStrategy; - -namespace BTCPayServer -{ - public class DerivationStrategy : ISupportedPaymentMethod - { - private DerivationStrategyBase _DerivationStrategy; - private BTCPayNetwork _Network; - - public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network) - { - this._DerivationStrategy = result; - this._Network = network; - } - - public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (derivationStrategy == null) - throw new ArgumentNullException(nameof(derivationStrategy)); - var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy); - return new DerivationStrategy(result, network); - } - - public BTCPayNetwork Network { get { return this._Network; } } - - public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy; - - public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike); - - public override string ToString() - { - return _DerivationStrategy.ToString(); - } - } -} diff --git a/BTCPayServer/Events/InvoiceEvent.cs b/BTCPayServer/Events/InvoiceEvent.cs index 87d647a82..7a227e1b0 100644 --- a/BTCPayServer/Events/InvoiceEvent.cs +++ b/BTCPayServer/Events/InvoiceEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/BTCPayServer/Events/InvoiceNewAddressEvent.cs b/BTCPayServer/Events/InvoiceNewAddressEvent.cs index b485cfa19..5c7c68153 100644 --- a/BTCPayServer/Events/InvoiceNewAddressEvent.cs +++ b/BTCPayServer/Events/InvoiceNewAddressEvent.cs @@ -7,7 +7,7 @@ namespace BTCPayServer.Events { public class InvoiceNewAddressEvent { - public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network) + public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetworkBase network) { Address = address; InvoiceId = invoiceId; @@ -16,7 +16,7 @@ namespace BTCPayServer.Events public string Address { get; set; } public string InvoiceId { get; set; } - public BTCPayNetwork Network { get; set; } + public BTCPayNetworkBase Network { get; set; } public override string ToString() { return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}"; diff --git a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs index 5774140ff..62d7ba8c4 100644 --- a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs +++ b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs @@ -8,14 +8,14 @@ namespace BTCPayServer.Events { public class NBXplorerStateChangedEvent { - public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState) + public NBXplorerStateChangedEvent(BTCPayNetworkBase network, NBXplorerState old, NBXplorerState newState) { Network = network; NewState = newState; OldState = old; } - public BTCPayNetwork Network { get; set; } + public BTCPayNetworkBase Network { get; set; } public NBXplorerState NewState { get; set; } public NBXplorerState OldState { get; set; } diff --git a/BTCPayServer/ExplorerClientProvider.cs b/BTCPayServer/ExplorerClientProvider.cs index b3888e30c..44614243b 100644 --- a/BTCPayServer/ExplorerClientProvider.cs +++ b/BTCPayServer/ExplorerClientProvider.cs @@ -33,7 +33,7 @@ namespace BTCPayServer Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}"); if (setting.ExplorerUri != null) { - _Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile)); + _Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile)); } } } @@ -58,21 +58,21 @@ namespace BTCPayServer public ExplorerClient GetExplorerClient(string cryptoCode) { - var network = _NetworkProviders.GetNetwork(cryptoCode); + var network = _NetworkProviders.GetNetwork(cryptoCode); if (network == null) return null; _Clients.TryGetValue(network.CryptoCode, out ExplorerClient client); return client; } - public ExplorerClient GetExplorerClient(BTCPayNetwork network) + public ExplorerClient GetExplorerClient(BTCPayNetworkBase network) { if (network == null) throw new ArgumentNullException(nameof(network)); return GetExplorerClient(network.CryptoCode); } - public bool IsAvailable(BTCPayNetwork network) + public bool IsAvailable(BTCPayNetworkBase network) { return IsAvailable(network.CryptoCode); } @@ -84,7 +84,7 @@ namespace BTCPayServer public BTCPayNetwork GetNetwork(string cryptoCode) { - var network = _NetworkProviders.GetNetwork(cryptoCode); + var network = _NetworkProviders.GetNetwork(cryptoCode); if (network == null) return null; if (_Clients.ContainsKey(network.CryptoCode)) @@ -94,7 +94,7 @@ namespace BTCPayServer public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll() { - foreach (var net in _NetworkProviders.GetAll()) + foreach (var net in _NetworkProviders.GetAll().OfType()) { if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer)) { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 91168c85f..2774fdcce 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -81,7 +81,7 @@ namespace BTCPayServer } public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info) { - return new PaymentMethodId(info.CryptoCode, Enum.Parse(info.PaymentType)); + return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType)); } public static async Task CloseSocket(this WebSocket webSocket) { @@ -132,6 +132,12 @@ namespace BTCPayServer return str; return $"/{str}"; } + public static string WithoutEndingSlash(this string str) + { + if (str.EndsWith("/", StringComparison.InvariantCulture)) + return str.Substring(0, str.Length - 1); + return str; + } public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value) { @@ -179,7 +185,7 @@ namespace BTCPayServer } if(IPAddress.TryParse(server, out var ip)) { - return ip.IsLocal(); + return ip.IsLocal() || ip.IsRFC1918(); } return false; } @@ -200,6 +206,11 @@ namespace BTCPayServer request.PathBase.ToUriComponent()); } + public static Uri GetAbsoluteRootUri(this HttpRequest request) + { + return new Uri(request.GetAbsoluteRoot()); + } + public static string GetCurrentUrl(this HttpRequest request) { return string.Concat( @@ -311,13 +322,6 @@ namespace BTCPayServer NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value); } - public static void AddRange(this HashSet hashSet, IEnumerable items) - { - foreach (var item in items) - { - hashSet.Add(item); - } - } public static bool GetIsBitpayAPI(this HttpContext ctx) { return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) && @@ -329,10 +333,15 @@ namespace BTCPayServer NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value); } - public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx) + public static bool TryGetBitpayAuth(this HttpContext ctx, out (string Signature, String Id, String Authorization) result) { - ctx.Items.TryGetValue("BitpayAuth", out object obj); - return ((string Signature, String Id, String Authorization))obj; + if (ctx.Items.TryGetValue("BitpayAuth", out object obj)) + { + result = ((string Signature, String Id, String Authorization))obj; + return true; + } + result = default; + return false; } public static StoreData GetStoreData(this HttpContext ctx) diff --git a/BTCPayServer/Extensions/EmailSenderExtensions.cs b/BTCPayServer/Extensions/EmailSenderExtensions.cs index b1010b57b..933e46fb9 100644 --- a/BTCPayServer/Extensions/EmailSenderExtensions.cs +++ b/BTCPayServer/Extensions/EmailSenderExtensions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Encodings.Web; -using System.Threading.Tasks; -using BTCPayServer.Services; using BTCPayServer.Services.Mails; namespace BTCPayServer.Services diff --git a/BTCPayServer/Extensions/ModelStateExtensions.cs b/BTCPayServer/Extensions/ModelStateExtensions.cs new file mode 100644 index 000000000..3576f4d66 --- /dev/null +++ b/BTCPayServer/Extensions/ModelStateExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace BTCPayServer +{ + public static class ModelStateExtensions + { + public static void AddModelError( + this ModelStateDictionary modelState, + Expression> ex, + string message + ) + { + var key = ExpressionHelper.GetExpressionText(ex); + modelState.AddModelError(key, message); + } + + public static void AddModelError(this TModel source, + Expression> ex, + string message, + ModelStateDictionary modelState) + { + var key = ExpressionHelper.GetExpressionText(ex); + modelState.AddModelError(key, message); + } + } +} diff --git a/BTCPayServer/Extensions/OpenIddictExtensions.cs b/BTCPayServer/Extensions/OpenIddictExtensions.cs new file mode 100644 index 000000000..befccf6fa --- /dev/null +++ b/BTCPayServer/Extensions/OpenIddictExtensions.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Security.Cryptography; +using BTCPayServer.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using NETCore.Encrypt.Extensions.Internal; + +namespace BTCPayServer +{ + public static class OpenIddictExtensions + { + public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder, + IConfiguration configuration) + { + + var file = Path.Combine(configuration.GetDataDir(), "rsaparams"); + + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(2048); + RsaSecurityKey key = null; + + if (File.Exists(file)) + { + RSA.FromXmlString2( File.ReadAllText(file)); + } + else + { + var contents = RSA.ToXmlString2(true); + File.WriteAllText(file,contents ); + } + + RSAParameters KeyParam = RSA.ExportParameters(true); + key = new RsaSecurityKey(KeyParam); + return builder.AddSigningKey(key); + } + } +} diff --git a/BTCPayServer/Extensions/RsaKeyExtensions.cs b/BTCPayServer/Extensions/RsaKeyExtensions.cs new file mode 100644 index 000000000..c201917bb --- /dev/null +++ b/BTCPayServer/Extensions/RsaKeyExtensions.cs @@ -0,0 +1,97 @@ +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Xml; + +namespace NETCore.Encrypt.Extensions.Internal +{ + /// + /// .net core's implementatiosn are still marked as unsupported because of stupid decisions( https://github.com/dotnet/corefx/issues/23686) + /// + internal static class RsaKeyExtensions + { + #region XML + + public static void FromXmlString2(this RSA rsa, string xmlString) + { + RSAParameters parameters = new RSAParameters(); + + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlString); + + if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue", StringComparison.InvariantCulture)) + { + foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) + { + switch (node.Name) + { + case "Modulus": + parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "Exponent": + parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "P": + parameters.P = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "Q": + parameters.Q = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "DP": + parameters.DP = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "DQ": + parameters.DQ = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "InverseQ": + parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + case "D": + parameters.D = (string.IsNullOrEmpty(node.InnerText) + ? null + : Convert.FromBase64String(node.InnerText)); + break; + } + } + } + else + { + throw new Exception("Invalid XML RSA key."); + } + + rsa.ImportParameters(parameters); + } + + public static string ToXmlString2(this RSA rsa, bool includePrivateParameters) + { + RSAParameters parameters = rsa.ExportParameters(includePrivateParameters); + + return string.Format(CultureInfo.InvariantCulture, + "{0}{1}

{2}

{3}{4}{5}{6}{7}
", + parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null, + parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null, + parameters.P != null ? Convert.ToBase64String(parameters.P) : null, + parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null, + parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null, + parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null, + parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null, + parameters.D != null ? Convert.ToBase64String(parameters.D) : null); + } + + #endregion + } +} diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 288066bbb..c12fb2bb8 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -42,7 +42,6 @@ namespace BTCPayServer.HostedServices IBackgroundJobClient _JobClient; EventAggregator _EventAggregator; InvoiceRepository _InvoiceRepository; - BTCPayNetworkProvider _NetworkProvider; private readonly EmailSenderFactory _EmailSenderFactory; public InvoiceNotificationManager( @@ -51,20 +50,18 @@ namespace BTCPayServer.HostedServices EventAggregator eventAggregator, InvoiceRepository invoiceRepository, BTCPayNetworkProvider networkProvider, - ILogger logger, EmailSenderFactory emailSenderFactory) { _Client = httpClientFactory.CreateClient(); _JobClient = jobClient; _EventAggregator = eventAggregator; _InvoiceRepository = invoiceRepository; - _NetworkProvider = networkProvider; _EmailSenderFactory = emailSenderFactory; } void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification) { - var dto = invoice.EntityToDTO(_NetworkProvider); + var dto = invoice.EntityToDTO(); var notification = new InvoicePaymentNotificationEventWrapper() { Data = new InvoicePaymentNotification() diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 35c6de91a..79ad1f773 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -1,7 +1,5 @@ using NBXplorer; using Microsoft.Extensions.Logging; -using NBXplorer.DerivationStrategy; -using NBXplorer.Models; using System; using System.Collections.Generic; using System.Linq; @@ -11,12 +9,8 @@ using BTCPayServer.Logging; using System.Threading; using Microsoft.Extensions.Hosting; using System.Collections.Concurrent; -using BTCPayServer.Services.Wallets; -using BTCPayServer.Controllers; using BTCPayServer.Events; -using Microsoft.AspNetCore.Hosting; using BTCPayServer.Services.Invoices; -using BTCPayServer.Services; namespace BTCPayServer.HostedServices { @@ -42,16 +36,16 @@ namespace BTCPayServer.HostedServices InvoiceRepository _InvoiceRepository; EventAggregator _EventAggregator; - BTCPayNetworkProvider _NetworkProvider; + ExplorerClientProvider _ExplorerClientProvider; public InvoiceWatcher( - BTCPayNetworkProvider networkProvider, InvoiceRepository invoiceRepository, - EventAggregator eventAggregator) + EventAggregator eventAggregator, + ExplorerClientProvider explorerClientProvider) { _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); - _NetworkProvider = networkProvider; + _ExplorerClientProvider = explorerClientProvider; } CompositeDisposable leases = new CompositeDisposable(); @@ -71,11 +65,10 @@ namespace BTCPayServer.HostedServices } var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); - var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider); - var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider); + var allPaymentMethods = invoice.GetPaymentMethods(); + var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting); if (paymentMethod == null) return; - var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired) { if (accounting.Paid >= accounting.MinimumTotalDue) @@ -128,7 +121,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == InvoiceStatus.Paid) { - var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network)); + var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) @@ -152,7 +145,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == InvoiceStatus.Confirmed) { - var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); + var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)); if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed)); @@ -163,15 +156,13 @@ namespace BTCPayServer.HostedServices } - public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting, BTCPayNetworkProvider networkProvider) + public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting) { PaymentMethod result = null; accounting = null; decimal nearestToZero = 0.0m; foreach (var paymentMethod in allPaymentMethods) { - if (networkProvider != null && networkProvider.GetNetwork(paymentMethod.GetId().CryptoCode) == null) - continue; var currentAccounting = paymentMethod.Calculate(); var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC)); if (result == null || distanceFromZero < nearestToZero) @@ -285,8 +276,18 @@ namespace BTCPayServer.HostedServices if (invoice.Status == InvoiceStatus.Complete || ((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { - if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) + var extendInvoiceMonitoring = await UpdateConfirmationCount(invoice); + + // we extend monitor time if we haven't reached max confirmation count + // say user used low fee and we only got 3 confirmations right before it's time to remove + if (extendInvoiceMonitoring) + { + await _InvoiceRepository.ExtendInvoiceMonitor(invoice.Id); + } + else if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) + { _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); + } break; } @@ -304,6 +305,45 @@ namespace BTCPayServer.HostedServices } } + private async Task UpdateConfirmationCount(InvoiceEntity invoice) + { + bool extendInvoiceMonitoring = false; + var updateConfirmationCountIfNeeded = invoice + .GetPayments() + .Select>(async payment => + { + var paymentData = payment.GetCryptoPaymentData(); + if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData) + { + // Do update if confirmation count in the paymentData is not up to date + if ((onChainPaymentData.ConfirmationCount < payment.Network.MaxTrackedConfirmation && payment.Accounted) + && (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) + { + var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash); + var confirmationCount = transactionResult?.Confirmations ?? 0; + onChainPaymentData.ConfirmationCount = confirmationCount; + payment.SetCryptoPaymentData(onChainPaymentData); + + // we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods + if (confirmationCount < payment.Network.MaxTrackedConfirmation) + extendInvoiceMonitoring = true; + + return payment; + } + } + return null; + }) + .ToArray(); + await Task.WhenAll(updateConfirmationCountIfNeeded); + var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList(); + if (updatedPaymentData.Count > 0) + { + await _InvoiceRepository.UpdatePayments(updatedPaymentData); + } + + return extendInvoiceMonitoring; + } + public async Task StopAsync(CancellationToken cancellationToken) { if (_Cts == null) diff --git a/BTCPayServer/HostedServices/MigratorHostedService.cs b/BTCPayServer/HostedServices/MigratorHostedService.cs index f96e77738..c509bd8de 100644 --- a/BTCPayServer/HostedServices/MigratorHostedService.cs +++ b/BTCPayServer/HostedServices/MigratorHostedService.cs @@ -71,6 +71,12 @@ namespace BTCPayServer.HostedServices settings.ConvertCrowdfundOldSettings = true; await _Settings.UpdateSetting(settings); } + if (!settings.ConvertWalletKeyPathRoots) + { + await ConvertConvertWalletKeyPathRoots(); + settings.ConvertWalletKeyPathRoots = true; + await _Settings.UpdateSetting(settings); + } } catch (Exception ex) { @@ -79,6 +85,35 @@ namespace BTCPayServer.HostedServices } } + private async Task ConvertConvertWalletKeyPathRoots() + { + bool save = false; + using (var ctx = _DBContextFactory.CreateContext()) + { + foreach (var store in await ctx.Stores.ToArrayAsync()) + { +#pragma warning disable CS0618 // Type or member is obsolete + var blob = store.GetStoreBlob(); + if (blob.WalletKeyPathRoots == null) + continue; + foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider).OfType()) + { + if (blob.WalletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), out var root)) + { + scheme.AccountKeyPath = new NBitcoin.KeyPath(root); + store.SetSupportedPaymentMethod(scheme); + save = true; + } + } + blob.WalletKeyPathRoots = null; + store.SetStoreBlob(blob); +#pragma warning restore CS0618 // Type or member is obsolete + } + if (save) + await ctx.SaveChangesAsync(); + } + } + private async Task ConvertCrowdfundOldSettings() { using (var ctx = _DBContextFactory.CreateContext()) @@ -159,7 +194,7 @@ namespace BTCPayServer.HostedServices if (lightning.IsLegacy) { method.SetLightningUrl(lightning); - store.SetSupportedPaymentMethod(method.PaymentId, method); + store.SetSupportedPaymentMethod(method); } } } diff --git a/BTCPayServer/HostedServices/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs index f1308b421..04cdf858c 100644 --- a/BTCPayServer/HostedServices/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -24,13 +24,13 @@ namespace BTCPayServer.HostedServices { public class NBXplorerSummary { - public BTCPayNetwork Network { get; set; } + public BTCPayNetworkBase Network { get; set; } public NBXplorerState State { get; set; } public StatusResult Status { get; set; } public string Error { get; set; } } ConcurrentDictionary _Summaries = new ConcurrentDictionary(); - public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status, string error) + public void Publish(BTCPayNetworkBase network, NBXplorerState state, StatusResult status, string error) { var summary = new NBXplorerSummary() { Network = network, State = state, Status = status, Error = error }; _Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary); @@ -49,7 +49,7 @@ namespace BTCPayServer.HostedServices } public NBXplorerSummary Get(string cryptoCode) { - _Summaries.TryGetValue(cryptoCode, out var summary); + _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out var summary); return summary; } public IEnumerable GetAll() diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 419f6dbb6..7cf22bc95 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -1,9 +1,7 @@ using BTCPayServer.Configuration; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Hosting; using System; -using System.Collections.Generic; -using System.Text; +using System.IdentityModel.Tokens.Jwt; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.AspNetCore.Http; @@ -12,7 +10,6 @@ using NBitcoin; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; using System.IO; -using NBXplorer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; using BTCPayServer.Services; @@ -21,44 +18,46 @@ using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Fees; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -using Microsoft.AspNetCore.Authorization; using BTCPayServer.Controllers; using BTCPayServer.Services.Mails; using Microsoft.AspNetCore.Identity; -using BTCPayServer.Models; -using System.Threading.Tasks; using System.Threading; using BTCPayServer.Services.Wallets; using BTCPayServer.Authentication; -using Microsoft.Extensions.Caching.Memory; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Logging; using BTCPayServer.HostedServices; using System.Security.Claims; using BTCPayServer.PaymentRequest; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; using BTCPayServer.Security; using BTCPayServer.Services.PaymentRequests; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc.ModelBinding; using NBXplorer.DerivationStrategy; using NicolasDorier.RateLimits; using Npgsql; using BTCPayServer.Services.Apps; +using OpenIddict.EntityFrameworkCore.Models; +using BTCPayServer.Services.U2F; using BundlerMinifier.TagHelpers; - +using System.Collections.Generic; namespace BTCPayServer.Hosting { public static class BTCPayServerServices { - public static IServiceCollection AddBTCPayServer(this IServiceCollection services) + public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext((provider, o) => { var factory = provider.GetRequiredService(); factory.ConfigureBuilder(o); + o.UseOpenIddict, BTCPayOpenIdToken, string>(); }); services.AddHttpClient(); services.TryAddSingleton(); @@ -66,7 +65,8 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(o => o.GetRequiredService>().Value); + services.TryAddSingleton(o => + o.GetRequiredService>().Value); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); @@ -74,12 +74,13 @@ namespace BTCPayServer.Hosting var dbpath = Path.Combine(opts.DataDir, "InvoiceDB"); if (!Directory.Exists(dbpath)) Directory.CreateDirectory(dbpath); - return new InvoiceRepository(dbContext, dbpath); + return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService()); }); services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(o => { @@ -174,15 +175,18 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); - services.AddSingleton, Payments.Bitcoin.BitcoinLikePaymentHandler>(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton, Payments.Lightning.LightningLikePaymentHandler>(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetService()); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(provider => provider.GetService()); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -217,7 +221,7 @@ namespace BTCPayServer.Hosting // bundling services.AddAuthorization(o => Policies.AddBTCPayPolicies(o)); - BitpayAuthentication.AddAuthentication(services); + services.AddBtcPayServerAuthenticationSchemes(configuration); services.AddSingleton(); services.AddTransient(provider => @@ -229,9 +233,9 @@ namespace BTCPayServer.Hosting return bundle; }); - services.AddCors(options=> + services.AddCors(options => { - options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); + options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); }); var rateLimits = new RateLimitService(); @@ -239,6 +243,22 @@ namespace BTCPayServer.Hosting services.AddSingleton(rateLimits); return services; } + + private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); + + services.AddAuthentication() + .AddJwtBearer(options => + { +// options.RequireHttpsMetadata = false; +// options.TokenValidationParameters.ValidateAudience = false; + options.TokenValidationParameters.ValidateIssuer = false; + }) + .AddCookie() + .AddBitpayAuthentication(); + } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { diff --git a/BTCPayServer/Hosting/ResourceBundleProvider.cs b/BTCPayServer/Hosting/ResourceBundleProvider.cs index 0d7875f1a..6f10cd214 100644 --- a/BTCPayServer/Hosting/ResourceBundleProvider.cs +++ b/BTCPayServer/Hosting/ResourceBundleProvider.cs @@ -28,8 +28,8 @@ namespace BTCPayServer.Hosting return JArray.Parse(content).OfType() .Select(jobj => new Bundle() { - Name = jobj.Property("name")?.Value.Value() ?? jobj.Property("outputFileName").Value.Value(), - OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName").Value.Value()) + Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value(), + OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value()) }).ToDictionary(o => o.Name, o => o); } }, true); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index d0165ea6e..d0662234f 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -1,45 +1,28 @@ using Microsoft.AspNetCore.Hosting; -using System.Reflection; -using System.Linq; using Microsoft.AspNetCore.Builder; using System; -using System.Text; using Microsoft.Extensions.DependencyInjection; - -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.AspNetCore.Mvc; -using NBitpayClient; -using BTCPayServer.Authentication; -using Microsoft.EntityFrameworkCore; using BTCPayServer.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using BTCPayServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.HttpOverrides; using BTCPayServer.Data; using Microsoft.Extensions.Logging; using BTCPayServer.Logging; -using Microsoft.AspNetCore.Authorization; -using System.Threading.Tasks; -using BTCPayServer.Controllers; -using BTCPayServer.Services.Stores; -using BTCPayServer.Services.Mails; using Microsoft.Extensions.Configuration; using BTCPayServer.Configuration; using System.IO; using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Threading; -using Microsoft.Extensions.Options; -using Microsoft.AspNetCore.Mvc.Cors.Internal; +using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; +using BTCPayServer.Security; using Microsoft.AspNetCore.Server.Kestrel.Core; +using OpenIddict.Abstractions; +using OpenIddict.EntityFrameworkCore.Models; using System.Net; using BTCPayServer.PaymentRequest; -using BTCPayServer.Security; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; -using BTCPayServer.Storage.Services.Providers.FileSystemStorage; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.FileProviders; namespace BTCPayServer.Hosting { @@ -65,11 +48,14 @@ namespace BTCPayServer.Hosting services.AddMemoryCache(); services.AddIdentity() .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - services.AddSignalR(); - services.AddBTCPayServer(); + .AddDefaultTokenProviders(); + + ConfigureOpenIddict(services); + + services.AddBTCPayServer(Configuration); services.AddProviderStorage(); services.AddSession(); + services.AddSignalR(); services.AddMvc(o => { o.Filters.Add(new XFrameOptionsAttribute("DENY")); @@ -96,6 +82,13 @@ namespace BTCPayServer.Hosting options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; + options.Password.RequireUppercase = false; + // Configure Identity to use the same JWT claims as OpenIddict instead + // of the legacy WS-Federation claims it uses by default (ClaimTypes), + // which saves you from doing the mapping in your authorization controller. + options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name; + options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject; + options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role; }); // If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be. string httpsCertificateFilePath = Configuration.GetOrDefault("HttpsCertificateFilePath", null); @@ -135,6 +128,50 @@ namespace BTCPayServer.Hosting } } + private void ConfigureOpenIddict(IServiceCollection services) + { +// Register the OpenIddict services. + services.AddOpenIddict() + .AddCore(options => + { + // Configure OpenIddict to use the Entity Framework Core stores and entities. + options.UseEntityFrameworkCore() + .UseDbContext() + .ReplaceDefaultEntities, + BTCPayOpenIdToken, string>(); + }) + .AddServer(options => + { + // Register the ASP.NET Core MVC binder used by OpenIddict. + // Note: if you don't call this method, you won't be able to + // bind OpenIdConnectRequest or OpenIdConnectResponse parameters. + options.UseMvc(); + + // Enable the token endpoint (required to use the password flow). + options.EnableTokenEndpoint("/connect/token"); + options.EnableAuthorizationEndpoint("/connect/authorize"); + options.EnableAuthorizationEndpoint("/connect/logout"); + + // Allow client applications various flows + options.AllowImplicitFlow(); + options.AllowClientCredentialsFlow(); + options.AllowRefreshTokenFlow(); + options.AllowPasswordFlow(); + options.AllowAuthorizationCodeFlow(); + options.UseRollingTokens(); + options.UseJsonWebTokens(); + + options.RegisterScopes( + OpenIdConnectConstants.Scopes.OpenId, + OpenIdConnectConstants.Scopes.OfflineAccess, + OpenIdConnectConstants.Scopes.Email, + OpenIdConnectConstants.Scopes.Profile, + OpenIddictConstants.Scopes.Roles); + + options.ConfigureSigningKey(Configuration); + }); + } + public void Configure( IApplicationBuilder app, IHostingEnvironment env, @@ -162,6 +199,8 @@ namespace BTCPayServer.Hosting { app.UseDeveloperExceptionPage(); } + + app.UseCors(); var forwardingOptions = new ForwardedHeadersOptions() { diff --git a/BTCPayServer/Migrations/20190225091644_AddOpenIddict.Designer.cs b/BTCPayServer/Migrations/20190225091644_AddOpenIddict.Designer.cs new file mode 100644 index 000000000..abc0aa76c --- /dev/null +++ b/BTCPayServer/Migrations/20190225091644_AddOpenIddict.Designer.cs @@ -0,0 +1,787 @@ +// +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20190225091644_AddOpenIddict")] + partial class AddOpenIddict + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationId"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("Properties"); + + b.Property("Scopes"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(25); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(450); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(100); + + b.Property("ClientSecret"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("ConsentType"); + + b.Property("DisplayName"); + + b.Property("Permissions"); + + b.Property("PostLogoutRedirectUris"); + + b.Property("Properties"); + + b.Property("RedirectUris"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationId"); + + b.Property("AuthorizationId"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("CreationDate"); + + b.Property("ExpirationDate"); + + b.Property("Payload"); + + b.Property("Properties"); + + b.Property("ReferenceId") + .HasMaxLength(100); + + b.Property("Status") + .IsRequired() + .HasMaxLength(25); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(450); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppType"); + + b.Property("Created"); + + b.Property("Name"); + + b.Property("Settings"); + + b.Property("StoreDataId"); + + b.Property("TagAllInvoices"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId"); + + b.Property("UniqueId"); + + b.Property("Message"); + + b.Property("Timestamp"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DefaultCrypto"); + + b.Property("DerivationStrategies"); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("Description"); + + b.Property("DisplayName"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Properties"); + + b.Property("Resources"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b => + { + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("OpenIdClients") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b => + { + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Invoices") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PairedSINs") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20190225091644_AddOpenIddict.cs b/BTCPayServer/Migrations/20190225091644_AddOpenIddict.cs new file mode 100644 index 000000000..e2181ce56 --- /dev/null +++ b/BTCPayServer/Migrations/20190225091644_AddOpenIddict.cs @@ -0,0 +1,167 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddOpenIddict : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + ClientId = table.Column(maxLength: 100, nullable: false), + ClientSecret = table.Column(nullable: true), + ConcurrencyToken = table.Column(maxLength: 50, nullable: true), + ConsentType = table.Column(nullable: true), + DisplayName = table.Column(nullable: true), + Id = table.Column(nullable: false), + Permissions = table.Column(nullable: true), + PostLogoutRedirectUris = table.Column(nullable: true), + Properties = table.Column(nullable: true), + RedirectUris = table.Column(nullable: true), + Type = table.Column(maxLength: 25, nullable: false), + ApplicationUserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + ConcurrencyToken = table.Column(maxLength: 50, nullable: true), + Description = table.Column(nullable: true), + DisplayName = table.Column(nullable: true), + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 200, nullable: false), + Properties = table.Column(nullable: true), + Resources = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + ApplicationId = table.Column(nullable: true), + ConcurrencyToken = table.Column(maxLength: 50, nullable: true), + Id = table.Column(nullable: false), + Properties = table.Column(nullable: true), + Scopes = table.Column(nullable: true), + Status = table.Column(maxLength: 25, nullable: false), + Subject = table.Column(maxLength: 450, nullable: false), + Type = table.Column(maxLength: 25, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + ApplicationId = table.Column(nullable: true), + AuthorizationId = table.Column(nullable: true), + ConcurrencyToken = table.Column(maxLength: 50, nullable: true), + CreationDate = table.Column(nullable: true), + ExpirationDate = table.Column(nullable: true), + Id = table.Column(nullable: false), + Payload = table.Column(nullable: true), + Properties = table.Column(nullable: true), + ReferenceId = table.Column(maxLength: 100, nullable: true), + Status = table.Column(maxLength: 25, nullable: false), + Subject = table.Column(maxLength: 450, nullable: false), + Type = table.Column(maxLength: 25, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ApplicationUserId", + table: "OpenIddictApplications", + column: "ApplicationUserId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + } + } +} diff --git a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs new file mode 100644 index 000000000..07894522f --- /dev/null +++ b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs @@ -0,0 +1,667 @@ +// +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20190425081749_AddU2fDevices")] + partial class AddU2fDevices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.8-servicing-32085"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppType"); + + b.Property("Created"); + + b.Property("Name"); + + b.Property("Settings"); + + b.Property("StoreDataId"); + + b.Property("TagAllInvoices"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId"); + + b.Property("UniqueId"); + + b.Property("Message"); + + b.Property("Timestamp"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DefaultCrypto"); + + b.Property("DerivationStrategies"); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("AttestationCert") + .IsRequired(); + + b.Property("Counter"); + + b.Property("KeyHandle") + .IsRequired(); + + b.Property("Name"); + + b.Property("PublicKey") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("FileName"); + + b.Property("StorageFileName"); + + b.Property("Timestamp"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Invoices") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PairedSINs") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs new file mode 100644 index 000000000..7b90d6cca --- /dev/null +++ b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddU2fDevices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.DropColumn( + name: "Facade", + table: "PairedSINData"); + } + + migrationBuilder.CreateTable( + name: "U2FDevices", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(nullable: true), + KeyHandle = table.Column(nullable: false), + PublicKey = table.Column(nullable: false), + AttestationCert = table.Column(nullable: false), + Counter = table.Column(nullable: false), + ApplicationUserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_U2FDevices", x => x.Id); + table.ForeignKey( + name: "FK_U2FDevices_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_U2FDevices_ApplicationUserId", + table: "U2FDevices", + column: "ApplicationUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "U2FDevices"); + //if it did not support dropping it, then it is still here and re-adding it would throw + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.AddColumn( + name: "Facade", + table: "PairedSINData", + nullable: true); + } + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 919b92944..945d5f436 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -16,6 +16,131 @@ namespace BTCPayServer.Migrations modelBuilder .HasAnnotation("ProductVersion", "2.1.8-servicing-32085"); + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationId"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("Properties"); + + b.Property("Scopes"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(25); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(450); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(100); + + b.Property("ClientSecret"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("ConsentType"); + + b.Property("DisplayName"); + + b.Property("Permissions"); + + b.Property("PostLogoutRedirectUris"); + + b.Property("Properties"); + + b.Property("RedirectUris"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationId"); + + b.Property("AuthorizationId"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("CreationDate"); + + b.Property("ExpirationDate"); + + b.Property("Payload"); + + b.Property("Properties"); + + b.Property("ReferenceId") + .HasMaxLength(100); + + b.Property("Status") + .IsRequired() + .HasMaxLength(25); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(450); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens"); + }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { b.Property("Address") @@ -137,8 +262,6 @@ namespace BTCPayServer.Migrations b.Property("Id") .ValueGeneratedOnAdd(); - b.Property("Facade"); - b.Property("Label"); b.Property("PairingTime"); @@ -348,6 +471,33 @@ namespace BTCPayServer.Migrations b.ToTable("PaymentRequests"); }); + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("AttestationCert") + .IsRequired(); + + b.Property("Counter"); + + b.Property("KeyHandle") + .IsRequired(); + + b.Property("Name"); + + b.Property("PublicKey") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => { b.Property("Id") @@ -475,6 +625,60 @@ namespace BTCPayServer.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50); + + b.Property("Description"); + + b.Property("DisplayName"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Properties"); + + b.Property("Resources"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b => + { + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("OpenIdClients") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b => + { + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") @@ -576,6 +780,13 @@ namespace BTCPayServer.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => { b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") diff --git a/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs b/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs index 2b47c1024..e7fc7f32f 100644 --- a/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs +++ b/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.ModelBinders var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider)); var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue; - var network = networkProvider.GetNetwork(cryptoCode ?? "BTC"); + var network = networkProvider.GetNetwork(cryptoCode ?? "BTC"); try { var data = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(key); diff --git a/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs b/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs index 2a29acd6b..c59e7eccf 100644 --- a/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs +++ b/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs @@ -23,5 +23,8 @@ namespace BTCPayServer.Models.AccountViewModels [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } + + [Display(Name = "Is administrator?")] + public bool IsAdmin { get; set; } } } diff --git a/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs new file mode 100644 index 000000000..5ffaeecc3 --- /dev/null +++ b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Services.U2F.Models; + +namespace BTCPayServer.Models.AccountViewModels +{ + public class SecondaryLoginViewModel + { + public LoginWith2faViewModel LoginWith2FaViewModel { get; set; } + public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/Models/ApplicationUser.cs b/BTCPayServer/Models/ApplicationUser.cs index 31360f52c..9c87bb965 100644 --- a/BTCPayServer/Models/ApplicationUser.cs +++ b/BTCPayServer/Models/ApplicationUser.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Authentication.OpenId.Models; using Microsoft.AspNetCore.Identity; using BTCPayServer.Data; +using BTCPayServer.Services.U2F.Models; using BTCPayServer.Storage.Models; namespace BTCPayServer.Models @@ -22,10 +24,14 @@ namespace BTCPayServer.Models get; set; } + public List OpenIdClients { get; set; } + public List StoredFiles { get; set; } + + public List U2FDevices { get; set; } } } diff --git a/BTCPayServer/Models/ErrorViewModel.cs b/BTCPayServer/Models/ErrorViewModel.cs index b32ee4e43..8fc78ae22 100644 --- a/BTCPayServer/Models/ErrorViewModel.cs +++ b/BTCPayServer/Models/ErrorViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; namespace BTCPayServer.Models { @@ -7,5 +8,11 @@ namespace BTCPayServer.Models public string RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + [Display(Name = "Error")] + public string Error { get; set; } + + [Display(Name = "Description")] + public string ErrorDescription { get; set; } } -} \ No newline at end of file +} diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 8df1a47f9..9fd8b923e 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -70,5 +70,16 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } + + public List SupportedTransactionCurrencies + { + get; + set; + } + public SelectList AvailablePaymentMethods + { + get; + set; + } } } diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index b0faaba49..8aa5b0a28 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; namespace BTCPayServer.Models.InvoicingModels { @@ -25,7 +26,8 @@ namespace BTCPayServer.Models.InvoicingModels public string RedirectUrl { get; set; } public string InvoiceId { get; set; } - public string Status { get; set; } + public InvoiceStatus Status { get; set; } + public string StatusString { get; set; } public bool CanMarkComplete { get; set; } public bool CanMarkInvalid { get; set; } public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid; @@ -33,5 +35,7 @@ namespace BTCPayServer.Models.InvoicingModels public string ExceptionStatus { get; set; } public string AmountCurrency { get; set; } public string StatusMessage { get; set; } + + public InvoiceDetailsModel Details { get; set; } } } diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index abd199035..1921e70a6 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -134,7 +134,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels public string AmountCollectedFormatted { get; set; } public string AmountFormatted { get; set; } public bool AnyPendingInvoice { get; set; } + public bool PendingInvoiceHasPayments { get; set; } public string HubPath { get; set; } + public string StatusMessage { get; set; } public class PaymentRequestInvoice { diff --git a/BTCPayServer/Models/StatusMessageModel.cs b/BTCPayServer/Models/StatusMessageModel.cs index 9c70d3818..2b63fece6 100644 --- a/BTCPayServer/Models/StatusMessageModel.cs +++ b/BTCPayServer/Models/StatusMessageModel.cs @@ -23,6 +23,7 @@ namespace BTCPayServer.Models Html = model.Html; Message = model.Message; Severity = model.Severity; + AllowDismiss = model.AllowDismiss; } else { @@ -38,6 +39,7 @@ namespace BTCPayServer.Models public string Message { get; set; } public string Html { get; set; } public StatusSeverity Severity { get; set; } + public bool AllowDismiss { get; set; } = true; public string SeverityCSS { @@ -51,6 +53,8 @@ namespace BTCPayServer.Models return "danger"; case StatusSeverity.Success: return "success"; + case StatusSeverity.Warning: + return "warning"; default: throw new ArgumentOutOfRangeException(); } @@ -74,7 +78,8 @@ namespace BTCPayServer.Models { Info, Error, - Success + Success, + Warning } } } diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 0353c3601..298792339 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using NBitcoin; @@ -25,6 +26,7 @@ namespace BTCPayServer.Models.StoreViewModels public string CryptoCode { get; set; } public string KeyPath { get; set; } + public string RootFingerprint { get; set; } [Display(Name = "Hint address")] public string HintAddress { get; set; } public bool Confirmation { get; set; } @@ -32,5 +34,13 @@ namespace BTCPayServer.Models.StoreViewModels public string StatusMessage { get; internal set; } public KeyPath RootKeyPath { get; set; } + + [Display(Name = "Coldcard Wallet File")] + public IFormFile ColdcardPublicFile{ get; set; } + public string Config { get; set; } + public string Source { get; set; } + public string DerivationSchemeFormat { get; set; } + public string AccountKey { get; set; } + public BTCPayNetwork Network { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/SignWithSeedViewModel.cs b/BTCPayServer/Models/WalletViewModels/SignWithSeedViewModel.cs new file mode 100644 index 000000000..7ff09d4ba --- /dev/null +++ b/BTCPayServer/Models/WalletViewModels/SignWithSeedViewModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using NBitcoin; + +namespace BTCPayServer.Models.WalletViewModels +{ + public class SignWithSeedViewModel + { + [Required] + public string PSBT { get; set; } + [Required][Display(Name = "BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)")] + public string SeedOrKey { get; set; } + + [Display(Name = "Optional seed passphrase")] + public string Passphrase { get; set; } + + public ExtKey GetExtKey(Network network) + { + ExtKey extKey = null; + try + { + var mnemonic = new Mnemonic(SeedOrKey); + extKey = mnemonic.DeriveExtKey(Passphrase); + } + catch (Exception) + { + } + + if (extKey == null) + { + try + { + extKey = ExtKey.Parse(SeedOrKey, network); + } + catch (Exception) + { + } + } + return extKey; + } + } +} diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs new file mode 100644 index 000000000..6b856d8a8 --- /dev/null +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NBitcoin; + +namespace BTCPayServer.Models.WalletViewModels +{ + public class WalletPSBTCombineViewModel + { + public string OtherPSBT { get; set; } + [Display(Name = "PSBT to combine with...")] + public string PSBT { get; set; } + [Display(Name = "Upload PSBT from file...")] + public IFormFile UploadedPSBTFile { get; set; } + + public PSBT GetSourcePSBT(Network network) + { + if (!string.IsNullOrEmpty(OtherPSBT)) + { + try + { + return NBitcoin.PSBT.Parse(OtherPSBT, network); + } + catch + { } + } + return null; + } + public async Task GetPSBT(Network network) + { + if (UploadedPSBTFile != null) + { + if (UploadedPSBTFile.Length > 500 * 1024) + return null; + byte[] bytes = new byte[UploadedPSBTFile.Length]; + using (var stream = UploadedPSBTFile.OpenReadStream()) + { + await stream.ReadAsync(bytes, 0, (int)UploadedPSBTFile.Length); + } + try + { + return NBitcoin.PSBT.Load(bytes, network); + } + catch + { + return null; + } + } + if (!string.IsNullOrEmpty(PSBT)) + { + try + { + return NBitcoin.PSBT.Parse(PSBT, network); + } + catch + { } + } + return null; + } + } +} diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs new file mode 100644 index 000000000..985ee8ba8 --- /dev/null +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace BTCPayServer.Models.WalletViewModels +{ + public class WalletPSBTReadyViewModel + { + public string PSBT { get; set; } + public string SigningKey { get; set; } + public string SigningKeyPath { get; set; } + public string GlobalError { get; set; } + + public class DestinationViewModel + { + public bool Positive { get; set; } + public string Destination { get; set; } + public string Balance { get; set; } + } + + public class InputViewModel + { + public int Index { get; set; } + public string Error { get; set; } + public bool Positive { get; set; } + public string BalanceChange { get; set; } + } + public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error)); + public string BalanceChange { get; set; } + public bool CanCalculateBalance { get; set; } + public bool Positive { get; set; } + public List Destinations { get; set; } = new List(); + public List Inputs { get; set; } = new List(); + public string FeeRate { get; set; } + + internal void SetErrors(IList errors) + { + foreach (var err in errors) + { + Inputs[(int)err.InputIndex].Error = err.Message; + } + } + } +} diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs new file mode 100644 index 000000000..05f79dce4 --- /dev/null +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NBitcoin; + +namespace BTCPayServer.Models.WalletViewModels +{ + public class WalletPSBTViewModel + { + public string Decoded { get; set; } + string _FileName; + public string FileName + { + get + { + return string.IsNullOrEmpty(_FileName) ? "psbt-export.psbt" : _FileName; + } + set + { + _FileName = value; + } + } + public string PSBT { get; set; } + public List Errors { get; set; } = new List(); + + [Display(Name = "Upload PSBT from file...")] + public IFormFile UploadedPSBTFile { get; set; } + + public async Task GetPSBT(Network network) + { + if (UploadedPSBTFile != null) + { + if (UploadedPSBTFile.Length > 500 * 1024) + return null; + byte[] bytes = new byte[UploadedPSBTFile.Length]; + using (var stream = UploadedPSBTFile.OpenReadStream()) + { + await stream.ReadAsync(bytes, 0, (int)UploadedPSBTFile.Length); + } + try + { + return NBitcoin.PSBT.Load(bytes, network); + } + catch + { + return null; + } + } + if (!string.IsNullOrEmpty(PSBT)) + { + try + { + return NBitcoin.PSBT.Parse(PSBT, network); + } + catch + { } + } + return null; + } + } +} diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs index 948903714..2709fd591 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs @@ -7,10 +7,9 @@ namespace BTCPayServer.Models.WalletViewModels { public class WalletSendLedgerModel { - public int FeeSatoshiPerByte { get; set; } - public bool SubstractFees { get; set; } - public decimal Amount { get; set; } - public string Destination { get; set; } - public bool NoChange { get; set; } + public string WebsocketPath { get; set; } + public string PSBT { get; set; } + public string HintChange { get; set; } + public string SuccessPath { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index 7faa04a13..4c5569a52 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -8,22 +8,27 @@ namespace BTCPayServer.Models.WalletViewModels { public class WalletSendModel { - [Required] - public string Destination { get; set; } + + public List Outputs { get; set; } = new List(); - [Range(0.0, double.MaxValue)] - [Required] - public decimal? Amount { get; set; } + public class TransactionOutput + { + [Display(Name = "Destination Address")] + [Required] + public string DestinationAddress { get; set; } + [Display(Name = "Amount")] [Required] [Range(0.0, double.MaxValue)]public decimal? Amount { get; set; } + + + [Display(Name = "Subtract fees from amount")] + public bool SubtractFeesFromOutput { get; set; } + } public decimal CurrentBalance { get; set; } public string CryptoCode { get; set; } public int RecommendedSatoshiPerByte { get; set; } - [Display(Name = "Subtract fees from amount")] - public bool SubstractFees { get; set; } - [Range(1, int.MaxValue)] [Display(Name = "Fee rate (satoshi per byte)")] [Required] @@ -31,10 +36,12 @@ namespace BTCPayServer.Models.WalletViewModels [Display(Name = "Make sure no change UTXO is created")] public bool NoChange { get; set; } - public bool AdvancedMode { get; set; } public decimal? Rate { get; set; } public int Divisibility { get; set; } public string Fiat { get; set; } public string RateError { get; set; } + public bool SupportRBF { get; set; } + [Display(Name = "Disable RBF")] + public bool DisableRBF { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSettingsViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSettingsViewModel.cs new file mode 100644 index 000000000..40d93844b --- /dev/null +++ b/BTCPayServer/Models/WalletViewModels/WalletSettingsViewModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Models.WalletViewModels +{ + public class WalletSettingsViewModel + { + public string Label { get; set; } + public string DerivationScheme { get; set; } + public string DerivationSchemeInput { get; set; } + [Display(Name = "Is signing key")] + public string SelectedSigningKey { get; set; } + public bool IsMultiSig => AccountKeys.Count > 1; + + public List AccountKeys { get; set; } = new List(); + } + + public class WalletSettingsAccountKeyViewModel + { + public string AccountKey { get; set; } + [Validation.HDFingerPrintValidator] + public string MasterFingerprint { get; set; } + [Validation.KeyPathValidator] + public string AccountKeyPath { get; set; } + } +} diff --git a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs index 28080d541..9f9af4bf2 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using System.Threading; @@ -23,6 +24,8 @@ namespace BTCPayServer.PaymentRequest public const string PaymentReceived = "PaymentReceived"; public const string InfoUpdated = "InfoUpdated"; public const string InvoiceError = "InvoiceError"; + public const string CancelInvoiceError = "CancelInvoiceError"; + public const string InvoiceCancelled = "InvoiceCancelled"; public PaymentRequestHub(PaymentRequestController paymentRequestController) { @@ -61,6 +64,23 @@ namespace BTCPayServer.PaymentRequest } } + public async Task CancelUnpaidPendingInvoice() + { + _PaymentRequestController.ControllerContext.HttpContext = Context.GetHttpContext(); + var result = + await _PaymentRequestController.CancelUnpaidPendingInvoice(Context.Items["pr-id"].ToString(), false); + switch (result) + { + case OkObjectResult okObjectResult: + await Clients.Group(Context.Items["pr-id"].ToString()).SendCoreAsync(InvoiceCancelled, System.Array.Empty()); + break; + + default: + await Clients.Caller.SendCoreAsync(CancelInvoiceError, System.Array.Empty()); + break; + } + } + public static string GetHubPath(HttpRequest request) { return request.GetRelativePathOrAbsolute("/payment-requests/hub"); diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index 8f59fd973..9bb46dcb5 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Models.PaymentRequestViewModels; +using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -80,6 +82,7 @@ namespace BTCPayServer.PaymentRequest var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true); var amountDue = blob.Amount - paymentStats.TotalCurrency; + var pendingInvoice = invoices.SingleOrDefault(entity => entity.Status == InvoiceStatus.New); return new ViewPaymentRequestViewModel(pr) { @@ -90,7 +93,9 @@ namespace BTCPayServer.PaymentRequest AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency), CurrencyData = _currencies.GetCurrencyData(blob.Currency, true), LastUpdated = DateTime.Now, - AnyPendingInvoice = invoices.Any(entity => entity.Status == InvoiceStatus.New), + AnyPendingInvoice = pendingInvoice != null, + PendingInvoiceHasPayments = pendingInvoice != null && + pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None, Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice() { Id = entity.Id, @@ -101,26 +106,15 @@ namespace BTCPayServer.PaymentRequest Status = entity.GetInvoiceState().ToString(), Payments = entity.GetPayments().Select(paymentEntity => { - var paymentNetwork = _BtcPayNetworkProvider.GetNetwork(paymentEntity.GetCryptoCode()); var paymentData = paymentEntity.GetCryptoPaymentData(); - string link = null; - string txId = null; - switch (paymentData) - { - case Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData: - txId = onChainPaymentData.Outpoint.Hash.ToString(); - link = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, - txId); - break; - case LightningLikePaymentData lightningLikePaymentData: - txId = lightningLikePaymentData.BOLT11; - break; - } + var paymentMethodId = paymentEntity.GetPaymentMethodId(); + string txId = paymentData.GetPaymentId(); + string link = GetTransactionLink(paymentMethodId, txId); return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment() { Amount = paymentData.GetValue(), - PaymentMethod = paymentEntity.GetPaymentMethodId().ToString(), + PaymentMethod = paymentMethodId.ToString(), Link = link, Id = txId }; @@ -128,5 +122,13 @@ namespace BTCPayServer.PaymentRequest }).ToList() }; } + + private string GetTransactionLink(PaymentMethodId paymentMethodId, string txId) + { + var network = _BtcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + if (network == null) + return null; + return paymentMethodId.PaymentType.GetTransactionLink(network, txId); + } } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs index 30e25801c..58e1bd1d9 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs @@ -10,10 +10,7 @@ namespace BTCPayServer.Payments.Bitcoin { public class BitcoinLikeOnChainPaymentMethod : IPaymentMethodDetails { - public PaymentTypes GetPaymentType() - { - return PaymentTypes.BTCLike; - } + public PaymentType GetPaymentType() => PaymentTypes.BTCLike; public string GetPaymentDestination() { diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index 2f1f236fb..a7dc1739d 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -11,7 +11,7 @@ namespace BTCPayServer.Payments.Bitcoin public class BitcoinLikePaymentData : CryptoPaymentData { - public PaymentTypes GetPaymentType() + public PaymentType GetPaymentType() { return PaymentTypes.BTCLike; } @@ -27,6 +27,8 @@ namespace BTCPayServer.Payments.Bitcoin RBF = rbf; } [JsonIgnore] + public BTCPayNetworkBase Network { get; set; } + [JsonIgnore] public OutPoint Outpoint { get; set; } [JsonIgnore] public TxOut Output { get; set; } @@ -54,12 +56,12 @@ namespace BTCPayServer.Payments.Bitcoin return Output.Value.ToDecimal(MoneyUnit.BTC); } - public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) + public bool PaymentCompleted(PaymentEntity entity) { - return ConfirmationCount >= network.MaxTrackedConfirmation; + return ConfirmationCount >= Network.MaxTrackedConfirmation; } - public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) + public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy) { if (speedPolicy == SpeedPolicy.HighSpeed) { @@ -80,14 +82,14 @@ namespace BTCPayServer.Payments.Bitcoin return false; } - public BitcoinAddress GetDestination(BTCPayNetwork network) + public BitcoinAddress GetDestination() { - return Output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork); + return Output.ScriptPubKey.GetDestinationAddress(((BTCPayNetwork)Network).NBitcoinNetwork); } - string CryptoPaymentData.GetDestination(BTCPayNetwork network) + string CryptoPaymentData.GetDestination() { - return GetDestination(network).ToString(); + return GetDestination().ToString(); } } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index e7a830b86..9b0823c78 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -1,29 +1,36 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using NBitcoin; +using NBitpayClient; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments.Bitcoin { - public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase + public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase { ExplorerClientProvider _ExplorerProvider; + private readonly BTCPayNetworkProvider _networkProvider; private IFeeProviderFactory _FeeRateProviderFactory; private Services.Wallets.BTCPayWalletProvider _WalletProvider; public BitcoinLikePaymentHandler(ExplorerClientProvider provider, - IFeeProviderFactory feeRateProviderFactory, - Services.Wallets.BTCPayWalletProvider walletProvider) + BTCPayNetworkProvider networkProvider, + IFeeProviderFactory feeRateProviderFactory, + Services.Wallets.BTCPayWalletProvider walletProvider) { - if (provider == null) - throw new ArgumentNullException(nameof(provider)); _ExplorerProvider = provider; - this._FeeRateProviderFactory = feeRateProviderFactory; + _networkProvider = networkProvider; + _FeeRateProviderFactory = feeRateProviderFactory; _WalletProvider = walletProvider; } @@ -33,21 +40,92 @@ namespace BTCPayServer.Payments.Bitcoin public Task ReserveAddress; } - public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network) + public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse) + { + var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentTypes.BTCLike); + + var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); + var network = _networkProvider.GetNetwork(model.CryptoCode); + model.IsLightning = false; + model.PaymentMethodName = GetPaymentMethodName(network); + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21; + model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21; + } + + public override string GetCryptoImage(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetCryptoImage(network); + } + + private string GetCryptoImage(BTCPayNetworkBase network) + { + return network.CryptoImagePath; + } + + public override string GetPaymentMethodName(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetPaymentMethodName(network); + } + + public override async Task IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, + Dictionary> rate, Money amount, PaymentMethodId paymentMethodId) + { + if (storeBlob.OnChainMinValue == null) + { + return null; + } + + var limitValueRate = + await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)]; + + if (limitValueRate.BidAsk != null) + { + var limitValueCrypto = Money.Coins(storeBlob.OnChainMinValue.Value / limitValueRate.BidAsk.Bid); + + if (amount > limitValueCrypto) + { + return null; + } + } + + return "The amount of the invoice is too low to be paid on chain"; + } + + public override IEnumerable GetSupportedPaymentMethods() + { + return _networkProvider.GetAll() + .Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike)); + } + + private string GetPaymentMethodName(BTCPayNetworkBase network) + { + return network.DisplayName; + } + + public override object PreparePayment(DerivationSchemeSettings supportedPaymentMethod, StoreData store, + BTCPayNetworkBase network) { return new Prepare() { GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(), - ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase) + ReserveAddress = _WalletProvider.GetWallet(network) + .ReserveAddressAsync(supportedPaymentMethod.AccountDerivation) }; } - public override async Task CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject) + public override PaymentType PaymentType => PaymentTypes.BTCLike; + + public override async Task CreatePaymentMethodDetails( + DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, + BTCPayNetwork network, object preparePaymentObject) { if (!_ExplorerProvider.IsAvailable(network)) throw new PaymentMethodUnavailableException($"Full node not available"); var prepare = (Prepare)preparePaymentObject; - Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); + Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = + new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); onchainMethod.NetworkFeeMode = store.GetStoreBlob().NetworkFeeMode; onchainMethod.FeeRate = await prepare.GetFeeRate; switch (onchainMethod.NetworkFeeMode) diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 9a867a8d7..bd01ab410 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -37,7 +37,8 @@ namespace BTCPayServer.Payments.Bitcoin public NBXplorerListener(ExplorerClientProvider explorerClients, BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, - EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime) + EventAggregator aggregator, + Microsoft.Extensions.Hosting.IApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); _Wallets = wallets; @@ -238,6 +239,10 @@ namespace BTCPayServer.Payments.Bitcoin } } + // if needed add invoice back to pending to track number of confirmations + if (paymentData.ConfirmationCount < wallet.Network.MaxTrackedConfirmation) + await _InvoiceRepository.AddPendingInvoiceIfNotPresent(invoice.Id); + if (updated) updatedPaymentEntities.Add(payment); } @@ -306,7 +311,7 @@ namespace BTCPayServer.Payments.Bitcoin return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value)); } - private async Task FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network) + private async Task FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetworkBase network) { int totalPayment = 0; var invoices = await _InvoiceRepository.GetPendingInvoices(); @@ -343,10 +348,10 @@ namespace BTCPayServer.Payments.Bitcoin return totalPayment; } - private DerivationStrategyBase GetDerivationStrategy(InvoiceEntity invoice, BTCPayNetwork network) + private DerivationStrategyBase GetDerivationStrategy(InvoiceEntity invoice, BTCPayNetworkBase network) { - return invoice.GetSupportedPaymentMethod(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), _ExplorerClients.NetworkProviders) - .Select(d => d.DerivationStrategyBase) + return invoice.GetSupportedPaymentMethod(new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike)) + .Select(d => d.AccountDerivation) .FirstOrDefault(); } @@ -356,7 +361,7 @@ namespace BTCPayServer.Payments.Bitcoin invoice = (await UpdatePaymentStates(wallet, invoice.Id)); if (invoice == null) return null; - var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders); + var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike); if (paymentMethod != null && paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc && btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.Output.ScriptPubKey && diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index eab9c8853..f950a833c 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -16,7 +16,7 @@ namespace BTCPayServer.Payments /// /// string GetPaymentDestination(); - PaymentTypes GetPaymentType(); + PaymentType GetPaymentType(); /// /// Returns fee that the merchant charge to the customer for the next payment /// diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index 11687caef..5cf3403cc 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -1,9 +1,15 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Rating; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; +using NBitcoin; +using NBitpayClient; +using Newtonsoft.Json.Linq; +using InvoiceResponse = BTCPayServer.Models.InvoiceResponse; namespace BTCPayServer.Payments { @@ -19,8 +25,10 @@ namespace BTCPayServer.Payments /// /// /// + /// /// - Task CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject); + Task CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, + PaymentMethod paymentMethod, StoreData store, BTCPayNetworkBase network, object preparePaymentObject); /// /// This method called before the rate have been fetched @@ -29,38 +37,72 @@ namespace BTCPayServer.Payments /// /// /// - object PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network); + object PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetworkBase network); + + void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse); + string GetCryptoImage(PaymentMethodId paymentMethodId); + string GetPaymentMethodName(PaymentMethodId paymentMethodId); + + Task IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, + Dictionary> rate, + Money amount, PaymentMethodId paymentMethodId); + + IEnumerable GetSupportedPaymentMethods(); } - public interface IPaymentMethodHandler : IPaymentMethodHandler where T : ISupportedPaymentMethod + public interface IPaymentMethodHandler : IPaymentMethodHandler + where TSupportedPaymentMethod : ISupportedPaymentMethod + where TBTCPayNetwork : BTCPayNetworkBase { - Task CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject); + Task CreatePaymentMethodDetails(TSupportedPaymentMethod supportedPaymentMethod, + PaymentMethod paymentMethod, StoreData store, TBTCPayNetwork network, object preparePaymentObject); } - public abstract class PaymentMethodHandlerBase : IPaymentMethodHandler where T : ISupportedPaymentMethod + public abstract class PaymentMethodHandlerBase : IPaymentMethodHandler< + TSupportedPaymentMethod, TBTCPayNetwork> + where TSupportedPaymentMethod : ISupportedPaymentMethod + where TBTCPayNetwork : BTCPayNetworkBase { - - public abstract Task CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject); - public virtual object PreparePayment(T supportedPaymentMethod, StoreData store, BTCPayNetwork network) + public abstract PaymentType PaymentType { get; } + + public abstract Task CreatePaymentMethodDetails( + TSupportedPaymentMethod supportedPaymentMethod, + PaymentMethod paymentMethod, StoreData store, TBTCPayNetwork network, object preparePaymentObject); + + public abstract void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse); + public abstract string GetCryptoImage(PaymentMethodId paymentMethodId); + public abstract string GetPaymentMethodName(PaymentMethodId paymentMethodId); + + public abstract Task IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, + Dictionary> rate, Money amount, PaymentMethodId paymentMethodId); + + public abstract IEnumerable GetSupportedPaymentMethods(); + + public virtual object PreparePayment(TSupportedPaymentMethod supportedPaymentMethod, StoreData store, + BTCPayNetworkBase network) { return null; } - object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network) + public Task CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, + StoreData store, BTCPayNetworkBase network, object preparePaymentObject) { - if (supportedPaymentMethod is T method) + if (supportedPaymentMethod is TSupportedPaymentMethod method && network is TBTCPayNetwork correctNetwork) { - return PreparePayment(method, store, network); + return CreatePaymentMethodDetails(method, paymentMethod, store, correctNetwork, preparePaymentObject); } + throw new NotSupportedException("Invalid supportedPaymentMethod"); } - Task IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject) + object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, + BTCPayNetworkBase network) { - if (supportedPaymentMethod is T method) + if (supportedPaymentMethod is TSupportedPaymentMethod method) { - return CreatePaymentMethodDetails(method, paymentMethod, store, network, preparePaymentObject); + return PreparePayment(method, store, network); } + throw new NotSupportedException("Invalid supportedPaymentMethod"); } } diff --git a/BTCPayServer/Payments/ISupportedPaymentMethod.cs b/BTCPayServer/Payments/ISupportedPaymentMethod.cs index 225786a04..9b3adc0cd 100644 --- a/BTCPayServer/Payments/ISupportedPaymentMethod.cs +++ b/BTCPayServer/Payments/ISupportedPaymentMethod.cs @@ -7,8 +7,8 @@ using BTCPayServer.Services.Invoices; namespace BTCPayServer.Payments { /// - /// This class represent a mode of payment supported by a store. - /// It is stored at the store level and cloned to the invoice during invoice creation. + /// A class for configuration of a type of payment method stored on a store level. + /// It is cloned to invoices of the store during invoice creation. /// This object will be serialized in database in json /// public interface ISupportedPaymentMethod diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs index e7246e34a..94bbc53be 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs @@ -14,13 +14,15 @@ namespace BTCPayServer.Payments.Lightning { public class LightningLikePaymentData : CryptoPaymentData { + [JsonIgnore] + public BTCPayNetworkBase Network { get; set; } [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } public string BOLT11 { get; set; } [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] public uint256 PaymentHash { get; set; } - public string GetDestination(BTCPayNetwork network) + public string GetDestination() { return BOLT11; } @@ -34,7 +36,7 @@ namespace BTCPayServer.Payments.Lightning return PaymentHash?.ToString() ?? BOLT11; } - public PaymentTypes GetPaymentType() + public PaymentType GetPaymentType() { return PaymentTypes.LightningLike; } @@ -49,12 +51,12 @@ namespace BTCPayServer.Payments.Lightning return Amount.ToDecimal(LightMoneyUnit.BTC); } - public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) + public bool PaymentCompleted(PaymentEntity entity) { return true; } - public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) + public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy) { return true; } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 8791b9430..4d65c8936 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -1,43 +1,56 @@ using System; -using System.Net.Sockets; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; +using BTCPayServer.Models; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Rating; using BTCPayServer.Services.Invoices; using BTCPayServer.Services; +using BTCPayServer.Services.Rates; using NBitcoin; +using NBitpayClient; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments.Lightning { - public class LightningLikePaymentHandler : PaymentMethodHandlerBase + public class LightningLikePaymentHandler : PaymentMethodHandlerBase { public static int LIGHTNING_TIMEOUT = 5000; NBXplorerDashboard _Dashboard; private readonly LightningClientFactoryService _lightningClientFactory; + private readonly BTCPayNetworkProvider _networkProvider; private readonly SocketFactory _socketFactory; public LightningLikePaymentHandler( NBXplorerDashboard dashboard, LightningClientFactoryService lightningClientFactory, + BTCPayNetworkProvider networkProvider, SocketFactory socketFactory) { _Dashboard = dashboard; _lightningClientFactory = lightningClientFactory; + _networkProvider = networkProvider; _socketFactory = socketFactory; } - public override async Task CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject) + + public override PaymentType PaymentType => PaymentTypes.LightningLike; + public override async Task CreatePaymentMethodDetails( + LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, + BTCPayNetwork network, object preparePaymentObject) { + //direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers var storeBlob = store.GetStoreBlob(); - var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network); + var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, (BTCPayNetwork)network); var invoice = paymentMethod.ParentEntity; var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8); - var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network); + var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), (BTCPayNetwork)network); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) expiry = TimeSpan.FromSeconds(1); @@ -125,5 +138,67 @@ namespace BTCPayServer.Payments.Lightning throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})"); } } + + public override IEnumerable GetSupportedPaymentMethods() + { + return _networkProvider.GetAll() + .Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)); + } + + + public override async Task IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, + Dictionary> rate, Money amount, PaymentMethodId paymentMethodId) + { + if (storeBlob.OnChainMinValue == null) + { + return null; + } + + var limitValueRate = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.OnChainMinValue.Currency)]; + + if (limitValueRate.BidAsk != null) + { + var limitValueCrypto = Money.Coins(storeBlob.OnChainMinValue.Value / limitValueRate.BidAsk.Bid); + + if (amount < limitValueCrypto) + { + return null; + } + } + return "The amount of the invoice is too high to be paid with lightning"; + } + + public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse) + { + var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike); + + var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); + var network = _networkProvider.GetNetwork(model.CryptoCode); + model.IsLightning = true; + model.PaymentMethodName = GetPaymentMethodName(network); + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BOLT11; + model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant(); + } + + public override string GetCryptoImage(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetCryptoImage(network); + } + + private string GetCryptoImage(BTCPayNetworkBase network) + { + return ((BTCPayNetwork)network).LightningImagePath; + } + public override string GetPaymentMethodName(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetPaymentMethodName(network); + } + + private string GetPaymentMethodName(BTCPayNetworkBase network) + { + return $"{network.DisplayName} (Lightning)"; + } } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index c8edcba93..89744930d 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -17,7 +17,7 @@ namespace BTCPayServer.Payments.Lightning return BOLT11; } - public PaymentTypes GetPaymentType() + public PaymentType GetPaymentType() { return PaymentTypes.LightningLike; } diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 1038f03ce..57101a677 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -33,8 +33,7 @@ namespace BTCPayServer.Payments.Lightning InvoiceRepository invoiceRepository, IMemoryCache memoryCache, BTCPayNetworkProvider networkProvider, - LightningClientFactoryService lightningClientFactory, - IHttpClientFactory httpClientFactory) + LightningClientFactoryService lightningClientFactory) { _Aggregator = aggregator; _InvoiceRepository = invoiceRepository; @@ -96,17 +95,17 @@ namespace BTCPayServer.Payments.Lightning { var listenedInvoices = new List(); var invoice = await _InvoiceRepository.GetInvoice(invoiceId); - foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider) + foreach (var paymentMethod in invoice.GetPaymentMethods() .Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) { var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; if (lightningMethod == null) continue; - var lightningSupportedMethod = invoice.GetSupportedPaymentMethod(_NetworkProvider) + var lightningSupportedMethod = invoice.GetSupportedPaymentMethod() .FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); if (lightningSupportedMethod == null) continue; - var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); + var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); listenedInvoices.Add(new ListenedInvoice() { diff --git a/BTCPayServer/Payments/PaymentMethodExtensions.cs b/BTCPayServer/Payments/PaymentMethodExtensions.cs index b63fe4364..be6941012 100644 --- a/BTCPayServer/Payments/PaymentMethodExtensions.cs +++ b/BTCPayServer/Payments/PaymentMethodExtensions.cs @@ -1,50 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Payments.Changelly; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments { public class PaymentMethodExtensions { - public static ISupportedPaymentMethod Deserialize(PaymentMethodId paymentMethodId, JToken value, BTCPayNetwork network) - { - // Legacy - if (paymentMethodId.PaymentType == PaymentTypes.BTCLike) - { - return BTCPayServer.DerivationStrategy.Parse(((JValue)value).Value(), network); - } - ////////// - else if (paymentMethodId.PaymentType == PaymentTypes.LightningLike) - { - return JsonConvert.DeserializeObject(value.ToString()); - } - throw new NotSupportedException(); - } - - public static IPaymentMethodDetails DeserializePaymentMethodDetails(PaymentMethodId paymentMethodId, JObject jobj) - { - if(paymentMethodId.PaymentType == PaymentTypes.BTCLike) - { - return JsonConvert.DeserializeObject(jobj.ToString()); - } - if (paymentMethodId.PaymentType == PaymentTypes.LightningLike) - { - return JsonConvert.DeserializeObject(jobj.ToString()); - } - throw new NotSupportedException(paymentMethodId.PaymentType.ToString()); - } - - public static JToken Serialize(ISupportedPaymentMethod factory) { // Legacy if (factory.PaymentId.PaymentType == PaymentTypes.BTCLike) { - return new JValue(((DerivationStrategy)factory).DerivationStrategyBase.ToString()); + var derivation = (DerivationSchemeSettings)factory; + var str = derivation.Network.NBXplorerNetwork.Serializer.ToString(derivation); + return JObject.Parse(str); } ////////////// else @@ -52,8 +20,6 @@ namespace BTCPayServer.Payments var str = JsonConvert.SerializeObject(factory); return JObject.Parse(str); } - throw new NotSupportedException(); } - } } diff --git a/BTCPayServer/Payments/PaymentMethodId.cs b/BTCPayServer/Payments/PaymentMethodId.cs index 658c30777..d637beb22 100644 --- a/BTCPayServer/Payments/PaymentMethodId.cs +++ b/BTCPayServer/Payments/PaymentMethodId.cs @@ -11,12 +11,14 @@ namespace BTCPayServer.Payments /// public class PaymentMethodId { - public PaymentMethodId(string cryptoCode, PaymentTypes paymentType) + public PaymentMethodId(string cryptoCode, PaymentType paymentType) { if (cryptoCode == null) throw new ArgumentNullException(nameof(cryptoCode)); + if (paymentType == null) + throw new ArgumentNullException(nameof(paymentType)); PaymentType = paymentType; - CryptoCode = cryptoCode; + CryptoCode = cryptoCode.ToUpperInvariant(); } [Obsolete("Should only be used for legacy stuff")] @@ -29,7 +31,7 @@ namespace BTCPayServer.Payments } public string CryptoCode { get; private set; } - public PaymentTypes PaymentType { get; private set; } + public PaymentType PaymentType { get; private set; } public override bool Equals(object obj) @@ -62,34 +64,13 @@ namespace BTCPayServer.Payments public override string ToString() { - return ToString(false); + //BTCLike case is special because it is in legacy mode. + return PaymentType == PaymentTypes.BTCLike ? CryptoCode : $"{CryptoCode}_{PaymentType}"; } - public string ToString(bool pretty) + public string ToPrettyString() { - if (pretty) - { - return $"{CryptoCode} ({PrettyMethod(PaymentType)})"; - } - else - { - if (PaymentType == PaymentTypes.BTCLike) - return CryptoCode; - return CryptoCode + "_" + PaymentType.ToString(); - } - } - - private static string PrettyMethod(PaymentTypes paymentType) - { - switch (paymentType) - { - case PaymentTypes.BTCLike: - return "On-Chain"; - case PaymentTypes.LightningLike: - return "Off-Chain"; - default: - return paymentType.ToString(); - } + return $"{CryptoCode} ({PaymentType.ToPrettyString()})"; } public static bool TryParse(string str, out PaymentMethodId paymentMethodId) @@ -98,22 +79,11 @@ namespace BTCPayServer.Payments var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0 || parts.Length > 2) return false; - PaymentTypes type = PaymentTypes.BTCLike; + PaymentType type = PaymentTypes.BTCLike; if (parts.Length == 2) { - switch (parts[1].ToLowerInvariant()) - { - case "btclike": - case "onchain": - type = PaymentTypes.BTCLike; - break; - case "lightninglike": - case "offchain": - type = PaymentTypes.LightningLike; - break; - default: - return false; - } + if (!PaymentTypes.TryParse(parts[1], out type)) + return false; } paymentMethodId = new PaymentMethodId(parts[0], type); return true; diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs new file mode 100644 index 000000000..a2d631874 --- /dev/null +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Payments.Bitcoin; +using BTCPayServer.Services.Invoices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Payments +{ + public class BitcoinPaymentType : PaymentType + { + public static BitcoinPaymentType Instance { get; } = new BitcoinPaymentType(); + private BitcoinPaymentType() + { + + } + + public override string ToPrettyString() => "On-Chain"; + public override string GetId() => "BTCLike"; + + public override CryptoPaymentData DeserializePaymentData(string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (value == null) + throw new ArgumentNullException(nameof(value)); + var net = (BTCPayNetwork)network; + if (value is JObject jobj) + { + var scheme = net.NBXplorerNetwork.Serializer.ToObject(jobj); + scheme.Network = net; + return scheme; + } + // Legacy + return DerivationSchemeSettings.Parse(((JValue)value).Value(), net); + } + + public override string GetTransactionLink(BTCPayNetworkBase network, string txId) + { + if (txId == null) + throw new ArgumentNullException(nameof(txId)); + if (network?.BlockExplorerLink == null) + return null; + return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); + } + } +} diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs new file mode 100644 index 000000000..2f42c6157 --- /dev/null +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Invoices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Payments +{ + public class LightningPaymentType : PaymentType + { + public static LightningPaymentType Instance { get; } = new LightningPaymentType(); + private LightningPaymentType() + { + + } + + public override string ToPrettyString() => "Off-Chain"; + public override string GetId() => "LightningLike"; + + public override CryptoPaymentData DeserializePaymentData(string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) + { + return JsonConvert.DeserializeObject(value.ToString()); + } + + public override string GetTransactionLink(BTCPayNetworkBase network, string txId) + { + return null; + } + } +} diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index a1387884b..1203401c1 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -2,21 +2,64 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments { /// /// The different ways to pay an invoice /// - public enum PaymentTypes + public static class PaymentTypes { /// /// On-Chain UTXO based, bitcoin compatible /// - BTCLike, + public static BitcoinPaymentType BTCLike => BitcoinPaymentType.Instance; /// /// Lightning payment /// - LightningLike + public static LightningPaymentType LightningLike => LightningPaymentType.Instance; + + public static bool TryParse(string paymentType, out PaymentType type) + { + switch (paymentType.ToLowerInvariant()) + { + case "btclike": + case "onchain": + type = PaymentTypes.BTCLike; + break; + case "lightninglike": + case "offchain": + type = PaymentTypes.LightningLike; + break; + default: + type = null; + return false; + } + return true; + } + public static PaymentType Parse(string paymentType) + { + if (!TryParse(paymentType, out var result)) + throw new FormatException("Invalid payment type"); + return result; + } + } + + public abstract class PaymentType + { + public abstract string ToPrettyString(); + public override string ToString() + { + return GetId(); + } + + public abstract string GetId(); + public abstract CryptoPaymentData DeserializePaymentData(string str); + public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str); + public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); + + public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); } } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index 22b7affe9..ec1458aca 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -5,12 +5,14 @@ "launchBrowser": true, "environmentVariables": { "BTCPAY_NETWORK": "regtest", + "BTCPAY_LAUNCHSETTINGS": "true", "BTCPAY_BUNDLEJSCSS": "false", "BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/", "BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true", "BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", + "BTCPAY_ALLOW-ADMIN-REGISTRATION": "true", "BTCPAY_DISABLE-REGISTRATION": "false", "ASPNETCORE_ENVIRONMENT": "Development", "BTCPAY_CHAINS": "btc,ltc", @@ -23,6 +25,7 @@ "launchBrowser": true, "environmentVariables": { "BTCPAY_NETWORK": "regtest", + "BTCPAY_LAUNCHSETTINGS": "true", "BTCPAY_PORT": "14142", "BTCPAY_HttpsUseDefaultCertificate": "true", "BTCPAY_BUNDLEJSCSS": "false", @@ -33,6 +36,7 @@ "BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake", "BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", + "BTCPAY_ALLOW-ADMIN-REGISTRATION": "true", "BTCPAY_DISABLE-REGISTRATION": "false", "ASPNETCORE_ENVIRONMENT": "Development", "BTCPAY_CHAINS": "btc,ltc", diff --git a/BTCPayServer/Security/AuthenticationExtensions.cs b/BTCPayServer/Security/AuthenticationExtensions.cs new file mode 100644 index 000000000..4378162aa --- /dev/null +++ b/BTCPayServer/Security/AuthenticationExtensions.cs @@ -0,0 +1,15 @@ +using System; +using BTCPayServer.Security.Bitpay; +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security +{ + public static class AuthenticationExtensions + { + public static AuthenticationBuilder AddBitpayAuthentication(this AuthenticationBuilder builder) + { + builder.AddScheme(Policies.BitpayAuthentication, o => { }); + return builder; + } + } +} diff --git a/BTCPayServer/Security/BTCPayClaimsFilter.cs b/BTCPayServer/Security/BTCPayClaimsFilter.cs index 5ab53ef12..e73f54fdc 100644 --- a/BTCPayServer/Security/BTCPayClaimsFilter.cs +++ b/BTCPayServer/Security/BTCPayClaimsFilter.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; +using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Models; using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; namespace BTCPayServer.Security @@ -17,13 +12,13 @@ namespace BTCPayServer.Security public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions { - UserManager _UserManager; + UserManager _userManager; StoreRepository _StoreRepository; public BTCPayClaimsFilter( UserManager userManager, StoreRepository storeRepository) { - _UserManager = userManager; + _userManager = userManager; _StoreRepository = storeRepository; } @@ -34,29 +29,32 @@ namespace BTCPayServer.Security public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { + if (context.HttpContext.User?.Identity?.AuthenticationType != Policies.CookieAuthentication) + return; var principal = context.HttpContext.User; - if (!context.HttpContext.GetIsBitpayAPI()) + var identity = ((ClaimsIdentity)principal.Identity); + if (principal.IsInRole(Roles.ServerAdmin)) { - var identity = ((ClaimsIdentity)principal.Identity); - if (principal.IsInRole(Roles.ServerAdmin)) + identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); + } + + if (context.RouteData.Values.TryGetValue("storeId", out var storeId)) + { + var userid = _userManager.GetUserId(principal); + + if (!string.IsNullOrEmpty(userid)) { - identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); - } - if (context.RouteData.Values.TryGetValue("storeId", out var storeId)) - { - var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); - if (claim != null) + var store = await _StoreRepository.FindStore((string)storeId, userid); + if (store == null) { - var store = await _StoreRepository.FindStore((string)storeId, claim.Value); - if (store == null) - context.Result = new ChallengeResult(Policies.CookieAuthentication); - else + context.Result = new ChallengeResult(); + } + else + { + context.HttpContext.SetStoreData(store); + if (store != null) { - context.HttpContext.SetStoreData(store); - if (store != null) - { - identity.AddClaims(store.GetClaims()); - } + identity.AddClaims(store.GetClaims()); } } } diff --git a/BTCPayServer/Security/Bitpay/BitpayAuthenticationHandler.cs b/BTCPayServer/Security/Bitpay/BitpayAuthenticationHandler.cs new file mode 100644 index 000000000..69f12570c --- /dev/null +++ b/BTCPayServer/Security/Bitpay/BitpayAuthenticationHandler.cs @@ -0,0 +1,173 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http.Extensions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Authentication; +using BTCPayServer.Services; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBitpayClient; +using NBitpayClient.Extensions; +using Newtonsoft.Json.Linq; +using BTCPayServer.Logging; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.Authentication; +using System.Text.Encodings.Web; + + +namespace BTCPayServer.Security.Bitpay +{ + public class BitpayAuthenticationHandler : AuthenticationHandler + { + StoreRepository _StoreRepository; + TokenRepository _TokenRepository; + public BitpayAuthenticationHandler( + TokenRepository tokenRepository, + StoreRepository storeRepository, + IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + _TokenRepository = tokenRepository; + _StoreRepository = storeRepository; + } + + protected override async Task HandleAuthenticateAsync() + { + List claims = new List(); + if (!Context.Request.HttpContext.TryGetBitpayAuth(out var bitpayAuth)) + return AuthenticateResult.NoResult(); + string storeId = null; + bool anonymous = true; + bool? success = null; + if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) + { + var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims); + storeId = result.StoreId; + success = result.SuccessAuth; + anonymous = false; + } + else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) + { + storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization); + success = storeId != null; + anonymous = false; + } + else + { + if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues)) + { + storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty; + success = true; + anonymous = true; + } + } + + if (success is true) + { + if (storeId != null) + { + claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId)); + var store = await _StoreRepository.FindStore(storeId); + if (store == null || + (anonymous && !store.GetStoreBlob().AnyoneCanInvoice)) + { + return AuthenticateResult.Fail("Invalid credentials"); + } + store.AdditionalClaims.AddRange(claims); + Context.Request.HttpContext.SetStoreData(store); + } + return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication)); + } + else if (success is false) + { + return AuthenticateResult.Fail("Invalid credentials"); + } + return AuthenticateResult.NoResult(); + } + + private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List claims) + { + httpContext.Request.EnableRewind(); + + string storeId = null; + string body = string.Empty; + if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) + { + using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) + { + body = reader.ReadToEnd(); + } + httpContext.Request.Body.Position = 0; + } + + var url = httpContext.Request.GetEncodedUrl(); + try + { + var key = new PubKey(id); + if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) + { + var sin = key.GetBitIDSIN(); + claims.Add(new Claim(Claims.SIN, sin)); + + string token = null; + if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) + { + token = tokenValues[0]; + } + + if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") + { + try + { + token = JObject.Parse(body)?.Property("token", StringComparison.OrdinalIgnoreCase)?.Value?.Value(); + } + catch { } + } + + if (token != null) + { + var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault(); + if (bitToken == null) + { + return (null, false); + } + storeId = bitToken.StoreId; + } + } + else + { + return (storeId, false); + } + } + catch (FormatException) { } + return (storeId, true); + } + + private async Task CheckLegacyAPIKey(HttpContext httpContext, string auth) + { + var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string apiKey = null; + try + { + apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); + } + catch + { + return null; + } + return await _TokenRepository.GetStoreIdFromAPIKey(apiKey); + } + } +} diff --git a/BTCPayServer/Security/Bitpay/BitpayAuthenticationOptions.cs b/BTCPayServer/Security/Bitpay/BitpayAuthenticationOptions.cs new file mode 100644 index 000000000..57ff252bb --- /dev/null +++ b/BTCPayServer/Security/Bitpay/BitpayAuthenticationOptions.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.Bitpay +{ + public class BitpayAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/BTCPayServer/Security/BitpayAuthentication.cs b/BTCPayServer/Security/BitpayAuthentication.cs deleted file mode 100644 index d33ff5910..000000000 --- a/BTCPayServer/Security/BitpayAuthentication.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Http.Extensions; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using BTCPayServer.Authentication; -using BTCPayServer.Models; -using BTCPayServer.Services; -using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Options; -using NBitcoin; -using NBitcoin.DataEncoders; -using NBitpayClient; -using NBitpayClient.Extensions; -using Newtonsoft.Json.Linq; -using BTCPayServer.Logging; -using Microsoft.AspNetCore.Http.Internal; -using Microsoft.AspNetCore.Authentication; -using System.Text.Encodings.Web; -using Microsoft.Extensions.DependencyInjection; - -namespace BTCPayServer.Security -{ - public class BitpayAuthentication - { - public class BitpayAuthOptions : AuthenticationSchemeOptions - { - - } - class BitpayAuthHandler : AuthenticationHandler - { - StoreRepository _StoreRepository; - TokenRepository _TokenRepository; - public BitpayAuthHandler( - TokenRepository tokenRepository, - StoreRepository storeRepository, - IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - _TokenRepository = tokenRepository; - _StoreRepository = storeRepository; - } - - protected override async Task HandleAuthenticateAsync() - { - if (Context.Request.HttpContext.GetIsBitpayAPI()) - { - List claims = new List(); - var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth(); - string storeId = null; - bool anonymous = true; - bool? success = null; - if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) - { - var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims); - storeId = result.StoreId; - success = result.SuccessAuth; - anonymous = false; - } - else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) - { - storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization); - success = storeId != null; - anonymous = false; - } - else - { - if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues)) - { - storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty; - success = true; - anonymous = true; - } - } - - if (success is true) - { - if (storeId != null) - { - claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId)); - var store = await _StoreRepository.FindStore(storeId); - if (store == null || - (anonymous && !store.GetStoreBlob().AnyoneCanInvoice)) - { - return AuthenticateResult.Fail("Invalid credentials"); - } - store.AdditionalClaims.AddRange(claims); - Context.Request.HttpContext.SetStoreData(store); - } - return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication)); - } - else if (success is false) - { - return AuthenticateResult.Fail("Invalid credentials"); - } - // else if (success is null) - } - return AuthenticateResult.NoResult(); - } - - private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List claims) - { - httpContext.Request.EnableRewind(); - - string storeId = null; - string body = string.Empty; - if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) - { - using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) - { - body = reader.ReadToEnd(); - } - httpContext.Request.Body.Position = 0; - } - - var url = httpContext.Request.GetEncodedUrl(); - try - { - var key = new PubKey(id); - if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) - { - var sin = key.GetBitIDSIN(); - claims.Add(new Claim(Claims.SIN, sin)); - - string token = null; - if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) - { - token = tokenValues[0]; - } - - if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") - { - try - { - token = JObject.Parse(body)?.Property("token")?.Value?.Value(); - } - catch { } - } - - if (token != null) - { - var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault(); - if (bitToken == null) - { - return (null, false); - } - storeId = bitToken.StoreId; - } - } - else - { - return (storeId, false); - } - } - catch (FormatException) { } - return (storeId, true); - } - - private async Task CheckLegacyAPIKey(HttpContext httpContext, string auth) - { - var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - string apiKey = null; - try - { - apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); - } - catch - { - return null; - } - return await _TokenRepository.GetStoreIdFromAPIKey(apiKey); - } - } - internal static void AddAuthentication(IServiceCollection services, Action bitpayAuth = null) - { - bitpayAuth = bitpayAuth ?? new Action((o) => { }); - services.AddAuthentication().AddScheme(Policies.BitpayAuthentication, bitpayAuth); - } - } -} diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs index f9ce9a1f7..c7dd4c16e 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/Policies.cs @@ -8,6 +8,7 @@ namespace BTCPayServer.Security { public static class Policies { + public const string BitpayAuthentication = "Bitpay.Auth"; public const string CookieAuthentication = "Identity.Application"; public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) @@ -31,10 +32,10 @@ namespace BTCPayServer.Security { public const string Key = "btcpay.store.canmodifystoresettings"; } - public class CanCreateInvoice { public const string Key = "btcpay.store.cancreateinvoice"; } + } } diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 72897a30d..e9cfd5d14 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -16,6 +16,7 @@ using BTCPayServer.Security; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Stores; using Ganss.XSS; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Http.Extensions; @@ -34,20 +35,20 @@ namespace BTCPayServer.Services.Apps ApplicationDbContextFactory _ContextFactory; private readonly InvoiceRepository _InvoiceRepository; CurrencyNameTable _Currencies; + private readonly StoreRepository _storeRepository; private readonly HtmlSanitizer _HtmlSanitizer; - private readonly BTCPayNetworkProvider _Networks; public CurrencyNameTable Currencies => _Currencies; public AppService(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository, - BTCPayNetworkProvider networks, CurrencyNameTable currencies, + StoreRepository storeRepository, HtmlSanitizer htmlSanitizer) { _ContextFactory = contextFactory; _InvoiceRepository = invoiceRepository; _Currencies = currencies; + _storeRepository = storeRepository; _HtmlSanitizer = htmlSanitizer; - _Networks = networks; } public async Task GetAppInfo(string appId) @@ -247,12 +248,9 @@ namespace BTCPayServer.Services.Apps } } - public async Task GetStore(AppData app) + public Task GetStore(AppData app) { - using (var ctx = _ContextFactory.CreateContext()) - { - return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId); - } + return _storeRepository.FindStore(app.StoreDataId); } @@ -324,7 +322,7 @@ namespace BTCPayServer.Services.Apps var paymentMethodContribution = new Contribution(); paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId(); paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId, _Networks).Rate; + var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId).Rate; paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value; return paymentMethodContribution; }) diff --git a/BTCPayServer/Services/BTCPayServerEnvironment.cs b/BTCPayServer/Services/BTCPayServerEnvironment.cs index df68506bf..3c8f6eaaa 100644 --- a/BTCPayServer/Services/BTCPayServerEnvironment.cs +++ b/BTCPayServer/Services/BTCPayServerEnvironment.cs @@ -68,6 +68,8 @@ namespace BTCPayServer.Services } } + public HttpContext Context => httpContext.HttpContext; + public override string ToString() { StringBuilder txt = new StringBuilder(); diff --git a/BTCPayServer/Services/Fees/IFeeProviderFactory.cs b/BTCPayServer/Services/Fees/IFeeProviderFactory.cs index 07d2af778..3b2cb5bec 100644 --- a/BTCPayServer/Services/Fees/IFeeProviderFactory.cs +++ b/BTCPayServer/Services/Fees/IFeeProviderFactory.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Services { public interface IFeeProviderFactory { - IFeeProvider CreateFeeProvider(BTCPayNetwork network); + IFeeProvider CreateFeeProvider(BTCPayNetworkBase network); } } diff --git a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs index f5f1f8d3b..a2129d04b 100644 --- a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs +++ b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Services.Fees public FeeRate Fallback { get; set; } public int BlockTarget { get; set; } - public IFeeProvider CreateFeeProvider(BTCPayNetwork network) + public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network) { return new NBXplorerFeeProvider(this, _ExplorerClients.GetExplorerClient(network)); } diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index cc1a9a7e2..f799d2790 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -20,125 +20,30 @@ namespace BTCPayServer.Services public HardwareWalletException(string message) : base(message) { } public HardwareWalletException(string message, Exception inner) : base(message, inner) { } } - public class HardwareWalletService : IDisposable + public abstract class HardwareWalletService : IDisposable { - class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable + public abstract string Device { get; } + public abstract Task Test(CancellationToken cancellation); + + public abstract Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation); + public virtual async Task GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) { - private readonly WebSocket webSocket; - - public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) - { - if (webSocket == null) - throw new ArgumentNullException(nameof(webSocket)); - this.webSocket = webSocket; - } - - SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1); - public async Task Exchange(byte[][] apdus, CancellationToken cancellationToken) - { - await _Semaphore.WaitAsync(); - List responses = new List(); - try - { - foreach (var apdu in apdus) - { - await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cancellationToken); - } - foreach (var apdu in apdus) - { - byte[] response = new byte[300]; - var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cancellationToken); - Array.Resize(ref response, result.Count); - responses.Add(response); - } - } - finally - { - _Semaphore.Release(); - } - return responses.ToArray(); - } - - public void Dispose() - { - _Semaphore.Dispose(); - } + return (await GetExtPubKey(network, keyPath, cancellation)).GetPublicKey(); } - private readonly LedgerClient _Ledger; - public LedgerClient Ledger - { - get - { - return _Ledger; - } - } - WebSocketTransport _Transport = null; - public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) - { - if (ledgerWallet == null) - throw new ArgumentNullException(nameof(ledgerWallet)); - _Transport = new WebSocketTransport(ledgerWallet); - _Ledger = new LedgerClient(_Transport); - _Ledger.MaxAPDUSize = 90; - } - - public async Task Test(CancellationToken cancellation) - { - var version = await Ledger.GetFirmwareVersionAsync(cancellation); - return new LedgerTestResult() { Success = true }; - } - - public async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - - var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit; - var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation); - var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions() - { - P2SH = segwit, - Legacy = !segwit - }); - return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath }; - } - - private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) - { - try - { - var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation); - try - { - pubKey.GetAddress(network.NBitcoinNetwork); - } - catch - { - if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) - throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); - } - var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray(); - var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); - return extpubkey; - } - catch (FormatException) - { - throw new HardwareWalletException("Unsupported ledger app"); - } - } - - public async Task CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation) - { - var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation); - return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey; - } - - public async Task FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) + public async Task FindKeyPathFromDerivation(BTCPayNetwork network, DerivationStrategyBase derivationScheme, CancellationToken cancellation) { + var pubKeys = derivationScheme.GetExtPubKeys().Select(k => k.GetPublicKey()).ToArray(); + var derivation = derivationScheme.Derive(new KeyPath(0)); List derivations = new List(); if (network.NBitcoinNetwork.Consensus.SupportSegwit) - derivations.Add(new KeyPath("49'")); + { + if (derivation.Redeem?.IsWitness is true || + derivation.ScriptPubKey.IsWitness) // Native or p2sh segwit + derivations.Add(new KeyPath("49'")); + if (derivation.Redeem == null && derivation.ScriptPubKey.IsWitness) // Native segwit + derivations.Add(new KeyPath("84'")); + } derivations.Add(new KeyPath("44'")); KeyPath foundKeyPath = null; foreach (var account in @@ -146,46 +51,21 @@ namespace BTCPayServer.Services .Select(purpose => purpose.Derive(network.CoinType)) .SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true)))) { - try + var pubkey = await GetPubKey(network, account, cancellation); + if (pubKeys.Contains(pubkey)) { - var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation); - if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) - { - foundKeyPath = account; - break; - } - } - catch (FormatException) - { - throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}"); + foundKeyPath = account; + break; } } return foundKeyPath; } - public async Task SignTransactionAsync(SignatureRequest[] signatureRequests, - Transaction unsigned, - KeyPath changeKeyPath, - CancellationToken cancellationToken) - { - try - { - var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); - if (signedTransaction == null) - throw new Exception("The ledger failed to sign the transaction"); - return signedTransaction; - } - catch (Exception ex) - { - throw new Exception("The ledger failed to sign the transaction", ex); - } - } + public abstract Task SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken); - public void Dispose() + public virtual void Dispose() { - if (_Transport != null) - _Transport.Dispose(); } } @@ -194,11 +74,4 @@ namespace BTCPayServer.Services public bool Success { get; set; } public string Error { get; set; } } - - public class GetXPubResult - { - public string ExtPubKey { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] - public KeyPath KeyPath { get; set; } - } } diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 248f23e55..49261304a 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; @@ -15,9 +16,8 @@ namespace BTCPayServer.Services.Invoices.Export public BTCPayNetworkProvider Networks { get; } public CurrencyNameTable Currencies { get; } - public InvoiceExport(BTCPayNetworkProvider networks, CurrencyNameTable currencies) + public InvoiceExport(CurrencyNameTable currencies) { - Networks = networks; Currencies = currencies; } public string Process(InvoiceEntity[] invoices, string fileFormat) @@ -67,7 +67,7 @@ namespace BTCPayServer.Services.Invoices.Export var cryptoCode = payment.GetPaymentMethodId().CryptoCode; var pdata = payment.GetCryptoPaymentData(); - var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks); + var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId()); var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee; invoiceDue -= paidAfterNetworkFees * pmethod.Rate; @@ -77,8 +77,8 @@ namespace BTCPayServer.Services.Invoices.Export PaymentId = pdata.GetPaymentId(), CryptoCode = cryptoCode, ConversionRate = pmethod.Rate, - PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain", - Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)), + PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(), + Destination = pdata.GetDestination(), Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture), // Adding NetworkFee because Paid doesn't take into account network fees diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index aa70db03a..45765f943 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using BTCPayServer.Models; using Newtonsoft.Json.Linq; using NBitcoin.DataEncoders; @@ -14,6 +15,7 @@ using NBXplorer.DerivationStrategy; using BTCPayServer.Payments; using NBitpayClient; using BTCPayServer.Payments.Bitcoin; +using System.ComponentModel.DataAnnotations.Schema; namespace BTCPayServer.Services.Invoices { @@ -113,6 +115,8 @@ namespace BTCPayServer.Services.Invoices } public class InvoiceEntity { + [JsonIgnore] + public BTCPayNetworkProvider Networks { get; set; } public const int InternalTagSupport_Version = 1; public const int Lastest_Version = 1; public int Version { get; set; } @@ -190,18 +194,18 @@ namespace BTCPayServer.Services.Invoices get; set; } - public IEnumerable GetSupportedPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod + public IEnumerable GetSupportedPaymentMethod(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod { return - GetSupportedPaymentMethod(networks) + GetSupportedPaymentMethod() .Where(p => paymentMethodId == null || p.PaymentId == paymentMethodId) .OfType(); } - public IEnumerable GetSupportedPaymentMethod(BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod + public IEnumerable GetSupportedPaymentMethod() where T : ISupportedPaymentMethod { - return GetSupportedPaymentMethod(null, networks); + return GetSupportedPaymentMethod(null); } - public IEnumerable GetSupportedPaymentMethod(BTCPayNetworkProvider networks) + public IEnumerable GetSupportedPaymentMethod() { #pragma warning disable CS0618 bool btcReturned = false; @@ -211,21 +215,21 @@ namespace BTCPayServer.Services.Invoices foreach (var strat in strategies.Properties()) { var paymentMethodId = PaymentMethodId.Parse(strat.Name); - var network = networks.GetNetwork(paymentMethodId.CryptoCode); + var network = Networks.GetNetwork(paymentMethodId.CryptoCode); if (network != null) { - if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike) + if (network == Networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike) btcReturned = true; - yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network); + yield return paymentMethodId.PaymentType.DeserializeSupportedPaymentMethod(network, strat.Value); } } } if (!btcReturned && !string.IsNullOrEmpty(DerivationStrategy)) { - if (networks.BTC != null) + if (Networks.BTC != null) { - yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC); + yield return BTCPayServer.DerivationSchemeSettings.Parse(DerivationStrategy, Networks.BTC); } } #pragma warning restore CS0618 @@ -238,8 +242,8 @@ namespace BTCPayServer.Services.Invoices { obj.Add(strat.PaymentId.ToString(), PaymentMethodExtensions.Serialize(strat)); #pragma warning disable CS0618 - if (strat.PaymentId.IsBTCOnChain) - DerivationStrategy = ((JValue)PaymentMethodExtensions.Serialize(strat)).Value(); + // This field should eventually disappear + DerivationStrategy = null; } DerivationStrategies = JsonConvert.SerializeObject(obj); #pragma warning restore CS0618 @@ -278,7 +282,7 @@ namespace BTCPayServer.Services.Invoices { return Payments.Where(p => p.CryptoCode == cryptoCode).ToList(); } - public List GetPayments(BTCPayNetwork network) + public List GetPayments(BTCPayNetworkBase network) { return GetPayments(network.CryptoCode); } @@ -362,7 +366,7 @@ namespace BTCPayServer.Services.Invoices return DateTimeOffset.UtcNow > ExpirationTime; } - public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider) + public InvoiceResponse EntityToDTO() { ServerUrl = ServerUrl ?? ""; InvoiceResponse dto = new InvoiceResponse @@ -391,7 +395,7 @@ namespace BTCPayServer.Services.Invoices dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.CryptoInfo = new List(); dto.MinerFees = new Dictionary(); - foreach (var info in this.GetPaymentMethods(networkProvider)) + foreach (var info in this.GetPaymentMethods()) { var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); @@ -419,7 +423,6 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.ExRates = exrates; var paymentId = info.GetId(); - var scheme = info.Network.UriScheme; cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; cryptoInfo.Payments = GetPayments(info.Network).Select(entity => @@ -430,47 +433,50 @@ namespace BTCPayServer.Services.Invoices Id = data.GetPaymentId(), Fee = entity.NetworkFee, Value = data.GetValue(), - Completed = data.PaymentCompleted(entity, info.Network), - Confirmed = data.PaymentConfirmed(entity, SpeedPolicy, info.Network), - Destination = data.GetDestination(info.Network), + Completed = data.PaymentCompleted(entity), + Confirmed = data.PaymentConfirmed(entity, SpeedPolicy), + Destination = data.GetDestination(), PaymentType = data.GetPaymentType().ToString(), ReceivedDate = entity.ReceivedTime.DateTime }; }).ToList(); - if (paymentId.PaymentType == PaymentTypes.BTCLike) - { - var minerInfo = new MinerFeeInfo(); - minerInfo.TotalFee = accounting.NetworkFee.Satoshi; - minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi; - dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo); - var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; - cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() - { - BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", - }; - } if (paymentId.PaymentType == PaymentTypes.LightningLike) { - cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() + cryptoInfo.PaymentUrls = new InvoicePaymentUrls() { BOLT11 = $"lightning:{cryptoInfo.Address}" }; } -#pragma warning disable CS0618 - if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike) + else if (paymentId.PaymentType == PaymentTypes.BTCLike) { - dto.BTCPrice = cryptoInfo.Price; - dto.Rate = cryptoInfo.Rate; - dto.ExRates = cryptoInfo.ExRates; - dto.BitcoinAddress = cryptoInfo.Address; - dto.BTCPaid = cryptoInfo.Paid; - dto.BTCDue = cryptoInfo.Due; - dto.PaymentUrls = cryptoInfo.PaymentUrls; - } + var scheme = info.Network.UriScheme; -#pragma warning restore CS0618 + var minerInfo = new MinerFeeInfo(); + minerInfo.TotalFee = accounting.NetworkFee.Satoshi; + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate + .GetFee(1).Satoshi; + dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); + cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() + { + BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", + }; + +#pragma warning disable 618 + if (info.CryptoCode == "BTC") + { + dto.BTCPrice = cryptoInfo.Price; + dto.Rate = cryptoInfo.Rate; + dto.ExRates = cryptoInfo.ExRates; + dto.BitcoinAddress = cryptoInfo.Address; + dto.BTCPaid = cryptoInfo.Paid; + dto.BTCDue = cryptoInfo.Due; + dto.PaymentUrls = cryptoInfo.PaymentUrls; + } +#pragma warning restore 618 + } + dto.CryptoInfo.Add(cryptoInfo); dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi); @@ -510,21 +516,21 @@ namespace BTCPayServer.Services.Invoices internal bool Support(PaymentMethodId paymentMethodId) { - var rates = GetPaymentMethods(null); + var rates = GetPaymentMethods(); return rates.TryGet(paymentMethodId) != null; } - public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider) + public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId) { - GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data); + GetPaymentMethods().TryGetValue(paymentMethodId, out var data); return data; } - public PaymentMethod GetPaymentMethod(BTCPayNetwork network, PaymentTypes paymentType, BTCPayNetworkProvider networkProvider) + public PaymentMethod GetPaymentMethod(BTCPayNetworkBase network, PaymentType paymentType) { - return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider); + return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType)); } - public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider) + public PaymentMethodDictionary GetPaymentMethods() { PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); var serializer = new Serializer(Dummy); @@ -538,9 +544,8 @@ namespace BTCPayServer.Services.Invoices r.CryptoCode = paymentMethodId.CryptoCode; r.PaymentType = paymentMethodId.PaymentType.ToString(); r.ParentEntity = this; - r.Network = networkProvider?.GetNetwork(r.CryptoCode); - if (r.Network != null || networkProvider == null) - paymentMethods.Add(r); + r.Network = Networks?.UnfilteredNetworks.GetNetwork(r.CryptoCode); + paymentMethods.Add(r); } } #pragma warning restore CS0618 @@ -551,7 +556,7 @@ namespace BTCPayServer.Services.Invoices public void SetPaymentMethod(PaymentMethod paymentMethod) { - var dict = GetPaymentMethods(null); + var dict = GetPaymentMethods(); dict.AddOrReplace(paymentMethod); SetPaymentMethods(dict); } @@ -730,7 +735,7 @@ namespace BTCPayServer.Services.Invoices [JsonIgnore] public InvoiceEntity ParentEntity { get; set; } [JsonIgnore] - public BTCPayNetwork Network { get; set; } + public BTCPayNetworkBase Network { get; set; } [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] [Obsolete("Use GetId().CryptoCode instead")] public string CryptoCode { get; set; } @@ -748,7 +753,7 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodId GetId() { #pragma warning disable CS0618 // Type or member is obsolete - return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : Enum.Parse(PaymentType)); + return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType)); #pragma warning restore CS0618 // Type or member is obsolete } @@ -781,7 +786,7 @@ namespace BTCPayServer.Services.Invoices } else { - var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails); + IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(PaymentMethodDetails.ToString()); if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike) { btcLike.NextNetworkFee = NextNetworkFee; @@ -830,7 +835,7 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodAccounting Calculate(Func paymentPredicate = null) { paymentPredicate = paymentPredicate ?? new Func((p) => true); - var paymentMethods = ParentEntity.GetPaymentMethods(null); + var paymentMethods = ParentEntity.GetPaymentMethods(); var totalDue = ParentEntity.ProductInformation.Price / Rate; var paid = 0m; @@ -840,8 +845,8 @@ namespace BTCPayServer.Services.Invoices var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision)); bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); int txRequired = 0; - var payments = - ParentEntity.GetPayments() + + _ = ParentEntity.GetPayments() .Where(p => p.Accounted && paymentPredicate(p)) .OrderBy(p => p.ReceivedTime) .Select(_ => @@ -852,15 +857,16 @@ namespace BTCPayServer.Services.Invoices { totalDue += txFee; } + paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision); if (GetId() == _.GetPaymentMethodId()) { cryptoPaid += _.GetCryptoPaymentData().GetValue(); txRequired++; } + return _; - }) - .ToArray(); + }).ToArray(); var accounting = new PaymentMethodAccounting(); accounting.TxCount = txRequired; @@ -893,6 +899,9 @@ namespace BTCPayServer.Services.Invoices public class PaymentEntity { + [NotMapped] + [JsonIgnore] + public BTCPayNetwork Network { get; set; } public int Version { get; set; } public DateTimeOffset ReceivedTime { @@ -932,33 +941,33 @@ namespace BTCPayServer.Services.Invoices public CryptoPaymentData GetCryptoPaymentData() { -#pragma warning disable CS0618 - if (string.IsNullOrEmpty(CryptoPaymentDataType)) + CryptoPaymentData paymentData = null; +#pragma warning disable CS0618 // Type or member is obsolete + if (string.IsNullOrEmpty(CryptoPaymentData)) { // For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety - var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData(); - paymentData.Outpoint = Outpoint; - paymentData.Output = Output; - paymentData.RBF = true; - paymentData.ConfirmationCount = 0; - paymentData.Legacy = true; - return paymentData; + var bitcoin = new BitcoinLikePaymentData(); + bitcoin.Network = Network; + bitcoin.Outpoint = Outpoint; + bitcoin.Output = Output; + bitcoin.RBF = true; + bitcoin.ConfirmationCount = 0; + bitcoin.Legacy = true; + bitcoin.Output = Output; + bitcoin.Outpoint = Outpoint; + paymentData = bitcoin; } - if (GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike) + else { - var paymentData = JsonConvert.DeserializeObject(CryptoPaymentData); - // legacy - paymentData.Output = Output; - paymentData.Outpoint = Outpoint; - return paymentData; + paymentData = GetPaymentMethodId().PaymentType.DeserializePaymentData(CryptoPaymentData); + paymentData.Network = Network; + if (paymentData is BitcoinLikePaymentData bitcoin) + { + bitcoin.Output = Output; + bitcoin.Outpoint = Outpoint; + } } - if (GetPaymentMethodId().PaymentType == PaymentTypes.LightningLike) - { - return JsonConvert.DeserializeObject(CryptoPaymentData); - } - - throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType); -#pragma warning restore CS0618 + return paymentData; } public PaymentEntity SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData) @@ -978,6 +987,7 @@ namespace BTCPayServer.Services.Invoices } internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null) { + value = value ?? this.GetCryptoPaymentData().GetValue(); var to = paymentMethodId; var from = this.GetPaymentMethodId(); @@ -994,7 +1004,7 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodId GetPaymentMethodId() { #pragma warning disable CS0618 // Type or member is obsolete - return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse(CryptoPaymentDataType)); + return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(CryptoPaymentDataType)); #pragma warning restore CS0618 // Type or member is obsolete } @@ -1005,9 +1015,13 @@ namespace BTCPayServer.Services.Invoices #pragma warning restore CS0618 } } - + /// + /// A record of a payment + /// public interface CryptoPaymentData { + [JsonIgnore] + BTCPayNetworkBase Network { get; set; } /// /// Returns an identifier which uniquely identify the payment /// @@ -1024,10 +1038,10 @@ namespace BTCPayServer.Services.Invoices /// /// The amount paid decimal GetValue(); - bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network); - bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network); + bool PaymentCompleted(PaymentEntity entity); + bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy); - PaymentTypes GetPaymentType(); - string GetDestination(BTCPayNetwork network); + PaymentType GetPaymentType(); + string GetDestination(); } } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 57b02f414..8585c0e78 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -1,4 +1,4 @@ -using DBriize; +using DBriize; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -37,8 +37,11 @@ namespace BTCPayServer.Services.Invoices } private ApplicationDbContextFactory _ContextFactory; + private readonly BTCPayNetworkProvider _Networks; private CustomThreadPool _IndexerThread; - public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath) + + public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, + BTCPayNetworkProvider networks) { int retryCount = 0; retry: @@ -49,6 +52,17 @@ retry: catch when (retryCount++ < 5) { goto retry; } _IndexerThread = new CustomThreadPool(1, "Invoice Indexer"); _ContextFactory = contextFactory; + _Networks = networks.UnfilteredNetworks; + } + + public InvoiceEntity CreateNewInvoice() + { + return new InvoiceEntity() + { + Networks = _Networks, + Version = InvoiceEntity.Lastest_Version, + InvoiceTime = DateTimeOffset.UtcNow, + }; } public async Task RemovePendingInvoice(string invoiceId) @@ -118,10 +132,25 @@ retry: } } - public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider) + public async Task ExtendInvoiceMonitor(string invoiceId) + { + using (var ctx = _ContextFactory.CreateContext()) + { + var invoiceData = await ctx.Invoices.FindAsync(invoiceId); + + var invoice = ToObject(invoiceData.Blob); + invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); + invoiceData.Blob = ToBytes(invoice, null); + + await ctx.SaveChangesAsync(); + } + } + + public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice) { List textSearch = new List(); - invoice = Clone(invoice, null); + invoice = ToObject(ToBytes(invoice)); + invoice.Networks = _Networks; invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); #pragma warning disable CS0618 invoice.Payments = new List(); @@ -143,13 +172,13 @@ retry: CustomerEmail = invoice.RefundMail }); - foreach (var paymentMethod in invoice.GetPaymentMethods(networkProvider)) + foreach (var paymentMethod in invoice.GetPaymentMethods()) { if (paymentMethod.Network == null) throw new InvalidOperationException("CryptoCode unsupported"); var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); - string address = GetDestination(paymentMethod, paymentMethod.Network.NBitcoinNetwork); + string address = GetDestination(paymentMethod); context.AddressInvoices.Add(new AddressInvoiceData() { InvoiceDataId = invoice.Id, @@ -198,18 +227,19 @@ retry: } } - private static string GetDestination(PaymentMethod paymentMethod, Network network) + private string GetDestination(PaymentMethod paymentMethod) { // For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike) { - return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetDepositAddress(network).ScriptPubKey.Hash.ToString(); + var network = (BTCPayNetwork)paymentMethod.Network; + return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetDepositAddress(network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(); } /////////////// return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); } - public async Task NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetwork network) + public async Task NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetworkBase network) { using (var context = _ContextFactory.CreateContext()) { @@ -217,8 +247,8 @@ retry: if (invoice == null) return false; - var invoiceEntity = ToObject(invoice.Blob, network.NBitcoinNetwork); - var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType(), null); + var invoiceEntity = ToObject(invoice.Blob); + var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType()); if (currencyData == null) return false; @@ -237,14 +267,14 @@ retry: } #pragma warning restore CS0618 invoiceEntity.SetPaymentMethod(currencyData); - invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork); + invoice.Blob = ToBytes(invoiceEntity, network); context.AddressInvoices.Add(new AddressInvoiceData() { InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow } - .Set(GetDestination(currencyData, network.NBitcoinNetwork), currencyData.GetId())); + .Set(GetDestination(currencyData), currencyData.GetId())); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoiceId, @@ -257,6 +287,18 @@ retry: } } + public async Task AddPendingInvoiceIfNotPresent(string invoiceId) + { + using (var context = _ContextFactory.CreateContext()) + { + if (!context.PendingInvoices.Any(a => a.Id == invoiceId)) + { + context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId }); + await context.SaveChangesAsync(); + } + } + } + public async Task AddInvoiceEvent(string invoiceId, object evt) { using (var context = _ContextFactory.CreateContext()) @@ -278,7 +320,7 @@ retry: private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, PaymentMethodId paymentMethodId) { - foreach (var address in entity.GetPaymentMethods(null)) + foreach (var address in entity.GetPaymentMethods()) { if (paymentMethodId != null && paymentMethodId != address.GetId()) continue; @@ -298,7 +340,7 @@ retry: var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; - var invoiceEntity = ToObject(invoiceData.Blob, null); + var invoiceEntity = ToObject(invoiceData.Blob); MarkUnassigned(invoiceId, invoiceEntity, context, null); try { @@ -393,20 +435,20 @@ retry: private InvoiceEntity ToEntity(Data.InvoiceData invoice) { - var entity = ToObject(invoice.Blob, null); + var entity = ToObject(invoice.Blob); PaymentMethodDictionary paymentMethods = null; #pragma warning disable CS0618 entity.Payments = invoice.Payments.Select(p => { var paymentEntity = ToObject(p.Blob, null); + paymentEntity.Network = _Networks.GetNetwork(paymentEntity.CryptoCode); paymentEntity.Accounted = p.Accounted; - // PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee. // We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity. if (paymentEntity.Version == 0) { if (paymentMethods == null) - paymentMethods = entity.GetPaymentMethods(null); + paymentMethods = entity.GetPaymentMethods(); var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails(); if (paymentMethodDetails != null) // == null should never happen, but we never know. paymentEntity.NetworkFee = paymentMethodDetails.GetNextNetworkFee(); @@ -563,7 +605,7 @@ retry: return status; } - public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network) + public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, BTCPayNetwork network) { if (outputs.Length == 0) return; @@ -584,7 +626,7 @@ retry: await context.SaveChangesAsync().ConfigureAwait(false); } - var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(network)).Where(a => a != null).ToArray(); + var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)).Where(a => a != null).ToArray(); AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray()); } @@ -597,15 +639,15 @@ retry: /// /// /// The PaymentEntity or null if already added - public async Task AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetwork network, bool accounted = false) + public async Task AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false) { using (var context = _ContextFactory.CreateContext()) { var invoice = context.Invoices.Find(invoiceId); if (invoice == null) return null; - InvoiceEntity invoiceEntity = ToObject(invoice.Blob, network.NBitcoinNetwork); - PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null); + InvoiceEntity invoiceEntity = ToObject(invoice.Blob); + PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType())); IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); PaymentEntity entity = new PaymentEntity { @@ -615,7 +657,8 @@ retry: #pragma warning restore CS0618 ReceivedTime = date.UtcDateTime, Accounted = accounted, - NetworkFee = paymentMethodDetails.GetNextNetworkFee() + NetworkFee = paymentMethodDetails.GetNextNetworkFee(), + Network = network as BTCPayNetwork }; entity.SetCryptoPaymentData(paymentData); @@ -626,7 +669,7 @@ retry: bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.FeeRate.GetFee(100); // assume price for 100 bytes paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod); invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork); + invoice.Blob = ToBytes(invoiceEntity, network); } PaymentData data = new PaymentData { @@ -669,24 +712,33 @@ retry: } } - private T ToObject(byte[] value, Network network) + private InvoiceEntity ToObject(byte[] value) { - return NBitcoin.JsonConverters.Serializer.ToObject(ZipUtils.Unzip(value), network); + var entity = NBitcoin.JsonConverters.Serializer.ToObject(ZipUtils.Unzip(value), null); + entity.Networks = _Networks; + return entity; + } + private T ToObject(byte[] value, BTCPayNetworkBase network) + { + if (network == null) + { + return NBitcoin.JsonConverters.Serializer.ToObject(ZipUtils.Unzip(value), null); + } + return network.ToObject(ZipUtils.Unzip(value)); } - private byte[] ToBytes(T obj, Network network) + private byte[] ToBytes(T obj, BTCPayNetworkBase network = null) { - return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj, network)); + return ZipUtils.Zip(ToString(obj, network)); } - private T Clone(T invoice, Network network) + private string ToString(T data, BTCPayNetworkBase network) { - return NBitcoin.JsonConverters.Serializer.ToObject(ToString(invoice, network), network); - } - - private string ToString(T data, Network network) - { - return NBitcoin.JsonConverters.Serializer.ToString(data, network); + if (network == null) + { + return NBitcoin.JsonConverters.Serializer.ToString(data, null); + } + return network.ToString(data); } public void Dispose() diff --git a/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs b/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs index 05904588a..5f3fb6e92 100644 --- a/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs +++ b/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using BTCPayServer.Payments; @@ -63,7 +62,7 @@ namespace BTCPayServer.Services.Invoices _Inner.TryGetValue(paymentMethodId, out var value); return value; } - public PaymentMethod TryGet(string network, PaymentTypes paymentType) + public PaymentMethod TryGet(string network, PaymentType paymentType) { if (network == null) throw new ArgumentNullException(nameof(network)); diff --git a/BTCPayServer/Services/Invoices/PaymentMethodHandlerDictionary.cs b/BTCPayServer/Services/Invoices/PaymentMethodHandlerDictionary.cs new file mode 100644 index 000000000..e05458234 --- /dev/null +++ b/BTCPayServer/Services/Invoices/PaymentMethodHandlerDictionary.cs @@ -0,0 +1,35 @@ +using System.Collections; +using System.Collections.Generic; +using BTCPayServer.Payments; + +namespace BTCPayServer.Services.Invoices +{ + public class PaymentMethodHandlerDictionary : IEnumerable + { + private readonly Dictionary _mappedHandlers = + new Dictionary(); + + public PaymentMethodHandlerDictionary(IEnumerable paymentMethodHandlers) + { + foreach (var paymentMethodHandler in paymentMethodHandlers) + { + foreach (var supportedPaymentMethod in paymentMethodHandler.GetSupportedPaymentMethods()) + { + _mappedHandlers.Add(supportedPaymentMethod, paymentMethodHandler); + } + } + } + + public IPaymentMethodHandler this[PaymentMethodId index] => _mappedHandlers[index]; + public bool Support(PaymentMethodId paymentMethod) => _mappedHandlers.ContainsKey(paymentMethod); + public IEnumerator GetEnumerator() + { + return _mappedHandlers.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/BTCPayServer/Services/LedgerHardwareWalletService.cs b/BTCPayServer/Services/LedgerHardwareWalletService.cs new file mode 100644 index 000000000..5711fa91e --- /dev/null +++ b/BTCPayServer/Services/LedgerHardwareWalletService.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using LedgerWallet; +using NBitcoin; + +namespace BTCPayServer.Services +{ + public class LedgerHardwareWalletService : HardwareWalletService + { + class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable + { + private readonly WebSocket webSocket; + + public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) + { + if (webSocket == null) + throw new ArgumentNullException(nameof(webSocket)); + this.webSocket = webSocket; + } + + SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1); + public async Task Exchange(byte[][] apdus, CancellationToken cancellationToken) + { + await _Semaphore.WaitAsync(); + List responses = new List(); + try + { + foreach (var apdu in apdus) + { + await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cancellationToken); + } + foreach (var apdu in apdus) + { + byte[] response = new byte[300]; + var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cancellationToken); + Array.Resize(ref response, result.Count); + responses.Add(response); + } + } + finally + { + _Semaphore.Release(); + } + return responses.ToArray(); + } + + public void Dispose() + { + _Semaphore.Dispose(); + } + } + + private readonly LedgerClient _Ledger; + public LedgerClient Ledger + { + get + { + return _Ledger; + } + } + + public override string Device => "Ledger wallet"; + + WebSocketTransport _Transport = null; + public LedgerHardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) + { + if (ledgerWallet == null) + throw new ArgumentNullException(nameof(ledgerWallet)); + _Transport = new WebSocketTransport(ledgerWallet); + _Ledger = new LedgerClient(_Transport); + _Ledger.MaxAPDUSize = 90; + } + + public override async Task Test(CancellationToken cancellation) + { + var version = await Ledger.GetFirmwareVersionAsync(cancellation); + return new LedgerTestResult() { Success = true }; + } + + public override async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + return await GetExtPubKey(network, keyPath, false, cancellation); + } + public override async Task GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + return (await GetExtPubKey(network, keyPath, false, cancellation)).GetPublicKey(); + } + + private async Task GetExtPubKey(BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) + { + var pubKey = await Ledger.GetWalletPubKeyAsync(account, cancellation: cancellation); + try + { + pubKey.GetAddress(network.NBitcoinNetwork); + } + catch + { + if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) + throw new HardwareWalletException($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); + } + var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await Ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); + var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), + pubKey.ChainCode, + (byte)account.Indexes.Length, + parentFP, + account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork); + return extpubkey; + } + + public override async Task SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken) + { + var unsigned = psbt.GetGlobalTransaction(); + var changeKeyPath = psbt.Outputs.HDKeysFor(accountKey, accountKeyPath) + .Where(o => changeHint == null ? true : changeHint == o.Coin.ScriptPubKey) + .Select(o => o.RootedKeyPath.KeyPath) + .FirstOrDefault(); + var signatureRequests = psbt + .Inputs + .HDKeysFor(accountKey, accountKeyPath) + .Where(hd => !hd.Coin.PartialSigs.ContainsKey(hd.PubKey)) // Don't want to sign something twice + .GroupBy(hd => hd.Coin) + .Select(i => new SignatureRequest() + { + InputCoin = i.Key.GetSignableCoin(), + InputTransaction = i.Key.NonWitnessUtxo, + KeyPath = i.First().RootedKeyPath.KeyPath, + PubKey = i.First().PubKey + }).ToArray(); + await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); + psbt = psbt.Clone(); + foreach (var signature in signatureRequests) + { + if (signature.Signature == null) + continue; + var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint); + if (input == null) + continue; + input.PartialSigs.Add(signature.PubKey, signature.Signature); + } + return psbt; + } + + public override void Dispose() + { + if (_Transport != null) + _Transport.Dispose(); + } + } +} diff --git a/BTCPayServer/Services/MigrationSettings.cs b/BTCPayServer/Services/MigrationSettings.cs index 57f94210e..58d497e2d 100644 --- a/BTCPayServer/Services/MigrationSettings.cs +++ b/BTCPayServer/Services/MigrationSettings.cs @@ -12,5 +12,6 @@ namespace BTCPayServer.Services public bool ConvertMultiplierToSpread { get; set; } public bool ConvertNetworkFeeProperty { get; set; } public bool ConvertCrowdfundOldSettings { get; set; } + public bool ConvertWalletKeyPathRoots { get; set; } } } diff --git a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs index d120ae493..6401f9418 100644 --- a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs +++ b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using NBitcoin; @@ -17,11 +18,14 @@ namespace BTCPayServer.Services.PaymentRequests { private readonly ApplicationDbContextFactory _ContextFactory; private readonly InvoiceRepository _InvoiceRepository; + private readonly StoreRepository _storeRepository; - public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository) + public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository, + StoreRepository storeRepository) { _ContextFactory = contextFactory; _InvoiceRepository = invoiceRepository; + _storeRepository = storeRepository; } @@ -52,11 +56,12 @@ namespace BTCPayServer.Services.PaymentRequests using (var context = _ContextFactory.CreateContext()) { - return await context.PaymentRequests.Include(x => x.StoreData) + var result = await context.PaymentRequests.Include(x => x.StoreData) .Where(data => string.IsNullOrEmpty(userId) || (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))) .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + return result; } } diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index 3781b711f..ee3a72a09 100644 --- a/BTCPayServer/Services/Rates/CurrencyNameTable.cs +++ b/BTCPayServer/Services/Rates/CurrencyNameTable.cs @@ -99,7 +99,7 @@ namespace BTCPayServer.Services.Rates AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode); } } - return _CurrencyProviders.TryGet(currency); + return _CurrencyProviders.TryGet(currency.ToUpperInvariant()); } } diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index fcaea916b..43fac04e0 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; using Microsoft.EntityFrameworkCore; namespace BTCPayServer.Services.Stores @@ -13,6 +14,7 @@ namespace BTCPayServer.Services.Stores public class StoreRepository { private ApplicationDbContextFactory _ContextFactory; + public StoreRepository(ApplicationDbContextFactory contextFactory) { _ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); @@ -24,7 +26,8 @@ namespace BTCPayServer.Services.Stores return null; using (var ctx = _ContextFactory.CreateContext()) { - return await ctx.FindAsync(storeId).ConfigureAwait(false); + var result = await ctx.FindAsync(storeId).ConfigureAwait(false); + return result; } } @@ -170,7 +173,7 @@ namespace BTCPayServer.Services.Stores { Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)), StoreName = name, - SpeedPolicy = Invoices.SpeedPolicy.MediumSpeed + SpeedPolicy = SpeedPolicy.MediumSpeed }; var userStore = new UserStore { diff --git a/BTCPayServer/Services/TorServices.cs b/BTCPayServer/Services/TorServices.cs index 11487d462..abaec7fef 100644 --- a/BTCPayServer/Services/TorServices.cs +++ b/BTCPayServer/Services/TorServices.cs @@ -11,9 +11,11 @@ namespace BTCPayServer.Services { public class TorServices { + private readonly BTCPayNetworkProvider _networks; BTCPayServerOptions _Options; - public TorServices(BTCPayServerOptions options) + public TorServices(BTCPayServer.BTCPayNetworkProvider networks, BTCPayServerOptions options) { + _networks = networks; _Options = options; } @@ -58,6 +60,11 @@ namespace BTCPayServer.Services }; if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase)) torService.ServiceType = TorServiceType.BTCPayServer; + else if (TryParseP2PService(service.ServiceName, out var network)) + { + torService.ServiceType = TorServiceType.P2P; + torService.Network = network; + } result.Add(torService); } catch (Exception ex) @@ -72,11 +79,22 @@ namespace BTCPayServer.Services } Services = result.ToArray(); } + + private bool TryParseP2PService(string name, out BTCPayNetworkBase network) + { + network = null; + var splitted = name.Trim().Split('-'); + if (splitted.Length != 2 || splitted[1] != "P2P") + return false; + network = _networks.GetNetwork(splitted[0]); + return network != null; + } } public class TorService { public TorServiceType ServiceType { get; set; } = TorServiceType.Other; + public BTCPayNetworkBase Network { get; set; } public string Name { get; set; } public string OnionHost { get; set; } public int VirtualPort { get; set; } @@ -85,6 +103,7 @@ namespace BTCPayServer.Services public enum TorServiceType { BTCPayServer, + P2P, Other } } diff --git a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs index 1bdd0f05d..de5c0e7e2 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Wallets _NetworkProvider = networkProvider; _Options = memoryCacheOption; - foreach(var network in networkProvider.GetAll()) + foreach(var network in networkProvider.GetAll().OfType()) { var explorerClient = _Client.GetExplorerClient(network.CryptoCode); if (explorerClient == null) @@ -33,7 +33,7 @@ namespace BTCPayServer.Services.Wallets Dictionary _Wallets = new Dictionary(); - public BTCPayWallet GetWallet(BTCPayNetwork network) + public BTCPayWallet GetWallet(BTCPayNetworkBase network) { if (network == null) throw new ArgumentNullException(nameof(network)); @@ -47,7 +47,7 @@ namespace BTCPayServer.Services.Wallets return result; } - public bool IsAvailable(BTCPayNetwork network) + public bool IsAvailable(BTCPayNetworkBase network) { return _Client.IsAvailable(network); } diff --git a/BTCPayServer/Storage/Services/FileService.cs b/BTCPayServer/Storage/Services/FileService.cs index 3e5e1d95a..b2ba7c467 100644 --- a/BTCPayServer/Storage/Services/FileService.cs +++ b/BTCPayServer/Storage/Services/FileService.cs @@ -34,20 +34,21 @@ namespace BTCPayServer.Storage.Services return storedFile; } - public async Task GetFileUrl(string fileId) + public async Task GetFileUrl(Uri baseUri, string fileId) { var settings = await _SettingsRepository.GetSettingAsync(); var provider = GetProvider(settings); var storedFile = await _FileRepository.GetFile(fileId); - return storedFile == null ? null: await provider.GetFileUrl(storedFile, settings); + return storedFile == null ? null: await provider.GetFileUrl(baseUri, storedFile, settings); } - public async Task GetTemporaryFileUrl(string fileId, DateTimeOffset expiry, bool isDownload) + public async Task GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry, + bool isDownload) { var settings = await _SettingsRepository.GetSettingAsync(); var provider = GetProvider(settings); var storedFile = await _FileRepository.GetFile(fileId); - return storedFile == null ? null: await provider.GetTemporaryFileUrl(storedFile, settings,expiry,isDownload); + return storedFile == null ? null: await provider.GetTemporaryFileUrl(baseUri, storedFile, settings,expiry,isDownload); } public async Task RemoveFile(string fileId, string userId) diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs index 931e640a2..72cc828ae 100644 --- a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.Storage.Services.Providers.Models; +using Microsoft.AspNetCore.Mvc; using TwentyTwenty.Storage.Azure; namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration { + [ModelMetadataType(typeof(AzureBlobStorageConfigurationMetadata))] public class AzureBlobStorageConfiguration : AzureProviderOptions, IBaseStorageConfiguration { [Required] @@ -12,8 +14,5 @@ namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration [RegularExpression(@"[a-z0-9-]+", ErrorMessage = "Characters must be lowercase or digits or -")] public string ContainerName { get; set; } - - [Required][AzureBlobStorageConnectionStringValidator] - public new string ConnectionString { get; set; } } } diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs new file mode 100644 index 000000000..a16165488 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration +{ + public class AzureBlobStorageConfigurationMetadata + { + [Required] + [AzureBlobStorageConnectionStringValidator] + public string ConnectionString { get; set; } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs b/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs index 5f326b540..03b5551ab 100644 --- a/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs +++ b/BTCPayServer/Storage/Services/Providers/BaseTwentyTwentyStorageFileProviderServiceBase.cs @@ -38,14 +38,15 @@ namespace BTCPayServer.Storage.Services.Providers }; } - public virtual async Task GetFileUrl(StoredFile storedFile, StorageSettings configuration) + public virtual async Task GetFileUrl(Uri baseUri, StoredFile storedFile, StorageSettings configuration) { var providerConfiguration = GetProviderConfiguration(configuration); var provider = await GetStorageProvider(providerConfiguration); return provider.GetBlobUrl(providerConfiguration.ContainerName, storedFile.StorageFileName); } - public virtual async Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, + public virtual async Task GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile, + StorageSettings configuration, DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read) { var providerConfiguration = GetProviderConfiguration(configuration); diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs index f42ab3a03..b91670869 100644 --- a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/FileSystemFileProviderService.cs @@ -2,11 +2,11 @@ using System; using System.IO; using System.Threading.Tasks; using BTCPayServer.Configuration; -using BTCPayServer.Services; using BTCPayServer.Storage.Models; using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using ExchangeSharp; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; using TwentyTwenty.Storage; using TwentyTwenty.Storage.Local; @@ -15,16 +15,11 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage public class FileSystemFileProviderService : BaseTwentyTwentyStorageFileProviderServiceBase { - private readonly BTCPayServerEnvironment _BtcPayServerEnvironment; - private readonly BTCPayServerOptions _Options; - private readonly IHttpContextAccessor _HttpContextAccessor; + private readonly BTCPayServerOptions _options; - public FileSystemFileProviderService(BTCPayServerEnvironment btcPayServerEnvironment, - BTCPayServerOptions options, IHttpContextAccessor httpContextAccessor) + public FileSystemFileProviderService(BTCPayServerOptions options) { - _BtcPayServerEnvironment = btcPayServerEnvironment; - _Options = options; - _HttpContextAccessor = httpContextAccessor; + _options = options; } public const string LocalStorageDirectoryName = "LocalStorage"; @@ -32,7 +27,12 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage { return Path.Combine(options.DataDir, LocalStorageDirectoryName); } - + + + public static string GetTempStorageDir(BTCPayServerOptions options) + { + return Path.Combine(GetStorageDir(options), "tmp"); + } public override StorageProvider StorageProvider() { return Storage.Models.StorageProvider.FileSystem; @@ -41,26 +41,38 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage protected override Task GetStorageProvider(FileSystemStorageConfiguration configuration) { return Task.FromResult( - new LocalStorageProvider(new DirectoryInfo(GetStorageDir(_Options)).FullName)); + new LocalStorageProvider(new DirectoryInfo(GetStorageDir(_options)).FullName)); } - public override async Task GetFileUrl(StoredFile storedFile, StorageSettings configuration) + public override async Task GetFileUrl(Uri baseUri, StoredFile storedFile, StorageSettings configuration) { - var baseResult = await base.GetFileUrl(storedFile, configuration); - var url = - _HttpContextAccessor.HttpContext.Request.IsOnion() - ? _BtcPayServerEnvironment.OnionUrl - : $"{_BtcPayServerEnvironment.ExpectedProtocol}://" + - $"{_BtcPayServerEnvironment.ExpectedHost}" + - $"{_Options.RootPath}{LocalStorageDirectoryName}"; - return baseResult.Replace(new DirectoryInfo(GetStorageDir(_Options)).FullName, url, + var baseResult = await base.GetFileUrl(baseUri, storedFile, configuration); + var url = new Uri(baseUri,LocalStorageDirectoryName ); + return baseResult.Replace(new DirectoryInfo(GetStorageDir(_options)).FullName, url.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase); } - public override Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload, + public override async Task GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile, + StorageSettings configuration, DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read) { - return GetFileUrl(storedFile, configuration); + + var localFileDescriptor = new TemporaryLocalFileDescriptor() + { + Expiry = expiry, + FileId = storedFile.Id, + IsDownload = isDownload + }; + var name = Guid.NewGuid().ToString(); + var fullPath = Path.Combine(GetTempStorageDir(_options), name); + if (!File.Exists(fullPath)) + { + File.Create(fullPath).Dispose(); + } + + await File.WriteAllTextAsync(Path.Combine(GetTempStorageDir(_options), name), JsonConvert.SerializeObject(localFileDescriptor)); + + return new Uri(baseUri,$"{LocalStorageDirectoryName}tmp/{name}" ).AbsoluteUri; } } } diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileDescriptor.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileDescriptor.cs new file mode 100644 index 000000000..df4504d7f --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileDescriptor.cs @@ -0,0 +1,11 @@ +using System; + +namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage +{ + public class TemporaryLocalFileDescriptor + { + public string FileId { get; set; } + public bool IsDownload { get; set; } + public DateTimeOffset Expiry { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileProvider.cs b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileProvider.cs new file mode 100644 index 000000000..bccf4ef37 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/FileSystemStorage/TemporaryLocalFileProvider.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; + +namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage +{ + public class TemporaryLocalFileProvider : IFileProvider + { + private readonly DirectoryInfo _fileRoot; + private readonly StoredFileRepository _storedFileRepository; + private readonly DirectoryInfo _root; + + public TemporaryLocalFileProvider(DirectoryInfo tmpRoot, DirectoryInfo fileRoot, StoredFileRepository storedFileRepository) + { + _fileRoot = fileRoot; + _storedFileRepository = storedFileRepository; + _root = tmpRoot; + } + public IFileInfo GetFileInfo(string tmpFileId) + { + tmpFileId =tmpFileId.TrimStart('/', '\\'); + var path = Path.Combine(_root.FullName,tmpFileId) ; + if (!File.Exists(path)) + { + return new NotFoundFileInfo(tmpFileId); + } + + var text = File.ReadAllText(path); + var descriptor = JsonConvert.DeserializeObject(text); + if (descriptor.Expiry < DateTime.Now) + { + File.Delete(path); + return new NotFoundFileInfo(tmpFileId); + } + + var storedFile = _storedFileRepository.GetFile(descriptor.FileId).GetAwaiter().GetResult(); + return new PhysicalFileInfo(new FileInfo(Path.Combine(_fileRoot.FullName, storedFile.StorageFileName))); + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + throw new System.NotImplementedException(); + } + + public IChangeToken Watch(string filter) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs b/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs index 770678001..72375f395 100644 --- a/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs +++ b/BTCPayServer/Storage/Services/Providers/IStorageProviderService.cs @@ -10,8 +10,8 @@ namespace BTCPayServer.Storage.Services.Providers { Task AddFile(IFormFile formFile, StorageSettings configuration); Task RemoveFile(StoredFile storedFile, StorageSettings configuration); - Task GetFileUrl(StoredFile storedFile, StorageSettings configuration); - Task GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, + Task GetFileUrl(Uri baseUri, StoredFile storedFile, StorageSettings configuration); + Task GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload, BlobUrlAccess access = BlobUrlAccess.Read); StorageProvider StorageProvider(); } diff --git a/BTCPayServer/Storage/StorageExtensions.cs b/BTCPayServer/Storage/StorageExtensions.cs index 9cc9a56e4..b64c66799 100644 --- a/BTCPayServer/Storage/StorageExtensions.cs +++ b/BTCPayServer/Storage/StorageExtensions.cs @@ -1,15 +1,16 @@ +using System; using System.IO; using BTCPayServer.Configuration; using BTCPayServer.Storage.Services; using BTCPayServer.Storage.Services.Providers; -using BTCPayServer.Storage.Services.Providers.AmazonS3Storage; using BTCPayServer.Storage.Services.Providers.AzureBlobStorage; using BTCPayServer.Storage.Services.Providers.FileSystemStorage; -using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using NBitcoin.Logging; namespace BTCPayServer.Storage { @@ -27,24 +28,62 @@ namespace BTCPayServer.Storage public static void UseProviderStorage(this IApplicationBuilder builder, BTCPayServerOptions options) { - var dir = FileSystemFileProviderService.GetStorageDir(options); + try + { + var dir = FileSystemFileProviderService.GetStorageDir(options); + var tmpdir = FileSystemFileProviderService.GetTempStorageDir(options); + DirectoryInfo dirInfo; + if (!Directory.Exists(dir)) + { + dirInfo = Directory.CreateDirectory(dir); + } + else + { + dirInfo = new DirectoryInfo(dir); + } - DirectoryInfo dirInfo; - if (!Directory.Exists(dir)) - { - dirInfo = Directory.CreateDirectory(dir); - } - else - { - dirInfo = new DirectoryInfo(dir); - } + DirectoryInfo tmpdirInfo; + if (!Directory.Exists(tmpdir)) + { + tmpdirInfo = Directory.CreateDirectory(tmpdir); + } + else + { + tmpdirInfo = new DirectoryInfo(tmpdir); + } - builder.UseStaticFiles(new StaticFileOptions() + builder.UseStaticFiles(new StaticFileOptions() + { + ServeUnknownFileTypes = true, + RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}"), + FileProvider = new PhysicalFileProvider(dirInfo.FullName), + OnPrepareResponse = context => + { + if (context.Context.Request.Query.ContainsKey("download")) + { + context.Context.Response.Headers["Content-Disposition"] = "attachment"; + } + } + }); + builder.UseStaticFiles(new StaticFileOptions() + { + ServeUnknownFileTypes = true, + RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}tmp"), + FileProvider = new TemporaryLocalFileProvider(tmpdirInfo, dirInfo, + builder.ApplicationServices.GetService()), + OnPrepareResponse = context => + { + if (context.Context.Request.Query.ContainsKey("download")) + { + context.Context.Response.Headers["Content-Disposition"] = "attachment"; + } + } + }); + } + catch (Exception e) { - ServeUnknownFileTypes = true, - RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}"), - FileProvider = new PhysicalFileProvider(dirInfo.FullName) - }); + Logs.Utils.LogError(e, $"Could not initialize the Local File Storage system(uploading and storing files locally)"); + } } } } diff --git a/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs b/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs new file mode 100644 index 000000000..193507512 --- /dev/null +++ b/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs @@ -0,0 +1,12 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class AddU2FDeviceViewModel + { + public string AppId{ get; set; } + public string Challenge { get; set; } + public string Version { get; set; } + public string DeviceResponse { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs b/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs new file mode 100644 index 000000000..33daa2efe --- /dev/null +++ b/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Services.U2F.Models +{ + public class LoginWithU2FViewModel + { + public string UserId { get; set; } + [Required] + [Display(Name = "App id")] + public string AppId { get; set; } + + [Required] + [Display(Name = "Version")] + public string Version { get; set; } + + [Required] + [Display(Name = "Device Response")] + public string DeviceResponse { get; set; } + + [Display(Name = "Challenges")] + public string Challenges { get; set; } + + [Display(Name = "Challenge")] + public string Challenge { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/ServerChallenge.cs b/BTCPayServer/U2F/Models/ServerChallenge.cs new file mode 100644 index 000000000..6c72324e1 --- /dev/null +++ b/BTCPayServer/U2F/Models/ServerChallenge.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class ServerChallenge + { + public string challenge { get; set; } + public string version { get; set; } + public string appId { get; set; } + public string keyHandle { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/ServerRegisterResponse.cs b/BTCPayServer/U2F/Models/ServerRegisterResponse.cs new file mode 100644 index 000000000..aaf7716b9 --- /dev/null +++ b/BTCPayServer/U2F/Models/ServerRegisterResponse.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class ServerRegisterResponse + { + public string AppId { get; set; } + public string Challenge { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs b/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs new file mode 100644 index 000000000..34e8e5cc7 --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FAuthenticationViewModel + { + public string StatusMessage { get; set; } + public List Devices { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/U2FDevice.cs b/BTCPayServer/U2F/Models/U2FDevice.cs new file mode 100644 index 000000000..5debe133a --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FDevice.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using BTCPayServer.Models; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FDevice + { + public string Id { get; set; } + + public string Name { get; set; } + + [Required] public byte[] KeyHandle { get; set; } + + [Required] public byte[] PublicKey { get; set; } + + [Required] public byte[] AttestationCert { get; set; } + + [Required] public int Counter { get; set; } + + public string ApplicationUserId { get; set; } + public ApplicationUser ApplicationUser { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs b/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs new file mode 100644 index 000000000..d4679f403 --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FDeviceAuthenticationRequest + { + public string KeyHandle { get; set; } + + [Required] public string Challenge { get; set; } + + [Required] [StringLength(200)] public string AppId { get; set; } + + [Required] [StringLength(50)] public string Version { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/U2FService.cs b/BTCPayServer/U2F/U2FService.cs new file mode 100644 index 000000000..e64cd70e6 --- /dev/null +++ b/BTCPayServer/U2F/U2FService.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Services.U2F.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using NBitcoin; +using U2F.Core.Models; +using U2F.Core.Utils; + +namespace BTCPayServer.Services.U2F +{ + public class U2FService + { + private readonly ApplicationDbContextFactory _contextFactory; + + private ConcurrentDictionary> UserAuthenticationRequests + { + get; + set; + } + = new ConcurrentDictionary>(); + + public U2FService(ApplicationDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task> GetDevices(string userId) + { + using (var context = _contextFactory.CreateContext()) + { + return await context.U2FDevices + .Where(device => device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) + .ToListAsync(); + } + } + + public async Task RemoveDevice(string id, string userId) + { + using (var context = _contextFactory.CreateContext()) + { + var device = await context.U2FDevices.FindAsync(id); + if (device == null || !device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) + { + return; + } + + context.U2FDevices.Remove(device); + await context.SaveChangesAsync(); + } + } + + public async Task HasDevices(string userId) + { + using (var context = _contextFactory.CreateContext()) + { + return await context.U2FDevices.AnyAsync(fDevice => fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)); + } + } + + + public ServerRegisterResponse StartDeviceRegistration(string userId, string appId) + { + var startedRegistration = global::U2F.Core.Crypto.U2F.StartRegistration(appId); + + UserAuthenticationRequests.AddOrReplace(userId, new List() + { + new U2FDeviceAuthenticationRequest() + { + AppId = startedRegistration.AppId, + Challenge = startedRegistration.Challenge, + Version = global::U2F.Core.Crypto.U2F.U2FVersion, + } + }); + + return new ServerRegisterResponse + { + AppId = startedRegistration.AppId, + Challenge = startedRegistration.Challenge, + Version = startedRegistration.Version + }; + } + + public async Task CompleteRegistration(string userId, string deviceResponse, string name) + { + if (string.IsNullOrWhiteSpace(deviceResponse)) + return false; + + if (!UserAuthenticationRequests.ContainsKey(userId) || !UserAuthenticationRequests[userId].Any()) + { + return false; + } + + var registerResponse = RegisterResponse.FromJson(deviceResponse); + + //There is only 1 request when registering device + var authenticationRequest = UserAuthenticationRequests[userId].First(); + + var startedRegistration = + new StartedRegistration(authenticationRequest.Challenge, authenticationRequest.AppId); + var registration = global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse); + + UserAuthenticationRequests.AddOrReplace(userId, new List()); + using (var context = _contextFactory.CreateContext()) + { + var duplicate = context.U2FDevices.Any(device => + device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && + device.KeyHandle.Equals(registration.KeyHandle) && + device.PublicKey.Equals(registration.PublicKey)); + + if (duplicate) + { + throw new InvalidOperationException("The U2F Device has already been registered with this user"); + } + + await context.U2FDevices.AddAsync(new U2FDevice() + { + AttestationCert = registration.AttestationCert, + Counter = Convert.ToInt32(registration.Counter), + Name = name, + KeyHandle = registration.KeyHandle, + PublicKey = registration.PublicKey, + ApplicationUserId = userId + }); + + await context.SaveChangesAsync(); + } + + return true; + } + + public async Task AuthenticateUser(string userId, string deviceResponse) + { + if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(deviceResponse)) + return false; + + var authenticateResponse = + AuthenticateResponse.FromJson(deviceResponse); + + using (var context = _contextFactory.CreateContext()) + { + var device = await context.U2FDevices.SingleOrDefaultAsync(fDevice => + fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && + fDevice.KeyHandle.SequenceEqual(authenticateResponse.KeyHandle.Base64StringToByteArray())); + + if (device == null) + return false; + + // User will have a authentication request for each device they have registered so get the one that matches the device key handle + + var authenticationRequest = + UserAuthenticationRequests[userId].First(f => + f.KeyHandle.Equals(authenticateResponse.KeyHandle, StringComparison.InvariantCulture)); + var registration = new DeviceRegistration(device.KeyHandle, device.PublicKey, + device.AttestationCert, Convert.ToUInt32(device.Counter)); + + var authentication = new StartedAuthentication(authenticationRequest.Challenge, + authenticationRequest.AppId, authenticationRequest.KeyHandle); + + global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration); + + + UserAuthenticationRequests.AddOrReplace(userId, new List()); + + device.Counter = Convert.ToInt32(registration.Counter); + await context.SaveChangesAsync(); + } + + return true; + } + + public async Task> GenerateDeviceChallenges(string userId, string appId) + { + using (var context = _contextFactory.CreateContext()) + { + var devices = await context.U2FDevices.Where(fDevice => + fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)).ToListAsync(); + + if (devices.Count == 0) + return null; + + var requests = new List(); + + + + var serverChallenges = new List(); + foreach (var registeredDevice in devices) + { + var challenge = global::U2F.Core.Crypto.U2F.StartAuthentication(appId, + new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey, + registeredDevice.AttestationCert, (uint)registeredDevice.Counter)); + serverChallenges.Add(new ServerChallenge() + { + challenge = challenge.Challenge, + appId = challenge.AppId, + version = challenge.Version, + keyHandle = challenge.KeyHandle + }); + + requests.Add( + new U2FDeviceAuthenticationRequest() + { + AppId = appId, + Challenge = challenge.Challenge, + KeyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), + Version = global::U2F.Core.Crypto.U2F.U2FVersion + }); + } + + UserAuthenticationRequests.AddOrReplace(userId, requests); + return serverChallenges; + } + } + } +} diff --git a/BTCPayServer/Validation/HDFingerPrintValidator.cs b/BTCPayServer/Validation/HDFingerPrintValidator.cs new file mode 100644 index 000000000..e148ac213 --- /dev/null +++ b/BTCPayServer/Validation/HDFingerPrintValidator.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace BTCPayServer.Validation +{ + public class HDFingerPrintValidator : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var str = value as string; + if (string.IsNullOrWhiteSpace(str)) + { + return ValidationResult.Success; + } + + try + { + new HDFingerprint(Encoders.Hex.DecodeData(str)); + return ValidationResult.Success; + } + catch + { + return new ValidationResult("Invalid fingerprint"); + } + } + } +} diff --git a/BTCPayServer/Validation/KeyPathValidator.cs b/BTCPayServer/Validation/KeyPathValidator.cs new file mode 100644 index 000000000..6bcf253a4 --- /dev/null +++ b/BTCPayServer/Validation/KeyPathValidator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace BTCPayServer.Validation +{ + public class KeyPathValidator : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var str = value as string; + if (string.IsNullOrWhiteSpace(str)) + { + return ValidationResult.Success; + } + if (KeyPath.TryParse(str, out _)) + { + return ValidationResult.Success; + } + else + { + return new ValidationResult("Invalid keypath"); + } + } + } +} diff --git a/BTCPayServer/Views/Account/ForgotPassword.cshtml b/BTCPayServer/Views/Account/ForgotPassword.cshtml index 0747d28b1..7835f783a 100644 --- a/BTCPayServer/Views/Account/ForgotPassword.cshtml +++ b/BTCPayServer/Views/Account/ForgotPassword.cshtml @@ -5,11 +5,14 @@
+ @if (TempData.ContainsKey("StatusMessage")) + {
+ }

@ViewData["Title"]

diff --git a/BTCPayServer/Views/Account/Login.cshtml b/BTCPayServer/Views/Account/Login.cshtml index c9af42dde..bde631f8d 100644 --- a/BTCPayServer/Views/Account/Login.cshtml +++ b/BTCPayServer/Views/Account/Login.cshtml @@ -1,8 +1,4 @@ -@using System.Collections.Generic -@using System.Linq -@using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Http.Authentication -@model LoginViewModel +@model LoginViewModel @inject SignInManager SignInManager @{ @@ -10,80 +6,41 @@ }
-
-
-
-

@ViewData["Title"]

-
-
-
-
+
+
-
-
-
-

Use another service to log in.

-
- @{ - var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); - if(loginProviders.Count == 0) - { -
-

- There are no external authentication services configured. See this article - for details on setting up this ASP.NET application to support logging in via external services. -

-
- } - else - { -
-
-

- @foreach(var provider in loginProviders) - { - - } -

-
-
- } - } -
+
+
diff --git a/BTCPayServer/Views/Account/LoginWith2fa.cshtml b/BTCPayServer/Views/Account/LoginWith2fa.cshtml index e5ce75168..2438df386 100644 --- a/BTCPayServer/Views/Account/LoginWith2fa.cshtml +++ b/BTCPayServer/Views/Account/LoginWith2fa.cshtml @@ -1,45 +1,38 @@ -@model LoginWith2faViewModel -@{ - ViewData["Title"] = "Two-factor authentication"; -} +@model LoginWith2faViewModel
-
+
-

@ViewData["Title"]

+

Two-factor authentication


-
-
-
- -
-
- - - + +
+ +
+ + + +
+
+
+
-
-
- -
-
-
- -
- -
-
+
+
+ +
+ +
-
-
+

Don't have access to your authenticator device? You can log in with a recovery code. @@ -48,6 +41,7 @@

+ @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml b/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml index 00d04a8a2..6298b2c96 100644 --- a/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml +++ b/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml @@ -3,26 +3,35 @@ ViewData["Title"] = "Recovery code verification"; } -

@ViewData["Title"]

-
-

- You have requested to login with a recovery code. This login will not be remembered until you provide - an authenticator app code at login or disable 2FA and login again. -

-
-
-
-
-
- - - + + +
+
+
+
+

@ViewData["Title"]

+
+

+ You have requested to login with a recovery code. This login will not be remembered until you provide + an authenticator app code at login or disable 2FA and login again. +

- - +
+
+
+
+
+ + + +
+ +
+
-
+
+ @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") -} \ No newline at end of file +} diff --git a/BTCPayServer/Views/Account/LoginWithU2F.cshtml b/BTCPayServer/Views/Account/LoginWithU2F.cshtml new file mode 100644 index 000000000..2e7476467 --- /dev/null +++ b/BTCPayServer/Views/Account/LoginWithU2F.cshtml @@ -0,0 +1,53 @@ +@model BTCPayServer.Services.U2F.Models.LoginWithU2FViewModel + +
+ + + + + + + +
+ +
+
+
+
+

U2F Authentication

+
+ +

Insert your U2F device or a hardware wallet into your computer's USB port. If it has a button, tap on it.

+
+
+
+ +
+
+
+ + + diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index 0b2c4d939..7be8bbca3 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -4,39 +4,55 @@ }
-
-
-
- -
-
-
-
-

@ViewData["Title"]

-
-
-
-
-

Create a new account.

-
-
-
- - - -
-
- - - -
-
- - - -
- -
+
+
diff --git a/BTCPayServer/Views/Account/ResetPassword.cshtml b/BTCPayServer/Views/Account/ResetPassword.cshtml index d802fc462..a5be48c47 100644 --- a/BTCPayServer/Views/Account/ResetPassword.cshtml +++ b/BTCPayServer/Views/Account/ResetPassword.cshtml @@ -5,11 +5,14 @@
-
-
- + @if (TempData.ContainsKey("StatusMessage")) + { +
+
+ +
-
+ }
diff --git a/BTCPayServer/Views/Account/SecondaryLogin.cshtml b/BTCPayServer/Views/Account/SecondaryLogin.cshtml new file mode 100644 index 000000000..4c6c2dd5e --- /dev/null +++ b/BTCPayServer/Views/Account/SecondaryLogin.cshtml @@ -0,0 +1,47 @@ +@model SecondaryLoginViewModel +@{ + ViewData["Title"] = "Two-factor/U2F authentication"; +} + +
+
+ @if (Model.LoginWith2FaViewModel != null && Model.LoginWithU2FViewModel != null) + { +
+
+

@ViewData["Title"]

+
+ +
+
+
+ }else if (Model.LoginWith2FaViewModel == null && Model.LoginWithU2FViewModel == null) + { +
+
+

Both 2FA and U2F Authentication Methods are not available. Please go to the https endpoint

+
+
+
+ } + + +
+ @if (Model.LoginWith2FaViewModel != null) + { +
+ +
+ } + @if (Model.LoginWithU2FViewModel != null) + { +
+ +
+ } +
+
+
+@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Apps/CreateApp.cshtml b/BTCPayServer/Views/Apps/CreateApp.cshtml index 8cb9517cb..50bc80c55 100644 --- a/BTCPayServer/Views/Apps/CreateApp.cshtml +++ b/BTCPayServer/Views/Apps/CreateApp.cshtml @@ -28,7 +28,7 @@
- +
Back to the app list diff --git a/BTCPayServer/Views/Apps/ListApps.cshtml b/BTCPayServer/Views/Apps/ListApps.cshtml index 4bf07936e..e5a54283d 100644 --- a/BTCPayServer/Views/Apps/ListApps.cshtml +++ b/BTCPayServer/Views/Apps/ListApps.cshtml @@ -3,16 +3,16 @@ @{ ViewData["Title"] = "Apps"; } -
- -
-
- + @if (TempData.ContainsKey("TempDataProperty-StatusMessage")) + { +
+
+ +
-
- + }

@ViewData["Title"]

@@ -23,7 +23,7 @@ diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index 9986b3bf8..503097ce6 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -1,4 +1,5 @@ @addTagHelper *, BundlerMinifier.TagHelpers +@using System.Globalization @model UpdateCrowdfundViewModel @{ ViewData["Title"] = "Update Crowdfund"; @@ -15,7 +16,7 @@
-
-
-
- + @if (TempData.ContainsKey("TempDataProperty-StatusMessage")) + { +
+
+ +
-
+ }
@@ -43,7 +47,6 @@
-
@@ -66,10 +69,11 @@
-
- +
+
- @@ -80,7 +84,6 @@
- +
+
- @@ -138,46 +143,44 @@
- @if (Model.NotificationEmailWarning) { } - +
- +
- +
- +
- +
- +
- +
-
@@ -185,10 +188,9 @@
- +
-
@@ -196,7 +198,7 @@
- +
@@ -204,26 +206,22 @@
- - + -
- @section Scripts { - + - - } - diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index f69a1a20c..cf91bef14 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -6,23 +6,22 @@ -
@@ -30,11 +29,14 @@
-
-
- + @if (TempData.ContainsKey("TempDataProperty-StatusMessage")) + { +
+
+ +
-
+ }
@@ -119,7 +121,7 @@ { } - +
@@ -127,9 +129,9 @@
- +
- +
@@ -141,7 +143,6 @@
-

You can host point of sale buttons in an external website with the following code.

@@ -163,7 +164,7 @@

@@ -171,17 +172,17 @@
You can embed the POS using an iframe @{ - var iframe = $""; + var iframe = $""; }
@iframe
-
+

@@ -196,26 +197,23 @@
  • Verify that the orderId is from your backend, that the price is correct and that status is either confirmed or complete
  • You can then ship your order
  • -

    +

    +
    -
    Back to the app list - - +
    - @section Scripts { - - - } - diff --git a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml index 2d12036ea..13fde935c 100644 --- a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml +++ b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml @@ -12,10 +12,10 @@

    {{srvModel.title}} - Starts {{startDateRelativeTime}} + Starts in {{startDiff}} - Ends {{endDateRelativeTime}} + Ends in {{endDiff}} Currently Active! @@ -58,7 +58,7 @@
    -
    +
    {{ raisedAmount }} {{targetCurrency}}
    Raised
    @@ -73,23 +73,7 @@

    Contributors
    -
    -
    - {{endDiff}} -
    -
    Left
    - -
      -
    • - {{started? "Started" : "Starts"}} {{startDate}} -
    • -
    • - {{ended? "Ended" : "Ends"}} {{endDate}} -
    • -
    -
    -
    -
    +
    {{startDiff}}
    @@ -106,7 +90,23 @@
    -
    +
    +
    + {{endDiff}} +
    +
    Left
    + +
      +
    • + {{started? "Started" : "Starts"}} {{startDate}} +
    • +
    • + {{ended? "Ended" : "Ends"}} {{endDate}} +
    • +
    +
    +
    +
    Campaign
    diff --git a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml b/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml index 4fe792a0a..c8e63464d 100644 --- a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml +++ b/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml @@ -12,7 +12,7 @@ @Model.Title - + @@ -20,12 +20,12 @@ - + @if (Model.CustomCSSLink != null) { - + } - + @if (Model.EnableShoppingCart) { @@ -33,7 +33,7 @@ - + } - - - - - - - - - - - - -@if (Model.EnableShoppingCart) -{ - - -
    - -
    -
    -
    -
    - -   - - 0 - + + -
    -
    - -
    + +
    +
    + + @for (var index = 0; index < Model.Items.Length; index++) + { + var item = Model.Items[index]; + var image = item.Image; + var description = item.Description; + +
    + @if (!String.IsNullOrWhiteSpace(image)) + { + @:Card image cap + } +
    +
    @item.Title
    + @if (!String.IsNullOrWhiteSpace(description)) + { +

    @description

    + } +
    + +
    + }
    -
    -
    + + +
    + } + else + { +
    +
    +

    @Model.Title

    + +
    + @for (int x = 0; x < Model.Items.Length; x++) { - var item = Model.Items[index]; - var image = item.Image; - var description = item.Description; + var item = Model.Items[x]; -
    - @if (!String.IsNullOrWhiteSpace(image)) +
    + @if (!String.IsNullOrWhiteSpace(item.Image)) { - @:Card image cap + Card image cap } -
    -
    @item.Title
    - @if (!String.IsNullOrWhiteSpace(description)) +
    +
    @item.Title
    + @if (!String.IsNullOrWhiteSpace(item.Description)) { -

    @description

    +

    @item.Description

    + } + +
    + - + } + @if (Model.ShowCustomAmount) + { +
    +
    +
    Custom Amount
    +

    Create invoice to pay custom amount

    + +
    +
    }
    - - - -
    -} -else -{ -
    -
    -

    @Model.Title

    - -
    - @for (int x = 0; x < Model.Items.Length; x++) - { - var item = Model.Items[x]; - -
    - @if (!String.IsNullOrWhiteSpace(item.Image)) - { - Card image cap - } -
    -
    @item.Title
    - @if (!String.IsNullOrWhiteSpace(item.Description)) - { -

    @item.Description

    - } - -
    - -
    - } - @if (Model.ShowCustomAmount) - { -
    -
    -
    Custom Amount
    -

    Create invoice to pay custom amount

    - -
    - -
    - } -
    -
    -
    -} + } diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index 7b035e8e9..5a655f745 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -109,6 +109,9 @@
    +
    + {{$t("NotPaid_ExtraTransaction")}} +
    {{$t("Order Amount")}}
    diff --git a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml index 5a8f5d158..0edee5a80 100644 --- a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml +++ b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml @@ -68,7 +68,12 @@
    - + + + +
    +
    +
    Back to List diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 00e7e13ac..c64c540a2 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -5,35 +5,26 @@ @section HeaderContent{ } - -
    - -
    -
    - + @if (!string.IsNullOrEmpty(Model.StatusMessage)) + { +
    +
    + +
    -
    + }
    @@ -42,257 +33,162 @@
    -
    -
    -

    Information

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Store@Model.StoreName
    Id@Model.Id
    State@Model.State
    Created date@Model.CreatedDate.ToBrowserDate()
    Expiration date@Model.ExpirationDate.ToBrowserDate()
    Monitoring date@Model.MonitoringDate.ToBrowserDate()
    Transaction speed@Model.TransactionSpeed
    Refund email@Model.RefundEmail
    Order Id@Model.OrderId
    Total fiat due@Model.Fiat
    Notification Email@Model.NotificationEmail
    Notification Url@Model.NotificationUrl
    Redirect Url@Model.RedirectUrl
    -
    - -
    -

    Buyer information

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Name@Model.BuyerInformation.BuyerName
    Email@Model.BuyerInformation.BuyerEmail
    Phone@Model.BuyerInformation.BuyerPhone
    Address 1@Model.BuyerInformation.BuyerAddress1
    Address 2@Model.BuyerInformation.BuyerAddress2
    City@Model.BuyerInformation.BuyerCity
    State@Model.BuyerInformation.BuyerState
    Country@Model.BuyerInformation.BuyerCountry
    Zip@Model.BuyerInformation.BuyerZip
    - @if (Model.PosData.Count == 0) - { -

    Product information

    - - - - - - - - - - - - - - - - - -
    Item code@Model.ProductInformation.ItemCode
    Item Description@Model.ProductInformation.ItemDesc
    Price@Model.Fiat
    Tax included@Model.TaxIncluded
    - } -
    -
    - - @if (Model.PosData.Count != 0) - {
    -

    Product information

    - +

    Information

    +
    - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + +
    Item code@Model.ProductInformation.ItemCodeStore@Model.StoreName
    Item Description@Model.ProductInformation.ItemDescId@Model.Id
    PriceState@Model.State
    Created date@Model.CreatedDate.ToBrowserDate()
    Expiration date@Model.ExpirationDate.ToBrowserDate()
    Monitoring date@Model.MonitoringDate.ToBrowserDate()
    Transaction speed@Model.TransactionSpeed
    Refund email@Model.RefundEmail
    Order Id@Model.OrderId
    Total fiat due @Model.Fiat
    Tax included@Model.TaxIncludedNotification Email@Model.NotificationEmail
    Notification Url@Model.NotificationUrl
    Redirect Url@Model.RedirectUrl
    -

    Point of Sale Data

    - -
    -
    - } - -
    -
    -

    Paid summary

    - - - - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - - - @foreach (var payment in Model.CryptoPayments) - { - - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - } - +

    Buyer information

    +
    Payment methodAddressRatePaidDueOverpaid
    @payment.PaymentMethod@payment.Address@payment.Rate@payment.Paid@payment.Due@payment.Overpaid
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name@Model.BuyerInformation.BuyerName
    Email@Model.BuyerInformation.BuyerEmail
    Phone@Model.BuyerInformation.BuyerPhone
    Address 1@Model.BuyerInformation.BuyerAddress1
    Address 2@Model.BuyerInformation.BuyerAddress2
    City@Model.BuyerInformation.BuyerCity
    State@Model.BuyerInformation.BuyerState
    Country@Model.BuyerInformation.BuyerCountry
    Zip@Model.BuyerInformation.BuyerZip
    + @if (Model.PosData.Count == 0) + { +

    Product information

    + + + + + + + + + + + + + + + + + +
    Item code@Model.ProductInformation.ItemCode
    Item Description@Model.ProductInformation.ItemDesc
    Price@Model.Fiat
    Tax included@Model.TaxIncluded
    + }
    - @if (Model.OnChainPayments.Count > 0) + + @if (Model.PosData.Count != 0) {
    -
    -

    On-Chain payments

    - - - - - - - - - - - @foreach (var payment in Model.OnChainPayments) - { - var replaced = payment.Replaced ? "class='linethrough'" : ""; - - - - - - - } - -
    CryptoDeposit addressTransaction IdConfirmations
    @payment.Crypto@payment.DepositAddress - - @payment.TransactionId - - @payment.Confirmations
    -
    -
    - } - @if (Model.OffChainPayments.Count > 0) - { -
    -
    -

    Off-Chain payments

    - - - - - - - - - @foreach (var payment in Model.OffChainPayments) - { - - - - - } - +
    +

    Product information

    +
    CryptoBOLT11
    @payment.Crypto@payment.BOLT11
    + + + + + + + + + + + + + + + +
    Item code@Model.ProductInformation.ItemCode
    Item Description@Model.ProductInformation.ItemDesc
    Price@Model.Fiat
    Tax included@Model.TaxIncluded
    +
    +

    Point of Sale Data

    + +
    } + +

    Events

    diff --git a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml new file mode 100644 index 000000000..2f5fca1bc --- /dev/null +++ b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml @@ -0,0 +1,101 @@ +@model InvoiceDetailsModel + +
    +
    +

    Paid summary

    + + + + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + + + @foreach (var payment in Model.CryptoPayments) + { + + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + } + +
    Payment methodAddressRatePaidDueOverpaid
    @payment.PaymentMethod + @payment.Address + @payment.Rate@payment.Paid@payment.Due@payment.Overpaid
    +
    +
    +@if (Model.OnChainPayments.Count > 0) +{ +
    +
    +

    On-Chain payments

    + + + + + + + + + + + @foreach (var payment in Model.OnChainPayments) + { + var replaced = payment.Replaced ? "class='linethrough'" : ""; + + + + + + + } + +
    CryptoDeposit addressTransaction IdConfirmations
    @payment.Crypto@payment.DepositAddress + + @payment.Confirmations
    +
    +
    +} +@if (Model.OffChainPayments.Count > 0) +{ +
    +
    +

    Off-Chain payments

    + + + + + + + + + @foreach (var payment in Model.OffChainPayments) + { + + + + + } + +
    CryptoBOLT11
    @payment.Crypto
    @payment.BOLT11
    +
    +
    +} diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 011be41e2..88942f65d 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -2,19 +2,20 @@ @{ ViewData["Title"] = "Invoices"; } - @section HeadScripts { } - +@Html.HiddenFor(a => a.Count)
    - -
    -
    - + @if (!string.IsNullOrEmpty(Model.StatusMessage)) + { +
    +
    + +
    -
    + }
    @@ -46,8 +47,7 @@ -
    + @* Custom Range Modal *@ + + + @* Custom Range Modal *@ +
    @@ -89,9 +184,9 @@ - + - + @@ -103,12 +198,12 @@ - - + + + + }
    OrderIdOrderId InvoiceIdStatusStatus Amount Actions
    @invoice.Date.ToBrowserDate() - + + @if (invoice.RedirectUrl != string.Empty) { - @invoice.OrderId + @invoice.OrderId } else { @@ -116,13 +211,41 @@ } @invoice.InvoiceId@invoice.Status + @if (invoice.CanMarkStatus) + { +
    + + +
    + } + else + { + @invoice.StatusString + } +
    @invoice.AmountCurrency @if (invoice.ShowCheckout) { - Checkout + Checkout [^] @if (!invoice.CanMarkStatus) { @@ -130,33 +253,25 @@ } } - @if (invoice.CanMarkStatus) - { - - - } - Details +   + Details + @**@ +   + + + +
    -
    - +
    diff --git a/BTCPayServer/Views/Manage/AddU2FDevice.cshtml b/BTCPayServer/Views/Manage/AddU2FDevice.cshtml new file mode 100644 index 000000000..83639021e --- /dev/null +++ b/BTCPayServer/Views/Manage/AddU2FDevice.cshtml @@ -0,0 +1,58 @@ +@model BTCPayServer.Services.U2F.Models.AddU2FDeviceViewModel +@{ + ViewData.SetActivePageAndTitle(ManageNavPages.U2F, "Add U2F device"); +} + + + + +
    +

    Registering U2F Device

    +
    +

    Insert your U2F device or a hardware wallet into your computer's USB port. If it has a button, tap on it.

    + + +
    + + +
    + +@section Scripts { + + + +} diff --git a/BTCPayServer/Views/Manage/ChangePassword.cshtml b/BTCPayServer/Views/Manage/ChangePassword.cshtml index 37b70dd98..8788380b7 100644 --- a/BTCPayServer/Views/Manage/ChangePassword.cshtml +++ b/BTCPayServer/Views/Manage/ChangePassword.cshtml @@ -3,7 +3,6 @@ ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Change password"); } -

    @ViewData["Title"]

    @@ -24,7 +23,7 @@
    - +
    diff --git a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml index b98f364cb..b8be2ddc1 100644 --- a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml +++ b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml @@ -3,7 +3,6 @@ ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Enable authenticator"); } -

    @ViewData["Title"]

    To use an authenticator app go through the following steps:

      diff --git a/BTCPayServer/Views/Manage/GenerateRecoveryCodes.cshtml b/BTCPayServer/Views/Manage/GenerateRecoveryCodes.cshtml index 7041dca04..5fe9916cf 100644 --- a/BTCPayServer/Views/Manage/GenerateRecoveryCodes.cshtml +++ b/BTCPayServer/Views/Manage/GenerateRecoveryCodes.cshtml @@ -3,7 +3,6 @@ ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Recovery codes"); } -

      @ViewData["Title"]