Compare commits

..

112 Commits

Author SHA1 Message Date
ae4b2ab1fd Fix 2019-06-03 16:46:35 +09:00
2ca8cc6ca3 bump 2019-06-03 15:57:13 +09:00
3b57e2684e Add NotPaid_ExtraTransaction 2019-06-03 15:56:25 +09:00
898c672193 decrease number conf funding channel required for lightningd 2019-06-03 15:53:26 +09:00
18a7bc9278 Decrease number of confirmations requires for channels of lnd in tests 2019-06-03 15:51:13 +09:00
bb29ee10c5 Only execute external_tests on master 2019-06-03 15:41:44 +09:00
5441ae537a Fix docker-entrypoint for tests 2019-06-03 15:36:50 +09:00
0a0ddafd67 Add permission to run-tests 2019-06-03 15:34:09 +09:00
a3b914d8b4 Remove code 2019-06-03 15:32:46 +09:00
39f75d3742 Refactor test run by circleci 2019-06-03 15:32:20 +09:00
3dd77a4f2c Rename CircleCI steps and dockerfiles 2019-06-03 15:20:20 +09:00
6782e82972 Update translations 2019-06-02 18:13:04 +09:00
120fce0288 fix test container 2019-06-02 17:41:34 +09:00
aa57531ed7 fix test container 2019-06-02 17:37:44 +09:00
8f76bc0bcb Extract version in separate csproj to have better dockerfile caching 2019-06-02 17:33:35 +09:00
189280e602 Fix docker images 2019-06-02 17:06:00 +09:00
78ca26cf78 bump 2019-06-02 16:53:36 +09:00
0c5c6233c7 Add bisq as supporting P2P service 2019-06-02 16:30:44 +09:00
4fe480ee55 Fix selenium test 2019-05-31 15:40:59 +09:00
be6560e08c Merge pull request from rockstardev/uifixes
Tweaking UI styles, updating default table style, and extra notification for payment for paidPartial
2019-05-31 15:39:17 +09:00
0f58f6da36 fix cryptoimage 2019-05-31 08:00:32 +02:00
dcaf0463a7 Displaying notification for extra transaction if paidPartial 2019-05-31 07:48:42 +02:00
5c6643270b Bugfixing path to crypto image
Will need to move all these paths to absolute, rather than relative
2019-05-31 07:48:42 +02:00
7b337bde49 Restoring table border styling done by KayBeSee 2019-05-31 07:48:42 +02:00
7056aae301 Do not show custom amount field in cart when not enabled ()
fixes 
2019-05-31 14:29:16 +09:00
1b6eb9cab0 Update explorer to blockstream.info () 2019-05-31 14:26:41 +09:00
80e23beda9 Update TwentyTwenty 2019-05-31 14:16:17 +09:00
d70e120acc Fix test 2019-05-31 12:17:47 +09:00
c877937fdf Show the inputs of the PSBT in the review screen 2019-05-31 00:23:23 +09:00
8379b07de0 Use a redirect for update 2019-05-31 00:00:20 +09:00
c8c33245b8 Revert table changes from 2019-05-30 23:42:56 +09:00
0faf2fe83e Fix buttons in PSBT 2019-05-30 23:36:01 +09:00
19bc511f39 Can update PSBT, fix the PSBT review page 2019-05-30 23:16:05 +09:00
916323bb3b [WIP] Further abstractions to Payment Handlers ()
* mark items to abstract


wip


wip


wip


wip


wip


wip


wip


cleanup


parse other types


compile and fix tests


fix bug 


fix warnings


fix rebase error


reduce payment method handler passings


more cleanup


switch tests to Fast mode 


fix obsolete warning


remove argument requirement 


rebase fixes


remove overcomplicated code


better parsing


remove dependency on environement


remove async

* fixes and simplification

* simplify

* clean up even more

* replace nuglify dependency

* remove extra space

* Fix tests

* fix booboo

* missing setter

* change url resolver

* reduce payment method handlers

* wrap payment method handlers in a custom type

* fix tests

* make invoice controller UI selectlist population cleaner

* make store controller use payment handler dictionary

* fix ln flag

* fix store controller test

* remove null checks on payment handlers

* remove unused imports

* BitcoinSpecificBtcPayNetwork - abstract BTCPayNetwork

* some type fixes

* fix tests

* simplify fetching handler in invoice controller

* rename network base and bitcoin classes

* abstract serializer to network level

* fix serializer when network not provided

* fix serializer when network not provided

* fix serializer when network not provided

* Abstract more payment type specific logic to handlers

* fix merge issue

* small fixes

* make use of repository instead of direct context usage

* reduce redundant code

* sanity check

* test fixes
2019-05-30 16:02:52 +09:00
0e568e2af5 Make sure that only the log directory can be read on /server/logs 2019-05-30 11:46:09 +09:00
dde841383a Don't throw exception if derivation scheme is not found 2019-05-30 11:24:43 +09:00
81dae7d350 BTCPay Abstractions: Move PaymentMethod specific logic to their handlers () 2019-05-29 23:33:31 +09:00
d3e3c31b0c Btcpay abstract BTCPayNetwork -- Alternative PR to ()
* BitcoinSpecificBtcPayNetwork - abstract BTCPayNetwork

* some type fixes

* fix tests

* simplify fetching handler in invoice controller

* rename network base and bitcoin classes

* abstract serializer to network level

* fix serializer when network not provided

* fix serializer when network not provided

* fix serializer when network not provided

* try fixes for isolating pull request
2019-05-29 18:43:50 +09:00
90852fe951 updated styles on user server page ()
* updated styles on user server page

* moved files out of bootstrap.css

* removed old css classes from initial commit

* move css changes to site.css

* add missing }
2019-05-28 21:40:10 +09:00
a2251d245f Merge pull request from rockstardev/extendconfcount
Extending invoice monitoring time if we haven't reached MaxTrackedConfirmation
2019-05-26 11:18:22 +09:00
112f9c4241 Adding invoice back to pending to track confirmations if less than max 2019-05-25 17:30:27 -05:00
b300404bc7 Extending invoice monitoring if max confirmation count not reached 2019-05-25 17:20:17 -05:00
19161b52f5 Fiat denomination box disappeared from the wallet (fix ) 2019-05-25 22:23:32 +09:00
429170520e Make sure fingerprint/hdpath are passed down to AddDerivationScheme.
Close ledger popup on account selection.

Add additional info after pairing
2019-05-25 12:53:03 +09:00
5571413a78 Put Ledger Wallet pairing in a popup, prepare code for Trezor pairing ()
* Allowing for POS to be displayed at website root

* Switching to asp attributes for form post action

* Applying default formatting rules on HTML

* The destination pays mining fees => Subtract fees from amount

* small cleanup ()

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies ()

* Part 1: OpenIddict - Minor Changes & Config prep

* Part 1: OpenIddict - Minor Changes & Config prep

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* pr changes

* pr changes

* fix merge

* pr fixes

* remove config for openid -- no need for it for now

* fix compile

* fix compile 

* remove extra ns using

* Update Startup.cs

* compile

* adjust settings a bit

* remove duplicate

* remove external login provider placeholder html

* remove unused directives

* regenerate db snapshot model

* Remove dynamic policy

* Provide Pretty descriptions for payment methods from their handlers ()

* small cleanup

* Provide Pretty descriptions for payment methods from their handlers

* remove PrettyMethod()

* integration with trezor

* rough load xpub from trezor

* update deriv scheme trezor

* move ledger import to dialog

* add import from hw wallet dropdown

* Support temporary links for local file system provider ()

* wip

* Support temporary links for local file system provider

* pass base url to file services

* fix test

* do not crash on errors with local filesystem

* remove console

* fix paranthesis

* work on trezor.net integration

* pushed non compiling sign wallet code

* comment out wallet code

* abstract ledger ws in add deriv

* Auto stash before merge of "trezor" and "btcpayserver/master"

* final add changes

* cleanup

* improve connectivity and fix e2e tests

* fix selenium

* add experimental warning for trezor

* move import button to right and convert to text link

* switch to defer and async scripts in add deriv scheme

* make defer not async

* more elaborate import trezor dialog

* Fix small issues

* hide trezor for now
2019-05-25 11:45:36 +09:00
512ee16620 Refactor invoice entity to not have to inject the NetworkProvider () 2019-05-24 22:22:38 +09:00
15dc0d60db Split projects () 2019-05-24 18:42:22 +09:00
d86cc9192e Support temporary links for local file system provider ()
* wip

* Support temporary links for local file system provider

* pass base url to file services

* fix test

* do not crash on errors with local filesystem

* remove console

* fix paranthesis
2019-05-24 15:44:23 +09:00
25b08b21fa Provide Pretty descriptions for payment methods from their handlers ()
* small cleanup

* Provide Pretty descriptions for payment methods from their handlers

* remove PrettyMethod()
2019-05-24 15:38:47 +09:00
ef9c2e8af1 Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies ()
* Part 1: OpenIddict - Minor Changes & Config prep

* Part 1: OpenIddict - Minor Changes & Config prep

* Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* pr changes

* pr changes

* fix merge

* pr fixes

* remove config for openid -- no need for it for now

* fix compile

* fix compile 

* remove extra ns using

* Update Startup.cs

* compile

* adjust settings a bit

* remove duplicate

* remove external login provider placeholder html

* remove unused directives

* regenerate db snapshot model

* Remove dynamic policy
2019-05-24 15:17:02 +09:00
9bee48c601 small cleanup () 2019-05-24 15:11:38 +09:00
961942ff6a Merge branch 'posroot' 2019-05-24 15:08:49 +09:00
de1c2b0150 Allowing for POS to be displayed at website root ()
* Allowing for POS to be displayed at website root

* Switching to asp attributes for form post action

* Applying default formatting rules on HTML
2019-05-24 15:07:09 +09:00
5a73358bca The destination pays mining fees => Subtract fees from amount 2019-05-24 14:28:09 +09:00
1812ea90b5 Applying default formatting rules on HTML 2019-05-23 10:36:23 -05:00
d98a416ed9 Switching to asp attributes for form post action 2019-05-23 10:35:57 -05:00
b947749382 Allowing for POS to be displayed at website root 2019-05-22 14:30:47 -05:00
c4d0b061c9 bump 2019-05-21 19:06:27 +09:00
3bada5d443 Fix multi send substract fees 2019-05-21 19:04:39 +09:00
06a35787aa Make WalletSend match exactly the design of before without additional destination 2019-05-21 18:44:49 +09:00
88c931ec13 Make wallet able to send to multiple destinations ()
* Make wallet able to send to multiple destinations

* fix tests

* update e2e tests

* fix e2e part 2

* make headless again

* pr changes

* make wallet look exactly as old one when only 1 dest
2019-05-21 17:10:07 +09:00
3d436c3b0e Add Eclair to connection string examples ()
* Add Eclair to connection string examples

* bump LN nuget
2019-05-20 10:13:11 +09:00
b4bb44d3e6 Adding Paid filter, invoice confirmed/paid/complete () 2019-05-20 10:02:57 +09:00
39157e6883 Fix tests 2019-05-20 00:06:03 +09:00
9b6b9e6113 Fix build 2019-05-19 23:33:32 +09:00
87df34e064 Can actually upload PSBT file in PSBT Combine and PSBT view.
Validate transaction before allowing any broadcast and show errors nicely.
2019-05-19 23:27:18 +09:00
55a48ff84a Add some notice in sign with seed 2019-05-17 14:42:28 +09:00
aed98f16bb Show fee rate in transaction detail review 2019-05-16 12:56:06 +09:00
2926865c1b bump 2019-05-15 22:57:14 +09:00
20fb7fc188 Fixing bug induced with server converting UTC times to server local () 2019-05-15 22:02:39 +09:00
461462eafc fix file timezone and isDownload for local files temp urls ()
closes 
2019-05-15 22:02:03 +09:00
7aa6d1a8d7 fix test on Circle CI for selenium 2019-05-15 19:27:30 +09:00
d914fe2f48 Make sure that the accountkey can sign a transaction 2019-05-15 19:00:26 +09:00
e100edce24 Add selenium test for the manual seed signing 2019-05-15 16:00:03 +09:00
a68915d6cf WalletPSBTReady show the summary of the transaction, signing with the seed respect the keypath of the wallet settings 2019-05-15 15:00:09 +09:00
210d680b21 nameofify 2019-05-15 01:07:46 +09:00
8dc4acdc34 Make sure people does not use launchsettings by mistake 2019-05-15 01:06:26 +09:00
eb54a18fcd Wallet & PSBT: Sign with seed or key ()
* Allow signing a PSBT with an extkey/wif or mnemonic seed

* reword things

* small text
2019-05-15 01:03:48 +09:00
cf436e11ae Part 1: OpenIddict - Minor Changes & Config prep ()
* Part 1: OpenIddict - Minor Changes & Config prep

* add missing nuget

* pr changes

* pr fixes

* remove config for openid -- no need for it for now

* remove unused extension

* Add tests

* use pay tester http client

* check redirecturl in tests
2019-05-15 00:46:43 +09:00
e22b7f74c7 Increase test coverage of selenium 2019-05-14 23:33:46 +09:00
f8fca7434c Hack selenium again 2019-05-14 22:29:05 +09:00
66d303c4ba Install chrome driver on alpine 2019-05-14 19:58:28 +09:00
186ed8beb2 Hack selenium 2019-05-14 19:27:26 +09:00
fac546cc0b Print sources if Selenium test fail 2019-05-14 19:19:23 +09:00
9d2d2d0d64 Catch errors in AssertNoErrors 2019-05-14 19:13:55 +09:00
f58043b07f dump logs of selenium before failiing test 2019-05-14 19:09:26 +09:00
289c6fa10e Remove docker-compose update 2019-05-14 18:36:07 +09:00
00e24ab249 Make sure BTCPay is operational before starting tests 2019-05-14 18:35:22 +09:00
7b69e334d7 update docker-compose so selenium tests works 2019-05-14 18:05:16 +09:00
12507b6743 Add some logs related to selenium 2019-05-14 17:34:19 +09:00
3750842833 Show in logs where the btcpay tester is binding 2019-05-14 16:56:03 +09:00
1dcec3e1fb bind to all interface if inside the test container 2019-05-14 16:13:55 +09:00
d8e1edd6d3 bump nbx in tests 2019-05-14 16:07:46 +09:00
a1f1e90626 Make selenium work on CI 2019-05-14 16:06:51 +09:00
522d745883 Update NBXplorer and NBitcoin 2019-05-14 16:06:43 +09:00
8ffb81cdf3 Refactor Selenium tests to properly cleanup resources 2019-05-13 18:42:20 +09:00
ae5254c65e disable selenium test for a while 2019-05-13 18:14:31 +09:00
9d53888524 fix typo 2019-05-13 18:03:41 +09:00
cd6dd78759 Click around in selenium, and do not forget to run the selenium tests on circleCI 2019-05-13 17:59:15 +09:00
27fd49e61c Add --allow-admin-registration, useful for tests 2019-05-13 17:00:58 +09:00
a3a259556f Merge branch 'testcircleci' 2019-05-13 13:48:46 +09:00
803da75636 Only run integrations tests in btcpayserver repository 2019-05-13 13:48:09 +09:00
d1556eb6cd bump 2019-05-13 08:55:26 +09:00
a7edbfe5e9 Remove useless code 2019-05-13 08:23:24 +09:00
663b5beac1 remove useless code 2019-05-13 08:22:29 +09:00
7e164d2ec3 make sure we don't sign same input twice 2019-05-13 08:21:54 +09:00
f9fb0bb477 Simplify logic in LedgerHardwareWalletSerivce by using NBitcoin helper methods. 2019-05-13 08:18:12 +09:00
702c7f2c30 Fix tests 2019-05-13 00:35:06 +09:00
8b348ade75 Can select the signing key in WalletSettings 2019-05-13 00:30:28 +09:00
bf37f44795 Add Wallet settings menu, do not rebase keypaths when create the PSBT 2019-05-13 00:13:55 +09:00
698033b0cf Selenium Chrome Tests 2019-05-12 18:49:28 +09:00
10496363f5 Change button style on WalletPSBTReadyView 2019-05-12 16:19:27 +09:00
248 changed files with 6433 additions and 1915 deletions
.circleci
BTCPayServer.Common
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
Authentication/OpenId/Models
BTCPayServer.csproj
Configuration
Controllers
Data
DerivationSchemeSettings.cs
Events
ExplorerClientProvider.csExtensions.cs
Extensions
HostedServices
Hosting
Migrations
ModelBinders
Models
PaymentRequest
Payments
Properties
Security
Services
Storage
Validation
Views
wwwroot
Common.csprojDockerfile.linuxamd64Version.csprojamd64.Dockerfilearm32v7.Dockerfilebtcpayserver.slnpublish-docker.ps1

@ -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_PULL_REQUEST" ] || [ -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: /.*/

8
.circleci/run-tests.sh Executable file

@ -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

@ -3,10 +3,9 @@ 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
{
@ -44,9 +43,66 @@ 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<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
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<T>(string json)
{
return NBXplorerNetwork.Serializer.ToObject<T>(json);
}
public override string ToString<T>(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; }
@ -62,24 +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<string>();
public bool SupportRBF { get; internal set; }
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
public override string ToString()
{
return CryptoCode;
}
internal KeyPath GetRootKeyPath()
public virtual T ToObject<T>(string json)
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
return JsonConvert.DeserializeObject<T>(json);
}
public virtual string ToString<T>(T obj)
{
return JsonConvert.SerializeObject(obj);
}
}
}

@ -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,7 +16,7 @@ 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",

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -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;

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;

@ -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;

@ -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<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
Dictionary<string, BTCPayNetworkBase> _Networks = new Dictionary<string, BTCPayNetworkBase>();
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
@ -29,7 +26,7 @@ namespace BTCPayServer
{
NetworkType = filtered.NetworkType;
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
_Networks = new Dictionary<string, BTCPayNetwork>();
_Networks = new Dictionary<string, BTCPayNetworkBase>();
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
foreach (var network in filtered._Networks)
{
@ -57,15 +54,15 @@ namespace BTCPayServer
InitViacoin();
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks)
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
{
if(network.Value.ElectrumMapping.Count == 0)
if(network.ElectrumMapping.Count == 0)
{
network.Value.ElectrumMapping = GetNetwork("BTC").ElectrumMapping;
if (!network.Value.NBitcoinNetwork.Consensus.SupportSegwit)
network.ElectrumMapping = GetNetwork<BTCPayNetwork>("BTC").ElectrumMapping;
if (!network.NBitcoinNetwork.Consensus.SupportSegwit)
{
network.Value.ElectrumMapping =
network.Value.ElectrumMapping
network.ElectrumMapping =
network.ElectrumMapping
.Where(kv => kv.Value == DerivationType.Legacy)
.ToDictionary(k => k.Key, k => k.Value);
}
@ -89,20 +86,14 @@ namespace BTCPayServer
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC
{
get
{
return GetNetwork("BTC");
}
}
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
public void Add(BTCPayNetwork network)
public void Add(BTCPayNetworkBase network)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
public IEnumerable<BTCPayNetwork> GetAll()
public IEnumerable<BTCPayNetworkBase> GetAll()
{
return _Networks.Values.ToArray();
}
@ -112,14 +103,14 @@ namespace BTCPayServer
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
public BTCPayNetwork GetNetwork(string cryptoCode)
public T GetNetwork<T>(string cryptoCode) where T: BTCPayNetworkBase
{
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetworkBase network))
{
if (cryptoCode == "XBT")
return GetNetwork("BTC");
return GetNetwork<T>("BTC");
}
return network;
return network as T;
}
}
}

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="NBitcoin" Version="4.1.2.35" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.17" />
</ItemGroup>
</Project>

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace BTCPayServer
{
class CustomThreadPool : IDisposable
public class CustomThreadPool : IDisposable
{
CancellationTokenSource _Cancel = new CancellationTokenSource();
TaskCompletionSource<bool> _Exited;

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer
{
public static class UtilitiesExtensions
{
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
{
foreach (var item in items)
{
hashSet.Add(item);
}
}
}
}

@ -6,7 +6,7 @@ using System.Text;
namespace BTCPayServer
{
class ZipUtils
public class ZipUtils
{
public static byte[] Zip(string unzipped)
{

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Folder Include="Providers\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
</ItemGroup>
</Project>

@ -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<BTCPayNetworkBase>(potentialCryptoName);
if (network != null)
{
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));

@ -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)

@ -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;

@ -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

@ -11,6 +11,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="74.0.3729.6" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
@ -31,6 +33,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>

@ -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,14 +149,17 @@ namespace BTCPayServer.Tests
.UseStartup<Startup>()
.Build();
_Host.Start();
var urls = _Host.ServerFeatures.Get<IServerAddressesFeature>().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));
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
while(!dashBoard.IsFullySynched())
{
Thread.Sleep(10);
}
if (MockRates)
{
@ -210,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<NBXplorerDashboard>();
while (!dashBoard.IsFullySynched())
{
await Task.Delay(10).ConfigureAwait(false);
}
}
private string FindBTCPayServerDirectory()
@ -236,6 +275,8 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>();
}
public IServiceProvider ServiceProvider => _Host.Services;
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
{
var context = new DefaultHttpContext();
@ -245,7 +286,7 @@ namespace BTCPayServer.Tests
if (userId != null)
{
List<Claim> claims = new List<Claim>();
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));

@ -27,7 +27,7 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Fact(Timeout = 60000)]
[Trait("Integration", "Integration")]
public async void CanSetChangellyPaymentMethod()
{

@ -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"]

@ -2,13 +2,61 @@
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});");
}
/// <summary>
/// Sometimes the chrome driver is fucked up and we need some magic to click on the element.
/// </summary>
/// <param name="element"></param>
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<T>(this IActionResult result)
{
Assert.NotNull(result);

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
@ -50,10 +51,17 @@ namespace BTCPayServer.Tests
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
var walletId = new WalletId(user.StoreId, "BTC");
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
var sendModel = new WalletSendModel()
{
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
Amount = 0.1m,
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = sendDestination,
Amount = 0.1m,
}
},
FeeSatoshiPerByte = 1,
CurrentBalance = 1.5m
};
@ -63,23 +71,27 @@ namespace BTCPayServer.Tests
Assert.NotNull(vmLedger.SuccessPath);
Assert.NotNull(vmLedger.WebsocketPath);
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
var redirectedPSBT = (string)Assert.IsType<RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmPSBT.Decoded);
var filePSBT = (FileContentResult)(await walletController.WalletSend(walletId, sendModel, command: "save-psbt"));
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTViewModel>();
Assert.NotEmpty(vmPSBT2.Errors);
Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.ExtKey);
signedPSBT.SignAll(user.DerivationScheme, user.ExtKey);
vmPSBT.PSBT = signedPSBT.ToBase64();
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
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<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
vmPSBT.PSBT = unsignedPSBT.ToBase64();
@ -102,10 +114,10 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
}

@ -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();
}
}
}

@ -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);
}
}
}
}

@ -44,14 +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);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("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);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("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<BTCPayNetwork>("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
var btc = NetworkProvider.GetNetwork<BTCPayNetwork>("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);

@ -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<ServerController>(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<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
//
// //the file list should tell us it's not configured:
// var viewFilesViewModelInitial =
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
//Once we select a provider, redirect to its view
var localResult = Assert
@ -196,6 +189,7 @@ namespace BTCPayServer.Tests
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
//check if file was uploaded and saved in db
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
@ -203,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<RedirectToActionResult>(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, string>()
{
{"</a>", 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<RedirectToActionResult>(await controller.DeleteFile(fileId))
.RouteValues["statusMessage"].ToString()).Severity);
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}

@ -95,7 +95,7 @@ namespace BTCPayServer.Tests
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));

@ -60,6 +60,7 @@ 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
@ -98,44 +99,58 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
#pragma warning disable CS0618
InvoiceEntity invoiceEntity = new InvoiceEntity();
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() { PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary};
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
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 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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
}
.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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() {Value = accounting.Due}
}));
accounting = btc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped);
@ -195,57 +210,81 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
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() {PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary};
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
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 = new InvoiceEntity() {PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary};
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<PaymentEntity>();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
@ -254,9 +293,17 @@ namespace BTCPayServer.Tests
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m });
entity.Payments.Add(new PaymentEntity()
{
CryptoCode = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.1m,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -273,8 +320,14 @@ namespace BTCPayServer.Tests
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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -293,7 +346,14 @@ 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,
PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -310,7 +370,8 @@ namespace BTCPayServer.Tests
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
@ -318,29 +379,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 entity = new InvoiceEntity();
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() {PaymentMethodHandlerDictionary = paymentMethodHandlerDictionary};
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
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);
}
@ -1463,8 +1569,8 @@ namespace BTCPayServer.Tests
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"));
var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit
@ -1507,16 +1613,16 @@ namespace BTCPayServer.Tests
result = testnetParser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC"));
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
var parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString());
// Let's make sure we can't generate segwit with dogecoin
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
parsed = regtestParser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
}
@ -1533,7 +1639,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork("BTC");
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
@ -1636,20 +1742,27 @@ namespace BTCPayServer.Tests
var wallet = tester.PayTester.GetController<WalletsController>();
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendModel()
{
Amount = 0.5m,
Destination = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork).ToString(),
Outputs = new List<WalletSendModel.TransactionOutput>()
{
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.StartsWith(onchainBTC.AccountKeyPath.ToString(), keyPath.Value.Item2.ToString());
Assert.Equal(root.Derive(keyPath.Value.Item2).GetPublicKey(), keyPath.Key);
Assert.Equal(keyPath.Value.Item1, onchainBTC.RootFingerprint.Value);
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());
});
}
}
@ -2139,14 +2252,14 @@ 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);
var handler = tester.PayTester.GetService<BitcoinLikePaymentHandler>();
TestUtils.Eventually(() =>
{
var exportResultPaid = user.GetController<InvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(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($",\"{handler.PrettyDescription}\",\"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);
});
@ -2666,16 +2779,17 @@ donation:
[Trait("Fast", "Fast")]
public void ParseDerivationSchemeSettings()
{
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork("BTC");
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork<BTCPayNetwork>("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.RootFingerprint);
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.AccountKeyPath.ToString());
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");
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("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));

@ -71,7 +71,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.40
image: nicolasdorier/nbxplorer:2.0.0.48
restart: unless-stopped
ports:
- "32838:32838"
@ -134,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:
@ -177,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
@ -240,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
@ -270,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

@ -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

@ -0,0 +1,6 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdAuthorization : OpenIddictAuthorization<string, BTCPayOpenIdClient, BTCPayOpenIdToken> { }
}

@ -0,0 +1,11 @@
using BTCPayServer.Models;
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdClient: OpenIddictApplication<string, BTCPayOpenIdAuthorization, BTCPayOpenIdToken>
{
public string ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
}
}

@ -0,0 +1,6 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Authentication.OpenId.Models
{
public class BTCPayOpenIdToken : OpenIddictToken<string, BTCPayOpenIdClient, BTCPayOpenIdAuthorization> { }
}

@ -1,12 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.102</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -34,11 +30,10 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.18" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.19" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@ -47,16 +42,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBriize" Version="1.0.0.4" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="OpenIddict" Version="2.0.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
@ -70,11 +65,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.11.2" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
@ -138,6 +133,11 @@
<Folder Include="wwwroot\vendor\u2f" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Views\Apps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>

@ -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<NBXplorerConnectionSetting> 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<string>("datadir", defaultSettings.DefaultDataDirectory);
DataDir = conf.GetDataDir(NetworkType);
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
if (conf.GetOrDefault<bool>("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<string>("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<BTCPayNetworkBase>(chain) == null)
throw new ConfigException($"Invalid chains \"{chain}\"");
}
var validChains = new List<string>();
foreach (var net in NetworkProvider.GetAll())
foreach (var net in NetworkProvider.GetAll().OfType<BTCPayNetwork>())
{
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<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
@ -273,6 +272,7 @@ namespace BTCPayServer.Configuration
get;
set;
}
public bool AllowAdminRegistration { get; set; }
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
public SSHSettings SSHSettings
{

@ -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);
}
}
}

@ -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,7 +46,7 @@ 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<BTCPayNetwork>())
{
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
@ -120,7 +121,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<BTCPayNetwork>())
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");

@ -368,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();
}
@ -378,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<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
return RedirectToAction(nameof(HomeController.Index), "Home");
@ -389,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);

@ -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,

@ -91,6 +91,77 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> 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<PointOfSaleSettings>();
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<string>() { 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<IActionResult> 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<PointOfSaleSettings>();
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<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{

@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers
HttpClientFactory = httpClientFactory;
_cachedServerSettings = cachedServerSettings;
}
public async Task<IActionResult> 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");
}

@ -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<InvoiceResponse>(resp);
return new DataWrapper<InvoiceResponse>(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);
}

@ -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;
@ -77,12 +75,15 @@ namespace BTCPayServer.Controllers
StatusMessage = StatusMessage
};
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
model.Addresses = invoice.HistoricalAddresses.Select(h =>
new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod =
invoice.PaymentMethodHandlerDictionary[h.GetPaymentMethodId()]
.ToPrettyString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
var details = InvoicePopulatePayments(invoice);
model.CryptoPayments = details.CryptoPayments;
@ -91,34 +92,35 @@ namespace BTCPayServer.Controllers
return View(model);
}
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var model = new InvoiceDetailsModel();
foreach (var data in invoice.GetPaymentMethods(null))
foreach (var data in invoice.GetPaymentMethods())
{
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.PaymentMethod = invoice.PaymentMethodHandlerDictionary[paymentMethodId]
.ToPrettyString(data.GetId());
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);
model.CryptoPayments.Add(cryptoPayment);
}
foreach (var payment in invoice.GetPayments())
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentNetwork = _NetworkProvider.GetNetwork<BTCPayNetwork>(payment.GetCryptoCode());
if (paymentNetwork == null)
{
continue;
}
var paymentData = payment.GetCryptoPaymentData();
//TODO: abstract
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
@ -154,21 +156,6 @@ namespace BTCPayServer.Controllers
return 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;
}
return $"{paymentMethodId.CryptoCode} ({type})";
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
@ -223,7 +210,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
//TODO: abstract
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
@ -236,10 +223,11 @@ namespace BTCPayServer.Controllers
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetwork>(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<BTCPayNetwork>().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
}
if (invoice == null || network == null)
@ -248,18 +236,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 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;
@ -281,20 +269,18 @@ namespace BTCPayServer.Controllers
(1m + (changelly.AmountMarkupPercentage / 100m)))
: (decimal?)null;
var paymentMethodHandler = invoice.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(),
@ -310,13 +296,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
@ -332,38 +312,40 @@ 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 =
invoice.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"
@ -539,6 +521,14 @@ namespace BTCPayServer.Controllers
}
private SelectList GetPaymentMethodsSelectList()
{
return new SelectList(_paymentMethodHandlerDictionary.Distinct().SelectMany(handler =>
handler.GetSupportedPaymentMethods()
.Select(id => new SelectListItem(handler.ToPrettyString(id), id.ToString()))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}
[HttpGet]
[Route("invoices/create")]
@ -553,15 +543,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods });
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = GetPaymentMethodsSelectList() });
}
[HttpPost]
@ -573,14 +555,7 @@ namespace BTCPayServer.Controllers
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
model.AvailablePaymentMethods = paymentMethods;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if (store == null)

@ -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;
}
@ -69,11 +69,7 @@ namespace BTCPayServer.Controllers
throw new UnauthorizedAccessException();
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 +144,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<BTCPayNetworkBase>(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
@ -164,9 +160,9 @@ namespace BTCPayServer.Controllers
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(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<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
@ -205,7 +201,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 () =>
{
@ -220,7 +216,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<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -243,11 +239,11 @@ namespace BTCPayServer.Controllers
}).ToArray());
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
{
try
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
var logPrefix = $"{handler.ToPrettyString(supportedPaymentMethod.PaymentId)}:";
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
@ -268,38 +264,15 @@ namespace BTCPayServer.Controllers
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}
Func<Money, Money, bool> 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

@ -154,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;

@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers
try
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
network);

@ -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<IActionResult> 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}. <a href='{url}' target='_blank'>{url}</a>"
$"Generated Temporary Url for file {file.FileName} which expires at {expiry.ToBrowserDate()}. <a href='{url}' target='_blank'>{url}</a>"
}.ToString(),
fileId,
});

@ -875,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))

@ -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<IActionResult> GetFile(string fileId)
{
var url = await _FileService.GetFileUrl(fileId);
var url = await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
return new RedirectResult(url);
}
}

@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
SetExistingValues(store, vm);
return View(vm);
}
@ -65,7 +66,7 @@ namespace BTCPayServer.Controllers
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new LedgerHardwareWalletService(webSocket);
object result = null;
var network = _NetworkProvider.GetNetwork(cryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
using (var normalOperationTimeout = new CancellationTokenSource())
{
@ -119,6 +120,8 @@ namespace BTCPayServer.Controllers
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
@ -157,6 +160,7 @@ namespace BTCPayServer.Controllers
return NotFound();
}
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
@ -202,10 +206,17 @@ namespace BTCPayServer.Controllers
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.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
@ -330,6 +341,7 @@ namespace BTCPayServer.Controllers
}
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View(vm);
}
}

@ -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<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try
{
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);

@ -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;
@ -56,7 +57,8 @@ namespace BTCPayServer.Controllers
LanguageService langService,
ChangellyClientProvider changellyClientProvider,
IOptions<MvcJsonOptions> 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 = _paymentMethodHandlerDictionary[o].ToPrettyString(o), 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<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
@ -472,36 +469,38 @@ namespace BTCPayServer.Controllers
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.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?.ToPrettyString() ?? 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<Payments.Lightning.LightningSupportedPaymentMethod>()
.ToDictionary(c => c.CryptoCode);
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.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 PaymentTypes.BTCLike:
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 PaymentTypes.LightningLike:
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()
{

@ -19,22 +19,28 @@ namespace BTCPayServer.Controllers
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
psbtRequest.Destinations.Add(psbtDestination);
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;
}
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
psbtDestination.Amount = Money.Coins(sendModel.Amount.Value);
psbtRequest.FeePreference = new FeePreference();
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
if (sendModel.NoChange)
{
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().Destination;
}
psbtDestination.SubstractFees = sendModel.SubstractFees;
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
if (psbt == null)
throw new NotSupportedException("You need to update your version of NBXplorer");
@ -43,9 +49,16 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{walletId}/psbt")]
public IActionResult WalletPSBT()
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm)
{
return View(new WalletPSBTViewModel());
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(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")]
@ -54,63 +67,183 @@ namespace BTCPayServer.Controllers
WalletId walletId,
WalletPSBTViewModel vm, string command = null)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var psbt = vm.GetPSBT(network.NBitcoinNetwork);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View(vm);
}
if (command == null)
switch (command)
{
vm.Decoded = psbt.ToString();
return View(vm);
}
else if (command == "ledger")
{
return ViewWalletSendLedger(psbt);
}
else if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
return ViewPSBT(psbt, errors);
}
var transaction = psbt.ExtractTransaction();
try
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
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)
{
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
ModelState.AddModelError(nameof(vm.PSBT), "You need to update your version of NBXplorer");
return View(vm);
}
}
catch (Exception ex)
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 ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
return await WalletPSBTReady(walletId, psbt.ToBase64());
}
return await RedirectToWalletTransaction(walletId, transaction);
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);
}
else if (command == "combine")
}
private async Task<PSBT> UpdatePSBT(DerivationSchemeSettings derivationSchemeSettings, PSBT psbt, BTCPayNetwork network)
{
var result = await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(new UpdatePSBTRequest()
{
ModelState.Remove(nameof(vm.PSBT));
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
}
else
{
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
return FilePSBT(psbt, "psbt-export.psbt");
}
PSBT = psbt,
DerivationScheme = derivationSchemeSettings.AccountDerivation,
});
if (result == null)
return null;
derivationSchemeSettings.RebaseKeyPaths(result.PSBT);
return result.PSBT;
}
[HttpGet]
[Route("{walletId}/psbt/ready")]
public IActionResult WalletPSBTReady(
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string psbt = null)
WalletId walletId, string psbt = null,
string signingKey = null,
string signingKeyPath = null)
{
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(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]
@ -120,23 +253,25 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
{
PSBT psbt = null;
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(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.Errors = new List<string>();
vm.Errors.Add("Invalid PSBT");
vm.GlobalError = "Invalid PSBT";
return View(vm);
}
if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
vm.Errors = new List<string>();
vm.Errors.AddRange(errors.Select(e => e.ToString()));
vm.SetErrors(errors);
return View(vm);
}
var transaction = psbt.ExtractTransaction();
@ -145,40 +280,44 @@ namespace BTCPayServer.Controllers
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
{
vm.Errors = new List<string>();
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
return View(vm);
}
}
catch (Exception ex)
{
vm.Errors = new List<string>();
vm.Errors.Add("Error while broadcasting: " + ex.Message);
vm.GlobalError = "Error while broadcasting: " + ex.Message;
return View(vm);
}
return await RedirectToWalletTransaction(walletId, transaction);
}
else if (command == "analyze-psbt")
{
return ViewPSBT(psbt);
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() });
}
else
{
vm.Errors = new List<string>();
vm.Errors.Add("Unknown command");
vm.GlobalError = "Unknown command";
return View(vm);
}
}
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
{
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
return ViewPSBT(psbt, null, errors?.Select(e => e.ToString()).ToList());
}
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
{
return ViewPSBT(psbt, null, errors);
}
private IActionResult ViewPSBT(PSBT psbt, string fileName, IEnumerable<string> 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()
});
@ -194,7 +333,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTCombineViewModel vm)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{

@ -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;
@ -153,20 +156,27 @@ namespace BTCPayServer.Controllers
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(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<WalletSendModel.TransactionOutput>()
{
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();
@ -201,58 +211,124 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
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<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
vm.SupportRBF = network.SupportRBF;
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
if (vm.Amount.HasValue)
decimal transactionAmountSum = 0;
if (command == "add-output")
{
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");
ModelState.Clear();
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 subtractFeesOutputsCount = new List<int>();
var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput);
for (var i = 0; i < vm.Outputs.Count; i++)
{
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);
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);
var psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
if (command == "ledger")
CreatePSBTResponse psbt = null;
try
{
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
}
else
catch (NBXplorerException ex)
{
try
{
if (command == "analyze-psbt")
return ViewPSBT(psbt.PSBT);
derivationScheme.RebaseKeyPaths(psbt.PSBT);
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
}
catch (NBXplorerException ex)
{
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
return View(vm);
}
catch (NotSupportedException)
{
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
return View(vm);
}
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)
@ -265,6 +341,82 @@ namespace BTCPayServer.Controllers
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
});
}
[HttpPost("{walletId}/psbt/seed")]
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, SignWithSeedViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (network == null)
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)
{
@ -281,7 +433,7 @@ namespace BTCPayServer.Controllers
private async Task<IActionResult> RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (transaction != null)
{
var wallet = _walletProvider.GetWallet(network);
@ -426,7 +578,7 @@ namespace BTCPayServer.Controllers
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
@ -452,14 +604,15 @@ namespace BTCPayServer.Controllers
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var accountKey = derivationSettings.GetSigningAccountKeySettings();
// Some deployment does not have the AccountKeyPath set, let's fix this...
if (derivationSettings.AccountKeyPath == null)
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
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
derivationSettings.AccountDerivation,
normalOperationTimeout.Token);
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
storeData.SetSupportedPaymentMethod(derivationSettings);
await Repository.UpdateStore(storeData);
}
@ -468,10 +621,10 @@ namespace BTCPayServer.Controllers
{
// 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 (derivationSettings.RootFingerprint == null)
if (accountKey.RootFingerprint == null)
{
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
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");
}
@ -479,15 +632,15 @@ namespace BTCPayServer.Controllers
else
{
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
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 (derivationSettings.RootFingerprint == null)
if (accountKey.RootFingerprint == null)
{
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
storeData.SetSupportedPaymentMethod(derivationSettings);
await Repository.UpdateStore(storeData);
}
@ -502,7 +655,7 @@ namespace BTCPayServer.Controllers
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
}
}
@ -528,6 +681,59 @@ namespace BTCPayServer.Controllers
}
return new EmptyResult();
}
[Route("{walletId}/settings")]
public async Task<IActionResult> WalletSettings(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
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<IActionResult> 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));
}
}

@ -1,4 +1,5 @@
using System.Linq;
using BTCPayServer.Authentication.OpenId.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
@ -6,6 +7,7 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.U2F.Models;
using BTCPayServer.Storage.Models;
using Microsoft.EntityFrameworkCore.Infrastructure;
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data
{
@ -225,6 +227,9 @@ namespace BTCPayServer.Data
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
}
}

@ -28,6 +28,9 @@ namespace BTCPayServer.Data
{
public class StoreData
{
[NotMapped]
[JsonIgnore]
public PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary { get; set; }
public string Id
{
get;
@ -84,14 +87,16 @@ 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<BTCPayNetwork>(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
PaymentMethodHandlerDictionary[paymentMethodId]
.DeserializeSupportedPaymentMethod(paymentMethodId, strat.Value);
}
}
}
@ -282,7 +287,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;
}

@ -65,6 +65,9 @@ namespace BTCPayServer
{
result.AccountOriginal = jobj["xpub"].Value<string>().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
}
@ -91,7 +94,7 @@ namespace BTCPayServer
{
try
{
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
}
catch { return false; }
}
@ -100,7 +103,7 @@ namespace BTCPayServer
{
try
{
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
}
catch { return false; }
}
@ -113,6 +116,7 @@ namespace BTCPayServer
{
}
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
{
if (network == null)
@ -121,20 +125,47 @@ namespace BTCPayServer
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
@ -143,16 +174,54 @@ namespace BTCPayServer
}
}
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<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
{
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
foreach (var accountKey in AccountKeySettings)
{
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
if (accountKey.AccountKeyPath != null && accountKey.RootFingerprint is HDFingerprint fp)
{
AccountKey = AccountKey,
AccountKeyPath = AccountKeyPath,
MasterFingerprint = fp
};
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
{
AccountKey = accountKey.AccountKey,
AccountKeyPath = accountKey.AccountKeyPath,
MasterFingerprint = fp
};
}
}
}
@ -181,8 +250,25 @@ namespace BTCPayServer
{
foreach (var rebase in GetPSBTRebaseKeyRules())
{
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
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;
}
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

@ -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}";

@ -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; }

@ -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<BTCPayNetwork>(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<BTCPayNetwork>(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<BTCPayNetwork>(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<BTCPayNetwork>())
{
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
{

@ -206,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(
@ -317,13 +322,6 @@ namespace BTCPayServer
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
}
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
{
foreach (var item in items)
{
hashSet.Add(item);
}
}
public static bool GetIsBitpayAPI(this HttpContext ctx)
{
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&

@ -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

@ -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<TModel, TProperty>(
this ModelStateDictionary modelState,
Expression<Func<TModel, TProperty>> ex,
string message
)
{
var key = ExpressionHelper.GetExpressionText(ex);
modelState.AddModelError(key, message);
}
public static void AddModelError<TModel, TProperty>(this TModel source,
Expression<Func<TModel, TProperty>> ex,
string message,
ModelStateDictionary modelState)
{
var key = ExpressionHelper.GetExpressionText(ex);
modelState.AddModelError(key, message);
}
}
}

@ -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);
}
}
}

@ -0,0 +1,97 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Xml;
namespace NETCore.Encrypt.Extensions.Internal
{
/// <summary>
/// .net core's implementatiosn are still marked as unsupported because of stupid decisions( https://github.com/dotnet/corefx/issues/23686)
/// </summary>
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,
"<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
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
}
}

@ -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<InvoiceNotificationManager> 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()

@ -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
{
@ -74,11 +68,11 @@ namespace BTCPayServer.HostedServices
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
var allPaymentMethods = invoice.GetPaymentMethods();
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider);
if (paymentMethod == null)
return;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethod.GetId().CryptoCode);
if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired)
{
if (accounting.Paid >= accounting.MinimumTotalDue)
@ -173,7 +167,7 @@ namespace BTCPayServer.HostedServices
decimal nearestToZero = 0.0m;
foreach (var paymentMethod in allPaymentMethods)
{
if (networkProvider != null && networkProvider.GetNetwork(paymentMethod.GetId().CryptoCode) == null)
if (networkProvider != null && networkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethod.GetId().CryptoCode) == null)
continue;
var currentAccounting = paymentMethod.Calculate();
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
@ -288,31 +282,18 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
var updateConfirmationCountIfNeeded = invoice
.GetPayments()
.Select<PaymentEntity, Task>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
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 < paymentNetwork.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);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
}
})
.ToArray();
await Task.WhenAll(updateConfirmationCountIfNeeded);
var extendInvoiceMonitoring = await UpdateConfirmationCount(invoice);
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
// 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;
}
@ -330,6 +311,46 @@ namespace BTCPayServer.HostedServices
}
}
private async Task<bool> UpdateConfirmationCount(InvoiceEntity invoice)
{
bool extendInvoiceMonitoring = false;
var updateConfirmationCountIfNeeded = invoice
.GetPayments()
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork<BTCPayNetwork>(payment.GetCryptoCode());
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 < paymentNetwork.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 < paymentNetwork.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)

@ -93,6 +93,7 @@ namespace BTCPayServer.HostedServices
foreach (var store in await ctx.Stores.ToArrayAsync())
{
#pragma warning disable CS0618 // Type or member is obsolete
_StoreRepository.PrepareEntity(store);
var blob = store.GetStoreBlob();
if (blob.WalletKeyPathRoots == null)
continue;
@ -138,6 +139,7 @@ namespace BTCPayServer.HostedServices
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
_StoreRepository.PrepareEntity(store);
var blob = store.GetStoreBlob();
#pragma warning disable CS0618 // Type or member is obsolete
if (blob.NetworkFeeDisabled != null)
@ -158,6 +160,7 @@ namespace BTCPayServer.HostedServices
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
_StoreRepository.PrepareEntity(store);
var blob = store.GetStoreBlob();
#pragma warning disable CS0612 // Type or member is obsolete
decimal multiplier = 1.0m;
@ -188,6 +191,7 @@ namespace BTCPayServer.HostedServices
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
_StoreRepository.PrepareEntity(store);
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
var lightning = method.GetLightningUrl();

@ -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<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
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);

@ -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,45 +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<ApplicationDbContext>((provider, o) =>
{
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o);
o.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
@ -67,15 +65,18 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<LightningClientFactoryService>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<BTCPayServerOptions>(o =>
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<InvoiceRepository>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
var dbContext = o.GetRequiredService<ApplicationDbContextFactory>();
var paymentMethodHandlerDictionary = o.GetService<PaymentMethodHandlerDictionary>();
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<BTCPayNetworkProvider>(),
paymentMethodHandlerDictionary);
});
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
@ -176,15 +177,18 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationSchemeSettings>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<BitcoinLikePaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<BitcoinLikePaymentHandler>());
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LightningLikePaymentHandler>());
services.AddSingleton<IHostedService, LightningListener>();
services.AddSingleton<PaymentMethodHandlerDictionary>();
services.AddSingleton<ChangellyClientProvider>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
@ -219,7 +223,7 @@ namespace BTCPayServer.Hosting
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBtcPayServerAuthenticationSchemes(configuration);
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
services.AddTransient<BundleOptions>(provider =>
@ -231,9 +235,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();
@ -241,6 +245,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)
{

@ -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<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.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<string>("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<ApplicationDbContext>()
.ReplaceDefaultEntities<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>,
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()
{

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