Compare commits

..

180 Commits

Author SHA1 Message Date
9aa0603d87 bump 2020-01-14 22:07:13 +09:00
36f980135f fix broken url (#1275)
* fix broken url

* fix test
2020-01-14 22:06:46 +09:00
63953e42a8 Merge pull request #1276 from NicolasDorier/norazor/compile
Remove build of razor view for better debug experience
2020-01-14 21:38:38 +09:00
4ba836f1ff Remove build of razor view for better debug experience 2020-01-14 21:33:13 +09:00
b7132ab66a Merge pull request #1274 from Kukks/route-fixes
fix broken links
2020-01-14 21:00:29 +09:00
67810d50cb Merge pull request #1272 from Kukks/u2ftests
U2F Tests + throw non non U2f exceptions
2020-01-14 20:46:25 +09:00
b7503c994c fix broken links 2020-01-14 12:46:07 +01:00
389695751f add u2f tests 2020-01-14 11:47:24 +01:00
dad3039c06 throw on non-u2f specific errors 2020-01-14 09:49:51 +01:00
9ccb472c7a Fix U2F 2020-01-14 12:31:10 +09:00
c35afd5e9a Fix arm64 image 2020-01-14 00:35:45 +09:00
74adaf1d1f Rename 2020-01-14 00:28:43 +09:00
0fce8d0739 bump 2020-01-13 23:58:01 +09:00
dbb6408acb Merge pull request #1264 from Kukks/coingecko
CoinGecko Rate Provider
2020-01-13 23:57:20 +09:00
5dbdb4b399 Keep coinaverage compatibility, improve UX, hardcode feed provider supported exchanges 2020-01-13 23:37:01 +09:00
58d9a48787 CoinGecko Rate Provider
The CoinGecko rate provider is similar to the bitcoin average one, in that you can ask it for a rate from its aggregated sourcing or you can get rates from specific exchanges. I've added support for both.
I haven't integrated it or replaced coinaverage just to see if we should use it as the default and switch everyone to it or what other action to take.
2020-01-13 20:20:00 +09:00
4baeb7bc71 Merge pull request #1268 from escapedcat/fix/1216_users-remove-name
fix(users): remove name from list #1216
2020-01-13 13:41:03 +09:00
1a3da096a7 Go back to bitcoind 0.18.0 2020-01-13 13:26:42 +09:00
ba0e501e38 bump bitcoind 2020-01-13 13:14:41 +09:00
bff95e4655 fix(users): remove name from list #1216
name is not used and can not be edited so it should be removed to avoid confusion
2020-01-12 15:23:58 +01:00
4f03f3c9cb Bump nbx in tests 2020-01-12 21:45:23 +09:00
d48334b97f Make test more reliable 2020-01-12 20:54:04 +09:00
90da81f68e Merge pull request #1238 from bolatovumar/fix-1236
Adjust invoice pagination behavior
2020-01-12 16:55:57 +09:00
9ba1403f5c bump libraries 2020-01-12 16:30:25 +09:00
846fd815ff Merge pull request #1267 from NicolasDorier/remove/uselesss
Remove useless dependencies
2020-01-12 16:16:03 +09:00
60e0f775ed Remove useless dependencies 2020-01-12 16:05:01 +09:00
529c2df1cc Make tests more resilient 2020-01-12 15:50:23 +09:00
430a9eb261 Merge pull request #1266 from NicolasDorier/bump/libs
Bump libraries
2020-01-12 15:44:05 +09:00
d3408b91be bump libraries 2020-01-12 15:32:26 +09:00
d5febb30e7 Fix build 2020-01-12 13:59:41 +09:00
63c4ec1809 Reactivate GoogleCloudStorage 2020-01-12 13:55:41 +09:00
6ac8cd19d3 Better logs for HappyMessage 2020-01-12 13:54:06 +09:00
c95bceef4d Fix circleci 2020-01-12 13:42:04 +09:00
3449bba4b3 Reactivate google storage 2020-01-12 13:39:42 +09:00
d94b016e63 Add google storage at build time 2020-01-12 13:34:29 +09:00
629dfcf152 Cleanup netcore21 specific code 2020-01-12 13:30:54 +09:00
9876208b7d Add arm64v8 support 2020-01-11 22:54:08 +09:00
df617d5186 Merge pull request #1258 from NicolasDorier/migration/netcore31
Switch to .netcoreapp3.1
2020-01-11 15:03:51 +09:00
21f715bfc6 Add runtime razor compilation during debug 2020-01-11 13:12:41 +09:00
18a2c1a00f Remove warning 2020-01-11 13:12:41 +09:00
6c2fdecebe Rewrite EF query for 3.1 compatibility 2020-01-11 13:12:40 +09:00
c3b7779ea3 Migrate dockerfile to .netcoreapp3.1 2020-01-11 13:12:40 +09:00
83ea95ed6d Switch to .netcoreapp3.1 2020-01-11 13:12:39 +09:00
a816e37621 Update libs 2020-01-11 13:12:24 +09:00
33c65a6548 Merge pull request #1246 from Kukks/generate-wallet-message
add error message on generate wallet failure
2020-01-10 20:03:59 +09:00
bf57701cf0 Passing ambient route parameters explicitely 2020-01-10 15:37:44 +09:00
bfcd90d8d1 Refactor test 2020-01-10 14:46:42 +09:00
0387306918 Adjust invoice listing pagination display 2020-01-09 20:04:55 -08:00
c99d26a55d Fix test for clightning overpaying 2020-01-10 11:13:54 +09:00
7efeeb7c28 Merge pull request #1231 from NicolasDorier/fix/mysqlmigrations
Fix MySQL support
2020-01-09 17:13:53 +09:00
3164783b31 Merge pull request #1253 from radWorx/lead-login-verbiage
"Lead-login" verbiage
2020-01-06 21:50:37 -06:00
28c441924a Merge pull request #1242 from jlopp/varFix
Fix variable spelling
2020-01-06 21:44:17 -06:00
3b3ec7fc21 Merge pull request #1237 from jlopp/spellCheck
Add spelling / grammar fixes and some clarifications for settings
2020-01-06 21:43:46 -06:00
dfa0201726 Merge pull request #1254 from pavlenex/year-update
Update Year 2020 in License
2020-01-06 21:43:20 -06:00
2b889d9e29 Update Year
Happy New Year.
2020-01-03 14:11:15 +01:00
08688f69c0 "lead-login" verbiage
missing final period. suggest changing verbiage to reflect btcpayserver.org verbiage for consistency.
2020-01-02 22:50:26 -05:00
4a0d29c700 Merge pull request #1247 from Kukks/liquid-wallet-changes
Liquid: Show limited wallet pages
2019-12-30 13:21:39 +09:00
fa916d4862 Liquid: Show limited wallet pages 2019-12-29 17:08:30 +01:00
1973647b51 add error message on generate wallet failure 2019-12-29 16:43:55 +01:00
12133cd7d3 fix variable spelling 2019-12-27 14:32:43 -05:00
9b66ba226b bump lightning lib 2019-12-27 21:59:06 +09:00
96731fabc7 bump lightning lib 2019-12-27 21:44:28 +09:00
87f1ab7caa bump lightning 2019-12-27 21:21:18 +09:00
4cf6f8e753 Rename ListInvoices function to ListInvoicesPage to avoid ambiguity 2019-12-26 15:08:43 -08:00
dc59c4ca47 Adjust invoice pagination behavior
fix #1236
2019-12-26 14:53:09 -08:00
4f046ed1d3 add spelling / grammar fixes and some clarifications for settings. 2019-12-26 07:32:32 -05:00
d689222e04 Merge pull request #1234 from NicolasDorier/rates/limit
Do not preemptively fetch rates of all exchanges
2019-12-26 16:36:53 +09:00
57985e78e5 Save the last update time instead of the next update time 2019-12-26 16:24:57 +09:00
731341b749 Do not preemptively fetch rates of all exchanges 2019-12-26 14:54:26 +09:00
f12186e09f The tests in btcpayserver should use only the services they use (#1233) 2019-12-24 18:11:21 +09:00
4d7480db15 Liquid & Liquid Assets Support (#1118) 2019-12-24 16:20:44 +09:00
0485a9178d Merge pull request #1232 from NicolasDorier/seed/bettererror
Make it mandatory for the user to set the master fingerprint in the w…
2019-12-23 23:32:14 +09:00
17d2b20cd5 User can use passphrase when importing seed 2019-12-23 23:31:39 +09:00
aa459d0ff3 Make it mandatory for the user to set the master fingerprint in the wallet settings for seed signing. Improve error messages. 2019-12-23 23:08:41 +09:00
656ff7029e Do not show on screen seed passphrase in sign with seed 2019-12-23 22:32:33 +09:00
8e00e6d8e3 Make sure that varchar(255) is used for mysql migration scripts 2019-12-23 15:03:06 +09:00
8bcb2381a0 Comment code to generate MySql migration scripts 2019-12-23 15:03:05 +09:00
a73d2db02a Fix tests 2019-12-19 16:36:04 +09:00
47eb087d1b Rename CanGenerateWallet to CanUseHotWallet, small refactoring on generatewallet 2019-12-18 22:28:03 +09:00
ed6a01469a Merge pull request #1212 from Kukks/generatewallet
Generate wallet
2019-12-18 16:35:34 +09:00
7cfe5f0421 failsafe with selenium 2019-12-16 12:10:03 +01:00
1aef7f7ea6 rebase fix 2019-12-16 10:25:07 +01:00
9142c48a0b return correct view 2019-12-16 09:38:00 +01:00
45e139c5b7 define view 2019-12-16 09:35:41 +01:00
6706658377 add policy to restrict generate wallet usage 2019-12-16 09:32:43 +01:00
a75b6201b7 update message 2019-12-16 09:01:27 +01:00
f724d6c0cf tests for importing keys to rpc 2019-12-16 09:01:27 +01:00
0dccbeac3d fix tests 2019-12-16 09:01:27 +01:00
7d2dc45dfb fix ident 2019-12-16 09:01:27 +01:00
f284ef9052 Add generate wallet e2e test 2019-12-16 09:01:27 +01:00
80790bd9b0 fixes 2019-12-16 09:00:41 +01:00
2da9434571 init work 2019-12-16 09:00:00 +01:00
579e0d2e09 Do not crash if empty destination is entered on WalletSend 2019-12-11 13:05:59 +09:00
33703b83a3 Fix device not found with Trezor T 2019-12-10 23:52:48 +09:00
23b9dfed2c Cache balance 2019-12-10 22:17:59 +09:00
b8f6ef8844 fix sdk version of tests 2019-12-10 21:46:06 +09:00
6f6b4c8ead Fix dockerfile versions 2019-12-10 21:43:21 +09:00
5d87dd5861 Merge pull request #1219 from bolatovumar/fix-1217
[Wallet] Show invalid address message when address is invalid
2019-12-10 21:39:43 +09:00
02c8bf4469 bump 2019-12-10 21:23:38 +09:00
540cb912f3 Can display address on device at confirmation screen 2019-12-10 21:22:46 +09:00
93f490f570 Fix pin entry 2019-12-10 20:10:13 +09:00
de2e222ae5 Improve signing vault UI 2019-12-10 18:58:12 +09:00
12055a000b Proper handling of passphrase for Trezor T 2019-12-10 18:16:52 +09:00
6addb3e481 [Wallet] Show invalid address message when address is invalid
fix #1217
2019-12-07 21:20:42 -08:00
d9cd916440 Merge pull request #1187 from dennisreimann/document-themeing
Document themeing
2019-12-07 17:22:47 +09:00
452a705b75 Update russian,portugal 2019-12-07 17:21:11 +09:00
1c8206c749 Merge pull request #1214 from MrPaz/feature/satsfix
Fixing sats exchange rate display
2019-12-07 17:18:54 +09:00
062c42e524 Fix build 2019-12-07 16:42:03 +09:00
2d932ebb21 Update to runtime .NET 2.1.14 2019-12-07 16:35:13 +09:00
0e0fa53517 Prompt passphrase for trezor T 2019-12-05 22:47:07 +09:00
4e20730379 Better message when the device is signing (#1207) 2019-12-05 22:37:04 +09:00
1d70d935b8 Provide better details for unknown error (#1208) 2019-12-05 22:03:35 +09:00
9b8f42cdf6 Merge pull request #1213 from rockstardev/feature/accessdeniedfix
Fixing AccessDenied page displayed
2019-12-04 15:38:40 -06:00
eb85b1a7b4 Fixing sats exchange rate display
Fixed #1147
2019-12-04 14:21:33 -06:00
df7e2073df Fixing AccessDenied page displayed 2019-12-04 13:45:09 -06:00
84d943d6cc Trying to fix TrezorT 2019-12-04 22:12:38 +09:00
a1d82b0e8b fix bug if selecting segwit with vault 2019-12-04 21:52:48 +09:00
ab7c124302 Properly show text to enter pin or passphrase for trezor T 2019-12-04 17:23:10 +09:00
544597348b Fix inverted boolean logic 2019-12-04 17:19:25 +09:00
1559c510be Trying to handle Trezor T correctly 2019-12-04 17:16:37 +09:00
3e08581e9b Fix fetching xpub for trezor 2019-12-04 16:34:25 +09:00
49c70d9b04 Vault: Reinite the popup in add derivation scheme if it is closed 2019-12-04 16:16:41 +09:00
50e7d8389c Vault: Allow user to pick the account number 2019-12-04 15:54:08 +09:00
ea5bd6d435 bump nbx 2019-12-04 13:51:26 +09:00
cbb37674e3 vault-confirm is not a popper 2019-12-04 12:56:39 +09:00
e70d5ceb08 Fix JS error on WalletSendVault after PIN is asked (#1210) 2019-12-04 12:52:54 +09:00
5d7b253edd Fix bug: Vault option now showing up in PSBT 2019-12-03 18:57:07 +09:00
df38b84bbb Fix alignment in psbt review 2019-12-03 14:30:29 +09:00
a3fc75ea3b Show proper error message if the keypath in wallet settings is not good (Fix #1209) 2019-12-03 14:26:52 +09:00
71a8166027 Handle "pin already prompted" error. (Fix #1209) 2019-12-03 13:53:58 +09:00
564dd95d81 Remove unused modern theme
We can start over with this once the general design decisions are decided on and we start implementing a new UI.
2019-12-02 11:19:17 +01:00
eb16b435df Document themeing approach
Sums up and adds to the comments made in #1175.

I wasn't sure where to put these kinds of docs, as tehy are code related and not end user facing. Let's discuss whether or not this fits in here or should become part of the docs repo.

fix
2019-12-02 11:18:33 +01:00
f94daed06d Merge pull request #1184 from bolatovumar/fix-1182
[Wallet] Specify validation range for wallet send amount
2019-12-02 18:29:45 +09:00
e841bfa148 Merge pull request #1186 from dennisreimann/custom-theme
UI: Add custom theme file option
2019-12-02 18:29:07 +09:00
03b76380e7 Merge pull request #1192 from dennisreimann/ln-node-certthumbprint
LN Node: Show ready to use certthumbprint
2019-12-02 18:28:11 +09:00
c6671f7122 Merge pull request #1200 from pavlenex/ipn-notification
Re-name the Invoice e-mail notification field
2019-12-02 18:27:52 +09:00
db56c2b106 Merge pull request #1202 from pavlenex/create-wallet
Change create with connect in wallet page
2019-12-02 18:27:40 +09:00
f7b2c836b7 Merge pull request #1203 from Kukks/nbxupdate
update nbx + prep bitcoin payment data ctor change
2019-12-02 18:27:28 +09:00
6ec379b538 Merge pull request #1198 from Eskyee/patch-3
Update vaultbridge.js
2019-12-02 18:27:02 +09:00
d6e1d34ebf update nbx docker tests 2019-12-02 10:10:24 +01:00
bc2a039ea2 bump nbx and make balance return decimal 2019-12-02 09:57:38 +01:00
e31e600144 update nbx + prep bitcoin payment data ctor change 2019-12-01 15:30:56 +01:00
91f83d751f Connect wallet clarification 2019-11-30 18:15:28 +01:00
b8288f1efa Use 127.0.0.1 instead of localhost for vault's http requests. (Fix https://github.com/btcpayserver/BTCPayServer.Vault/issues/9#issuecomment-559858955) 2019-11-30 23:34:47 +09:00
fa61c2bcdd Add error for invalid network 2019-11-30 23:33:42 +09:00
2d168e1d9b Re-name the Invoice e-mail notification field 2019-11-30 11:45:06 +01:00
3e7ad4a4f6 Update vaultbridge.js
replace localhost with 127.0.0.1 fixes BTCPay Vault in Apple Safari browser Version 13.0.3 macOS Catalina 10.15.1 (19B88)
thanks to Nicolas
2019-11-29 20:09:46 +00:00
d2357ee8b9 Do not ask for Pin on Trezor T 2019-11-25 11:50:49 +09:00
9f04c7d0bc Merge pull request #1193 from dennisreimann/heading-alignment
UI: Left align admin section headings
2019-11-25 11:44:25 +09:00
9baa7eed80 UI: Left align admin section headings
Removes the custom property and consistently align the headings left, which I agree is the better choice.

Context: https://chat.btcpayserver.org/btcpayserver/pl/upb3nzch7j8nmpbt7kfmoe6beo
2019-11-24 20:39:29 +01:00
287fbc523f Asks pin and passphrase if device is not ready 2019-11-24 22:51:13 +09:00
ea6df35c87 LN Node: Show ready to use certthumbprint
Turns the output of the `openssl` command from

`SHA256 Fingerprint=48:37:A4:D6:[…]`

into this, which can be copied and inserted directly:

`4837A4D6[…]`
2019-11-24 09:48:34 +01:00
2e0db1a430 Fix XML parsing error (#1185) 2019-11-23 14:04:26 +09:00
7ccf0c3612 UI: Add custom theme file option
This adds a "Custom Theme CSS file" to the "Server settings: Theme" page. It works similar to the custom bootstrap and creative start fields, but is only added to the head in case it is set.

The main idea here is that users would choose one of our predefined themes and have the option to override specific values. The other apporach would be to let the custom theme replace the predefined one, but this could lead to issues in case we extend the set of variables, which would be unset then.
This way users can copy a predefined theme and override all variables or just the ones they want to customize.

The help icon besides the label links to a page in the docs I'm currently working on.
2019-11-22 21:09:18 +01:00
1b58058796 Refactoring 2019-11-22 19:19:05 +09:00
98276bcb3d Remove useless try/catch 2019-11-22 19:16:32 +09:00
1aa4dbb90d bump 2019-11-22 19:14:15 +09:00
eef623fb4b Ask passphrase when appropriate (Fix #1185) 2019-11-22 19:13:04 +09:00
84bbbcbe10 [Wallet] Specify validation range for wallet send amount
fix #1182
2019-11-21 18:47:10 -08:00
a324e2aeaf Merge pull request #1181 from Kukks/register-login-link
add login button to register page
2019-11-21 18:01:39 +09:00
d6b3530384 add login button to register page 2019-11-21 09:28:35 +01:00
dfe8a10e1a bump 2019-11-21 16:39:18 +09:00
5afa847e6e Fix sign transaction 2019-11-21 16:38:43 +09:00
1bcbc9bab0 Should not show the password 2019-11-21 16:03:06 +09:00
b915544798 Prevent NRE on NBxplorer listener 2019-11-21 14:27:57 +09:00
2b9f390c64 update translation 2019-11-21 14:17:27 +09:00
ee42d5c7b4 bump 2019-11-21 14:15:04 +09:00
f809dd51a6 Merge pull request #1152 from NicolasDorier/feature/vault
Add hardware support via BTCPayServer Vault
2019-11-21 14:13:43 +09:00
1a8d6e5c05 Implement BTCPayServer vault derivation scheme import 2019-11-21 14:06:16 +09:00
869ba745b2 Merge pull request #1175 from bolatovumar/fix-1169
Add CSS variable for preformatted text color
2019-11-21 14:03:05 +09:00
180dfb6edf Add CSS variable for preformatted text color
fix #1169
2019-11-20 11:12:31 -08:00
45b08ac8d2 Add sponsor 2019-11-19 20:37:15 +09:00
9a4b385432 Add sponsor 2019-11-19 20:33:26 +09:00
08289b89c5 Merge pull request #1176 from pavlenex/supporters-sw
Add new supporter to readme
2019-11-19 20:32:42 +09:00
a31d1d81c8 Update README.md 2019-11-19 09:20:31 +01:00
e4c7bb0378 add wallet of satoshi to readme
add wallet of Satoshi, fix Lunanode spacing
2019-11-19 09:16:04 +01:00
374aaf2e2b add walletofsatoshi svg logo 2019-11-19 09:12:38 +01:00
52fd686993 Merge pull request #1174 from pavlenex/new-supporters-lw
add new supporter to readme
2019-11-19 00:31:06 +09:00
03c36ef0d2 add lunanode to readme 2019-11-18 15:55:56 +01:00
71a6ffac2e Adjust status message for WalletTransactions 2019-11-18 21:47:09 +09:00
217 changed files with 4873 additions and 1871 deletions

View File

@ -66,6 +66,20 @@ jobs:
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
arm64v8:
machine:
enabled: true
steps:
- checkout
- run:
command: |
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-arm64v8 -f arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
multiarch:
machine:
enabled: true
@ -80,9 +94,10 @@ jobs:
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
workflows:
@ -114,10 +129,17 @@ workflows:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
ignore: /.*/

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/tx/{0}" : "https://blockstream.info/testnet/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
CryptoImagePath = "imlegacy/bitcoin.svg",

View File

@ -12,7 +12,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BGold",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoingold",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoinplus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoinplus",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]

View File

@ -15,7 +15,6 @@ namespace BTCPayServer
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dash",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dogecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Feathercoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]

View File

@ -18,7 +18,6 @@ namespace BTCPayServer
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]

View File

@ -19,9 +19,13 @@ namespace BTCPayServer
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateRules = new[]
{
"LTC_X = LTC_BTC * BTC_X",
"LTC_BTC = coingecko(LTC_BTC)"
},
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Monacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "monacoin",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Ufo",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "ufo",
DefaultRateRules = new[]

View File

@ -17,7 +17,6 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Viacoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "viacoin",
DefaultRateRules = new[]

View File

@ -47,6 +47,8 @@ namespace BTCPayServer
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
NetworkType = networkType;
InitBitcoin();
InitLiquid();
InitLiquidAssets();
InitLitecoin();
InitBitcore();
InitDogecoin();
@ -93,6 +95,12 @@ namespace BTCPayServer
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
public void Add(BTCPayNetwork network)
{
if (network.NBitcoinNetwork == null)
return;
Add(network as BTCPayNetworkBase);
}
public void Add(BTCPayNetworkBase network)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
@ -109,7 +117,7 @@ namespace BTCPayServer
}
public BTCPayNetworkBase GetNetwork(string cryptoCode)
{
return GetNetwork<BTCPayNetworkBase>(cryptoCode);
return GetNetwork<BTCPayNetworkBase>(cryptoCode.ToUpperInvariant());
}
public T GetNetwork<T>(string cryptoCode) where T: BTCPayNetworkBase
{

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.Altcoins.Elements;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitLiquid()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LBTC");
Add(new ElementsBTCPayNetwork()
{
AssetId = NetworkType == NetworkType.Mainnet ? ElementsParams<Liquid>.PeggedAssetId: ElementsParams<Liquid.LiquidRegtest>.PeggedAssetId,
CryptoCode = "LBTC",
NetworkCryptoCode = "LBTC",
DisplayName = "Liquid Bitcoin",
DefaultRateRules = new[]
{
"LBTC_X = LBTC_BTC * BTC_X",
"LBTC_BTC = 1",
},
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
},
});
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitLiquidAssets()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LBTC");
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "USDt",
NetworkCryptoCode = "LBTC",
DefaultRateRules = new[]
{
"USDT_UST = 1",
"USDT_X = USDT_BTC * BTC_X",
"USDT_BTC = bitfinex(UST_BTC)",
},
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Tether USD",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
}
});
}
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
namespace BTCPayServer
{
public class ElementsBTCPayNetwork:BTCPayNetwork
{
public string NetworkCryptoCode { get; set; }
public uint256 AssetId { get; set; }
public override bool ReadonlyWallet { get; set; } = true;
public int Divisibility { get; set; } = 8;
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Where(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint);
});
}
}
}

View File

@ -14,6 +14,11 @@ namespace BTCPayServer
NetworkType == NetworkType.Mainnet
? "https://www.exploremonero.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]
{
"XMR_X = XMR_BTC * BTC_X",
"XMR_BTC = kraken(XMR_BTC)"
},
CryptoImagePath = "/imlegacy/monero.svg"
});
}

View File

@ -1,10 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
using Newtonsoft.Json;
namespace BTCPayServer
@ -46,7 +47,7 @@ namespace BTCPayServer
public class BTCPayNetwork:BTCPayNetworkBase
{
public Network NBitcoinNetwork { get; set; }
public Network NBitcoinNetwork { get { return NBXplorerNetwork?.NBitcoinNetwork; } }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public bool SupportRBF { get; internal set; }
public string LightningImagePath { get; set; }
@ -55,6 +56,9 @@ namespace BTCPayServer
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
public virtual bool WalletSupported { get; set; } = true;
public virtual bool ReadonlyWallet{ get; set; } = false;
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string UriScheme { get; internal set; }
public KeyPath GetRootKeyPath(DerivationType type)
@ -100,6 +104,14 @@ namespace BTCPayServer
{
return NBXplorerNetwork.Serializer.ToString(obj);
}
public virtual IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint);
});
}
}
public abstract class BTCPayNetworkBase

View File

@ -3,8 +3,7 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
<FrameworkReference Include="Microsoft.AspNetCore.App" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.26" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.1" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,3 @@
#if !NETCOREAPP21
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
@ -235,25 +234,3 @@ namespace Microsoft.Extensions.Logging.Abstractions.Internal
}
}
}
#else
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Provides programmatic configuration for JSON formatters using Newtonsoft.JSON.
/// </summary>
public class MvcNewtonsoftJsonOptions
{
IOptions<MvcJsonOptions> jsonOptions;
public MvcNewtonsoftJsonOptions(IOptions<MvcJsonOptions> jsonOptions)
{
this.jsonOptions = jsonOptions;
}
public JsonSerializerSettings SerializerSettings => this.jsonOptions.Value.SerializerSettings;
}
}
#endif

View File

@ -1,19 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="Microsoft.AspNetCore.App" AllowExplicitVersion="true" Version="2.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.19515.63" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp2.1'">
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.0.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.0.0-rc1.final" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.19515.63" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
</ItemGroup>
</Project>

View File

@ -9,10 +9,23 @@ namespace BTCPayServer.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
//public ApplicationDbContext(): base(CreateMySql())
//{
//}
//private static DbContextOptions CreateMySql()
//{
// return new DbContextOptionsBuilder<ApplicationDbContext>()
// .UseMySql("Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;")
// .Options;
//}
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{

View File

@ -45,11 +45,7 @@ namespace BTCPayServer.Data
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
#if NETCOREAPP21
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
#else
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, IMigrationsAnnotationProvider annotations, Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal.INpgsqlOptions opts) : base(dependencies, annotations, opts)
#endif
{
}

View File

@ -12,11 +12,12 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
ConcurrencyStamp = table.Column<string>(nullable: true),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
@ -30,7 +31,7 @@ namespace BTCPayServer.Migrations
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
AccessFailedCount = table.Column<int>(nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
@ -55,7 +56,7 @@ namespace BTCPayServer.Migrations
name: "Stores",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
DerivationStrategy = table.Column<string>(nullable: true),
SpeedPolicy = table.Column<int>(nullable: false),
StoreCertificate = table.Column<byte[]>(nullable: true),
@ -75,7 +76,7 @@ namespace BTCPayServer.Migrations
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
RoleId = table.Column<string>(nullable: false)
RoleId = table.Column<string>(nullable: false, maxLength: maxLength)
},
constraints: table =>
{
@ -96,7 +97,7 @@ namespace BTCPayServer.Migrations
.Annotation("Sqlite:Autoincrement", true),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
UserId = table.Column<string>(nullable: false, maxLength: maxLength)
},
constraints: table =>
{
@ -116,7 +117,7 @@ namespace BTCPayServer.Migrations
LoginProvider = table.Column<string>(nullable: false),
ProviderKey = table.Column<string>(nullable: false),
ProviderDisplayName = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false)
UserId = table.Column<string>(nullable: false, maxLength: maxLength)
},
constraints: table =>
{
@ -133,8 +134,8 @@ namespace BTCPayServer.Migrations
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(nullable: false),
RoleId = table.Column<string>(nullable: false)
UserId = table.Column<string>(nullable: false, maxLength: maxLength),
RoleId = table.Column<string>(nullable: false, maxLength: maxLength)
},
constraints: table =>
{
@ -157,7 +158,7 @@ namespace BTCPayServer.Migrations
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(nullable: false),
UserId = table.Column<string>(nullable: false, maxLength: maxLength),
LoginProvider = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: false),
Value = table.Column<string>(nullable: true)
@ -177,7 +178,7 @@ namespace BTCPayServer.Migrations
name: "Invoices",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Blob = table.Column<byte[]>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
CustomerEmail = table.Column<string>(nullable: true),
@ -185,7 +186,7 @@ namespace BTCPayServer.Migrations
ItemCode = table.Column<string>(nullable: true),
OrderId = table.Column<string>(nullable: true),
Status = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{
@ -202,8 +203,8 @@ namespace BTCPayServer.Migrations
name: "UserStore",
columns: table => new
{
ApplicationUserId = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: false, maxLength: maxLength),
StoreDataId = table.Column<string>(nullable: false, maxLength: maxLength),
Role = table.Column<string>(nullable: true)
},
constraints: table =>
@ -227,9 +228,9 @@ namespace BTCPayServer.Migrations
name: "Payments",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(nullable: true)
InvoiceDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{
@ -246,9 +247,9 @@ namespace BTCPayServer.Migrations
name: "RefundAddresses",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Blob = table.Column<byte[]>(nullable: true),
InvoiceDataId = table.Column<string>(nullable: true)
InvoiceDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -12,11 +12,12 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Value = table.Column<string>(nullable: true)
},
constraints: table =>

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<bool>(
name: "RequiresEmailConfirmation",
table: "AspNetUsers",

View File

@ -12,12 +12,13 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "AddressInvoices",
columns: table => new
{
Address = table.Column<string>(nullable: false),
InvoiceDataId = table.Column<string>(nullable: true)
Address = table.Column<string>(nullable: false, maxLength: this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)512 : null),
InvoiceDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -12,17 +12,18 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "PairedSINData",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
PairingTime = table.Column<DateTimeOffset>(nullable: false),
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{
@ -33,14 +34,14 @@ namespace BTCPayServer.Migrations
name: "PairingCodes",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
DateCreated = table.Column<DateTime>(nullable: false),
Expiration = table.Column<DateTimeOffset>(nullable: false),
Facade = table.Column<string>(nullable: true),
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
SIN = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength),
TokenValue = table.Column<string>(nullable: true)
},
constraints: table =>

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
@ -26,7 +27,7 @@ namespace BTCPayServer.Migrations
name: "PendingInvoices",
columns: table => new
{
Id = table.Column<string>(nullable: false)
Id = table.Column<string>(nullable: false, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<byte[]>(
name: "StoreBlob",
table: "Stores",

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<DateTimeOffset>(
name: "CreatedTime",
table: "AddressInvoices",
@ -21,7 +22,7 @@ namespace BTCPayServer.Migrations
name: "HistoricalAddressInvoices",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
InvoiceDataId = table.Column<string>(nullable: false, maxLength: maxLength),
Address = table.Column<string>(nullable: false),
Assigned = table.Column<DateTimeOffset>(nullable: false),
UnAssigned = table.Column<DateTimeOffset>(nullable: true)

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<bool>(
name: "Accounted",
table: "Payments",

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<string>(
name: "CryptoCode",
table: "HistoricalAddressInvoices",

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<string>(
name: "DerivationStrategies",
table: "Stores",

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<string>(
name: "DefaultCrypto",
table: "Stores",

View File

@ -12,12 +12,13 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "InvoiceEvents",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
UniqueId = table.Column<string>(nullable: false),
InvoiceDataId = table.Column<string>(nullable: false, maxLength: maxLength),
UniqueId = table.Column<string>(nullable: false, maxLength: maxLength),
Message = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},

View File

@ -12,16 +12,17 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "Apps",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
AppType = table.Column<string>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
Name = table.Column<string>(nullable: true),
Settings = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -12,6 +12,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "ApiKeys",
columns: table => new

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropForeignKey(

View File

@ -11,12 +11,13 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "PaymentRequests",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: true),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength),
Status = table.Column<int>(nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<bool>(
name: "TagAllInvoices",
table: "Apps",

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "OpenIddictApplications",
columns: table => new
@ -20,13 +21,13 @@ namespace BTCPayServer.Migrations
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
ConsentType = table.Column<string>(nullable: true),
DisplayName = table.Column<string>(nullable: true),
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Permissions = table.Column<string>(nullable: true),
PostLogoutRedirectUris = table.Column<string>(nullable: true),
Properties = table.Column<string>(nullable: true),
RedirectUris = table.Column<string>(nullable: true),
Type = table.Column<string>(maxLength: 25, nullable: false),
ApplicationUserId = table.Column<string>(nullable: true)
ApplicationUserId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{
@ -46,7 +47,7 @@ namespace BTCPayServer.Migrations
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
Description = table.Column<string>(nullable: true),
DisplayName = table.Column<string>(nullable: true),
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Name = table.Column<string>(maxLength: 200, nullable: false),
Properties = table.Column<string>(nullable: true),
Resources = table.Column<string>(nullable: true)
@ -60,9 +61,9 @@ namespace BTCPayServer.Migrations
name: "OpenIddictAuthorizations",
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true),
ApplicationId = table.Column<string>(nullable: true, maxLength: maxLength),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Properties = table.Column<string>(nullable: true),
Scopes = table.Column<string>(nullable: true),
Status = table.Column<string>(maxLength: 25, nullable: false),
@ -84,12 +85,12 @@ namespace BTCPayServer.Migrations
name: "OpenIddictTokens",
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true),
AuthorizationId = table.Column<string>(nullable: true),
ApplicationId = table.Column<string>(nullable: true, maxLength: maxLength),
AuthorizationId = table.Column<string>(nullable: true, maxLength: maxLength),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
CreationDate = table.Column<DateTimeOffset>(nullable: true),
ExpirationDate = table.Column<DateTimeOffset>(nullable: true),
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Payload = table.Column<string>(nullable: true),
Properties = table.Column<string>(nullable: true),
ReferenceId = table.Column<string>(maxLength: 100, nullable: true),

View File

@ -11,15 +11,16 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "Files",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
FileName = table.Column<string>(nullable: true),
StorageFileName = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTime>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: true)
ApplicationUserId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
@ -22,13 +23,13 @@ namespace BTCPayServer.Migrations
name: "U2FDevices",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Name = table.Column<string>(nullable: true),
KeyHandle = table.Column<byte[]>(nullable: false),
PublicKey = table.Column<byte[]>(nullable: false),
AttestationCert = table.Column<byte[]>(nullable: false),
Counter = table.Column<int>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: true)
ApplicationUserId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>
{

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "PaymentRequests",

View File

@ -11,11 +11,12 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "Wallets",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
@ -27,8 +28,8 @@ namespace BTCPayServer.Migrations
name: "WalletTransactions",
columns: table => new
{
WalletDataId = table.Column<string>(nullable: false),
TransactionId = table.Column<string>(nullable: false),
WalletDataId = table.Column<string>(nullable: false, maxLength: maxLength),
TransactionId = table.Column<string>(nullable: false, maxLength: maxLength),
Labels = table.Column<string>(nullable: true),
Blob = table.Column<byte[]>(nullable: true)
},

View File

@ -0,0 +1,60 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20200110064617_OpenIddictUpdate")]
public partial class OpenIddictUpdate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
migrationBuilder.AddColumn<string>(
name: "Requirements",
table: "OpenIddictApplications",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Requirements",
table: "OpenIddictApplications");
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
}
}
}

View File

@ -20,5 +20,9 @@ namespace BTCPayServer.Migrations
{
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static bool IsMySql(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider == "Pomelo.EntityFrameworkCore.MySql";
}
}
}

View File

@ -0,0 +1,24 @@
namespace BTCPayServer.Rating
{
public enum RateSource
{
Coingecko,
CoinAverage,
Direct
}
public class AvailableRateProvider
{
public string Name { get; }
public string Url { get; }
public string Id { get; }
public RateSource Source { get; }
public AvailableRateProvider(string id, string name, string url, RateSource source)
{
Id = id;
Name = name;
Url = url;
Source = source;
}
}
}

View File

@ -7,11 +7,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
<FrameworkReference Include="Microsoft.AspNetCore.App" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{

View File

@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{

View File

@ -8,16 +8,62 @@ using BTCPayServer.Rating;
using System.Threading;
using Microsoft.Extensions.Logging.Abstractions;
using BTCPayServer.Logging;
using Newtonsoft.Json;
using System.Reflection;
using System.Globalization;
namespace BTCPayServer.Services.Rates
{
public class BackgroundFetcherState
{
public string ExchangeName { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? LastRequested { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? LastUpdated { get; set; }
[JsonProperty(ItemConverterType = typeof(BackgroundFetcherRateJsonConverter))]
public List<BackgroundFetcherRate> Rates { get; set; }
}
public class BackgroundFetcherRate
{
public CurrencyPair Pair { get; set; }
public BidAsk BidAsk { get; set; }
}
//This make the json more compact
class BackgroundFetcherRateJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(BackgroundFetcherRate).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = (string)reader.Value;
var parts = value.Split('|');
return new BackgroundFetcherRate()
{
Pair = CurrencyPair.Parse(parts[0]),
BidAsk = new BidAsk(decimal.Parse(parts[1], CultureInfo.InvariantCulture), decimal.Parse(parts[2], CultureInfo.InvariantCulture))
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var rate = (BackgroundFetcherRate)value;
writer.WriteValue($"{rate.Pair}|{rate.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)}|{rate.BidAsk.Ask.ToString(CultureInfo.InvariantCulture)}");
}
}
/// <summary>
/// This class is a decorator which handle caching and pre-emptive query to the underlying exchange
/// </summary>
public class BackgroundFetcherRateProvider : IRateProvider
{
public class LatestFetch
{
public ExchangeRates Latest;
public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Updated;
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
@ -39,12 +85,57 @@ namespace BTCPayServer.Services.Rates
}
IRateProvider _Inner;
public IRateProvider Inner => _Inner;
public BackgroundFetcherRateProvider(IRateProvider inner)
public BackgroundFetcherRateProvider(string exchangeName, IRateProvider inner)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
if (exchangeName == null)
throw new ArgumentNullException(nameof(exchangeName));
_Inner = inner;
ExchangeName = exchangeName;
}
public BackgroundFetcherState GetState()
{
var state = new BackgroundFetcherState()
{
ExchangeName = ExchangeName,
LastRequested = LastRequested
};
if (_Latest is LatestFetch fetch)
{
state.LastUpdated = fetch.Updated;
state.Rates = fetch.Latest
.Where(e => e.Exchange == ExchangeName)
.Select(r => new BackgroundFetcherRate()
{
Pair = r.CurrencyPair,
BidAsk = r.BidAsk
}).ToList();
}
return state;
}
public void LoadState(BackgroundFetcherState state)
{
if (ExchangeName != state.ExchangeName)
throw new InvalidOperationException("The state does not belong to this fetcher");
if (state.LastRequested is DateTimeOffset lastRequested)
this.LastRequested = state.LastRequested;
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
{
var fetch = new LatestFetch()
{
ExchangeName = state.ExchangeName,
Latest = new ExchangeRates(rates.Select(r => new ExchangeRate(state.ExchangeName, r.Pair, r.BidAsk))),
Updated = updated,
NextRefresh = updated + RefreshRate,
Expiration = updated + ValidatyTime
};
_Latest = fetch;
}
}
TimeSpan _RefreshRate = TimeSpan.FromSeconds(30);
@ -112,33 +203,46 @@ namespace BTCPayServer.Services.Rates
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
LastRequested = DateTimeOffset.UtcNow;
var latest = _Latest;
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
{
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
latest = null;
}
return (latest ?? (await Fetch(cancellationToken))).GetResult();
}
private string GetExchangeName()
/// <summary>
/// The last time this rate provider has been used
/// </summary>
public DateTimeOffset? LastRequested { get; set; }
public string ExchangeName { get; }
public DateTimeOffset? Expiration
{
if (_Inner is IHasExchangeName exchangeName)
return exchangeName.ExchangeName ?? "???";
return "???";
get
{
if (_Latest is LatestFetch f)
{
return f.Expiration;
}
return null;
}
}
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var previous = _Latest;
var fetch = new LatestFetch();
fetch.ExchangeName = GetExchangeName();
fetch.ExchangeName = ExchangeName;
try
{
var rates = await _Inner.GetRatesAsync(cancellationToken);
fetch.Latest = rates;
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;
fetch.Updated = DateTimeOffset.UtcNow;
fetch.Expiration = fetch.Updated + ValidatyTime;
fetch.NextRefresh = fetch.Updated + RefreshRate;
}
catch (Exception ex)
{

View File

@ -1,13 +1,9 @@
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
@ -23,19 +19,6 @@ namespace BTCPayServer.Services.Rates
}
}
public class GetExchangeTickersResponse
{
public class Exchange
{
public string Name { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
public string[] Symbols { get; set; }
}
public bool Success { get; set; }
public Exchange[] Exchanges { get; set; }
}
public class RatesSetting
{
public string PublicKey { get; set; }
@ -200,32 +183,6 @@ namespace BTCPayServer.Services.Rates
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
return response;
}
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();
response.Success = jobj["success"].Value<bool>();
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;
return exchange;
})
.ToArray();
return response;
}
}
public class GetRateLimitsResponse

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
@ -20,125 +18,12 @@ namespace BTCPayServer.Services.Rates
return _Settings.AddHeader(message);
}
}
public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display, string url)
{
Name = name;
Display = display;
Url = url;
}
public string Name { get; set; }
public string Display { get; set; }
public string Url
{
get;
set;
}
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
{
public CoinAverageExchanges()
{
}
public void Add(CoinAverageExchange exchange)
{
if (!TryAdd(exchange.Name, exchange))
{
this.Remove(exchange.Name);
this.Add(exchange.Name, exchange);
}
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
{
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges();
public CoinAverageSettings()
{
//GENERATED BY:
//StringBuilder b = new StringBuilder();
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
//{
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new CoinAverageExchanges();
foreach (var item in
new[] {
(DisplayName: "Idex", Name: "idex"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "IDAX", Name: "idax"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "CEX.IO", Name: "cex"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "Stex", Name: "stex"),
(DisplayName: "CoinTiger", Name: "cointiger"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "Tokenomy", Name: "tokenomy"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Kryptono", Name: "kryptono"),
(DisplayName: "Bitso", Name: "bitso"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "Yobit", Name: "yobit"),
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Livecoin", Name: "livecoin"),
(DisplayName: "Hotbit", Name: "hotbit"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Coinbase Pro", Name: "coinbasepro"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Tokenize", Name: "tokenize"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Upbit", Name: "upbit"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Trade.io", Name: "tradeio"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Coinsquare", Name: "coinsquare"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Coinegg", Name: "coinegg"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "OasisDEX", Name: "oasisdex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "BitForex", Name: "bitforex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "itBit", Name: "itbit"),
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
}
// Keep back-compat
AvailableExchanges.Add(new CoinAverageExchange("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/coinbasepro"));
}
public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

View File

@ -9,6 +9,7 @@ using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
namespace BTCPayServer.Services.Rates
@ -81,7 +82,7 @@ namespace BTCPayServer.Services.Rates
provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
{
c.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
@ -97,8 +98,22 @@ namespace BTCPayServer.Services.Rates
return _DirectProviders;
}
}
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
{
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr", RateSource.Direct);
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries", RateSource.Direct);
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker", RateSource.Direct);
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker", RateSource.Direct);
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker", RateSource.Direct);
private void InitExchanges()
yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates", RateSource.Direct);
yield return new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", "https://apiv2.bitcoinaverage.com/indices/global/ticker/short", RateSource.Direct);
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD", RateSource.Direct);
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", RateSource.Direct);
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices", RateSource.Direct);
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates", RateSource.Direct);
}
void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
@ -107,11 +122,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true));
Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true));
// Cryptopia is often not available
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory));
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
@ -128,8 +140,8 @@ namespace BTCPayServer.Services.Rates
{
if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs
continue;
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
if(provider.Key == CoinAverageRateProvider.CoinAverageName)
var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]);
if (provider.Key == CoinGeckoRateProvider.CoinGeckoName)
{
prov.RefreshRate = CacheSpan;
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
@ -143,40 +155,146 @@ namespace BTCPayServer.Services.Rates
}
var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetSupportedExchanges())
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Key))
if (!Providers.ContainsKey(supportedExchange.Id))
{
var coinAverage = new CoinAverageRateProvider()
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Key,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
Exchange = supportedExchange.Id
};
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache)
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Key, cached);
Providers.Add(supportedExchange.Id, cached);
}
}
foreach (var supportedExchange in GetCoinAverageSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Id))
{
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Id
};
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Id, cached);
}
}
}
public CoinAverageExchanges GetSupportedExchanges()
IEnumerable<AvailableRateProvider> _AvailableRateProviders = null;
public IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
if (_AvailableRateProviders == null)
{
exchanges.Add(exchange.Value);
var availableProviders = new Dictionary<string, AvailableRateProvider>();
foreach (var exchange in GetDirectlySupportedExchanges())
{
availableProviders.Add(exchange.Id, exchange);
}
foreach (var exchange in GetCoinGeckoSupportedExchanges())
{
availableProviders.TryAdd(exchange.Id, exchange);
}
foreach (var exchange in GetCoinAverageSupportedExchanges())
{
availableProviders.TryAdd(exchange.Id, exchange);
}
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
}
return _AvailableRateProviders;
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
exchanges.Add(new CoinAverageExchange("ndax", "NDAX", "https://ndax.io/api/returnTicker"));
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
internal IEnumerable<AvailableRateProvider> GetCoinAverageSupportedExchanges()
{
foreach (var item in
new[] {
(DisplayName: "Idex", Name: "idex"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "IDAX", Name: "idax"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "CEX.IO", Name: "cex"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "Stex", Name: "stex"),
(DisplayName: "CoinTiger", Name: "cointiger"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "Tokenomy", Name: "tokenomy"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Kryptono", Name: "kryptono"),
(DisplayName: "Bitso", Name: "bitso"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "Yobit", Name: "yobit"),
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Livecoin", Name: "livecoin"),
(DisplayName: "Hotbit", Name: "hotbit"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Coinbase Pro", Name: "coinbasepro"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Tokenize", Name: "tokenize"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Upbit", Name: "upbit"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Trade.io", Name: "tradeio"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Coinsquare", Name: "coinsquare"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Coinegg", Name: "coinegg"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "OasisDEX", Name: "oasisdex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "BitForex", Name: "bitforex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "itBit", Name: "itbit"),
})
{
yield return new AvailableRateProvider(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}", RateSource.CoinAverage);
}
yield return new AvailableRateProvider("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/gdax", RateSource.CoinAverage);
}
return exchanges;
internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
{
return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["name"].ToString(),
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko))
.Concat(new[] { new AvailableRateProvider("gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
}
private string Normalize(string name)
{
if (name == "oasis_trade")
return "oasisdex";
if (name == "gdax")
return "coinbasepro";
return name;
}
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)

View File

@ -6,8 +6,6 @@ using System.Net;
using System.Threading.Tasks;
using System.Security.Claims;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Xunit;
using Xunit.Abstractions;
using System.Net.Http;
@ -66,8 +64,7 @@ namespace BTCPayServer.Tests
{
var json = await streamToReadFrom.ReadToEndAsync();
Assert.NotNull(json);
var configuration = OpenIdConnectConfiguration.Create(json);
Assert.NotNull(configuration);
JObject.Parse(json); // Should do more tests but good enough
}
}
}
@ -249,7 +246,7 @@ namespace BTCPayServer.Tests
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIddictResponse>();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
await TestApiAgainstAccessToken(result.AccessToken, tester, user);
@ -289,7 +286,7 @@ namespace BTCPayServer.Tests
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIddictResponse>();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
@ -330,7 +327,7 @@ namespace BTCPayServer.Tests
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIddictResponse>();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
@ -371,7 +368,7 @@ namespace BTCPayServer.Tests
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIddictResponse>();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;

View File

@ -1,24 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
<LangVersion>8.0</LangVersion>
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<DefineConstants>$(DefineConstants);NETCOREAPP21</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="78.0.3904.7000" />
<PackageReference Include="xunit" Version="2.4.1" />

View File

@ -63,6 +63,7 @@ namespace BTCPayServer.Tests
}
public Uri LTCNBXplorerUri { get; set; }
public Uri LBTCNBXplorerUri { get; set; }
public Uri ServerUri
{
@ -93,6 +94,9 @@ namespace BTCPayServer.Tests
public bool MockRates { get; set; } = true;
public HashSet<string> Chains { get; set; } = new HashSet<string>(){"BTC"};
public bool UseLightning { get; set; }
public async Task StartAsync()
{
if (!Directory.Exists(_Directory))
@ -109,20 +113,37 @@ namespace BTCPayServer.Tests
config.AppendLine($"bind=0.0.0.0");
}
config.AppendLine($"port={Port}");
config.AppendLine($"chains=btc,ltc");
config.AppendLine($"chains={string.Join(',', Chains)}");
if (Chains.Contains("BTC", StringComparer.OrdinalIgnoreCase))
{
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"btc.explorer.cookiefile=0");
}
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"btc.explorer.cookiefile=0");
if (UseLightning)
{
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json");
File.Copy(TestUtils.GetTestDataFullPath("LndSeedBackup/walletunlock.json"), localLndBackupFile, true);
config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}");
}
if (Chains.Contains("LTC", StringComparer.OrdinalIgnoreCase))
{
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
}
if (Chains.Contains("LBTC", StringComparer.OrdinalIgnoreCase))
{
config.AppendLine($"lbtc.explorer.url={LBTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"lbtc.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}");
config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}");
config.AppendLine($"debuglog=debug.log");
var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json");
File.Copy(TestUtils.GetTestDataFullPath("LndSeedBackup/walletunlock.json"), localLndBackupFile, true);
config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}");
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
config.AppendLine($"sshpassword={SSHPassword}");
if (!string.IsNullOrEmpty(SSHKeyFile))
@ -181,29 +202,29 @@ namespace BTCPayServer.Tests
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
BidAsk = new BidAsk(162m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
rateProvider.Providers.Add("coingecko", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
@ -231,6 +252,16 @@ namespace BTCPayServer.Tests
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
var bitfinex = new MockRateProvider();
bitfinex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitfinex",
CurrencyPair = CurrencyPair.Parse("UST_BTC"),
BidAsk = new BidAsk(0.000136m)
});
rateProvider.Providers.Add("bitfinex", bitfinex);
}

View File

@ -106,10 +106,14 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanUsePaymentMethodDropdown()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLTC();
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
@ -141,6 +145,7 @@ namespace BTCPayServer.Tests
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
elements.Single(element => element.Text.Contains("Lightning")).Click();
Thread.Sleep(1000);
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("Lightning", currencyDropdownButton.Text);
@ -150,10 +155,12 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningSatsFeature()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();

View File

@ -1,8 +1,7 @@
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /source
COPY nuget.config nuget.config
@ -11,20 +10,17 @@ 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.Data/BTCPayServer.Data.csproj BTCPayServer.Data/BTCPayServer.Data.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
RUN cd BTCPayServer && dotnet restore
COPY BTCPayServer.Common/. BTCPayServer.Common/.
COPY BTCPayServer.Rating/. BTCPayServer.Rating/.
COPY BTCPayServer.Data/. BTCPayServer.Data/.
COPY BTCPayServer/. BTCPayServer/.
COPY Build/Version.csproj Build/Version.csproj
ENV SCREEN_HEIGHT 600 \
SCREEN_WIDTH 1200
COPY . .
RUN cd BTCPayServer.Tests && dotnet build /p:CI_TESTS=true
RUN cd BTCPayServer.Tests && dotnet build /p:CI_TESTS=true /p:RazorCompileOnBuild=true
WORKDIR /source/BTCPayServer.Tests
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using NBitcoin;
using NBitcoin.RPC;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class ElementsTests
{
public const int TestTimeout = 60_000;
public ElementsTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Altcoins", "Altcoins")]
public async Task OnlyShowSupportedWallets()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLBTC();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("USDT");
Assert.Single(Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets);
}
}
[Fact]
[Trait("Fast", "Fast")]
public void LoadSubChainsAlways()
{
var options = new BTCPayServerOptions();
options.LoadArgs(new ConfigurationRoot(new List<IConfigurationProvider>()
{
new MemoryConfigurationProvider(new MemoryConfigurationSource()
{
InitialData = new[]
{
new KeyValuePair<string, string>("chains", "usdt"),
}
})
}));
Assert.NotNull(options.NetworkProvider.GetNetwork("LBTC"));
Assert.NotNull(options.NetworkProvider.GetNetwork("USDT"));
}
[Fact]
[Trait("Altcoins", "Altcoins")]
public async Task ElementsAssetsAreHandledCorrectly()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLBTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
//no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
.AssetId = tether.AssetId;
Logs.Tester.LogInformation($"Asset is {tether.AssetId}");
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
var ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC"));
//1 lbtc = 1 btc
Assert.Equal(1, ci.Rate);
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address,ci.Due, "", "", false, true,
1, "UNSET", lbtc.AssetId);
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC")).Payments);
});
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
1, "UNSET", tether.AssetId);
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
});
}
}
}
}

View File

@ -11,6 +11,11 @@ namespace BTCPayServer.Tests.Mocks
public class MockRateProvider : IRateProvider
{
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public MockRateProvider()
{
}
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(ExchangeRates);

View File

@ -13,6 +13,7 @@ using BTCPayServer.Rating;
namespace BTCPayServer.Tests
{
[Trait("Fast", "Fast")]
public class PaymentHandlerTest
{
private BitcoinLikePaymentHandler handlerBTC;

View File

@ -39,18 +39,18 @@ namespace BTCPayServer.Tests
};
}
public async Task StartAsync()
{
await Server.StartAsync();
ChromeOptions options = new ChromeOptions();
var isDebug = !Server.PayTester.InContainer;
if (!isDebug)
{
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x1000"); // Comment to view browser
}
}
options.AddArgument("shm-size=2g");
if (Server.PayTester.InContainer)
{
@ -72,7 +72,16 @@ namespace BTCPayServer.Tests
internal void AssertHappyMessage()
{
Assert.Single(Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed));
using var cts = new CancellationTokenSource(10_000);
while (!cts.IsCancellationRequested)
{
var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any();
if (success)
return;
Thread.Sleep(100);
}
Logs.Tester.LogInformation(this.Driver.PageSource);
Assert.True(false, "Should have shown happy message");
}
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
@ -105,10 +114,29 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("CreateStore")).Click();
Driver.FindElement(By.Id("Name")).SendKeys(usr);
Driver.FindElement(By.Id("Create")).Click();
return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value"));
}
public string GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.FindElement(By.Id("btn-generate")).ForceClick();
AssertHappyMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text;
}
Driver.FindElement(By.Id("Confirm")).ForceClick();
AssertHappyMessage();
return seed;
}
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
@ -117,7 +145,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Confirm")).ForceClick();
AssertHappyMessage();
}
public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
string connectionString = null;
@ -129,12 +157,12 @@ namespace BTCPayServer.Tests
connectionString = $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id($"save")).ForceClick();
}
public void AddInternalLightningNode(string cryptoCode)
{
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
@ -155,7 +183,7 @@ namespace BTCPayServer.Tests
}
}
public void Dispose()
{
@ -204,21 +232,26 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id(storeNavPage.ToString())).Click();
}
}
public void GoToInvoiceCheckout(string invoiceId)
{
Driver.FindElement(By.Id("Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors();
}
public void SetCheckbox(IWebElement element, bool value)
{
if ((value && !element.Selected) || (!value && element.Selected))
{
element.Click();
}
if (value != element.Selected)
{
SetCheckbox(element, value);
}
}
public void SetCheckbox(SeleniumTester s, string inputName, bool value)
@ -263,30 +296,30 @@ namespace BTCPayServer.Tests
}
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// };
//
// var jsErrors = Driver.Manage().Logs.GetLog(LogType.Browser).Where(x => errorStrings.Any(e => x.Message.Contains(e)));
//
// if (jsErrors.Any())
// {
// Logs.Tester.LogInformation("JavaScript error(s):" + Environment.NewLine + jsErrors.Aggregate("", (s, entry) => s + entry.Message + Environment.NewLine));
// }
// Assert.Empty(jsErrors);
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// };
//
// var jsErrors = Driver.Manage().Logs.GetLog(LogType.Browser).Where(x => errorStrings.Any(e => x.Message.Contains(e)));
//
// if (jsErrors.Any())
// {
// Logs.Tester.LogInformation("JavaScript error(s):" + Environment.NewLine + jsErrors.Aggregate("", (s, entry) => s + entry.Message + Environment.NewLine));
// }
// Assert.Empty(jsErrors);
}
}
}

View File

@ -34,6 +34,27 @@ namespace BTCPayServer.Tests
s.ClickOnAllSideMenus();
s.Driver.FindElement(By.LinkText("Services")).Click();
Logs.Tester.LogInformation("Let's check if we can access the logs");
s.Driver.FindElement(By.LinkText("Logs")).Click();
s.Driver.FindElement(By.PartialLinkText(".log")).Click();
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
s.Driver.Quit();
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLndSeedBackup()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLightning();
await s.StartAsync();
s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click();
s.Driver.AssertNoError();
s.Driver.FindElement(By.LinkText("Services")).Click();
Logs.Tester.LogInformation("Let's if we can access LND's seed");
Assert.Contains("server/services/lndseedbackup/BTC", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/lndseedbackup/BTC"));
@ -49,12 +70,6 @@ namespace BTCPayServer.Tests
s.AssertHappyMessage();
seedEl = s.Driver.FindElement(By.Id("SeedTextArea"));
Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase);
Logs.Tester.LogInformation("Let's check if we can access the logs");
s.Driver.FindElement(By.LinkText("Logs")).Click();
s.Driver.FindElement(By.PartialLinkText(".log")).Click();
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
s.Driver.Quit();
}
}
@ -408,14 +423,28 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
var storeId = 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 mnemonic = s.GenerateWallet("BTC", "", true, false);
var invoiceId = s.CreateInvoice(storeId.storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId);
mnemonic = s.GenerateWallet("BTC", "", true, true);
var root = new Mnemonic(mnemonic).DeriveExtKey();
s.AddDerivationScheme("BTC", "ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
invoiceId = s.CreateInvoice(storeId.storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
@ -429,8 +458,8 @@ namespace BTCPayServer.Tests
// 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);
// 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();
@ -471,8 +500,6 @@ namespace BTCPayServer.Tests
}
}
SignWith(mnemonic);
var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
SignWith(accountKey);
}
}
}

View File

@ -46,27 +46,15 @@ namespace BTCPayServer.Tests
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
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<BTCPayNetwork>("LTC").NBitcoinNetwork);
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<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);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
@ -77,6 +65,32 @@ namespace BTCPayServer.Tests
PayTester.SSHConnection = GetEnvironment("TESTS_SSHCONNECTION", "root@127.0.0.1:21622");
}
public void ActivateLTC()
{
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBitcoinNetwork);
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester.Chains.Add("LTC");
PayTester.LTCNBXplorerUri = LTCExplorerClient.Address;
}
public void ActivateLBTC()
{
LBTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LBTCRPCCONNECTION", "server=http://127.0.0.1:19332;liquid:liquid")), NetworkProvider.GetNetwork<BTCPayNetwork>("LBTC").NBitcoinNetwork);
LBTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork<BTCPayNetwork>("LBTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LBTCNBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester.Chains.Add("LBTC");
PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address;
}
public void ActivateLightning()
{
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);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
PayTester.UseLightning = true;
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
}
public bool Dockerized
{
get; set;
@ -148,12 +162,15 @@ namespace BTCPayServer.Tests
{
get; set;
}
public RPCClient LBTCExplorerNode { get; set; }
public ExplorerClient ExplorerClient
{
get; set;
}
public ExplorerClient LTCExplorerClient { get; set; }
public ExplorerClient LBTCExplorerClient { get; set; }
HttpClient _Http = new HttpClient();

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
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]"));
DerivationScheme = SupportedNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationScheme = DerivationScheme.ToString(),

View File

@ -5,9 +5,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
#if NETCOREAPP21
using Microsoft.AspNetCore.Http.Internal;
#endif
using Xunit.Sdk;
using System.Linq;

View File

@ -0,0 +1,130 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Mvc;
using U2F.Core.Models;
using U2F.Core.Utils;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class U2FTests
{
public const int TestTimeout = 60_000;
public U2FTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task U2ftest()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var accountController = tester.PayTester.GetController<AccountController>();
var manageController = user.GetController<ManageController>();
var mock = new MockU2FService(tester.PayTester.GetService<ApplicationDbContextFactory>());
manageController._u2FService = mock;
accountController._u2FService = mock;
Assert
.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email, Password = user.RegisterDetails.Password
}));
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addDeviceVM = Assert.IsType<AddU2FDeviceViewModel>(Assert
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
Assert.NotEmpty(addDeviceVM.Challenge);
Assert.Equal(addDeviceVM.Name, "testdevice");
Assert.NotEmpty(addDeviceVM.Version);
Assert.Null(addDeviceVM.DeviceResponse);
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray(),
Guid.NewGuid().ToByteArray(), 1);
mock.GetDevReg = () => devReg;
mock.StartedAuthentication = () =>
new StartedAuthentication("chocolate", addDeviceVM.AppId,
devReg.KeyHandle.ByteArrayToBase64String());
addDeviceVM.DeviceResponse = new RegisterResponse("ss",
Convert.ToBase64String(Encoding.UTF8.GetBytes("{typ:'x', challenge: 'fff'}"))).ToJson();
Assert
.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addDeviceVM));
Assert.Single(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var secondaryLoginViewModel = Assert.IsType<SecondaryLoginViewModel>(Assert
.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email, Password = user.RegisterDetails.Password
})).Model);
Assert.NotNull(secondaryLoginViewModel.LoginWithU2FViewModel);
Assert.Single(secondaryLoginViewModel.LoginWithU2FViewModel.Challenges);
Assert.Equal(secondaryLoginViewModel.LoginWithU2FViewModel.Challenge,
secondaryLoginViewModel.LoginWithU2FViewModel.Challenges.First().challenge);
secondaryLoginViewModel.LoginWithU2FViewModel.DeviceResponse = new AuthenticateResponse(
Convert.ToBase64String(Encoding.UTF8.GetBytes(
"{typ:'x', challenge: '" + secondaryLoginViewModel.LoginWithU2FViewModel.Challenge + "'}")),
"dd", devReg.KeyHandle.ByteArrayToBase64String()).ToJson();
Assert
.IsType<RedirectToActionResult>(
await accountController.LoginWithU2F(secondaryLoginViewModel.LoginWithU2FViewModel));
}
}
public class MockU2FService : U2FService
{
public Func<DeviceRegistration> GetDevReg;
public Func<StartedAuthentication> StartedAuthentication;
public MockU2FService(ApplicationDbContextFactory contextFactory) : base(contextFactory)
{
}
protected override StartedRegistration StartDeviceRegistrationCore(string appId)
{
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
}
protected override DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration,
RegisterResponse registerResponse)
{
return GetDevReg();
}
protected override StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
{
return StartedAuthentication();
}
protected override void FinishAuthenticationCore(StartedAuthentication authentication,
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
{
}
}
}
}

View File

@ -151,6 +151,7 @@ namespace BTCPayServer.Tests
new LightningLikePaymentHandler(null, null, networkProvider, null),
});
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Networks = networkProvider;
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.ProductInformation = new ProductInformation() {Price = 100};
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
@ -172,12 +173,15 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
CryptoCode = "BTC",
NetworkFee = 0.00000100m
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"),
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() {Value = Money.Coins(0.00151263m)}
}));
accounting = btc.Calculate();
@ -186,10 +190,12 @@ namespace BTCPayServer.Tests
{
Accounted = true,
CryptoCode = "BTC",
NetworkFee = 0.00000100m
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC")
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() {Value = accounting.Due}
}));
accounting = btc.Calculate();
@ -258,6 +264,7 @@ namespace BTCPayServer.Tests
new LightningLikePaymentHandler(null, null, networkProvider, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
@ -317,6 +324,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.Networks = networkProvider;
entity.ProductInformation = new ProductInformation() {Price = 5000};
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(
@ -445,6 +453,7 @@ namespace BTCPayServer.Tests
new LightningLikePaymentHandler(null, null, networkProvider, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
@ -551,10 +560,12 @@ namespace BTCPayServer.Tests
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSetLightningServer()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
@ -590,6 +601,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentCLightning()
{
await ProcessLightningPayment(LightningConnectionType.CLightning);
@ -597,6 +609,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentCharge()
{
await ProcessLightningPayment(LightningConnectionType.Charge);
@ -604,6 +617,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentLnd()
{
await ProcessLightningPayment(LightningConnectionType.LndREST);
@ -616,6 +630,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
@ -649,7 +664,8 @@ namespace BTCPayServer.Tests
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
// C-Lightning may overpay for privacy
Assert.Contains(localInvoice.ExceptionStatus.ToString(), new[] { "False", "paidOver" });
});
}
@ -797,21 +813,14 @@ namespace BTCPayServer.Tests
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC", true);
var btcDerivationScheme = acc.DerivationScheme;
acc.RegisterDerivationScheme("LTC", true);
var walletController = acc.GetController<WalletsController>();
WalletId walletId = new WalletId(acc.StoreId, "LTC");
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.False(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.False(rescan.IsSupportedByCurrency);
Assert.False(rescan.IsServerAdmin);
walletId = new WalletId(acc.StoreId, "BTC");
var walletId = new WalletId(acc.StoreId, "BTC");
acc.IsAdmin = true;
walletController = acc.GetController<WalletsController>();
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.True(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSupportedByCurrency);
@ -938,15 +947,14 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
acc.RegisterDerivationScheme("LTC");
var rateController = acc.GetController<RateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetBaseCurrencyRatesResult);
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
var rate = Assert.Single(GetBaseCurrencyRatesResult.Data);
Assert.Equal("BTC", rate.Code);
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
@ -956,7 +964,7 @@ namespace BTCPayServer.Tests
var store = acc.GetController<StoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates()).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait();
await store.Rates(ratesVM);
store = acc.GetController<StoresController>();
rateController = acc.GetController<RateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default)
@ -1180,13 +1188,13 @@ namespace BTCPayServer.Tests
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.NotEqual(apiKey, apiKey2);
////////
@ -1232,9 +1240,9 @@ namespace BTCPayServer.Tests
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
rates.Add(await CreateInvoice(tester, user, "coingecko"));
var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
@ -1245,13 +1253,13 @@ namespace BTCPayServer.Tests
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
private static async Task<decimal> CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
await storeController.Rates(vm);
var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 5000.0m,
Currency = currency,
@ -1332,7 +1340,7 @@ namespace BTCPayServer.Tests
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
await storeController.Rates(vm);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -1353,10 +1361,12 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
public async Task CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
@ -1430,46 +1440,46 @@ namespace BTCPayServer.Tests
var store = user.GetController<StoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, rateVm.PreferredExchange);
Assert.Equal(0.0, rateVm.Spread);
Assert.Null(rateVm.TestRateRules);
rateVm.PreferredExchange = "bitflyer";
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
rateVm.Spread = 10;
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test")).Model);
Assert.NotNull(rateVm.TestRateRules);
Assert.Equal(2, rateVm.TestRateRules.Count);
Assert.False(rateVm.TestRateRules[0].Error);
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.True(rateVm.TestRateRules[1].Error);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(rateVm.StoreId, user.StoreId);
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting);
rateVm.ScriptTest = "BTC_JPY";
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test")).Model);
Assert.True(rateVm.ShowScripting);
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = coinaverage(X_X);";
"X_X = coingecko(X_X);";
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test")).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(50, rateVm.Spread);
@ -1480,10 +1490,12 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
public async Task CanPayWithTwoCurrencies()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
@ -1557,7 +1569,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
@ -1697,10 +1709,14 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanAddDerivationSchemes()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLTC();
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
@ -1729,15 +1745,15 @@ namespace BTCPayServer.Tests
Assert.False(lightningVM.Enabled);
// Only Enabling/Disabling the payment method must redirect to store page
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
// Clicking next without changing anything should send to the confirmation screen
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
@ -1756,18 +1772,18 @@ namespace BTCPayServer.Tests
// Removing the derivation scheme, should redirect to store page
var oldScheme = derivationVM.DerivationScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = null;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
// Setting it again should redirect to the confirmation page
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = oldScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
@ -1775,7 +1791,7 @@ namespace BTCPayServer.Tests
// And with a good file? (upub)
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
@ -1835,21 +1851,50 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSetPaymentMethodLimitsLightning()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
vm.LightningMaxValue = "2 USD";
vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice()
@ -1864,19 +1909,6 @@ namespace BTCPayServer.Tests
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
}
}
@ -2648,16 +2680,19 @@ noninventoryitem:
public void CanQueryDirectProviders()
{
var factory = CreateBTCPayRateFactory();
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down
if (result.ExpectedName == "coinaverage")
continue; // no more free plan
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
@ -2685,6 +2720,47 @@ noninventoryitem:
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportBackgroundFetcherState()
{
var factory = CreateBTCPayRateFactory();
var provider = (BackgroundFetcherRateProvider)factory.Providers["kraken"];
await provider.GetRatesAsync(default);
var state = provider.GetState();
Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR"));
var provider2 = new BackgroundFetcherRateProvider("kraken", provider.Inner)
{
RefreshRate = provider.RefreshRate,
ValidatyTime = provider.ValidatyTime
};
using (var cts = new CancellationTokenSource())
{
cts.Cancel();
// Should throw
await Assert.ThrowsAsync<OperationCanceledException>(async () => await provider2.GetRatesAsync(cts.Token));
}
provider2.LoadState(state);
Assert.Equal(provider.LastRequested, provider2.LastRequested);
using (var cts = new CancellationTokenSource())
{
cts.Cancel();
// Should not throw, as things should be cached
await provider2.GetRatesAsync(cts.Token);
}
Assert.Equal(provider.ExchangeName, provider2.ExchangeName);
Assert.Equal(provider.NextUpdate, provider2.NextUpdate);
Assert.NotEqual(provider.LastRequested, provider2.LastRequested);
Assert.Equal(provider.Expiration, provider2.Expiration);
var str = JsonConvert.SerializeObject(state);
var state2 = JsonConvert.DeserializeObject<BackgroundFetcherState>(str);
var str2 = JsonConvert.SerializeObject(state2);
Assert.Equal(str, str2);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public void CanGetRateCryptoCurrenciesByDefault()
@ -2709,7 +2785,7 @@ noninventoryitem:
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
@ -2850,7 +2926,7 @@ noninventoryitem:
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
var fetch = new BackgroundFetcherRateProvider("spy", spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
@ -2902,10 +2978,14 @@ noninventoryitem:
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateInvoiceWithSpecificPaymentMethods()
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanCreateInvoiceWithSpecificPaymentMethods()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
tester.ActivateLTC();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();

View File

@ -76,7 +76,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.66
image: nicolasdorier/nbxplorer:2.1.5
restart: unless-stopped
ports:
- "32838:32838"
@ -84,7 +84,7 @@ services:
- "32838"
environment:
NBXPLORER_NETWORK: regtest
NBXPLORER_CHAINS: "btc,ltc"
NBXPLORER_CHAINS: "btc,ltc,lbtc"
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
@ -93,12 +93,17 @@ services:
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_LBTCRPCURL: "http://elementsd-liquid:19332/"
NBXPLORER_LBTCNODEENDPOINT: "elementsd-liquid:19444"
NBXPLORER_LBTCRPCUSER: "liquid"
NBXPLORER_LBTCRPCPASSWORD: "liquid"
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- bitcoind
- litecoind
- elementsd-liquid
bitcoind:
@ -127,7 +132,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.7.3-dev
image: btcpayserver/lightning:v0.8.0-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -174,7 +179,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.7.3-dev
image: btcpayserver/lightning:v0.8.0-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -215,6 +220,34 @@ services:
expose:
- "43782" # RPC
- "39388" # P2P
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.18.1.1-1
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |
mainchainrpcport=43782
mainchainrpchost=bitcoind
mainchainrpcuser=liquid
mainchainrpcpassword=liquid
rpcport=19332
rpcbind=0.0.0.0:19332
rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6
port=19444
whitelist=0.0.0.0/0
validatepegin=0
initialfreecoins=210000000000000
con_dyna_deploy_start=99999999999
expose:
- "19332"
- "19444"
ports:
- "19332:19332"
- "19444:19444"
volumes:
- "elementsd_liquid_datadir:/data"
postgres:
image: postgres:9.6.5
@ -287,6 +320,7 @@ services:
volumes:
sshd_datadir:
bitcoin_datadir:
elementsd_liquid_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:

View File

@ -2,8 +2,8 @@
set -e
FILTERS=" "
if [[ "$TEST_FILTERS" ]]; then
if [ ! -z "$TEST_FILTERS" ]; then
FILTERS="--filter $TEST_FILTERS"
fi
dotnet test $FILTERS --no-build -v n
dotnet test $FILTERS --no-build -v n < /dev/null

View File

@ -1,12 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<RazorCompileOnBuild>false</RazorCompileOnBuild>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\**" />
<Compile Remove="Storage\Services\Providers\GoogleCloudStorage\**" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\**" />
@ -27,52 +29,48 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.5" />
<PackageReference Include="BuildBundlerMinifier" Version="3.1.430" />
<PackageReference Include="BundlerMinifier.Core" Version="3.1.430" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.1.430" />
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.8" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="LedgerWallet" Version="2.0.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="NBitpayClient" Version="1.0.0.35" />
<PackageReference Include="DBriize" Version="1.0.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<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.9" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PackageReference Include="Text.Analyzers" Version="2.6.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" AllowExplicitVersion="true" Condition="'$(TargetFramework)' == 'netcoreapp2.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" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.19515.63" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.19515.63"></PackageReference>
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.19515.63"></PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp2.1'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.0.0"></PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
@ -201,13 +199,16 @@
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendVault.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Remove="Views\Server\EditGoogleCloudStorageStorageProvider.cshtml" Condition="'$(TargetFramework)' != 'netcoreapp2.1'">
<Content Remove="Views\Server\EditGoogleCloudStorageStorageProvider.cshtml">
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>

View File

@ -85,9 +85,17 @@ namespace BTCPayServer.Configuration
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());
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant()).ToHashSet();
var networkProvider = new BTCPayNetworkProvider(NetworkType);
var filtered = networkProvider.Filter(supportedChains.ToArray());
var elementsBased = filtered.GetAll().OfType<ElementsBTCPayNetwork>();
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
var allSubChains = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
.Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant());
supportedChains.AddRange(allSubChains);
NetworkProvider = networkProvider.Filter(supportedChains.ToArray());
foreach (var chain in supportedChains)
{
if (NetworkProvider.GetNetwork<BTCPayNetworkBase>(chain) == null)

View File

@ -23,6 +23,7 @@ using BTCPayServer.U2F.Models;
using Newtonsoft.Json;
using NicolasDorier.RateLimits;
using BTCPayServer.Data;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
{
@ -38,7 +39,7 @@ namespace BTCPayServer.Controllers
SettingsRepository _SettingsRepository;
Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly U2FService _u2FService;
public U2FService _u2FService;
ILogger _logger;
public AccountController(
@ -235,9 +236,8 @@ namespace BTCPayServer.Controllers
errorMessage = "Invalid login attempt.";
}
catch (Exception e)
catch (U2fException e)
{
errorMessage = e.Message;
}

View File

@ -22,7 +22,6 @@ using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.Server;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using OpenIddict.Server.AspNetCore;
namespace BTCPayServer.Controllers
@ -126,7 +125,7 @@ namespace BTCPayServer.Controllers
{
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
var authorization = await _authorizationManager.CreateAsync(User, user.Id, application.Id,
type, principal.GetScopes().ToImmutableArray());
type, principal.GetScopes());
principal.SetInternalAuthorizationId(authorization.Id);
}

View File

@ -19,10 +19,10 @@ namespace BTCPayServer.Controllers
private InvoiceRepository _InvoiceRepository;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository)
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_InvoiceRepository = invoceRepository;
_InvoiceRepository = invoiceRepository;
}
[HttpPost]

View File

@ -294,6 +294,10 @@ namespace BTCPayServer.Controllers
};
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob);
if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats")
{
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency);
}
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
@ -370,7 +374,7 @@ namespace BTCPayServer.Controllers
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
return;
CancellationTokenSource cts = new CancellationTokenSource();
using CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
try
{
@ -496,7 +500,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> CreateInvoice()
{
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
if (stores.Count() == 0)
if (!stores.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
@ -520,7 +524,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
{
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
return View(model);
@ -620,7 +624,7 @@ namespace BTCPayServer.Controllers
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
for (var i = 0; i < items.Count; i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Mvc;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
{
@ -65,7 +66,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction("U2FAuthentication");
}
}
catch (Exception e)
catch (U2fException e)
{
errorMessage = e.Message;
}

View File

@ -19,9 +19,6 @@ using System.Globalization;
using BTCPayServer.Security;
using BTCPayServer.U2F;
using BTCPayServer.Data;
#if NETCOREAPP21
using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
#endif
namespace BTCPayServer.Controllers
{
@ -35,7 +32,7 @@ namespace BTCPayServer.Controllers
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
IWebHostEnvironment _Env;
private readonly U2FService _u2FService;
public U2FService _u2FService;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
StoreRepository _StoreRepository;

View File

@ -10,10 +10,8 @@ using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
#if NETCOREAPP21
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
#endif
using BTCPayServer.Storage.Services.Providers.Models;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Views;
@ -221,11 +219,9 @@ namespace BTCPayServer.Controllers
case AmazonS3FileProviderService fileProviderService:
return View(nameof(EditAmazonS3StorageProvider),
fileProviderService.GetProviderConfiguration(data));
#if NETCOREAPP21
case GoogleCloudStorageFileProviderService fileProviderService:
return View(nameof(EditGoogleCloudStorageStorageProvider),
fileProviderService.GetProviderConfiguration(data));
#endif
case FileSystemFileProviderService fileProviderService:
if (data.Provider != BTCPayServer.Storage.Models.StorageProvider.FileSystem)
{
@ -252,14 +248,14 @@ namespace BTCPayServer.Controllers
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
}
#if NETCOREAPP21
[HttpPost("server/storage/GoogleCloudStorage")]
public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
GoogleCloudStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
}
#endif
[HttpPost("server/storage/FileSystem")]
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
{

View File

@ -1121,9 +1121,11 @@ namespace BTCPayServer.Controllers
{
try
{
var client = model.Settings.CreateSmtpClient();
var message = model.Settings.CreateMailMessage(new MailAddress(model.TestEmail), "BTCPay test", "BTCPay test");
await client.SendMailAsync(message);
using (var client = model.Settings.CreateSmtpClient())
using (var message = model.Settings.CreateMailMessage(new MailAddress(model.TestEmail), "BTCPay test", "BTCPay test"))
{
await client.SendMailAsync(message);
}
TempData[WellKnownTempData.SuccessMessage] = "Email sent to " + model.TestEmail + ", please, verify you received it";
}
catch (Exception ex)

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -17,7 +18,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -25,7 +28,7 @@ namespace BTCPayServer.Controllers
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
@ -40,7 +43,14 @@ namespace BTCPayServer.Controllers
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
SetExistingValues(store, vm);
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.CanUseHotWallet = await CanUseHotWallet();
return View(vm);
}
@ -86,7 +96,7 @@ namespace BTCPayServer.Controllers
var getxpubResult = new GetXPubs();
getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions()
var derivation = network.NBXplorerNetwork.DerivationStrategyFactory.CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions()
{
ScriptPubKeyType = segwit ? ScriptPubKeyType.SegwitP2SH : ScriptPubKeyType.Legacy
});
@ -107,7 +117,8 @@ namespace BTCPayServer.Controllers
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
using var cts = new CancellationTokenSource(2000);
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, cts.Token);
}
}
catch { }
@ -119,19 +130,6 @@ namespace BTCPayServer.Controllers
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
@ -140,8 +138,6 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
@ -162,7 +158,7 @@ namespace BTCPayServer.Controllers
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -179,7 +175,7 @@ namespace BTCPayServer.Controllers
Message = "Config file was not in the correct format"
});
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
@ -193,7 +189,7 @@ namespace BTCPayServer.Controllers
Message = "Coldcard public file was not in the correct format"
});
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
else
@ -229,7 +225,7 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
@ -246,7 +242,7 @@ namespace BTCPayServer.Controllers
var willBeExcluded = !vm.Enabled;
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
@ -280,7 +276,7 @@ namespace BTCPayServer.Controllers
{
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} has been modified.";
}
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
@ -320,6 +316,65 @@ namespace BTCPayServer.Controllers
return ShowAddresses(vm, strategy);
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
if (!await CanUseHotWallet())
{
return NotFound();
}
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var response = await client.GenerateWalletAsync(request);
if (response == null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "There was an error generating your wallet. Is your node available?"
});
return RedirectToAction("AddDerivationScheme", new {storeId, cryptoCode});
}
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = false,
Network = network,
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = network.GetRootKeyPath(),
CryptoCode = cryptoCode,
DerivationScheme = response.DerivationScheme.ToString(),
Source = "NBXplorer",
AccountKey = response.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = response.AccountKeyPath.KeyPath.ToString(),
Enabled = !store.GetStoreBlob()
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
}, cryptoCode);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = !string.IsNullOrEmpty(request.ExistingMnemonic)
? "Your wallet has been imported."
: $"Your wallet has been generated. Please store your seed securely! <br/><code>{response.Mnemonic}</code>"
});
return result;
}
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, BTCPayServer.Security.Policies.CanModifyServerSettings.Key)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
@ -328,7 +383,8 @@ namespace BTCPayServer.Controllers
}
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
private IActionResult
ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
@ -338,13 +394,18 @@ namespace BTCPayServer.Controllers
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((deposit.GetKeyPath((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
var keyPath = deposit.GetKeyPath((uint)i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive((uint)i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)i),
derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
}
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
}

View File

@ -9,7 +9,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
@ -23,18 +22,12 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
#if NETCOREAPP21
using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
#endif
namespace BTCPayServer.Controllers
{
@ -63,6 +56,8 @@ namespace BTCPayServer.Controllers
ChangellyClientProvider changellyClientProvider,
IWebHostEnvironment env, IHttpClientFactory httpClientFactory,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
SettingsRepository settingsRepository,
IAuthorizationService authorizationService,
CssThemeManager cssThemeManager)
{
_RateFactory = rateFactory;
@ -76,6 +71,8 @@ namespace BTCPayServer.Controllers
_Env = env;
_httpClientFactory = httpClientFactory;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_settingsRepository = settingsRepository;
_authorizationService = authorizationService;
_CssThemeManager = cssThemeManager;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
@ -100,6 +97,8 @@ namespace BTCPayServer.Controllers
IWebHostEnvironment _Env;
private IHttpClientFactory _httpClientFactory;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly SettingsRepository _settingsRepository;
private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _CssThemeManager;
[TempData]
@ -196,14 +195,15 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/rates")]
public IActionResult Rates()
{
var exchanges = GetSupportedExchanges();
var storeBlob = CurrentStore.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName);
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = CurrentStore.Id;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
vm.AvailableExchanges = exchanges;
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
@ -213,7 +213,16 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
if (command == "scripting-on")
{
return RedirectToAction(nameof(ShowRateRules), new {scripting = true,storeId = model.StoreId});
}else if (command == "scripting-off")
{
return RedirectToAction(nameof(ShowRateRules), new {scripting = false, storeId = model.StoreId});
}
var exchanges = GetSupportedExchanges();
model.SetExchangeRates(exchanges, model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null;
try
@ -236,14 +245,14 @@ namespace BTCPayServer.Controllers
var blob = CurrentStore.GetStoreBlob();
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
model.AvailableExchanges = GetSupportedExchanges();
model.AvailableExchanges = exchanges;
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase)))
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
@ -329,7 +338,7 @@ namespace BTCPayServer.Controllers
Description = scripting ?
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
ButtonClass = "btn-primary"
ButtonClass = scripting ? "btn-primary" : "btn-danger"
});
}
@ -473,12 +482,12 @@ namespace BTCPayServer.Controllers
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.ToDictionary(c => c.Network.CryptoCode);
.ToDictionary(c => c.Network.CryptoCode.ToUpperInvariant());
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.ToDictionary(c => c.CryptoCode);
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{
@ -486,9 +495,11 @@ namespace BTCPayServer.Controllers
{
case BitcoinPaymentType _:
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
Value = strategy?.ToPrettyString() ?? string.Empty,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null
@ -592,13 +603,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
private CoinAverageExchange[] GetSupportedExchanges()
private IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
.Where(r => !string.IsNullOrWhiteSpace(r.Value.Display))
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
var exchanges = _RateFactory.RateProviderFactory.GetSupportedExchanges();
return exchanges
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
}
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
@ -745,7 +756,7 @@ namespace BTCPayServer.Controllers
ViewBag.ShowMenu = false;
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
if (model.Stores.Count() == 0)
if (!model.Stores.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
@ -763,14 +774,17 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey()
public async Task<IActionResult> GenerateAPIKey(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
return RedirectToAction(nameof(ListTokens));
return RedirectToAction(nameof(ListTokens), new
{
storeId
});
}
[HttpGet]
@ -822,9 +836,9 @@ namespace BTCPayServer.Controllers
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
StoreNotConfigured = !store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
.Count() == 0;
.Any();
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode;

View File

@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
[Route("vault")]
public class VaultController : Controller
{
private readonly IAuthorizationService _authorizationService;
public VaultController(BTCPayNetworkProvider networks, IAuthorizationService authorizationService)
{
Networks = networks;
_authorizationService = authorizationService;
}
public BTCPayNetworkProvider Networks { get; }
[HttpGet]
[Route("{cryptoCode}/xpub")]
[Route("wallets/{walletId}/xpub")]
public async Task<IActionResult> VaultBridgeConnection(string cryptoCode = null,
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId = null)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
cryptoCode = cryptoCode ?? walletId.CryptoCode;
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
{
var cancellationToken = cts.Token;
var network = Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new HwiWebSocketTransport(websocket)
};
Hwi.HwiDeviceClient device = null;
HwiEnumerateEntry deviceEntry = null;
HDFingerprint? fingerprint = null;
string password = null;
var websocketHelper = new WebSocketHelper(websocket);
async Task<bool> RequireDeviceUnlocking()
{
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
return true;
}
if (deviceEntry.Code is HwiErrorCode.DeviceNotInitialized)
{
await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken);
return true;
}
if (deviceEntry.Code is HwiErrorCode.DeviceNotReady)
{
if (IsTrezorT(deviceEntry))
{
await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken);
return true;
}
else if (deviceEntry.NeedsPinSent is true)
{
await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);
return true;
}
else if (deviceEntry.NeedsPassphraseSent is true && password is null)
{
await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken);
return true;
}
}
return false;
}
JObject o = null;
try
{
while (true)
{
var command = await websocketHelper.NextMessageAsync(cancellationToken);
switch (command)
{
case "set-passphrase":
device.Password = await websocketHelper.NextMessageAsync(cancellationToken);
password = device.Password;
break;
case "ask-sign":
if (await RequireDeviceUnlocking())
{
continue;
}
if (walletId == null)
{
await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken);
continue;
}
if (fingerprint is null)
{
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
}
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings.Key);
if (!authorization.Succeeded)
{
await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);
continue;
}
var psbt = PSBT.Parse(o["psbt"].Value<string>(), network.NBitcoinNetwork);
var derivationSettings = GetDerivationSchemeSettings(walletId);
derivationSettings.RebaseKeyPaths(psbt);
var signing = derivationSettings.GetSigningAccountKeySettings();
if (signing.GetRootedKeyPath()?.MasterFingerprint != fingerprint)
{
await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken);
continue;
}
var signableInputs = psbt.Inputs
.SelectMany(i => i.HDKeyPaths)
.Where(i => i.Value.MasterFingerprint == fingerprint)
.ToArray();
if (signableInputs.Length > 0)
{
var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath)).GetPublicKey();
if (actualPubKey != signableInputs[0].Key)
{
await websocketHelper.Send("{ \"error\": \"wrong-keypath\"}", cancellationToken);
continue;
}
}
try
{
psbt = await device.SignPSBTAsync(psbt, cancellationToken);
}
catch (Hwi.HwiException)
{
await websocketHelper.Send("{ \"error\": \"user-reject\"}", cancellationToken);
continue;
}
o = new JObject();
o.Add("psbt", psbt.ToBase64());
await websocketHelper.Send(o.ToString(), cancellationToken);
break;
case "display-address":
if (await RequireDeviceUnlocking())
{
continue;
}
var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken);
await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);
break;
case "ask-pin":
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
try
{
await device.PromptPinAsync(cancellationToken);
}
catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked)
{
await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken);
continue;
}
await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken);
var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture);
if (await device.SendPinAsync(pin, cancellationToken))
{
goto askdevice;
}
else
{
await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken);
continue;
}
case "ask-xpub":
if (await RequireDeviceUnlocking())
{
continue;
}
await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);
var askedXpub = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var addressType = askedXpub["addressType"].Value<string>();
var accountNumber = askedXpub["accountNumber"].Value<int>();
JObject result = new JObject();
var factory = network.NBXplorerNetwork.DerivationStrategyFactory;
if (fingerprint is null)
{
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
}
result["fingerprint"] = fingerprint.Value.ToString();
DerivationStrategyBase strategy = null;
KeyPath keyPath = null;
BitcoinExtPubKey xpub = null;
if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy")
{
await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken);
continue;
}
if (addressType == "segwit")
{
keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(accountNumber, true);
xpub = await device.GetXPubAsync(keyPath);
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Segwit
});
}
else if (addressType == "segwitWrapped")
{
keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(accountNumber, true);
xpub = await device.GetXPubAsync(keyPath);
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
});
}
else if (addressType == "legacy")
{
keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(accountNumber, true);
xpub = await device.GetXPubAsync(keyPath);
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Legacy
});
}
else
{
await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken);
continue;
}
result.Add(new JProperty("strategy", strategy.ToString()));
result.Add(new JProperty("accountKey", xpub.ToString()));
result.Add(new JProperty("keyPath", keyPath.ToString()));
await websocketHelper.Send(result.ToString(), cancellationToken);
break;
case "ask-passphrase":
if (command == "ask-passphrase")
{
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
// The make the trezor T ask for password
await device.GetXPubAsync(new KeyPath("44'"), cancellationToken);
}
goto askdevice;
case "ask-device":
askdevice:
password = null;
deviceEntry = null;
device = null;
var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList();
deviceEntry = entries.FirstOrDefault();
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken);
continue;
}
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
fingerprint = device.Fingerprint;
JObject json = new JObject();
json.Add("model", device.Model.ToString());
json.Add("fingerprint", device.Fingerprint?.ToString());
await websocketHelper.Send(json.ToString(), cancellationToken);
break;
}
}
}
catch (FormatException ex)
{
JObject obj = new JObject();
obj.Add("error", "invalid-network");
obj.Add("details", ex.ToString());
try
{
await websocketHelper.Send(obj.ToString(), cancellationToken);
}
catch { }
}
catch (Exception ex)
{
JObject obj = new JObject();
obj.Add("error", "unknown-error");
obj.Add("message", ex.Message);
obj.Add("details", ex.ToString());
try
{
await websocketHelper.Send(obj.ToString(), cancellationToken);
}
catch { }
}
finally
{
await websocketHelper.DisposeAsync(cancellationToken);
}
}
return new EmptyResult();
}
private ScriptPubKeyType GetScriptPubKeyType(RootedKeyPath keyPath)
{
var path = keyPath.KeyPath.ToString();
if (path.StartsWith("84'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.Segwit;
if (path.StartsWith("49'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.SegwitP2SH;
if (path.StartsWith("44'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.Legacy;
throw new NotSupportedException("Unsupported keypath");
}
private bool SameSelector(DeviceSelector a, DeviceSelector b)
{
var aargs = new List<string>();
a.AddArgs(aargs);
var bargs = new List<string>();
b.AddArgs(bargs);
if (aargs.Count != bargs.Count)
return false;
for (int i = 0; i < aargs.Count; i++)
{
if (aargs[i] != bargs[i])
return false;
}
return true;
}
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
{
return (deviceEntry.Model == HardwareWalletModels.Trezor_T || deviceEntry.Model == HardwareWalletModels.Trezor_T_Simulator);
}
public StoreData CurrentStore
{
get
{
return HttpContext.GetStoreData();
}
}
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
{
var paymentMethod = CurrentStore
.GetSupportedPaymentMethods(Networks)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
}
}

View File

@ -55,12 +55,13 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTViewModel vm)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
vm.CryptoCode = network.CryptoCode;
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
{
vm.Decoded = psbt.ToString();
vm.PSBT = psbt.ToBase64();
}
return View(vm ?? new WalletPSBTViewModel());
return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
[Route("{walletId}/psbt")]
@ -72,6 +73,7 @@ namespace BTCPayServer.Controllers
if (command == null)
return await WalletPSBT(walletId, vm);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
vm.CryptoCode = network.CryptoCode;
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{
@ -88,6 +90,8 @@ namespace BTCPayServer.Controllers
vm.PSBT = psbt.ToBase64();
vm.FileName = vm.UploadedPSBTFile?.FileName;
return View(vm);
case "vault":
return ViewVault(walletId, psbt);
case "ledger":
return ViewWalletSendLedger(psbt);
case "update":
@ -156,7 +160,8 @@ namespace BTCPayServer.Controllers
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;
if (!psbtObject.IsAllFinalized())
psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? psbtObject;
IHDKey signingKey = null;
RootedKeyPath signingKeyPath = null;
try

View File

@ -202,7 +202,7 @@ namespace BTCPayServer.Controllers
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
Network: d.Network)))
.Where(_ => _.Wallet != null)
.Where(_ => _.Wallet != null && _.Network.WalletSupported)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
@ -246,28 +246,46 @@ namespace BTCPayServer.Controllers
var walletBlob = await walletBlobAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var model = new ListTransactionsViewModel();
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
if (transactions == null)
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
vm.IsConfirmed = tx.Confirmations != 0;
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
TempData.SetStatusMessageModel(new StatusMessageModel()
{
var labels = walletBlob.GetLabels(transactionInfo);
vm.Labels.AddRange(labels);
model.Labels.AddRange(labels);
vm.Comment = transactionInfo.Comment;
Severity = StatusMessageModel.StatusSeverity.Error,
Message =
"There was an error retrieving the transactions list. Is NBXplorer configured correctly?"
});
model.Transactions = new List<ListTransactionsViewModel.TransactionViewModel>();
}
else
{
foreach (var tx in transactions.UnconfirmedTransactions.Transactions
.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink,
vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
vm.Balance = tx.BalanceChange.ToString();
vm.IsConfirmed = tx.Confirmations != 0;
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{
var labels = walletBlob.GetLabels(transactionInfo);
vm.Labels.AddRange(labels);
model.Labels.AddRange(labels);
vm.Comment = transactionInfo.Comment;
}
if (labelFilter == null ||
vm.Labels.Any(l => l.Value.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
model.Transactions.Add(vm);
}
if (labelFilter == null || vm.Labels.Any(l => l.Value.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
model.Transactions.Add(vm);
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
}
@ -289,7 +307,7 @@ namespace BTCPayServer.Controllers
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
if (network == null || network.ReadonlyWallet)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
@ -313,7 +331,7 @@ namespace BTCPayServer.Controllers
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
model.CurrentBalance = await balance;
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
model.SupportRBF = network.SupportRBF;
@ -351,7 +369,7 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
decimal transactionAmountSum = 0;
@ -387,9 +405,20 @@ namespace BTCPayServer.Controllers
{
subtractFeesOutputsCount.Add(i);
}
var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Invalid address");
transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;
try
{
BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
}
catch
{
var inputName =
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
ModelState.AddModelError(inputName, "Invalid address");
}
if (transactionOutput.Amount.HasValue)
{
@ -425,7 +454,7 @@ namespace BTCPayServer.Controllers
}
}
if (!ModelState.IsValid)
if (!ModelState.IsValid)
return View(vm);
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
@ -449,6 +478,8 @@ namespace BTCPayServer.Controllers
switch (command)
{
case "vault":
return ViewVault(walletId, psbt.PSBT);
case "ledger":
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
case "seed":
@ -463,6 +494,16 @@ namespace BTCPayServer.Controllers
}
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
{
return View("WalletSendVault", new WalletSendVaultModel()
{
WalletId = walletId.ToString(),
PSBT = psbt.ToBase64(),
WebsocketPath = this.Url.Action(nameof(VaultController.VaultBridgeConnection), "Vault", new { walletId = walletId.ToString() })
});
}
private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null)
{
var vm = new PostRedirectViewModel()
@ -561,46 +602,46 @@ namespace BTCPayServer.Controllers
signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
if (rootedKeyPath == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
return View(viewModel);
}
// 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())
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 is unable to sign this transaction. Either the seed is incorrect, or the account path has not been properly configured in the Wallet Settings.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
return View(viewModel);
}
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
if (!changed)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
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 bool PSBTChanged(PSBT psbt, Action act)
{
var before = psbt.ToBase64();
act();
var after = psbt.ToBase64();
return before != after;
}
private string ValueToString(Money v, BTCPayNetworkBase network)
{
return v.ToString() + " " + network.CryptoCode;
}
private IDestination[] ParseDestination(string destination, Network network)
{
try
{
destination = destination?.Trim();
return new IDestination[] { BitcoinAddress.Create(destination, network) };
}
catch
{
return null;
}
}
private IActionResult RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
@ -716,7 +757,7 @@ namespace BTCPayServer.Controllers
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString(CultureInfo.InvariantCulture);
}
catch
{
@ -849,7 +890,7 @@ namespace BTCPayServer.Controllers
WalletId walletId)
{
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
if (derivationSchemeSettings == null || derivationSchemeSettings.Network.ReadonlyWallet)
return NotFound();
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var vm = new WalletSettingsViewModel()
@ -878,7 +919,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View(vm);
var derivationScheme = GetDerivationSchemeSettings(walletId);
if (derivationScheme == null)
if (derivationScheme == null || derivationScheme.Network.ReadonlyWallet)
return NotFound();
if (command == "save")
@ -900,7 +941,7 @@ namespace BTCPayServer.Controllers
}
else if (command == "prune")
{
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, cancellationToken);
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet is already pruned";

View File

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Data.PaymentRequestData;

View File

@ -10,6 +10,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
using Newtonsoft.Json;
using System.Text;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Data
{
@ -156,7 +157,7 @@ namespace BTCPayServer.Data
}
}
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? CoinGeckoRateProvider.CoinGeckoName : PreferredExchange;
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);

View File

@ -9,6 +9,7 @@ using BTCPayServer.Security;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
@ -50,7 +51,7 @@ namespace BTCPayServer.Data
{
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
return result;
}

View File

@ -52,7 +52,7 @@ namespace BTCPayServer
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
if (type == DerivationType.SegwitP2SH)
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
@ -83,7 +83,7 @@ namespace BTCPayServer
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);
var result = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
return FindMatch(hintedLabels, result);
}
catch
@ -150,12 +150,11 @@ namespace BTCPayServer
str = $"{str}-[{label}]";
}
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
return FindMatch(hintedLabels, BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str));
}
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
{
var facto = new DerivationStrategyFactory(Network);
var firstKeyPath = new KeyPath("0/0");
if (HintScriptPubKey == null)
return result;
@ -169,7 +168,7 @@ namespace BTCPayServer
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
var hinted = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
if (HintScriptPubKey == hinted.GetDerivation(firstKeyPath).ScriptPubKey)
return hinted;
}

View File

@ -18,7 +18,7 @@ namespace BTCPayServer
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
var result = network.NBXplorerNetwork.DerivationStrategyFactory.Parse(derivationStrategy);
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
}

View File

@ -63,8 +63,8 @@ namespace BTCPayServer
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
using (cancellation.Register(() => { tcs.TrySetCanceled(); subscription.Unsubscribe(); }))
using var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
using (cancellation.Register(() => { tcs.TrySetCanceled(); }))
{
return await tcs.Task.ConfigureAwait(false);
}

View File

@ -33,23 +33,24 @@ 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(nameof(ExplorerClientProvider)), _NetworkProviders.GetNetwork<BTCPayNetwork>(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
_Clients.TryAdd(setting.CryptoCode.ToUpperInvariant(), CreateExplorerClient(httpClientFactory.CreateClient(nameof(ExplorerClientProvider)), _NetworkProviders.GetNetwork<BTCPayNetwork>(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
var explorer = n.NBXplorerNetwork.CreateExplorerClient(uri);
explorer.SetClient(httpClient);
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
Logs.Configuration.LogWarning($"{explorer.CryptoCode}: Not using cookie authentication");
explorer.SetNoAuth();
}
if(!explorer.SetCookieAuth(cookieFile))
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
Logs.Configuration.LogWarning($"{explorer.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
}
return explorer;
}
@ -61,7 +62,7 @@ namespace BTCPayServer
var network = _NetworkProviders.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
return null;
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
_Clients.TryGetValue(network.NBXplorerNetwork.CryptoCode, out ExplorerClient client);
return client;
}
@ -79,6 +80,7 @@ namespace BTCPayServer
public bool IsAvailable(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
}
@ -87,7 +89,7 @@ namespace BTCPayServer
var network = _NetworkProviders.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
return null;
if (_Clients.ContainsKey(network.CryptoCode))
if (_Clients.ContainsKey(network.NBXplorerNetwork.CryptoCode))
return network;
return null;
}
@ -96,7 +98,7 @@ namespace BTCPayServer
{
foreach (var net in _NetworkProviders.GetAll().OfType<BTCPayNetwork>())
{
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
if (_Clients.TryGetValue(net.NBXplorerNetwork.CryptoCode, out ExplorerClient explorer))
{
yield return (net, explorer);
}

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