Compare commits

...

237 Commits

Author SHA1 Message Date
effbf169d3 Use Tor Http Proxy instead of our internal hack one 2020-06-10 13:21:36 +09:00
74b6aa7353 Merge pull request #1623 from Kukks/telemetry
Opt out from telemetry data in docker
2020-06-09 22:07:51 +09:00
0e0297d650 Merge pull request #1643 from NicolasDorier/fix/ln
Fix lightning implementation, docs and tests
2020-06-09 21:52:45 +09:00
8dd6ecc0b8 Fix lightning implementation, docs and tests 2020-06-09 17:24:34 +09:00
e84d4d3cb5 Merge pull request #1644 from btcpayserver/feat/lnd-0.10.1-beta
Bump LND version to 0.10.1-beta
2020-06-09 16:06:37 +09:00
da54154ae7 Bump LND version 2020-06-08 23:28:45 -05:00
a9dbbe1955 Merge pull request #1640 from btcpayserver/renametoscope
Rename Permission.StoreId to Permission.Scope
2020-06-07 23:27:11 +09:00
3fbe86c286 Rename Permission.StoreId to Permission.Scope 2020-06-07 23:17:48 +09:00
0022e71dd4 Merge pull request #1628 from Kukks/greenfield/consistentreturn
GreenField: Handle status codes, error models consistently
2020-06-07 18:59:22 +09:00
c74121fefc Opt out from telemetry data in docker
https://www.michaelcrump.net/part12-aspnetcore/
https://docs.microsoft.com/en-us/dotnet/core/tools/telemetry
2020-06-07 08:32:00 +02:00
cf00784096 fix swagger 2020-06-07 08:30:04 +02:00
db209af787 Updating ports for merchant lnd
I saw several instances where btcpayservertests_merchant_lnd_1 failed to start in circleci
2020-06-07 01:24:47 -05:00
30bd94ee43 fix tests 2020-06-07 08:21:45 +02:00
f313a5f221 GreenField: Handle status codes, error models consistently 2020-06-07 08:21:45 +02:00
b9ef5af5d7 Fixing variable reference
Who'll save Emperor if not me
2020-06-06 23:52:21 -05:00
56283df05a Can only sign with NBX if imported with NBX 2020-06-07 09:38:29 +09:00
ef271a088a Merge pull request #1634 from dennisreimann/minor-fixups
Minor fix ups
2020-06-06 09:48:28 +09:00
6043b7c75d Minor fix ups
Resolves https://github.com/btcpayserver/btcpayserver/pull/1633#discussion_r435655010
2020-06-05 11:38:55 +02:00
c338779f0e Merge pull request #1633 from dennisreimann/onion-location
Add Onion-Location HTTP header
2020-06-05 11:09:55 +09:00
67758609a7 Add Onion-Location HTTP header
Closes #1626.
2020-06-04 08:53:55 +02:00
f2bb24f6ab Fix build error #1588 2020-06-03 15:49:27 +09:00
44ee7c66ce Merge pull request #1620 from NicolasDorier/additionaldata
[GreenField] Add additional data to allow roundtrip of upper version fields
2020-06-01 19:23:35 +09:00
117622200b Merge pull request #1622 from Kukks/network-rates-decouple
Remove dependency on Common from Rating
2020-06-01 15:25:19 +09:00
340ba5c714 fix symbol 2020-06-01 08:13:39 +02:00
9e16b506e5 Small esthetic refacotring 2020-06-01 13:10:21 +09:00
e865d85c2c Merge pull request #1619 from NicolasDorier/adddoc
[Greenfield] Document store API
2020-06-01 11:47:16 +09:00
284e66c730 fix test 2020-05-31 20:54:50 +02:00
587f244f1d Remove dependency on Common from Rating
replaces #1591 and #1592 , closes #1571 Replaces the Currencies.txt to a json format which includes the networks too ( there is also a test to check now) Also makes CurrencyPair not depend on network and instead use CurrencyNameTable
2020-05-31 20:54:50 +02:00
379e5741e7 fix broken bootstrap theme generator link 2020-05-31 20:54:24 +02:00
ec4c346e0a [Greenfield] Document store API 2020-05-31 12:04:23 +09:00
5099f98d2a [GreenField] Add additional data to allow roundtrip of upper version fields 2020-05-31 11:51:36 +09:00
ebc99adc58 Merge pull request #1615 from NicolasDorier/refactor/timespan
Always use second based TimeSpan in Greenfield
2020-05-31 10:55:40 +09:00
048dff7e9e Always use second based TimeSpan in Greenfield 2020-05-31 08:52:32 +09:00
a70934938a Merge pull request #1605 from dennisreimann/pos-refactoring
Point of Sale Refactoring
2020-05-30 13:14:50 +09:00
d9a93f996f Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2020-05-29 18:47:40 +09:00
3456d87bb5 Merge pull request #1612 from Kukks/api/swaggerln
Api/swaggerln
2020-05-29 18:47:25 +09:00
4947fa4d45 Add also money json converter 2020-05-29 10:15:01 +02:00
37f4b34b5e Add Lightmoney json converter + do swagger defs for ln api 2020-05-29 10:04:28 +02:00
66206399e1 Rename LightningPrivateRouteHint =>privateRouteHints 2020-05-29 09:03:07 +09:00
1e3f62718d GreenField: Cross-implemenation Lightning Node API (#1566)
* GreenField: Cross-implemenation Lightning Node API

* switch to hard unrsstricted check

* fix

* set LightningPrivateRouteHints in swagger + stores api

* add priv route hint

* rename models and add swagger defs to models
2020-05-29 09:00:13 +09:00
114ab98059 Merge pull request #1611 from Kukks/fix/pr-expiry-big
Fix: Paid payment requests are not marked as completed if there is an expiry date set
2020-05-29 08:58:58 +09:00
47baa219fd Merge pull request #1609 from NicolasDorier/pj3
Adapt PJ implementation to latest BIP
2020-05-29 08:58:38 +09:00
96509717cb Fix: Paid payment requests are not marked as completed if there is an expiry date set 2020-05-28 17:45:06 +02:00
89bbcb6092 Merge pull request #1610 from pavlenex/supporter-new
Add new supporter to readme
2020-05-29 00:05:21 +09:00
95dbb65612 GreenField: Payment Request property missing jsonconverter 2020-05-28 16:57:03 +02:00
7db8d52624 add missing title in swagger payment request def 2020-05-28 16:53:50 +02:00
77073fa78c fix swagger schema 2020-05-28 16:42:47 +02:00
9e315f99d0 POS: Updates from code review 2020-05-28 12:50:08 +02:00
16ad3ee7fe Add test for payment request payment status update in greenfield 2020-05-28 09:48:47 +02:00
559015c70d POS: Test fixes 2020-05-28 09:40:34 +02:00
15d02dab4f Add new supporter to readme 2020-05-28 09:28:42 +02:00
332a1da167 POS: Updates from code review 2020-05-28 08:59:48 +02:00
e12aa9e657 POS: Use views instead of partials 2020-05-28 08:50:04 +02:00
9503e07dc8 Adapt PJ implementation to latest BIP 2020-05-28 13:35:48 +09:00
9b44370832 support version revision tags 2020-05-27 16:56:39 +02:00
568015c58f add new supporter 2020-05-27 16:41:50 +02:00
58f56eac90 POS: Updates from code review
Thanks @kukks
2020-05-27 14:20:21 +02:00
03532e4063 Merge pull request #1604 from janoside/master
Clarify/clean up warning shown when adding a lightning node
2020-05-27 12:34:36 +09:00
de6081c2dc Merge pull request #1602 from Kukks/electrum-file
Rename Coldcard to Electrum/Airgapped hardware wallets
2020-05-27 12:33:43 +09:00
197b46880c POS: Separate static and cart views 2020-05-26 21:53:23 +02:00
b7ec22ee89 POS: Set ViewType via optional URL parameter
Falls back to the DefaultView defined in the POS settings.

POS: Fix POST-Route
2020-05-26 21:53:22 +02:00
fad53a9fa2 POS: Replace EnableShoppingCart with DefaultView 2020-05-26 16:48:47 +02:00
5b4fec11ed Merge pull request #1596 from NicolasDorier/random-rbf
Randomize RBF if the user does not care
2020-05-26 03:23:33 +09:00
0ef4602a65 Clarify/clean up warning shown when adding a lightning node 2020-05-25 13:42:58 -04:00
c8f182f13f Rename Coldcard to Electrum/Airgapped hardware wallets
Also adds support to read the xpub from the file directly ( Cobo Vault support)
2020-05-25 15:49:44 +02:00
36630d9586 Randomize RBF if the user does not care 2020-05-25 19:46:42 +09:00
32f2acee53 Removed unexisting file in csproj 2020-05-25 16:08:16 +09:00
3be2ef5c91 Merge pull request #1599 from NicolasDorier/removeledger
Remove LedgerWallet direct integration
2020-05-25 07:50:33 +09:00
7e5d269c39 Remove LedgerWallet direct integration 2020-05-25 07:41:30 +09:00
6e0c090622 Merge pull request #1597 from NicolasDorier/refactor/wallet-param
Refactor parameter passing in wallet functions
2020-05-25 07:15:52 +09:00
cccf3ca617 The signing context should not be passed to PSBT screen 2020-05-25 07:05:01 +09:00
9e9b5945fe Ensure the payjoin can generate a high R signature if the payer is sending it 2020-05-25 06:47:43 +09:00
77f6019d82 Use EnforceLowR suggestion of NBXplorer for signing 2020-05-25 06:34:49 +09:00
35432d919c Refactor common data structure for wallet into SigningContext 2020-05-25 06:29:06 +09:00
25e6f82aa3 Refactor parameter passing in wallet functions 2020-05-25 04:55:28 +09:00
d22993871f Bump NBXplorer 2020-05-24 20:19:06 +09:00
80563de587 Add support for Bitpay invoice create property "paymentCurrencies" (#1589) 2020-05-24 19:11:26 +09:00
b5102c4269 Bump NBX (#1595) 2020-05-24 17:59:43 +09:00
9d215ea27d Reuse same html and handling of commands for PSBT signing/ Wallet Send (#1587)
* Reuse same html and handling of commands for PSBT signing/ Wallet Send

* fix ledger
2020-05-24 04:31:21 +09:00
cdf6886c39 Require loggedin user for docs? (#1567)
* Require loggedin user for docs?

We had talked before that docs should be for authorized users only. We had it in when we had Nswag but must have lost it after we switch to manual swagger files

* fix
2020-05-24 04:18:51 +09:00
79b034b505 GreenField: Add properties to Store API (#1573)
* GreenField: Add properties to Store API

* Update StoreBaseData.cs

* fix api

* fix swagger
2020-05-24 04:13:18 +09:00
bfba105aec Update packages (may fix #1590) 2020-05-22 19:23:37 +09:00
1bbdaa1251 Make sure replaced transactions have linethrough in the invoice list 2020-05-21 01:50:39 +09:00
ca462bec84 Merge pull request #1585 from btcpayserver/feat/private-route-hints
Adding private route hints based on store settings
2020-05-20 08:10:08 +09:00
eb5dcab32d Upgrading to BTCPayServer.Lightning to 1.1.15 2020-05-19 18:00:35 -05:00
ca3acdacdc Setting PriveRoute hints when creating lightning invoices 2020-05-19 16:47:26 -05:00
33f63508e8 Adding setting in checkout experience for inclusion of private route hints 2020-05-19 16:27:06 -05:00
5033cb3186 Support specifying payment method through apps (#1539)
* Support specifying payment method through apps

closes #1525
This is great if you want to offer items with an incentive to use a specific payment method without messing with the rates.
For example, you can have `Item A` which costs 25$ and your store is configured for USDT and BTC. You can create two items, with different prices, where Item A costs 20$ if you pay with bitcoin or 25$ if you paid in dirty fiat pegs

* make code cleaner

* Add Test

* fix test

* fix test
2020-05-20 03:54:24 +09:00
3d1122be7c Merge pull request #1583 from NicolasDorier/refactor/serverinfo
Refactor server info data structure
2020-05-20 03:43:39 +09:00
1d092a15fb Refactor server info data structure 2020-05-20 03:27:06 +09:00
501a21b89e bump .net core on dockerfile 2020-05-20 03:10:30 +09:00
b9006e4417 fix rebase
# Conflicts:
#	BTCPayServer.Tests/GreenfieldAPITests.cs
2020-05-19 20:09:19 +02:00
5b3b96b372 GreenField: Payment Requests CRUD (#1430)
* GreenField: Payment Requests CRUD

* fixes

* fix swagger

* fix swag

* rebase fixes

* Add new permissions for payment requests

* Adapt PR to archive

* fix tst

* add to contains policxy

* make decimals returned as string due to avoid shitty language parsing issues

* do not register decimal json converter as global

* fix cultureinfo for json covnerter

* pr changes

* add json convertet test

* fix json test

* fix rebase
2020-05-20 02:59:23 +09:00
338a0f9251 Merge pull request #1576 from dennisreimann/api-server-info
GreenField: Add Server Info API basics
2020-05-20 02:31:16 +09:00
29ba761d7c Merge pull request #1581 from NicolasDorier/syncpj
Sync payjoin receiver implementation to the bip
2020-05-19 21:10:35 +09:00
b96e668dfd Sync payjoin receiver implementation to the bip 2020-05-19 20:57:04 +09:00
de3753d04e Rename payjoin optional parameters, implement sender minFeeRate 2020-05-19 20:57:03 +09:00
2a030fe7fb Use different close button color variable (#1580)
fix #1579
2020-05-18 21:52:26 +02:00
f595d823b6 Decode item description on POS app page (#1578)
fix #938
2020-05-18 21:51:58 +02:00
78d191f7d8 Incorporate code review feedback 2020-05-18 16:51:21 +02:00
3b6dbe76c5 Add sync state 2020-05-18 16:02:15 +02:00
e3b348b55c GreenField: Add Server Info API basics 2020-05-18 15:29:04 +02:00
cf012a7946 fix dead link (#1575) 2020-05-18 08:44:12 +02:00
8d1ff01ee8 Adapt payjoin implementation to the BIP (#1569) 2020-05-17 05:07:24 +09:00
e9bda50054 Bumping LND to v0.9.2-beta in dev 2020-05-13 18:06:14 -05:00
8ca2824b00 Open wallet with BIP21 links (#1565)
* Open wallet with BIP21 links

https://i.imgur.com/IWefMKB.gifv

* remove debugger;

* Move to settings
2020-05-12 22:32:33 +09:00
f7d70daff3 Add invalid-transaction as wellknown error 2020-05-12 21:42:51 +09:00
42d27d69dc Merge db migrations 2020-05-10 00:53:21 +09:00
5a9d1e3257 Merge pull request #1560 from Kukks/archive-pr
Archive Payment reqeusts
2020-05-10 00:51:59 +09:00
9cc9a16bb9 Merge pull request #1556 from Kukks/invoice-archive
Archive Invoice
2020-05-10 00:51:11 +09:00
a3efe9a9dc Merge pull request #1561 from NicolasDorier/bp/new
Refactoring of payjoin to support new spec
2020-05-10 00:50:40 +09:00
94b0b9498d adapt tests 2020-05-09 15:17:59 +02:00
bb322d6bf3 add e2e test 2020-05-09 15:05:52 +02:00
b0f820e95a Implement noadjustfee, feebumpindex and v parameters for payjoin 2020-05-09 21:22:00 +09:00
da588380ed Remove httpCode from error message 2020-05-09 19:33:48 +09:00
8fa65408ed Archive Payment reqeusts
closes #1588
2020-05-08 12:33:47 +02:00
2d68d0da63 Redo App template editor with Vue based app (#1544)
* Redo App template editor with Vue based app

* fix styles better

* remove debug

* support methods

* make price required

* fix styling for validation errors

* show create or edit in title based on context

* add border bottom when image present

* escape/unescape

* more validation fixes

* fix responsive style

* Toggle template field

* add errors to the app

* enhance validation
2020-05-08 05:37:59 +09:00
2bc7fa0316 Show warning when enabling Payjoin but supported payment methods are not using a hot wallet (#1545)
* Show warning when enabling Payjoin but supported payment methods are not using a hot wallet

https://i.imgur.com/RXBhr0n.png fixes #1474

* expose p2p port
2020-05-08 05:37:10 +09:00
55f416c48c apply window scroll offset to copied notification offset (#1550)
resolves #1549
2020-05-08 05:35:25 +09:00
aff91f49ef Make fee rate options pretty (#1554) 2020-05-08 05:34:39 +09:00
ba02372d13 Try to tell browser that autocomplete is not welcome in generate wallet (#1555)
fixes #1553
2020-05-07 21:41:37 +09:00
e5a3ef3e22 Archive Invoice 2020-05-07 12:50:07 +02:00
137c3ef2ce Add changelog 2020-05-05 19:19:27 +09:00
87352f0b62 Make sure wallet support decimal fee, and allow user to select different fee rate based on expected confirmation time 2020-05-05 19:10:53 +09:00
2226884946 add qrcode margins (#1547)
addresses #1537 by adding a margin to the qrcodes in ShowLightningNodeInfo and WalletReceive (use same settings as Checkout)
2020-05-05 10:26:57 +02:00
9d2cd46464 fix tests and bump 2020-05-05 07:23:00 +09:00
f3b2b350ce Fix build 2020-05-05 07:06:32 +09:00
96c04481da bump NBXplorer 2020-05-05 06:55:19 +09:00
dad2642fa7 Make sure we match the user's sequence 2020-05-05 04:45:10 +09:00
59bdb943dd Reverting invoice row display so AM/PM is not cut off 2020-05-04 01:42:02 -05:00
67da6ee379 Decimal precision and filter valid transaction (#1538)
The liquid transactions list was showing all transactions to the wallet, even when it had nothing to do with the specific crypto code (e.g sending LBTC txs in USDT, LCAD in USDT, etc). This PR fixes that.

It also uses the previously introduced checkout decimal precision fix to the Wallets screen, specifically the balance amount on wallet llist and balance change on transaction list.
2020-05-04 01:04:34 +09:00
9c9c102e74 fix PadRight formatter 2020-05-03 09:32:10 +02:00
26241be6fa Ensure dropdown option doesn't overflow container (#1533)
fix #1526
2020-05-03 01:39:39 +09:00
2bb4dd5d01 Fix decimal points shown in Checkout UI based on currency ( always showed btc decimal precision before) (#1529)
* Fix decimal points shown in Checkout UI based on currency ( always showed btc decimal precision before)

* cleanup ShowMoney
2020-05-03 01:28:35 +09:00
5312bb1dee Merge pull request #1535 from Kukks/elementsbump
Bump elements and fix test
2020-05-02 20:47:22 +09:00
bf6f5aa335 Bump c-lightning 2020-05-02 20:33:55 +09:00
4a58763f98 Bump elements and fix test 2020-05-02 13:26:55 +02:00
b8202da7aa Fix tests 2020-05-02 00:59:36 +09:00
edfc82ac75 Merge pull request #1522 from Kukks/view-seed
Add View seed to wallet settings
2020-05-01 21:36:27 +09:00
b28fc85974 Fix: Do not returns HTML content if authentication to API fails 2020-05-01 21:33:42 +09:00
ab1b36bcdc add test for view seed 2020-05-01 13:34:11 +02:00
5443ac4688 Merge pull request #1531 from Kukks/api/store/prep
GreenField: Prep store for more data
2020-05-01 18:49:29 +09:00
d92d8ba0e4 Merge pull request #1524 from Kukks/txlist-labelfix
Add top Label filter + fix label link inconsistency
2020-05-01 18:47:48 +09:00
0f19d303eb Merge pull request #1527 from Kukks/app-inv-clean
Make App Inventory Updater run updates in order
2020-05-01 18:46:08 +09:00
1332f597e5 Merge pull request #1528 from Kukks/payjoin/ui-linkfix
Payjoin: Fix payjoin detection in checkout UI
2020-05-01 18:44:50 +09:00
de004074b7 Fix typo 2020-05-01 18:43:40 +09:00
29741f39ac Merge pull request #1523 from Kukks/sender-payjoin-label
Tag payjoin for sender too
2020-05-01 05:27:33 +09:00
33ea8984fc format 2020-04-30 16:44:27 +02:00
85517b0344 GreenField: Prep store for more data
refactored things around a bit to make it cleaner for when we add more properties to the store model
2020-04-30 16:43:16 +02:00
74c574255e Payjoin: Fix payjoin detection in checkout UI
sometimes, it would think there is a payjoin enabled invoice when really, it was just the address or asset id that has `pj` in it
2020-04-30 09:05:17 +02:00
70d4e98dff Make App Inventory Updater run updates in order
followed the same logic we used in the new auto labelling
2020-04-29 14:51:37 +02:00
f1900d30f2 clean format 2020-04-29 11:21:47 +02:00
463567cb07 Add top Label filter + fix label link inconsistency 2020-04-29 10:11:23 +02:00
53b0e675c3 Tag payjoin for sender too 2020-04-29 09:09:16 +02:00
d323bb35cc Add View seed to wallet settings 2020-04-29 08:28:13 +02:00
3e13e478ad Fix click on label 2020-04-29 01:43:41 +09:00
5f421b0679 Changlog 1.0.4.2 (#1513) 2020-04-29 01:23:02 +09:00
c99fe54db1 bump 2020-04-29 01:21:58 +09:00
05a2985c5b Changlog 1.0.4.2 2020-04-29 01:07:20 +09:00
47408498b9 Revert "View seed option if available (#1518)" (#1521)
This reverts commit e75b4ec6bfb41dfca018315350e7305f95cc39f0.
2020-04-29 00:57:41 +09:00
e75b4ec6bf View seed option if available (#1518) 2020-04-29 00:55:53 +09:00
ff99ab1239 Improve pay button with custom text (#1520)
Fixes #1517.
2020-04-29 00:52:48 +09:00
2841cd8498 Updates from design system (#1519)
* Use variable names as defined in the design system

* Use bootstrao version from design system
2020-04-29 00:48:55 +09:00
519f4af867 Bump pj original tx broadcast timeout 2020-04-29 00:23:51 +09:00
68cc3aba21 update translations 2020-04-29 00:22:48 +09:00
3a2970a495 Label Factory (#1516)
* Label Factory

* fix typo and format
2020-04-28 16:53:34 +09:00
b31fb1a269 Auto label utxos based on invoice and payjoin (#1499)
* Auto label utxos based on invoice and payjoin

This PR introduces automatic labelling to transactions.
* If it is an incoming tx to an invoice, it will tag it as such.
* If it was a payjoin tx , it will tag it as such.
* If a transaction's inputs were exposed to a payjoin sender, we tag it as such.

* wip

* wip

* support in coinselection

* remove ugly hack

* support wallet transactions page

* remove messy loop

* better label template helpers

* add tests and susbcribe to event

* simplify data

* fix test

* fix label  + color

* fix remove label

* renove useless call

* add toString

* fix potential crash by txid

* trim json keyword in manual label

* format file
2020-04-28 15:06:28 +09:00
3801eeec43 Payjoin: Better UIH1 & UIH2 based selection (#1473)
* Try to make SelectUTXO care about all inputs and outputs

* wip

* wip

* Add test and fix seelctor

* remove space

* review changes

* revert back to index check
2020-04-28 01:28:21 +09:00
94cdd399d5 Finetune colors (#1514)
* Better harmonic neutral color progression

fix

* Invert light/dark colors for dark theme

fix

* Lighten form control borders a bit

* Use correct text color variables
2020-04-27 23:57:18 +09:00
c784144a07 Greenfield: Add update store API (#1495)
* Greenfield: Add update store API

* update update store model

* change greenfield controller name to stop conflict
2020-04-27 20:13:20 +09:00
b600e5777e Add tooltip for store deriv + show "Not set" + "show end piece of string" (#1507)
* Add tooltip for store deriv + show "Not set" + "show end piece of string"

fixes #1506

* use regex witchraft

* adjust with regex
2020-04-27 19:55:46 +09:00
e49074d797 fix bip21 + coinselection combo bug in wallet send 2020-04-27 12:12:01 +02:00
02d26467f9 remove fake bundle 2020-04-27 12:04:18 +02:00
e68b45c76a Update lightning-charge to v0.4.19 (#1511)
Changes the default units from mBTC to sats
2020-04-27 18:44:12 +09:00
f410f7d4d1 Hide LN option for liquid Assets (#1512)
Was getting a bit crowded for stuff that isn't supported yet.
2020-04-27 18:15:38 +09:00
d4dbe6fe17 Typo fix (#1503) 2020-04-27 05:18:36 +09:00
c7305ba5e1 Bump lightning 2020-04-27 04:39:17 +09:00
e11963aca0 Checkout dark theme improvements (#1510)
Increase the contrast for some elements. Closes #1508.
2020-04-27 04:23:03 +09:00
2d77426e04 Bump lightning 2020-04-27 04:10:31 +09:00
7e0f9e1d28 Fix docker-customer-lightning-cli.ps1 pay doesn't work (Fix #1509) 2020-04-27 03:59:16 +09:00
18e181bb9f Bump lightning library, logs channel setup 2020-04-27 03:53:45 +09:00
c3c9585a95 Bump BTCPayServer.Lightning 2020-04-27 02:29:34 +09:00
4b5b941761 Update translations 2020-04-26 15:34:30 +09:00
79c70b31a3 Fix tests 2020-04-26 01:47:47 +09:00
072139f707 bump Nbitcoin 2020-04-26 01:11:50 +09:00
9d80db98c5 bump NBitcoin 2020-04-26 01:01:09 +09:00
a5df029d43 update test to loop through all formats for payjoin 2020-04-25 17:38:04 +02:00
47f16aadd5 Rely on NBitcoin to get the expected hash 2020-04-26 00:26:02 +09:00
f8b2b18c6e Fix PJ (#1502)
* Fix P2SH-P2WPKH case for Payjoin (Fix #1500)

* fix p2sh issue

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2020-04-26 00:19:24 +09:00
4be6c06af5 add payjoin e2e test for all compatible formats 2020-04-25 16:12:16 +02:00
92c58eea7f Fix: Sign with NBX option not present after decoding PSBT (#1497) 2020-04-23 22:02:26 +09:00
5e6049bf3f Payjoin fix: return psbt in same format it was received (#1496)
* Payjoin fix: return psbt in same format it was received

* fix formatting
2020-04-23 22:02:00 +09:00
7adaa146dc Merge pull request #1471 from dennisreimann/api-health-endpoint
GreenField: Add health check endpoint
2020-04-23 02:44:11 +09:00
e3b51f593e Merge pull request #1411 from Kukks/api/stores/get
GreenField: Create/Get/Delete Stores
2020-04-23 02:42:29 +09:00
e64094dfcc fix permissions for client api tester 2020-04-22 15:12:38 +02:00
cb6fcadb86 add scope test 2020-04-22 15:05:13 +02:00
297b84a18b add request name validation 2020-04-22 15:05:13 +02:00
b7c0e049b5 fix bug in permission store selector 2020-04-22 15:05:13 +02:00
aeef160d0b separate store swagger 2020-04-22 15:05:13 +02:00
34c1a304a9 Add Create Store 2020-04-22 15:05:13 +02:00
4f1ae4733c remove store api 2020-04-22 15:05:13 +02:00
e009c1a25a Fixing CheckNoDeadLink test now that btse blocks our call from circleci 2020-04-22 15:05:13 +02:00
79f12a7058 fix client 2020-04-22 15:05:13 +02:00
deb197cfa5 GreenField: Get Stores 2020-04-22 15:05:13 +02:00
2710130667 add error message instead of 500 status
fixes #1494
2020-04-22 14:18:36 +02:00
48163961ed Rename LCAD image (#1493)
Fixes #1491.
2020-04-22 08:47:44 +02:00
8658cb5f29 Re-add versioning for btcpay.js (#1489) 2020-04-22 08:24:29 +02:00
a6a56e4791 fix controller 2020-04-21 17:09:17 +02:00
22e39998e2 Updates from code review
Thanks @kukks!
2020-04-21 16:43:14 +02:00
70d1056d48 API: Health endpoint returns synchronized state 2020-04-21 16:29:54 +02:00
3bd5c3e1b5 API: Add health check endpoint 2020-04-21 14:59:33 +02:00
0a1a4fd3b5 Merge pull request #1490 from bolatovumar/fix-1488
Make both sides of payment request table look same
2020-04-21 20:15:33 +09:00
e508b22d34 Make both sides of payment request table look same
fix #1488
2020-04-20 20:45:59 -07:00
a7815f107e Merge pull request #1485 from Kukks/fix/pay-request-url
Fix: Payment request redirects to non-existing (404) URL after paymen…
2020-04-20 18:39:59 +09:00
ded5670108 Merge pull request #1486 from Kukks/hotwalletwarning
Show warning when using hot wallet on non admin
2020-04-20 01:56:18 +09:00
c1ffeb331b Fix typo 2020-04-20 01:47:27 +09:00
ad1148d3e2 Merge pull request #1487 from ketominer/master
Fix MySQL support (fixed width columns instead of blobs) replaces #1484
2020-04-19 21:26:36 +09:00
a7b926d907 fixed Address field size (back) 2020-04-19 13:48:05 +02:00
1f7a821c09 Show warning when using hot wallet on non admin
closes #1475
2020-04-19 13:45:51 +02:00
bf45edb5d8 Fix: Payment request redirects to non-existing (404) URL after payment completed
fixes #1479
2020-04-19 13:12:07 +02:00
686f5bf151 Fix MySQL support (fixed width columns instead of blobs) 2020-04-19 11:15:06 +02:00
426fe793e6 remove versioning for btcpay.js
fixes #1483
2020-04-19 09:29:20 +02:00
aee55103a3 Merge pull request #1477 from dennisreimann/asset-cache-busting
Improve static asset caching
2020-04-19 05:32:20 +09:00
778bf97079 Merge pull request #1478 from dennisreimann/assert-link-not-dead
Improve AssertLinkNotDead test output in case of exception
2020-04-19 03:10:45 +09:00
2dcb3111f8 Add changelog 2020-04-19 03:05:23 +09:00
03d1f98402 Improve AssertLinkNotDead test output in case of exception
If the test fails with an exception other than the `EqualException` the failed URL isn't logged. This general exception handler takes care of e.g. `HttpRequestException` and reports the URL that failed to be checked.

Stumbled upon this while checking why [this test run](https://app.circleci.com/pipelines/github/btcpayserver/btcpayserver/2934/workflows/0525d9fd-e4de-49dc-957b-d98b16a9abd4/jobs/7199/parallel-runs/0/steps/0-102) fails.
2020-04-18 19:33:53 +02:00
34755b32dc bump 2020-04-19 02:09:12 +09:00
6679ee1ca2 Merge pull request #1472 from Kukks/payjoin/p2shfixes
Fix Payjoin p2sh
2020-04-19 02:01:10 +09:00
8420c74b31 Improve static asset caching
Cache static assets for one year and set the correct cache control header. Adds the cache busting version based on file content to asset references to invalidate the cache on change. ([further details on the approach](https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/) and [why one year](https://ashton.codes/set-cache-control-max-age-1-year/))

Most of the changes are the additions of the `asp-append-version="true"` attribute, the main configuration change is in `Startup.cs`.
2020-04-18 17:56:05 +02:00
0077105a2d remove hacks 2020-04-18 08:29:07 +02:00
51db617584 fixes 2020-04-18 08:09:49 +02:00
514b695907 fix coin addition for p2sh 2020-04-18 08:09:49 +02:00
b470ce2dad fix p2sh test error codes 2020-04-18 08:09:49 +02:00
161850150a fix p2sh detection input 2020-04-18 08:09:49 +02:00
b02cfa9d41 fix subtract fees auto tick when clicking on balance 2020-04-17 15:24:27 +02:00
dfe655393d Fix enable payjoin when p2sh 2020-04-16 17:23:29 +02:00
8c81dae167 bump 2020-04-16 22:44:53 +09:00
297 changed files with 10631 additions and 3888 deletions

View File

@ -119,22 +119,23 @@ workflows:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags v1.0.0.88
# only act on version tags v1.0.0.88 or v1.0.2-1
# OR feature tags like vlndseedbackup
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
- arm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
- multiarch:
requires:
- amd64
@ -144,4 +145,4 @@ workflows:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/

View File

@ -5,7 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.29" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Client
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default)
@ -33,7 +33,7 @@ namespace BTCPayServer.Client
if (apikey == null)
throw new ArgumentNullException(nameof(apikey));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
}
}

View File

@ -0,0 +1,16 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ApiHealthData> GetHealth(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/health"), token);
return await HandleResponse<ApiHealthData>(response);
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/info",
method: HttpMethod.Get), token);
return await HandleResponse<LightningNodeInformationData>(response);
}
public async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels",
method: HttpMethod.Get), token);
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public async Task<string> OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task<string> GetLightningDepositAddress(string cryptoCode, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/address", method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/{invoiceId}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<LightningInvoiceData>(response);
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/info",
method: HttpMethod.Get), token);
return await HandleResponse<LightningNodeInformationData>(response);
}
public async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels",
method: HttpMethod.Get), token);
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public async Task<string> OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/address", method: HttpMethod.Post),
token);
return await HandleResponse<string>(response);
}
public async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{invoiceId}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<LightningInvoiceData>(response);
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token);
return await HandleResponse<IEnumerable<PaymentRequestData>>(response);
}
public virtual async Task<PaymentRequestData> GetPaymentRequest(string storeId, string paymentRequestId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}"), token);
return await HandleResponse<PaymentRequestData>(response);
}
public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<PaymentRequestData>(response);
}
public virtual async Task<PaymentRequestData> UpdatePaymentRequest(string storeId, string paymentRequestId,
UpdatePaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", bodyPayload: request,
method: HttpMethod.Put), token);
return await HandleResponse<PaymentRequestData>(response);
}
}
}

View File

@ -0,0 +1,16 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ServerInfoData> GetServerInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
return await HandleResponse<ServerInfoData>(response);
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<StoreData>> GetStores(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/stores"), token);
return await HandleResponse<IEnumerable<StoreData>>(response);
}
public virtual async Task<StoreData> GetStore(string storeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}"), token);
return await HandleResponse<StoreData>(response);
}
public virtual async Task RemoveStore(string storeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}", method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<StoreData> CreateStore(CreateStoreRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/stores", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<StoreData>(response);
}
public virtual async Task<StoreData> UpdateStore(string storeId, UpdateStoreRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}", bodyPayload: request, method: HttpMethod.Put), token);
return await HandleResponse<StoreData>(response);
}
}
}

View File

@ -43,14 +43,25 @@ namespace BTCPayServer.Client
_httpClient = httpClient ?? new HttpClient();
}
protected void HandleResponse(HttpResponseMessage message)
protected async Task HandleResponse(HttpResponseMessage message)
{
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
{
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); ;
throw new GreenFieldValidationException(err);
}
else if (message.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
throw new GreenFieldAPIException(err);
}
message.EnsureSuccessStatusCode();
}
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
{
HandleResponse(message);
await HandleResponse(message);
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
}
@ -96,7 +107,7 @@ namespace BTCPayServer.Client
foreach (KeyValuePair<string, object> keyValuePair in payload)
{
UriBuilder uriBuilder = uri;
if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
if (!(keyValuePair.Value is string) && keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
{
foreach (var item in (IEnumerable)keyValuePair.Value)
{

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Linq;
namespace BTCPayServer.Client
{
public class GreenFieldAPIException : Exception
{
public GreenFieldAPIException(Models.GreenfieldAPIError error):base(error.Message)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
APIError = error;
}
public Models.GreenfieldAPIError APIError { get; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public class GreenFieldValidationException : Exception
{
public GreenFieldValidationException(Models.GreenfieldValidationError[] errors) : base(BuildMessage(errors))
{
ValidationErrors = errors;
}
private static string BuildMessage(GreenfieldValidationError[] errors)
{
if (errors == null)
throw new ArgumentNullException(nameof(errors));
StringBuilder builder = new StringBuilder();
foreach (var error in errors)
{
builder.AppendLine($"{error.Path}: {error.Message}");
}
return builder.ToString();
}
public Models.GreenfieldValidationError[] ValidationErrors { get; }
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.JsonConverters
{
public class DecimalStringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(decimal) || objectType == typeof(decimal?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.String:
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
case JTokenType.Null when objectType == typeof(decimal?):
return null;
default:
throw new JsonSerializationException("Unexpected token type: " +
token.Type);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((decimal)value).ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@ -0,0 +1,15 @@
using System.Globalization;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class LightMoneyJsonConverter : BTCPayServer.Lightning.JsonConverters.LightMoneyJsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((LightMoney)value).MilliSatoshi.ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class MoneyJsonConverter : NBitcoin.JsonConverters.MoneyJsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
return new Money( long.Parse((string) reader.Value));
}
return base.ReadJson(reader, objectType, existingValue, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((Money)value).Satoshi.ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class NodeUriJsonConverter : JsonConverter<NodeInfo>
{
public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException(reader.Path, "Unexpected token type for NodeUri");
if (NodeInfo.TryParse((string)reader.Value, out var info))
return info;
throw new JsonObjectException(reader.Path, "Invalid NodeUri");
}
public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer)
{
if (value is NodeInfo)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class TimeSpanJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
var nullable = objectType == typeof(TimeSpan?);
if (reader.TokenType == JsonToken.Null)
{
if (nullable)
return null;
return TimeSpan.Zero;
}
if (reader.TokenType != JsonToken.Integer)
throw new JsonObjectException("Invalid timespan, expected integer", reader);
return TimeSpan.FromSeconds((long)reader.Value);
}
catch
{
throw new JsonObjectException("Invalid locktime", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is TimeSpan s)
{
writer.WriteValue((int)s.TotalSeconds);
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class ApiHealthData
{
public bool Synchronized { get; set; }
}
}

View File

@ -7,6 +7,7 @@ namespace BTCPayServer.Client.Models
{
public string ApiKey { get; set; }
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}

View File

@ -6,33 +6,20 @@ namespace BTCPayServer.Client.Models
/// the id of the user
/// </summary>
public string Id { get; set; }
/// <summary>
/// the email AND username of the user
/// </summary>
public string Email { get; set; }
/// <summary>
/// Whether the user has verified their email
/// </summary>
public bool EmailConfirmed { get; set; }
/// <summary>
/// whether the user needed to verify their email on account creation
/// </summary>
public bool RequiresEmailConfirmation { get; set; }
}
public class CreateApplicationUserRequest
{
/// <summary>
/// the email AND username of the new user
/// </summary>
public string Email { get; set; }
/// <summary>
/// password of the new user
/// </summary>
public string Password { get; set; }
/// <summary>
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
/// </summary>
public bool? IsAdministrator { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class ConnectToNodeRequest
{
public ConnectToNodeRequest()
{
}
public ConnectToNodeRequest(NodeInfo nodeInfo)
{
NodeURI = nodeInfo;
}
[JsonConverter(typeof(NodeUriJsonConverter))]
[JsonProperty("nodeURI")]
public NodeInfo NodeURI { get; set; }
}
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
@ -9,6 +6,7 @@ namespace BTCPayServer.Client.Models
public class CreateApiKeyRequest
{
public string Label { get; set; }
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
public Permission[] Permissions { get; set; }
}

View File

@ -0,0 +1,20 @@
namespace BTCPayServer.Client.Models
{
public class CreateApplicationUserRequest
{
/// <summary>
/// the email AND username of the new user
/// </summary>
public string Email { get; set; }
/// <summary>
/// password of the new user
/// </summary>
public string Password { get; set; }
/// <summary>
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
/// </summary>
public bool? IsAdministrator { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Security.Cryptography;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class CreateLightningInvoiceRequest
{
public CreateLightningInvoiceRequest()
{
}
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
{
Amount = amount;
Description = description;
Expiry = expiry;
}
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string Description { get; set; }
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
public TimeSpan Expiry { get; set; }
public bool PrivateRouteHints { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class CreatePaymentRequestRequest : PaymentRequestBaseData
{
}
}

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class CreateStoreRequest: StoreBaseData
{
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class GreenfieldAPIError
{
public GreenfieldAPIError()
{
}
public GreenfieldAPIError(string code, string message)
{
if (code == null)
throw new ArgumentNullException(nameof(code));
if (message == null)
throw new ArgumentNullException(nameof(message));
Code = code;
Message = message;
}
public string Code { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class GreenfieldValidationError
{
public GreenfieldValidationError()
{
}
public GreenfieldValidationError(string path, string message)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (message == null)
throw new ArgumentNullException(nameof(message));
Path = path;
Message = message;
}
public string Path { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class LightningInvoiceData
{
public string Id { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public LightningInvoiceStatus Status { get; set; }
[JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset ExpiresAt { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountReceived { get; set; }
}
}

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LightningNodeInformationData
{
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
public NodeInfo[] NodeURIs { get; set; }
public int BlockHeight { get; set; }
}
public class LightningChannelData
{
public string RemoteNode { get; set; }
public bool IsPublic { get; set; }
public bool IsActive { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Capacity { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney LocalBalance { get; set; }
public string ChannelPoint { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using MoneyJsonConverter = BTCPayServer.Client.JsonConverters.MoneyJsonConverter;
namespace BTCPayServer.Client.Models
{
public class OpenLightningChannelRequest
{
[JsonConverter(typeof(NodeUriJsonConverter))]
[JsonProperty("nodeURI")]
public NodeInfo NodeURI { get; set; }
[JsonConverter(typeof(MoneyJsonConverter))]
public Money ChannelAmount { get; set; }
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace BTCPayServer.Client.Models
{
public class PayLightningInvoiceRequest
{
[Newtonsoft.Json.JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class PaymentRequestBaseData
{
[JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))]
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class PaymentRequestData : PaymentRequestBaseData
{
[JsonConverter(typeof(StringEnumConverter))]
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
public DateTimeOffset Created { get; set; }
public string Id { get; set; }
public bool Archived { get; set; }
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models
{
public class ServerInfoData
{
/// <summary>
/// the BTCPay Server version
/// </summary>
public string Version { get; set; }
/// <summary>
/// the Tor hostname
/// </summary>
public string Onion { get; set; }
/// <summary>
/// the payment methods this server supports
/// </summary>
public IEnumerable<string> SupportedPaymentMethods { get; set; }
/// <summary>
/// are all chains fully synched
/// </summary>
public bool FullySynched { get; set; }
/// <summary>
/// detailed sync information per chain
/// </summary>
public IEnumerable<ServerInfoSyncStatusData> SyncStatus { get; set; }
}
public class ServerInfoSyncStatusData
{
public string CryptoCode { get; set; }
public int ChainHeight { get; set; }
public int? SyncHeight { get; set; }
public ServerInfoNodeData NodeInformation { get; set; }
}
public class ServerInfoNodeData
{
public int Headers { get; set; }
public int Blocks { get; set; }
public double VerificationProgress { get; set; }
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public abstract class StoreBaseData
{
/// <summary>
/// the name of the store
/// </summary>
public string Name { get; set; }
public string Website { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
[JsonConverter(typeof(TimeSpanJsonConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
[JsonConverter(typeof(StringEnumConverter))]
public SpeedPolicy SpeedPolicy { get; set; }
public string LightningDescriptionTemplate { get; set; }
public double PaymentTolerance { get; set; } = 0;
public bool AnyoneCanCreateInvoice { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool ShowRecommendedFee { get; set; } = true;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int RecommendedFeeBlockTarget { get; set; } = 1;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string DefaultLang { get; set; } = "en";
public bool LightningAmountInSatoshi { get; set; }
public string CustomLogo { get; set; }
public string CustomCSS { get; set; }
public string HtmlTitle { get; set; }
public bool RedirectAutomatically { get; set; }
public bool RequiresRefundEmail { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
public bool PayJoinEnabled { get; set; }
public bool LightningPrivateRouteHints { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
public enum SpeedPolicy
{
HighSpeed = 0,
MediumSpeed = 1,
LowSpeed = 2,
LowMediumSpeed = 3
}
}

View File

@ -0,0 +1,10 @@
namespace BTCPayServer.Client.Models
{
public class StoreData : StoreBaseData
{
/// <summary>
/// the id of the store
/// </summary>
public string Id { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class UpdatePaymentRequestRequest : PaymentRequestBaseData
{
}
}

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class UpdateStoreRequest : StoreBaseData
{
}
}

View File

@ -6,10 +6,16 @@ namespace BTCPayServer.Client
{
public class Policies
{
public const string CanCreateLightningInvoiceInternalNode = "btcpay.server.cancreatelightninginvoiceinternalnode";
public const string CanCreateLightningInvoiceInStore = "btcpay.store.cancreatelightninginvoice";
public const string CanUseInternalLightningNode = "btcpay.server.canuseinternallightningnode";
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
public const string CanViewProfile = "btcpay.user.canviewprofile";
public const string CanCreateUser = "btcpay.server.cancreateuser";
@ -22,10 +28,16 @@ namespace BTCPayServer.Client
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
yield return CanViewPaymentRequests;
yield return CanModifyPaymentRequests;
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanCreateUser;
yield return Unrestricted;
yield return CanUseInternalLightningNode;
yield return CanCreateLightningInvoiceInternalNode;
yield return CanUseLightningNodeInStore;
yield return CanCreateLightningInvoiceInStore;
}
}
public static bool IsValidPolicy(string policy)
@ -45,14 +57,14 @@ namespace BTCPayServer.Client
}
public class Permission
{
public static Permission Create(string policy, string storeId = null)
public static Permission Create(string policy, string scope = null)
{
if (TryCreatePermission(policy, storeId, out var r))
if (TryCreatePermission(policy, scope, out var r))
return r;
throw new ArgumentException("Invalid Permission");
}
public static bool TryCreatePermission(string policy, string storeId, out Permission permission)
public static bool TryCreatePermission(string policy, string scope, out Permission permission)
{
permission = null;
if (policy == null)
@ -60,9 +72,9 @@ namespace BTCPayServer.Client
policy = policy.Trim().ToLowerInvariant();
if (!Policies.IsValidPolicy(policy))
return false;
if (storeId != null && !Policies.IsStorePolicy(policy))
if (scope != null && !Policies.IsStorePolicy(policy))
return false;
permission = new Permission(policy, storeId);
permission = new Permission(policy, scope);
return true;
}
@ -96,12 +108,10 @@ namespace BTCPayServer.Client
}
}
internal Permission(string policy, string storeId)
internal Permission(string policy, string scope)
{
Policy = policy;
StoreId = storeId;
Scope = scope;
}
public bool Contains(Permission subpermission)
@ -115,7 +125,7 @@ namespace BTCPayServer.Client
}
if (!Policies.IsStorePolicy(subpermission.Policy))
return true;
return StoreId == null || subpermission.StoreId == this.StoreId;
return Scope == null || subpermission.Scope == this.Scope;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
@ -135,23 +145,30 @@ namespace BTCPayServer.Client
return true;
if (this.Policy == subpolicy)
return true;
if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile)
return true;
return false;
switch (subpolicy)
{
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
return true;
default:
return false;
}
}
public string StoreId { get; }
public string Scope { get; }
public string Policy { get; }
public override string ToString()
{
if (StoreId != null)
if (Scope != null)
{
return $"{Policy}:{StoreId}";
return $"{Policy}:{Scope}";
}
return Policy;
}

View File

@ -26,7 +26,8 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
SupportRBF = true,
SupportLightning = false
});
Add(new ElementsBTCPayNetwork()
@ -49,7 +50,8 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
SupportRBF = true,
SupportLightning = false
});
Add(new ElementsBTCPayNetwork()
@ -71,7 +73,8 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
SupportRBF = true,
SupportLightning = false
});
}
}

View File

@ -23,6 +23,31 @@ namespace BTCPayServer
});
}
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
{
return new TransactionInformationSet()
{
Transactions =
transactionInformationSet.Transactions.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
};
}
return new GetTransactionsResponse()
{
Height = response.Height,
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
ReplacedTransactions = Filter(response.ReplacedTransactions),
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
};
}
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
//precision 0: 10 = 0.00000010

View File

@ -62,6 +62,7 @@ namespace BTCPayServer
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string UriScheme { get; internal set; }
public bool SupportPayJoin { get; set; } = false;
public bool SupportLightning { get; set; } = true;
public KeyPath GetRootKeyPath(DerivationType type)
{
@ -119,6 +120,11 @@ namespace BTCPayServer
{
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
}
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
return response;
}
}
public abstract class BTCPayNetworkBase

View File

@ -4,6 +4,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.9" />
<PackageReference Include="NBXplorer.Client" Version="3.0.15" />
</ItemGroup>
</Project>

View File

@ -3,10 +3,16 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -79,6 +79,8 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool Archived { get; set; }
public List<PendingInvoiceData> PendingInvoices { get; set; }
}
}

View File

@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Data
{
@ -12,34 +13,13 @@ namespace BTCPayServer.Data
get; set;
}
public string StoreDataId { get; set; }
public bool Archived { get; set; }
public StoreData StoreData { get; set; }
public PaymentRequestStatus Status { get; set; }
public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; }
public byte[] Blob { get; set; }
public class PaymentRequestBlob
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
}
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View File

@ -1,98 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using System.Security.Claims;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Data
{
public enum SpeedPolicy
{
HighSpeed = 0,
MediumSpeed = 1,
LowSpeed = 2,
LowMediumSpeed = 3
}
public class StoreData
{
public string Id
{
get;
set;
}
public string Id { get; set; }
public List<UserStore> UserStores { get; set; }
public List<UserStore> UserStores
{
get; set;
}
public List<AppData> Apps
{
get; set;
}
public List<PaymentRequestData> PaymentRequests
{
get; set;
}
public List<AppData> Apps { get; set; }
public List<PaymentRequestData> PaymentRequests { get; set; }
public List<InvoiceData> Invoices { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get; set;
}
public string DerivationStrategy { get; set; }
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies
{
get;
set;
}
public string DerivationStrategies { get; set; }
public string StoreName
{
get; set;
}
public string StoreName { get; set; }
public SpeedPolicy SpeedPolicy
{
get; set;
}
public SpeedPolicy SpeedPolicy { get; set; } = SpeedPolicy.MediumSpeed;
public string StoreWebsite
{
get; set;
}
public string StoreWebsite { get; set; }
public byte[] StoreCertificate
{
get; set;
}
public byte[] StoreCertificate { get; set; }
[NotMapped]
public string Role
{
get; set;
}
[NotMapped] public string Role { get; set; }
public byte[] StoreBlob { get; set; }
public byte[] StoreBlob
{
get;
set;
}
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
}

View File

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Data
@ -16,69 +17,9 @@ namespace BTCPayServer.Data
public byte[] Blob { get; set; }
}
public class Label
{
public Label(string value, string color)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (color == null)
throw new ArgumentNullException(nameof(color));
Value = value;
Color = color;
}
public string Value { get; }
public string Color { get; }
public override bool Equals(object obj)
{
Label item = obj as Label;
if (item == null)
return false;
return Value.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
}
public static bool operator ==(Label a, Label b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.Value == b.Value;
}
public static bool operator !=(Label a, Label b)
{
return !(a == b);
}
public override int GetHashCode()
{
return Value.GetHashCode(StringComparison.OrdinalIgnoreCase);
}
}
public class WalletBlobInfo
{
public Dictionary<string, string> LabelColors { get; set; } = new Dictionary<string, string>();
public IEnumerable<Label> GetLabels(WalletTransactionInfo transactionInfo)
{
foreach (var label in transactionInfo.Labels)
{
if (LabelColors.TryGetValue(label, out var color))
{
yield return new Label(label, color);
}
}
}
public IEnumerable<Label> GetLabels()
{
foreach (var kv in LabelColors)
{
yield return new Label(kv.Key, kv.Value);
}
}
}
}

View File

@ -114,8 +114,8 @@ namespace BTCPayServer.Migrations
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(nullable: false),
ProviderKey = table.Column<string>(nullable: false),
LoginProvider = table.Column<string>(nullable: false, maxLength: 255),
ProviderKey = table.Column<string>(nullable: false, maxLength: 255),
ProviderDisplayName = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: false, maxLength: maxLength)
},
@ -159,8 +159,8 @@ namespace BTCPayServer.Migrations
columns: table => new
{
UserId = table.Column<string>(nullable: false, maxLength: maxLength),
LoginProvider = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: false),
LoginProvider = table.Column<string>(nullable: false, maxLength: 64),
Name = table.Column<string>(nullable: false, maxLength: 64),
Value = table.Column<string>(nullable: true)
},
constraints: table =>

View File

@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
Label = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
PairingTime = table.Column<DateTimeOffset>(nullable: false),
SIN = table.Column<string>(nullable: true),
SIN = table.Column<string>(nullable: true, maxLength: maxLength),
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength)
},
constraints: table =>

View File

@ -23,7 +23,7 @@ namespace BTCPayServer.Migrations
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false, maxLength: maxLength),
Address = table.Column<string>(nullable: false),
Address = table.Column<string>(nullable: false, maxLength: this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)512 : null),
Assigned = table.Column<DateTimeOffset>(nullable: false),
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
},

View File

@ -0,0 +1,38 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20200507092343_AddArchivedToInvoice")]
public class AddArchivedToInvoice : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Invoices",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "PaymentRequests",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Archived",
table: "Invoices");
migrationBuilder.DropColumn(
name: "Archived",
table: "PaymentRequests");
}
}
}
}

View File

@ -190,6 +190,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -348,6 +351,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");

View File

@ -4,14 +4,16 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
<EmbeddedResource Include="Currencies.json" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -6,35 +6,22 @@ using System.Reflection;
using System.Linq;
using System.Text;
using System.Globalization;
using BTCPayServer.Rating;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Rates
{
public class CurrencyData
{
public string Name
{
get;
internal set;
}
public string Code
{
get;
internal set;
}
public int Divisibility
{
get;
internal set;
}
public string Symbol
{
get;
internal set;
}
public string Name { get; set; }
public string Code { get; set; }
public int Divisibility { get; set; }
public string Symbol { get; set; }
public bool Crypto { get; set; }
}
public class CurrencyNameTable
{
public static CurrencyNameTable Instance = new CurrencyNameTable();
public CurrencyNameTable()
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
@ -94,16 +81,10 @@ namespace BTCPayServer.Services.Rates
catch { }
}
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
foreach (var curr in _Currencies.Where(pair => pair.Value.Crypto))
{
AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode);
AddCurrency(_CurrencyProviders, curr.Key, curr.Value.Divisibility, curr.Value.Symbol?? curr.Value.Code);
}
_CurrencyProviders.TryAdd("SATS",
new NumberFormatInfo()
{
CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3
});
}
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
}
@ -153,58 +134,15 @@ namespace BTCPayServer.Services.Rates
static CurrencyData[] LoadCurrency()
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt");
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Rating.Currencies.json");
string content = null;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>();
foreach (var currency in currencies)
{
var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length < 3)
continue;
CurrencyData info = new CurrencyData();
info.Name = splitted[0];
info.Code = splitted[1];
int divisibility;
if (!int.TryParse(splitted[2], out divisibility))
continue;
info.Divisibility = divisibility;
if (!dico.ContainsKey(info.Code))
dico.Add(info.Code, info);
if (splitted.Length >= 4)
{
info.Symbol = splitted[3];
}
}
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = network.Divisibility,
Name = network.CryptoCode,
Crypto = true
}))
{
dico[network.CryptoCode].Crypto = true;
}
}
dico.TryAdd("SATS", new CurrencyData()
{
Code = "SATS",
Crypto = true,
Divisibility = 0,
Name = "Satoshis",
Symbol = "Sats",
});
return dico.Values.ToArray();
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
return currencies;
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)

View File

@ -1,10 +1,10 @@
using System;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Rating
{
public class CurrencyPair
{
static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
public CurrencyPair(string left, string right)
{
if (right == null)
@ -47,13 +47,14 @@ namespace BTCPayServer.Rating
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
return true;
}
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);
var network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(potentialCryptoName);
if (network != null)
var currency = CurrencyNameTable.Instance.GetCurrencyData(potentialCryptoName, false);
if (currency != null)
{
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));
value = new CurrencyPair(currency.Code, currencyPair.Substring(i));
return true;
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace BTCPayServer.Rating
{
public static class Extensions
{
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
{
if (value != 0m)
{
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
}
return value;
}
}
}

View File

@ -1,13 +1,10 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using System.Threading;
using Microsoft.Extensions.Logging.Abstractions;
using BTCPayServer.Logging;
using Newtonsoft.Json;
using System.Reflection;
using System.Globalization;

View File

@ -530,7 +530,10 @@ namespace BTCPayServer.Rating
var rewriter = new ReplaceExchangeRateRewriter();
rewriter.Rates = ExchangeRates;
var result = rewriter.Visit(this.expression);
Errors.AddRange(rewriter.Errors);
foreach (var item in rewriter.Errors)
{
Errors.Add(item);
}
_Evaluated = result.NormalizeWhitespace("", "\n").ToString();
if (HasError)
return false;
@ -539,7 +542,10 @@ namespace BTCPayServer.Rating
calculate.Visit(result);
if (calculate.Values.Count != 1 || calculate.Errors.Count != 0)
{
Errors.AddRange(calculate.Errors);
foreach (var item in calculate.Errors)
{
Errors.Add(item);
}
return false;
}
_BidAsk = calculate.Values.Pop();

View File

@ -14,6 +14,7 @@ using Newtonsoft.Json;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Tests
{
@ -191,7 +192,7 @@ namespace BTCPayServer.Tests
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
var unrestricted = Permission.Create(Policies.Unrestricted, null);
var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Policies.CanModifyStoreSettings);
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
{
var resultStores =
@ -201,11 +202,11 @@ namespace BTCPayServer.Tests
foreach (var selectiveStorePermission in selectiveStorePermissions)
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit",
$"{TestApiPath}/me/stores/{selectiveStorePermission.Scope}/can-edit",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase));
data => data.Id.Equals(selectiveStorePermission.Scope, StringComparison.InvariantCultureIgnoreCase));
}
bool shouldBeAuthorized = false;

View File

@ -23,10 +23,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="80.0.3987.10600" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="83.0.4103.3900" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -147,6 +147,10 @@ namespace BTCPayServer.Tests
config.AppendLine($"socksendpoint={SocksEndpoint}");
config.AppendLine($"debuglog=debug.log");
if (SocksHTTPProxy is string v)
{
config.AppendLine($"sockshttpproxy={v}");
}
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
config.AppendLine($"sshpassword={SSHPassword}");
@ -291,6 +295,8 @@ namespace BTCPayServer.Tests
public string SSHPassword { get; internal set; }
public string SSHKeyFile { get; internal set; }
public string SSHConnection { get; set; }
public string SocksHTTPProxy { get; set; }
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
{
var context = new DefaultHttpContext();

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;

View File

@ -37,14 +37,13 @@ namespace BTCPayServer.Tests
{
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);
Assert.Equal(3, Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets.Count);
}
}

View File

@ -0,0 +1,74 @@
using System.Threading.Channels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.Tests
{
public class FakeServer : IDisposable
{
IWebHost webHost;
SemaphoreSlim semaphore;
CancellationTokenSource cts = new CancellationTokenSource();
public FakeServer()
{
_channel = Channel.CreateUnbounded<HttpContext>();
semaphore = new SemaphoreSlim(0);
}
Channel<HttpContext> _channel;
public async Task Start()
{
webHost = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://127.0.0.1:0")
.Configure(appBuilder =>
{
appBuilder.Run(async ctx =>
{
await _channel.Writer.WriteAsync(ctx);
await semaphore.WaitAsync(cts.Token);
});
})
.Build();
await webHost.StartAsync();
var port = new Uri(webHost.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First(), UriKind.Absolute)
.Port;
ServerUri = new Uri($"http://127.0.0.1:{port}/");
}
public Uri ServerUri { get; set; }
public void Done()
{
semaphore.Release();
}
public async Task Stop()
{
await webHost.StopAsync();
}
public void Dispose()
{
cts.Dispose();
webHost?.Dispose();
semaphore.Dispose();
}
public async Task<HttpContext> GetNextRequest()
{
return await _channel.Reader.ReadAsync();
}
}
}

View File

@ -1,17 +1,27 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection.Metadata;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
using BTCPayServer.JsonConverters;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNet.SignalR.Client;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Org.BouncyCastle.Utilities.Collections;
using Xunit;
using Xunit.Abstractions;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
using JsonReader = Newtonsoft.Json.JsonReader;
namespace BTCPayServer.Tests
{
@ -23,7 +33,7 @@ namespace BTCPayServer.Tests
public GreenfieldAPITests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
@ -44,10 +54,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(apiKeyData);
Assert.Equal(client.APIKey, apiKeyData.ApiKey);
Assert.Single(apiKeyData.Permissions);
//a client using Basic Auth has no business here
await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo());
//revoke current api key
await client.RevokeCurrentAPIKeyInfo();
await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo());
@ -55,6 +65,7 @@ namespace BTCPayServer.Tests
await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo());
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteAPIKeyViaAPI()
@ -68,18 +79,19 @@ namespace BTCPayServer.Tests
var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
});
Assert.Equal("Hello world", apiKey.Label);
var p = Assert.Single(apiKey.Permissions);
Assert.Equal(Policies.CanViewProfile, p.Policy);
var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey);
await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world2",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
}));
await AssertHttpError(403,
async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world2",
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
}));
await unrestricted.RevokeAPIKey(apiKey.ApiKey);
await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey));
@ -95,35 +107,54 @@ namespace BTCPayServer.Tests
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" }));
await AssertValidationError(new[] { "Email", "Password" },
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test@gmail.com"}));
// Pass too simple
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" }));
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"}));
// We have no admin, so it should work
var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
var user1 = await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test@gmail.com", Password = "abceudhqw"});
// We have no admin, so it should work
var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
var user2 = await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"});
// Duplicate email
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }));
await AssertValidationError(new[] { "Email" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}));
// Let's make an admin
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true });
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true
});
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(401,
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"}));
// But should be ok with subscriptions unlocked
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" });
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"});
// But it should be forbidden to create an admin without being authenticated
await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = true });
await AssertHttpError(403,
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = true});
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
@ -131,32 +162,110 @@ namespace BTCPayServer.Tests
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
// We should be forbidden to create a new user without proper admin permissions
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await AssertHttpError(403,
async () => await adminClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"}));
await AssertHttpError(403,
async () => await adminClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
// However, should be ok with the unrestricted permissions of an admin
adminClient = await adminAcc.CreateClient(Policies.Unrestricted);
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
await adminClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"});
// Even creating new admin should be ok
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
await adminClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
});
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings);
// User1 trying to get server management would still fail to create user
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403,
async () => await user1Client.CreateUser(
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"}));
// User1 should be able to create user if subscription unlocked
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" });
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
await user1Client.CreateUser(
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"});
// But not an admin
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await AssertHttpError(403,
async () => await user1Client.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoresControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
//create store
var newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
//update store
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() {Name = "B"});
Assert.Equal("B", updatedStore.Name);
Assert.Equal("B", (await client.GetStore(newStore.Id)).Name);
//list stores
var stores = await client.GetStores();
var storeIds = stores.Select(data => data.Id);
var storeNames = stores.Select(data => data.Name);
Assert.NotNull(stores);
Assert.Equal(2, stores.Count());
Assert.Contains(newStore.Id, storeIds);
Assert.Contains(user.StoreId, storeIds);
//get store
var store = await client.GetStore(user.StoreId);
Assert.Equal(user.StoreId, store.Id);
Assert.Contains(store.Name, storeNames);
//remove store
await client.RemoveStore(newStore.Id);
await AssertHttpError(403, async () =>
{
await client.GetStore(newStore.Id);
});
Assert.Single(await client.GetStores());
newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
var scopedClient =
await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
Assert.Single(await scopedClient.GetStores());
}
}
private async Task AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
foreach (var field in fields)
{
Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray());
remainingFields.Remove(field);
}
Assert.Empty(remainingFields);
}
private async Task AssertHttpError(int code, Func<Task> act)
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
@ -190,43 +299,200 @@ namespace BTCPayServer.Tests
await clientProfile.GetCurrentUser();
await clientBasic.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
}));
var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser);
var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser2);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}",
Password = Guid.NewGuid().ToString()
}));
await AssertValidationError(new[] { "Email" }, async () =>
await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Password = Guid.NewGuid().ToString()
}));
await AssertValidationError(new[] { "Password" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",}));
await AssertValidationError(new[] { "Email" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()}));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task HealthControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
var apiHealthData = await unauthClient.GetHealth();
Assert.NotNull(apiHealthData);
Assert.True(apiHealthData.Synchronized);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ServerInfoControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(401, async () => await unauthClient.GetServerInfo());
var user = tester.NewAccount();
user.GrantAccess();
var clientBasic = await user.CreateClient();
var serverInfoData = await clientBasic.GetServerInfo();
Assert.NotNull(serverInfoData);
Assert.NotNull(serverInfoData.Version);
Assert.NotNull(serverInfoData.Onion);
Assert.True(serverInfoData.FullySynched);
Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods);
Assert.NotNull(serverInfoData.SyncStatus);
Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC"));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task PaymentControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
//create payment request
//validation errors
await AssertValidationError(new[] { "Amount", "Currency" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"});
});
await AssertValidationError(new[] { "Amount" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0});
});
await AssertValidationError(new[] { "Currency" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
});
await AssertHttpError(403, async () =>
{
await viewOnly.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
});
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "USD", Amount = 1});
//list payment request
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
Assert.NotNull(paymentRequests);
Assert.Single(paymentRequests);
Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id);
//get payment request
var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(newPaymentRequest.Title, paymentRequest.Title);
//update payment request
var updateRequest = JObject.FromObject(paymentRequest).ToObject<UpdatePaymentRequestRequest>();
updateRequest.Title = "B";
await AssertHttpError(403, async () =>
{
await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
});
await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(updateRequest.Title, paymentRequest.Title);
//archive payment request
await AssertHttpError(403, async () =>
{
await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
});
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
Assert.DoesNotContain(paymentRequest.Id,
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
//let's test some payment stuff
await user.RegisterDerivationSchemeAsync("BTC");
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Amount = 0.1m, Currency = "BTC", Title = "Payment test title"});
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<PaymentRequestController>()
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void DecimalStringJsonConverterTests()
{
JsonReader Get(string val)
{
return new JsonTextReader(new StringReader(val));
}
var jsonConverter = new DecimalStringJsonConverter();
Assert.True(jsonConverter.CanConvert(typeof(decimal)));
Assert.True(jsonConverter.CanConvert(typeof(decimal?)));
Assert.False(jsonConverter.CanConvert(typeof(double)));
Assert.False(jsonConverter.CanConvert(typeof(float)));
Assert.False(jsonConverter.CanConvert(typeof(int)));
Assert.False(jsonConverter.CanConvert(typeof(string)));
var numberJson = "1";
var numberDecimalJson = "1.2";
var stringJson = "\"1.2\"";
Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null));
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null));
Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null));
Assert.Throws<JsonSerializationException>(() =>
{
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
});
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null));
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null));
}
}
}

View File

@ -66,10 +66,6 @@ namespace BTCPayServer.Tests
FeeSatoshiPerByte = 1,
CurrentBalance = 1.5m
};
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmLedger.WebsocketPath);
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
@ -79,20 +75,25 @@ namespace BTCPayServer.Tests
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
SigningContext = new SigningContextModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
SigningContext = new SigningContextModel()
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
@ -120,8 +121,11 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
@ -134,7 +138,7 @@ namespace BTCPayServer.Tests
var postRedirectView = Assert.IsType<ViewResult>(view);
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
Assert.Equal(actionName, postRedirectViewModel.AspAction);
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value;
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value;
return redirectedPSBT;
}
}

View File

@ -18,6 +18,8 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -26,6 +28,9 @@ using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
@ -38,7 +43,7 @@ namespace BTCPayServer.Tests
public PayJoinTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
@ -73,16 +78,16 @@ namespace BTCPayServer.Tests
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<PayJoinRepository>();
var outpoint = RandomOutpoint();
// Should not be locked
Assert.False(await repo.TryUnlock(outpoint));
// Can lock input
Assert.True(await repo.TryLockInputs(new [] { outpoint }));
Assert.True(await repo.TryLockInputs(new[] { outpoint }));
// Can't twice
Assert.False(await repo.TryLockInputs(new [] { outpoint }));
Assert.False(await repo.TryLockInputs(new[] { outpoint }));
Assert.False(await repo.TryUnlock(outpoint));
// Lock and unlock outpoint utxo
Assert.True(await repo.TryLock(outpoint));
Assert.True(await repo.TryUnlock(outpoint));
@ -90,6 +95,45 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task ChooseBestUTXOsForPayjoin()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
//Only one utxo, so obvious result
var utxos = new[] { FakeUTXO(1.0m) };
var paymentAmount = 0.5m;
var otherOutputs = new[] { 0.5m };
var inputs = new[] { 1m };
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo));
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
utxos = new[] { FakeUTXO(0.3m), FakeUTXO(0.7m) };
paymentAmount = 0.5m;
otherOutputs = new[] { 0.5m };
inputs = new[] { 1m };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
//when there is no change, anything works
utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) };
paymentAmount = 0.5m;
otherOutputs = new decimal[0];
inputs = new[] { 0.03m, 0.07m };
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
}
}
private Transaction RandomTransaction(BTCPayNetwork network)
{
var tx = network.NBitcoinNetwork.CreateTransaction();
@ -98,6 +142,15 @@ namespace BTCPayServer.Tests
return tx;
}
private UTXO FakeUTXO(decimal amount)
{
return new UTXO()
{
Value = new Money(amount, MoneyUnit.BTC),
Outpoint = RandomOutpoint()
};
}
private OutPoint RandomOutpoint()
{
return new OutPoint(RandomUtils.GetUInt256(), 0);
@ -121,18 +174,18 @@ namespace BTCPayServer.Tests
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
.AssertType<ScriptPubKeyType[]>()
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
var senderUser = tester.NewAccount();
senderUser.GrantAccess(true);
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
var receiverUser = tester.NewAccount();
receiverUser.GrantAccess(true);
@ -141,13 +194,16 @@ namespace BTCPayServer.Tests
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
var clientShouldError = unsupportedFormats.Contains(senderAddressType);
var errorCode = ( unsupportedFormats.Contains( receiverAddressType) || receiverAddressType != senderAddressType)? "unsupported-inputs" : null;
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = 50000, Currency = "sats", FullNotifications = true});
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
if (unsupportedFormats.Contains(receiverAddressType))
{
Assert.Null(TestAccount.GetPayjoinEndpoint(invoice, cashCow.Network));
continue;
}
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.AddCoins(senderCoin);
txBuilder.Send(invoiceAddress, invoice.BtcDue);
txBuilder.SetChange(await senderUser.GetNewAddress(network));
@ -157,132 +213,314 @@ namespace BTCPayServer.Tests
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
}
}
}
}
[Fact]
[Trait("Selenium", "Selenium")]
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePayjoinViaUI()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
// var payjoinRepository = s.Server.PayTester.GetService<PayJoinRepository>();
// var broadcaster = s.Server.PayTester.GetService<DelayedTransactionBroadcaster>();
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
//payjoin is not enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToHome();
s.GoToStore(receiver.storeId);
//payjoin is not enabled by default.
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
s.SetCheckbox(s,"PayJoinEnabled", true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToWalletSend(senderWalletId);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
foreach (var format in PayjoinClient.SupportedFormats)
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
//no funds in receiver wallet to do payjoin
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
s.GoToInvoices();
var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
//payjoin is not enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeName);
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
//let's do it all again, except now the receiver has funds and is able to payjoin
invoiceId = s.CreateInvoice(receiver.storeId);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
s.GoToHome();
s.GoToStore(receiver.storeId);
//payjoin is not enabled by default.
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
s.SetCheckbox(s, "PayJoinEnabled", true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
s.GoToWalletSend(senderWalletId);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("1");
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments();
Assert.Equal(2, payments.Count);
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
Assert.Equal(-1, ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.Equal(0, ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.False(originalPayment.Accounted);
Assert.True(coinjoinPayment.Accounted);
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
Assert.Equal(originalPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value,
coinjoinPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value);
});
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
s.GoToInvoices();
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
invoiceId = s.CreateInvoice(receiver.storeName);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl"))
.GetAttribute("value")));
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
//no funds in receiver wallet to do payjoin
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
s.GoToInvoices();
var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}"))
.FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
StringComparison.InvariantCultureIgnoreCase));
//let's do it all again, except now the receiver has funds and is able to payjoin
invoiceId = s.CreateInvoice(receiver.storeName);
s.GoToInvoiceCheckout(invoiceId);
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl"))
.GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("1");
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
return Task.CompletedTask;
});
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments();
Assert.Equal(2, payments.Count);
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
Assert.Equal(-1,
((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.Equal(0,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
Assert.False(originalPayment.Accounted);
Assert.True(coinjoinPayment.Accounted);
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
Assert.Equal(originalPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value,
coinjoinPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value);
});
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
});
s.GoToInvoices();
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}"))
.FindElement(By.ClassName("payment-value"));
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
StringComparison.InvariantCultureIgnoreCase));
TestUtils.Eventually(() =>
{
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
Assert.Contains(invoiceId, s.Driver.PageSource);
Assert.Contains("payjoin", s.Driver.PageSource);
//this label does not always show since input gets used
// Assert.Contains("payjoin-exposed", s.Driver.PageSource);
});
}
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoin2()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var pjClient = tester.PayTester.GetService<PayjoinClient>();
var nbx = tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC");
var notifications = await nbx.CreateWebsocketNotificationSessionAsync();
var alice = tester.NewAccount();
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
var address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
{
Destinations =
{
new CreatePSBTDestination()
{
Amount = Money.Coins(0.5m),
Destination = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest)
}
},
FeePreference = new FeePreference()
{
ExplicitFee = Money.Satoshis(3000)
}
})).PSBT;
var derivationSchemeSettings = alice.GetController<WalletsController>().GetDerivationSchemeSettings(new WalletId(alice.StoreId, "BTC"));
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
var changeIndex = Array.FindIndex(psbt.Outputs.ToArray(), (PSBTOutput o) => o.ScriptPubKey.IsScriptType(ScriptType.P2WPKH));
using var fakeServer = new FakeServer();
await fakeServer.Start();
var requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
var request = await fakeServer.GetNextRequest();
Assert.Equal("1", request.Request.Query["v"][0]);
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
Assert.Equal("3000", request.Request.Query["maxadditionalfeecontribution"][0]);
Logs.Tester.LogInformation("The payjoin receiver tries to make us pay lots of fee");
var originalPSBT = await ParsePSBT(request);
var proposalTx = originalPSBT.GetGlobalTransaction();
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3001);
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
fakeServer.Done();
var ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("too much fee", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
proposalTx.Outputs.Where((o, i) => i != changeIndex).First().Value += Money.Satoshis(1);
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1);
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
fakeServer.Done();
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("money to himself", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver can't increase the fee rate too much");
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3000);
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
fakeServer.Done();
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("increased the fee rate", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver can't decrease the fee rate too much");
pjClient.MinimumFeeRate = new FeeRate(50m);
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3000);
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
fakeServer.Done();
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("a too low fee rate", ex.Message);
pjClient.MinimumFeeRate = null;
Logs.Tester.LogInformation("Make sure the receiver implementation do not take more fee than allowed");
var bob = tester.NewAccount();
await bob.GrantAccessAsync();
await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync();
bob.ModifyStore(s => s.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
{
Destinations =
{
new CreatePSBTDestination()
{
Amount = invoiceBIP21.Amount,
Destination = invoiceBIP21.Address
}
},
FeePreference = new FeePreference()
{
ExplicitFee = Money.Satoshis(3001)
}
})).PSBT;
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
Assert.True(proposal.TryGetFee(out var newFee));
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
proposal.Finalize();
await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction());
await notifications.NextEventAsync();
Logs.Tester.LogInformation("Abusing minFeeRate should give not enough money error");
invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
{
Destinations =
{
new CreatePSBTDestination()
{
Amount = invoiceBIP21.Amount,
Destination = invoiceBIP21.Address
}
},
FeePreference = new FeePreference()
{
ExplicitFee = Money.Satoshis(3001)
}
})).PSBT;
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
}
}
private static async Task<PSBT> ParsePSBT(Microsoft.AspNetCore.Http.HttpContext request)
{
var bytes = await request.Request.Body.ReadBytesAsync(int.Parse(request.Request.Headers["Content-Length"].First()));
var str = Encoding.UTF8.GetString(bytes);
return PSBT.Parse(str, Network.RegTest);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoinFeeCornerCase()
@ -309,14 +547,15 @@ namespace BTCPayServer.Tests
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string lastInvoiceId = null;
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money", OriginalTxBroadcasted: true);
async Task<PSBT> RunVector(bool skipLockedCheck = false)
{
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true});
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true });
lastInvoiceId = invoice.Id;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.OptInRBF = true;
txBuilder.AddCoins(coin);
txBuilder.Send(invoiceAddress, vector.Paid);
txBuilder.SendFees(vector.Fee);
@ -327,6 +566,10 @@ namespace BTCPayServer.Tests
if (vector.ExpectedError is null)
{
Assert.Contains(pj.Inputs, o => o.PrevOut == receiverCoin.Outpoint);
foreach (var input in pj.GetGlobalTransaction().Inputs)
{
Assert.Equal(Sequence.OptInRBF, input.Sequence);
}
if (!skipLockedCheck)
Assert.True(await payjoinRepository.TryUnlock(receiverCoin.Outpoint));
}
@ -345,6 +588,19 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", invoice.Status);
});
}
psbt.Finalize();
var broadcasted = await tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC").BroadcastAsync(psbt.ExtractTransaction(), true);
if (vector.OriginalTxBroadcasted)
{
Assert.Equal("txn-already-in-mempool", broadcasted.RPCCodeMessage);
}
else
{
Assert.True(broadcasted.Success);
}
receiverCoin = await receiverUser.ReceiveUTXO(receiverCoin.Amount, network);
await LockAllButReceiverCoin();
return pj;
}
@ -363,32 +619,35 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
"there is not enough to pay the additional payjoin input. (going below the min relay fee" +
"However, the original tx has been broadcasted!");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money", OriginalTxBroadcasted: true);
await RunVector();
await LockAllButReceiverCoin();
Logs.Tester.LogInformation("We don't pay enough");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid", OriginalTxBroadcasted: true);
await RunVector();
Logs.Tester.LogInformation("We pay correctly");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
await RunVector();
await LockAllButReceiverCoin();
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
"The receiver should have added a fake output");
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
var proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
await LockAllButReceiverCoin();
PSBT proposedPSBT = null;
var outputCountReceived = new bool[2];
do
{
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
"The receiver should have added a fake output");
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.True(proposedPSBT.Outputs.Count == 1 || proposedPSBT.Outputs.Count == 2);
outputCountReceived[proposedPSBT.Outputs.Count - 1] = true;
cashCow.Generate(1);
} while (outputCountReceived.All(o => o));
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
"However, this has the side effect of having the receiver broadcasting the original tx");
await payjoinRepository.TryLock(receiverCoin.Outpoint);
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "out-of-utxos");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "unavailable|any UTXO available", OriginalTxBroadcasted: true);
await RunVector(true);
await LockAllButReceiverCoin();
var originalSenderUser = senderUser;
retry:
@ -398,7 +657,7 @@ namespace BTCPayServer.Tests
// The send pay remaining 86 sat from his pocket
// So total paid by sender should be 86 + 510 + 200 so we should get 1090 - (86 + 510 + 200) == 294 back)
Logs.Tester.LogInformation($"Check if we can take fee on overpaid utxo{(senderUser == receiverUser ? " (to self)" : "")}");
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
Assert.Contains(proposedPSBT.Outputs, o => o.Value == Money.Satoshis(500) + receiverCoin.Amount);
@ -430,14 +689,14 @@ namespace BTCPayServer.Tests
}
else
{
senderUser = originalSenderUser;
senderUser = originalSenderUser;
}
// Same as above. Except the sender send one satoshi less, so the change
// output would get below dust and would be removed completely.
// So we remove as much fee as we can, and still accept the transaction because it is above minrelay fee
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
// We should have our payment
@ -451,7 +710,7 @@ namespace BTCPayServer.Tests
Assert.True(result.Success);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUsePayjoin()
@ -459,7 +718,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>();
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(btcPayNetwork);
@ -471,7 +730,7 @@ namespace BTCPayServer.Tests
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
var invoice = senderUser.BitPay.CreateInvoice(
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
new Invoice() { Price = 100, Currency = "USD", FullNotifications = true });
//payjoin is not enabled by default.
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
@ -484,7 +743,7 @@ namespace BTCPayServer.Tests
await receiverUser.EnablePayJoin();
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
Money.Coins(0.06m));
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
@ -506,13 +765,21 @@ namespace BTCPayServer.Tests
//Let's start the harassment
invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
// Bad version should throw incorrect version
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork);
var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2",
new StringContent("", Encoding.UTF8, "text/plain"));
Assert.False(response.IsSuccessStatusCode);
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal("version-unsupported", error["errorCode"].Value<string>());
Assert.Equal(1, ((JArray)error["supported"]).Single().Value<int>());
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice2 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
@ -585,6 +852,7 @@ namespace BTCPayServer.Tests
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
//Result: Second Tx should be rejected.
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
var contributedInputsInvoice1Coin1ResponseTx =
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
@ -592,9 +860,9 @@ namespace BTCPayServer.Tests
//Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used");
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, expectedError: "unavailable|Some of those inputs have already been used");
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
var contributedInputsInvoice2Coin2ResponseTx =
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
@ -603,13 +871,13 @@ namespace BTCPayServer.Tests
//Result: reject on 4: the protocol should not worry about this complexity
var invoice3 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice4 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
@ -630,7 +898,7 @@ namespace BTCPayServer.Tests
//Result: proposed tx consolidates the outputs
var invoice5 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
@ -655,7 +923,7 @@ namespace BTCPayServer.Tests
new Money(0.1m, MoneyUnit.BTC)));
var invoice6 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
@ -670,7 +938,7 @@ namespace BTCPayServer.Tests
var invoice6Coin5 = invoice6Coin5TxBuilder
.BuildTransaction(true);
var Invoice6Coin5Response1Tx =await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
var Invoice6Coin5Response1Tx = await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
//broadcast the first payjoin
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
@ -698,17 +966,18 @@ namespace BTCPayServer.Tests
new Money(0.1m, MoneyUnit.BTC)));
var invoice7 = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
var txBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.OptInRBF = true;
var invoice7Coin6TxBuilder = txBuilder
.SetChange(senderChange)
.Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount)
.AddCoins(coin6.Coin)
.AddKeys(extKey.Derive(coin6.KeyPath))
.SendEstimatedFees(new FeeRate(100m))
.SetLockTime(0);
.SendEstimatedFees(new FeeRate(100m));
var invoice7Coin6Tx = invoice7Coin6TxBuilder
.BuildTransaction(true);
@ -717,14 +986,14 @@ namespace BTCPayServer.Tests
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
var contributedInputsInvoice7Coin6Response1TxSigned =
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
//broadcast the payjoin
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
Assert.True(res.Success);
// Paid with coinjoin
await TestUtils.EventuallyAsync(async () =>
{

View File

@ -61,34 +61,38 @@ namespace BTCPayServer.Tests
Description = "description"
};
var id = (Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request)).RouteValues.Values.First().ToString());
//permission guard for guests editing
Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(id));
request.Title = "update";
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Title);
Assert.False(string.IsNullOrEmpty(id));
Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model);
//Delete
Assert.IsType<ConfirmModel>(Assert
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
//Archive
Assert
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
Assert.True(Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived);
Assert.Empty(Assert.IsType<ListPaymentRequestsViewModel>(Assert.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items);
//unarchive
Assert
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
Assert.False(Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived);
Assert.Single(Assert.IsType<ListPaymentRequestsViewModel>(Assert.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items);
}
}

View File

@ -22,6 +22,7 @@ using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.Interactions;
@ -123,7 +124,7 @@ namespace BTCPayServer.Tests
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
@ -131,6 +132,8 @@ namespace BTCPayServer.Tests
Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.WaitForElement(By.Id("ScriptPubKeyType")).Click();
Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Logs.Tester.LogInformation("Trying to click btn-generate");
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
AssertHappyMessage();
@ -296,7 +299,7 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
}
public string CreateInvoice(string store, decimal amount = 100, string currency = "USD", string refundEmail = "")
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
@ -305,7 +308,7 @@ namespace BTCPayServer.Tests
currencyEl.Clear();
currencyEl.SendKeys(currency);
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName + Keys.Enter);
Driver.FindElement(By.Id("Create")).ForceClick();
Assert.True(Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
@ -316,7 +319,7 @@ namespace BTCPayServer.Tests
public async Task FundStoreWallet(WalletId walletId, int coins = 1, decimal denomination = 1m)
{
GoToWalletReceive(walletId);
GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
@ -333,7 +336,7 @@ namespace BTCPayServer.Tests
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
GoToWalletSend(walletId);
GoToWallet(walletId, WalletsNavPages.Send);
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
@ -369,14 +372,13 @@ namespace BTCPayServer.Tests
}
public void GoToWalletSend(WalletId walletId)
public void GoToWallet(WalletId walletId, WalletsNavPages navPages = WalletsNavPages.Send)
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/send"));
}
internal void GoToWalletReceive(WalletId walletId)
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/receive"));
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}"));
if (navPages != WalletsNavPages.Transactions)
{
Driver.FindElement(By.Id($"Wallet{navPages}")).Click();
}
}
}
}

View File

@ -14,6 +14,9 @@ using NBitcoin.Payment;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Views.Wallets;
using Newtonsoft.Json;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Tests
{
@ -239,10 +242,26 @@ namespace BTCPayServer.Tests
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
s.GoToInvoices();
s.CreateInvoice(store);
var invoiceId = s.CreateInvoice(store);
s.AssertHappyMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
//let's test archiving an invoice
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
s.AssertHappyMessage();
Assert.Contains("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
//check that it no longer appears in list
s.GoToInvoices();
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
//ok, let's unarchive and see that it shows again
s.Driver.Navigate().GoToUrl(invoiceUrl);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
s.AssertHappyMessage();
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
s.GoToInvoices();
Assert.Contains(invoiceId, s.Driver.PageSource);
// When logout we should not be able to access store and invoice details
s.Driver.FindElement(By.Id("Logout")).Click();
@ -361,11 +380,20 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("EnableShoppingCart")).Click();
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter);
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
s.Driver.Url = posBaseUrl + "/static";
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
s.Driver.Url = posBaseUrl + "/cart";
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
s.Driver.Quit();
}
}
@ -432,7 +460,7 @@ namespace BTCPayServer.Tests
var storeId = s.CreateNewStore().storeId;
s.GenerateWallet("BTC", "", false, true);
var walletId = new WalletId(storeId, "BTC");
s.GoToWalletReceive(walletId);
s.GoToWallet(walletId, WalletsNavPages.Receive);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
@ -456,7 +484,7 @@ namespace BTCPayServer.Tests
coin => coin.OutPoint == spentOutpoint);
});
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToWalletSend(walletId);
s.GoToWallet(walletId, WalletsNavPages.Send);
s.Driver.FindElement(By.Id("advancedSettings")).Click();
s.Driver.FindElement(By.Id("toggleInputSelection")).Click();
s.Driver.WaitForElement(By.Id(spentOutpoint.ToString()));
@ -540,7 +568,7 @@ namespace BTCPayServer.Tests
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeId);
var invoiceId = s.CreateInvoice(storeId.storeName);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
@ -553,7 +581,7 @@ namespace BTCPayServer.Tests
//lets import and save private keys
var root = mnemonic.DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeId);
invoiceId = s.CreateInvoice(storeId.storeName);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
@ -638,6 +666,13 @@ namespace BTCPayServer.Tests
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
s.AssertHappyMessage();
Assert.Equal(mnemonic.ToString(), s.Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text);
}
}
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)

View File

@ -56,6 +56,7 @@ namespace BTCPayServer.Tests
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
// since in dev we already can have some users / stores registered, while on CI database is being initalized
// for the first time and first registered user gets admin status by default
SocksHTTPProxy = GetEnvironment("TESTS_SOCKSHTTP", "http://127.0.0.1:8118/"),
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")
};
@ -96,7 +97,7 @@ namespace BTCPayServer.Tests
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);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
PayTester.UseLightning = true;
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
}
@ -118,6 +119,7 @@ namespace BTCPayServer.Tests
public async Task EnsureChannelsSetup()
{
Logs.Tester.LogInformation("Connecting channels");
BTCPayServer.Lightning.Tests.ConnectChannels.Logs = Logs.LogProvider.CreateLogger("Connect channels");
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
Logs.Tester.LogInformation("Channels connected");
}

View File

@ -23,6 +23,7 @@ using BTCPayServer.Data;
using Microsoft.AspNetCore.Identity;
using NBXplorer.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Events;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
@ -70,10 +71,22 @@ namespace BTCPayServer.Tests
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new ManageController.AddApiKeyViewModel()
{
PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem()
PermissionValues = permissions.Select(s =>
{
Permission = s,
Value = true
Permission.TryParse(s, out var p);
return p;
}).GroupBy(permission => permission.Policy).Select(p =>
{
var stores = p.Where(permission => !string.IsNullOrEmpty(permission.Scope))
.Select(permission => permission.Scope).ToList();
return new ManageController.AddApiKeyViewModel.PermissionValueItem()
{
Permission = p.Key,
Forbidden = false,
StoreMode = stores.Any()? ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific: ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores,
SpecificStores = stores,
Value = true
};
}).ToList()
}));
var statusMessage = manageController.TempData.GetStatusMessageModel();
@ -124,6 +137,13 @@ namespace BTCPayServer.Tests
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public Task ModifyStoreAsync(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
return storeController.UpdateStore(store);
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
@ -133,6 +153,10 @@ namespace BTCPayServer.Tests
public async Task CreateStoreAsync()
{
if (UserId is null)
{
await RegisterAsync();
}
var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() {Name = "Test Store"});
StoreId = store.CreatedStoreId;
@ -149,6 +173,8 @@ namespace BTCPayServer.Tests
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
bool importKeysToNBX = false)
{
if (StoreId is null)
await CreateStoreAsync();
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
@ -174,18 +200,9 @@ namespace BTCPayServer.Tests
return new WalletId(StoreId, cryptoCode);
}
public async Task EnablePayJoin()
public Task EnablePayJoin()
{
var storeController = parent.PayTester.GetController<StoresController>(UserId, StoreId);
var storeVM =
Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeController.UpdateStore()).Model);
storeVM.PayJoinEnabled = true;
Assert.Equal(nameof(storeController.UpdateStore),
Assert.IsType<RedirectToActionResult>(
await storeController.UpdateStore(storeVM)).ActionName);
return ModifyStoreAsync(s => s.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
@ -235,23 +252,39 @@ namespace BTCPayServer.Tests
public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
{
if (isMerchant)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
@ -314,7 +347,7 @@ namespace BTCPayServer.Tests
var endpoint = GetPayjoinEndpoint(invoice, psbt.Network);
if (endpoint == null)
{
return null;
throw new InvalidOperationException("No payjoin endpoint for the invoice");
}
var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>();
@ -338,7 +371,10 @@ namespace BTCPayServer.Tests
else
{
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
Assert.Equal(expectedError, ex.ErrorCode);
var split = expectedError.Split('|');
Assert.Equal(split[0], ex.ErrorCode);
if (split.Length > 1)
Assert.Contains(split[1], ex.ReceiverMessage);
}
return null;
}
@ -363,9 +399,13 @@ namespace BTCPayServer.Tests
new StringContent(content, Encoding.UTF8, "text/plain"));
if (expectedError != null)
{
var split = expectedError.Split('|');
Assert.False(response.IsSuccessStatusCode);
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(expectedError, error["errorCode"].Value<string>());
if (split.Length > 0)
Assert.Equal(split[0], error["errorCode"].Value<string>());
if (split.Length > 1)
Assert.Contains(split[1], error["message"].Value<string>());
return null;
}
else
@ -381,7 +421,7 @@ namespace BTCPayServer.Tests
return response;
}
private static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
public static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
{
var parsedBip21 = new BitcoinUrlBuilder(
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,

View File

@ -63,6 +63,8 @@ using BTCPayServer.Security.Bitpay;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
using Newtonsoft.Json.Schema;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using TwentyTwenty.Storage;
namespace BTCPayServer.Tests
{
@ -101,9 +103,13 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var sresp = await tester.PayTester.HttpClient.GetAsync("swagger/v1/swagger.json");
var acc = tester.NewAccount();
JObject swagger = JObject.Parse(await sresp.Content.ReadAsStringAsync());
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<HomeController>(acc.UserId, acc.StoreId)
.Swagger()).Value.ToJson();
JObject swagger = JObject.Parse(sresp);
using HttpClient client = new HttpClient();
var resp = await client.GetAsync(
"https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json");
@ -111,12 +117,13 @@ namespace BTCPayServer.Tests
IList<ValidationError> errors;
bool valid = swagger.IsValid(schema, out errors);
//the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas.
if (!valid && errors.Count == 1 && errors.Any(error =>
error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf))
var matchedError = errors.Where(error =>
error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf).ToList();
foreach (ValidationError validationError in matchedError)
{
errors = new List<ValidationError>();
valid = true;
errors.Remove(validationError);
}
valid = !errors.Any();
Assert.Empty(errors);
Assert.True(valid);
@ -158,9 +165,11 @@ namespace BTCPayServer.Tests
Assert.Equal(HttpStatusCode.OK, (await httpClient.SendAsync(request)).StatusCode);
Logs.Tester.LogInformation($"OK: {url} ({file})");
}
catch (EqualException ex)
catch (Exception ex)
{
Logs.Tester.LogInformation($"FAILED: {url} ({file}) {ex.Actual}");
var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message;
Logs.Tester.LogInformation($"FAILED: {url} ({file}) {details}");
throw;
}
}
@ -604,6 +613,12 @@ namespace BTCPayServer.Tests
{
Assert.Equal("Object not found", ex.Errors.First());
}
var req = new HttpRequestMessage(HttpMethod.Get, "/invoices/Cy9jfK82eeEED1T3qhwF3Y");
req.Headers.TryAddWithoutValidation("Authorization", "Basic dGVzdA==");
req.Content = new StringContent("{}", Encoding.UTF8, "application/json");
var result = await tester.PayTester.HttpClient.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
Assert.Equal(0, result.Content.Headers.ContentLength.Value);
}
}
@ -618,7 +633,7 @@ namespace BTCPayServer.Tests
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
})
{
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
@ -728,6 +743,91 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningAPI()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
var merchant = tester.NewAccount();
merchant.GrantAccess(true);
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60)));
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient("btcpay.server.canuseinternallightningnode");
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("503", err.Message);
// Not permission for the store!
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC"));
Assert.Contains("403", err.Message);
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}");
// Not permission for the server
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("403", err.Message);
var data = await client.GetLightningNodeChannels(user.StoreId, "BTC");
Assert.Equal(2, data.Count());
BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest);
invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = merchantInvoice.BOLT11
});
await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = "lol"
}));
var validationErr = await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
{
Amount = -1,
Expiry = TimeSpan.FromSeconds(-1),
Description = null
}));
Assert.Equal(2, validationErr.ValidationErrors.Length);
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
Assert.NotNull(invoice.PaidAt);
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
}
}
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
@ -889,12 +989,16 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseTorClient()
[Theory(Timeout = TestTimeout)]
[Xunit.InlineData(true)]
[Xunit.InlineData(false)]
public async Task CanUseTorClient(bool useInternalTorSocksProxy)
{
using (var tester = ServerTester.Create())
{
if (useInternalTorSocksProxy)
tester.PayTester.SocksHTTPProxy = null;
await tester.StartAsync();
var proxy = tester.PayTester.GetService<Socks5HttpProxyServer>();
void AssertConnectionDropped()
@ -1895,6 +1999,20 @@ namespace BTCPayServer.Tests
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
}
}
[Fact]
[Trait("Fast", "Fast")]
public void HasCurrencyDataForNetworks()
{
var btcPayNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
foreach (var network in btcPayNetworkProvider.GetAll())
{
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
Assert.NotNull(cd);
Assert.Equal(network.Divisibility, cd.Divisibility);
Assert.True(cd.Crypto);
}
}
[Fact]
[Trait("Fast", "Fast")]
@ -2107,7 +2225,7 @@ namespace BTCPayServer.Tests
.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.ElectrumWalletFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM
@ -2118,7 +2236,7 @@ namespace BTCPayServer.Tests
"{\"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>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
@ -2130,7 +2248,7 @@ namespace BTCPayServer.Tests
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
DerivationSchemeSettings.TryParseFromColdcard(content, onchainBTC.Network, out var expected);
DerivationSchemeSettings.TryParseFromElectrumWallet(content, onchainBTC.Network, out var expected);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
@ -2256,14 +2374,17 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
public async Task CanUsePoSApp()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
@ -2297,7 +2418,7 @@ donation:
var publicApps = user.GetController<AppsPublicController>();
var vmview =
Assert.IsType<ViewPointOfSaleViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
@ -2309,7 +2430,7 @@ donation:
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
//
var invoices = user.BitPay.GetInvoices();
@ -2320,7 +2441,7 @@ donation:
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
@ -2330,7 +2451,7 @@ donation:
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
@ -2346,7 +2467,7 @@ donation:
ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true),
(Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0,
ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false),
(Code: "BTC", ExpectedSymbol: "BTC", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8,
(Code: "BTC", ExpectedSymbol: "", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8,
ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true),
})
{
@ -2371,7 +2492,7 @@ donation:
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
publicApps = user.GetController<AppsPublicController>();
vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
Assert.Equal(test.Code, vmview.CurrencyCode);
Assert.Equal(test.ExpectedSymbol,
vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well);
@ -2402,17 +2523,17 @@ noninventoryitem:
//inventoryitem has 1 item available
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
//we already bought all available stock so this should fail
await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
//inventoryitem has unlimited items available
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
//verify invoices where created
invoices = user.BitPay.GetInvoices();
@ -2434,6 +2555,41 @@ noninventoryitem:
Assert.Equal(1,
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
btconly:
price: 1.0
title: good apple
payment_methods:
- BTC
normal:
price: 1.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal(PaymentTypes.BTCLike.ToString(),
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] {"BTC", "LTC"}.Contains(
s.CryptoCode));
}
}
@ -3396,7 +3552,7 @@ noninventoryitem:
var root = new Mnemonic(
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
.DeriveExtKey();
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
mainnet, out var settings));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
@ -3413,20 +3569,20 @@ noninventoryitem:
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("BTC");
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
@ -3541,6 +3697,34 @@ noninventoryitem:
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckOnionlocationForNonOnionHtmlRequests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var url = tester.PayTester.ServerUri.AbsoluteUri;
// check onion location is present for HTML page request
using var htmlRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
htmlRequest.Headers.TryAddWithoutValidation("Accept", "text/html,*/*");
var htmlResponse = await tester.PayTester.HttpClient.SendAsync(htmlRequest);
htmlResponse.EnsureSuccessStatusCode();
Assert.True(htmlResponse.Headers.TryGetValues("Onion-Location", out var onionLocation));
Assert.StartsWith("http://wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", onionLocation.FirstOrDefault() ?? "no-onion-location-header");
// no onion location for other mime types
using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*");
var otherResponse = await tester.PayTester.HttpClient.SendAsync(otherRequest);
otherResponse.EnsureSuccessStatusCode();
Assert.False(otherResponse.Headers.Contains("Onion-Location"));
}
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();

View File

@ -12,6 +12,7 @@ services:
environment:
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_SOCKSHTTP: http://tor:8118/
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_DB: "Postgres"
@ -78,7 +79,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.24
image: nicolasdorier/nbxplorer:2.1.30
restart: unless-stopped
ports:
- "32838:32838"
@ -127,6 +128,7 @@ services:
deprecatedrpc=signrawtransaction
ports:
- "43782:43782"
- "39388:39388"
expose:
- "43782" # RPC
- "39388" # P2P
@ -136,7 +138,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.8.0-dev
image: btcpayserver/lightning:v0.8.2-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -163,7 +165,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.11-standalone
image: shesek/lightning-charge:0.4.19-standalone
restart: unless-stopped
environment:
NETWORK: regtest
@ -183,7 +185,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.8.0-dev
image: btcpayserver/lightning:v0.8.2-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -228,7 +230,7 @@ services:
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.18.1.1-1
image: btcpayserver/elements:0.18.1.7
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |
@ -261,7 +263,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.8.2-beta
image: btcpayserver/lnd:v0.10.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -281,7 +283,7 @@ services:
debuglevel=debug
trickledelay=1000
ports:
- "53280:8080"
- "35531:8080"
expose:
- "9735"
volumes:
@ -291,7 +293,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.8.2-beta
image: btcpayserver/lnd:v0.10.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -311,7 +313,7 @@ services:
debuglevel=debug
trickledelay=1000
ports:
- "53281:8080"
- "35532:8080"
expose:
- "8080"
- "10009"
@ -327,9 +329,13 @@ services:
container_name: tor
environment:
TOR_PASSWORD: btcpayserver
TOR_EXTRA_ARGS: |
CookieAuthentication 1
HTTPTunnelPort 0.0.0.0:8118
ports:
- "9050:9050" # SOCKS
- "9051:9051" # Tor Control
- "8118:8118" # HTTP Proxy
volumes:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"

View File

@ -1,2 +1,2 @@
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
docker exec -ti $customer_lightning_container_id lightning-cli $args
docker exec -ti $customer_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc $args

View File

@ -1,4 +1,4 @@
#!/bin/bash
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
docker exec -ti $customer_lightning_container_id lightning-cli "$@"
docker exec -ti $customer_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc "$@"

View File

@ -0,0 +1 @@
docker exec -ti btcpayserver_elementsd_liquid elements-cli -datadir="/data" $args

View File

@ -1,2 +1,2 @@
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
docker exec -ti $merchant_lightning_container_id lightning-cli $args
docker exec -ti $merchant_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc $args

View File

@ -1,4 +1,4 @@
#!/bin/bash
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"
docker exec -ti $merchant_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc "$@"

View File

@ -1,5 +1,6 @@
{
"parallelizeTestCollections": false,
"longRunningTestSeconds": 60,
"diagnosticMessages": true
"diagnosticMessages": true,
"methodDisplay": "method"
}

View File

@ -27,21 +27,20 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.9" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.0" />
<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.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
<PackageReference Include="NBitpayClient" Version="1.0.0.38" />
<PackageReference Include="DBriize" Version="1.0.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
@ -202,9 +201,6 @@
<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>
@ -223,5 +219,5 @@
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.Configuration
private set;
}
public EndPoint SocksEndpoint { get; set; }
public Uri SocksHttpProxy { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
get;
@ -171,7 +171,11 @@ namespace BTCPayServer.Configuration
throw new ConfigException("Invalid value for socksendpoint");
SocksEndpoint = endpoint;
}
var socksuri = conf.GetOrDefault<Uri>("sockshttpproxy", null);
if (socksuri != null)
{
SocksHttpProxy = socksuri;
}
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))

View File

@ -44,6 +44,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--sockshttpproxy", "Socks v5 HTTP proxy, this is currently used only for sending payjoin to tor endpoints. You can get a socks http proxy with the HTTPTunnelPort setting on Tor 0.3.2.x. (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);

View File

@ -4,12 +4,9 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" inventory: 5\n" +
" custom: true";
EnableShoppingCart = false;
DefaultView = PosViewType.Static;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
@ -64,6 +61,7 @@ namespace BTCPayServer.Controllers
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
@ -95,13 +93,15 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
StoreId = app.StoreDataId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
DefaultView = vm.DefaultView,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
@ -194,7 +194,6 @@ namespace BTCPayServer.Controllers
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
});
await _AppService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";

View File

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Controllers
@ -40,23 +41,23 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
return View(new ViewPointOfSaleViewModel()
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
@ -84,11 +85,12 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
string email,
string orderId,
@ -105,12 +107,14 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
settings.DefaultView = settings.EnableShoppingCart? PosViewType.Cart : settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
}
string title = null;
var price = 0.0m;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
@ -130,17 +134,23 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
}
}
else
{
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
return NotFound();
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) &&
settings.EnableShoppingCart &&
if (!string.IsNullOrEmpty(posData) &&
settings.DefaultView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
{
@ -182,6 +192,7 @@ namespace BTCPayServer.Controllers
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
@ -270,6 +281,7 @@ namespace BTCPayServer.Controllers
var store = await _AppService.GetStore(app);
var title = settings.Title;
var price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
@ -290,6 +302,13 @@ namespace BTCPayServer.Controllers
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
}
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
@ -311,6 +330,7 @@ namespace BTCPayServer.Controllers
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
}, store, HttpContext.Request.GetAbsoluteRoot(),

View File

@ -12,16 +12,20 @@ namespace BTCPayServer.Controllers
{
public IActionResult Handle(int? statusCode = null)
{
if (statusCode.HasValue)
if (Request.Headers.TryGetValue("Accept", out var v) && v.Any(v => v.Contains("text/html", StringComparison.OrdinalIgnoreCase)))
{
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
if (statusCode.HasValue)
{
var viewName = statusCode.ToString();
return View(viewName);
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
{
var viewName = statusCode.ToString();
return View(viewName);
}
}
return View(statusCode);
}
return View(statusCode);
return this.StatusCode(statusCode.Value);
}
}
}

View File

@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<ActionResult<ApiKeyData>> CreateKey(CreateApiKeyRequest request)
{
if (request is null)
return BadRequest();
return NotFound();
var key = new APIKeyData()
{
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
@ -74,7 +74,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<IActionResult> RevokeKey(string apikey)
{
if (string.IsNullOrEmpty(apikey))
return BadRequest();
return NotFound();
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
return Ok();
else

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Controllers.GreenField
{
public static class GreenFieldUtils
{
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
foreach (var error in modelState)
{
foreach (var errorMessage in error.Value.Errors)
{
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
}
}
return controller.UnprocessableEntity(errors.ToArray());
}
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
{
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
}
}
}

View File

@ -0,0 +1,22 @@
using BTCPayServer.HostedServices;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[Controller]
public class HealthController : ControllerBase
{
[AllowAnonymous]
[HttpGet("~/api/v1/health")]
public ActionResult GetHealth(NBXplorerDashboard dashBoard)
{
ApiHealthData model = new ApiHealthData()
{
Synchronized = dashBoard.IsFullySynched()
};
return Ok(model);
}
}
}

View File

@ -0,0 +1,111 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[LightningUnavailableExceptionFilter]
public class InternalLightningNodeApiController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly LightningClientFactoryService _lightningClientFactory;
public InternalLightningNodeApiController(BTCPayServerOptions btcPayServerOptions,
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
{
_btcPayServerOptions = btcPayServerOptions;
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningClientFactory = lightningClientFactory;
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
{
return base.GetInfo(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
return base.ConnectToNode(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
{
return base.GetChannels(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
return base.OpenChannel(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
return base.GetInvoice(cryptoCode, id);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInternalNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
{
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
{
return null;
}
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
}
}
}

View File

@ -0,0 +1,125 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[LightningUnavailableExceptionFilter]
public class StoreLightningNodeApiController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public StoreLightningNodeApiController(
BTCPayServerOptions btcPayServerOptions,
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
{
_btcPayServerOptions = btcPayServerOptions;
_lightningClientFactory = lightningClientFactory;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
{
return base.GetInfo(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect")]
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
return base.ConnectToNode(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
{
return base.GetChannels(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
return base.OpenChannel(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
return base.GetInvoice(cryptoCode, id);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = HttpContext.GetStoreData();
if (network == null || store == null)
{
return null;
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
!CanUseInternalLightning(doingAdminThings)))
{
return null;
}
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
}
}
}

View File

@ -0,0 +1,299 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers.GreenField
{
public class LightningUnavailableExceptionFilter : Attribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is NBitcoin.JsonConverters.JsonObjectException jsonObject)
{
context.Result = new ObjectResult(new GreenfieldValidationError(jsonObject.Path, jsonObject.Message));
}
else
{
context.Result = new StatusCodeResult(503);
}
context.ExceptionHandled = true;
}
}
public abstract class LightningNodeApiController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly CssThemeManager _cssThemeManager;
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_btcPayServerEnvironment = btcPayServerEnvironment;
_cssThemeManager = cssThemeManager;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
var info = await lightningClient.GetInfo();
return Ok(new LightningNodeInformationData()
{
BlockHeight = info.BlockHeight,
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
});
}
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
if (request?.NodeURI is null)
{
ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to connect to");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var result = await lightningClient.ConnectTo(request.NodeURI);
switch (result)
{
case ConnectionResult.Ok:
return Ok();
case ConnectionResult.CouldNotConnect:
return this.CreateAPIError("could-not-connect", "Could not connect to the remote node");
}
return Ok();
}
public virtual async Task<IActionResult> GetChannels(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
var channels = await lightningClient.ListChannels();
return Ok(channels.Select(channel => new LightningChannelData()
{
Capacity = channel.Capacity,
ChannelPoint = channel.ChannelPoint.ToString(),
IsActive = channel.IsActive,
IsPublic = channel.IsPublic,
LocalBalance = channel.LocalBalance,
RemoteNode = channel.RemoteNode.ToString()
}));
}
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
if (request?.NodeURI is null)
{
ModelState.AddModelError(nameof(request.NodeURI),
"A valid node info was not provided to open a channel with");
}
if (request.ChannelAmount == null)
{
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing");
}
else if (request.ChannelAmount.Satoshi <= 0)
{
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0");
}
if (request.FeeRate == null)
{
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing");
}
else if (request.FeeRate.SatoshiPerByte <= 0)
{
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0");
}
if (ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
{
ChannelAmount = request.ChannelAmount,
FeeRate = request.FeeRate,
NodeInfo = request.NodeURI
});
string errorCode, errorMessage;
switch (response.Result)
{
case OpenChannelResult.Ok:
return Ok();
case OpenChannelResult.AlreadyExists:
errorCode = "channel-already-exists";
errorMessage = "The channel already exists";
break;
case OpenChannelResult.CannotAffordFunding:
errorCode = "cannot-afford-funding";
errorMessage = "Not enough money to open a channel";
break;
case OpenChannelResult.NeedMoreConf:
errorCode = "need-more-confirmations";
errorMessage = "Need to wait for more confirmations";
break;
case OpenChannelResult.PeerNotConnected:
errorCode = "peer-not-connected";
errorMessage = "Not connected to peer";
break;
default:
throw new NotSupportedException("Unknown OpenChannelResult");
}
return this.CreateAPIError(errorCode, errorMessage);
}
public virtual async Task<IActionResult> GetDepositAddress(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
}
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (lightningClient == null || network == null)
{
return NotFound();
}
if (lightningInvoice?.BOLT11 is null ||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
{
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var result = await lightningClient.Pay(lightningInvoice.BOLT11);
switch (result.Result)
{
case PayResult.CouldNotFindRoute:
return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer");
case PayResult.Error:
return this.CreateAPIError("generic-error", result.ErrorDetail);
case PayResult.Ok:
return Ok();
default:
throw new NotSupportedException("Unsupported Payresult");
}
}
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
if (lightningClient == null)
{
return NotFound();
}
var inv = await lightningClient.GetInvoice(id);
if (inv == null)
{
return NotFound();
}
return Ok(ToModel(inv));
}
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
if (lightningClient == null)
{
return NotFound();
}
if (request.Amount < LightMoney.Zero)
{
ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0");
}
if (request.Expiry <= TimeSpan.Zero)
{
ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.PrivateRouteHints
},
CancellationToken.None);
return Ok(ToModel(invoice));
}
private LightningInvoiceData ToModel(LightningInvoice invoice)
{
return new LightningInvoiceData()
{
Amount = invoice.Amount,
Id = invoice.Id,
Status = invoice.Status,
AmountReceived = invoice.AmountReceived,
PaidAt = invoice.PaidAt,
BOLT11 = invoice.BOLT11,
ExpiresAt = invoice.ExpiresAt
};
}
protected bool CanUseInternalLightning(bool doingAdminThings)
{
return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) ||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
}
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);
}
}

View File

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenFieldPaymentRequestsController : ControllerBase
{
private readonly PaymentRequestRepository _paymentRequestRepository;
private readonly CurrencyNameTable _currencyNameTable;
public GreenFieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
CurrencyNameTable currencyNameTable)
{
_paymentRequestRepository = paymentRequestRepository;
_currencyNameTable = currencyNameTable;
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
{
var prs = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, IncludeArchived = false});
return Ok(prs.Items.Select(FromModel));
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
public async Task<ActionResult<PaymentRequestData>> GetPaymentRequest(string storeId, string paymentRequestId)
{
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
if (pr.Total == 0)
{
return NotFound();
}
return Ok(FromModel(pr.Items.First()));
}
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
public async Task<ActionResult> ArchivePaymentRequest(string storeId, string paymentRequestId)
{
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}, IncludeArchived = false});
if (pr.Total == 0)
{
return NotFound();
}
var updatedPr = pr.Items.First();
updatedPr.Archived = true;
await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr);
return Ok();
}
[HttpPost("~/api/v1/stores/{storeId}/payment-requests")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var pr = new PaymentRequestData()
{
StoreDataId = storeId,
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
Created = DateTimeOffset.Now
};
pr.SetBlob(request);
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
return Ok(FromModel(pr));
}
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdatePaymentRequest(string storeId,
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
if (pr.Total == 0)
{
return NotFound();
}
var updatedPr = pr.Items.First();
updatedPr.SetBlob(request);
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
}
private IActionResult Validate(PaymentRequestBaseData data)
{
if (data is null)
return BadRequest();
if (data.Amount <= 0)
{
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");
}
if (string.IsNullOrEmpty(data.Currency) ||
_currencyNameTable.GetCurrencyData(data.Currency, false) == null)
ModelState.AddModelError(nameof(data.Currency), "Invalid currency");
if (string.IsNullOrEmpty(data.Title))
ModelState.AddModelError(nameof(data.Title), "Title is required");
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
return !ModelState.IsValid ? this.CreateValidationError(ModelState) :null;
}
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)
{
var blob = data.GetBlob();
return new Client.Models.PaymentRequestData()
{
Created = data.Created,
Id = data.Id,
Status = data.Status,
Archived = data.Archived,
Amount = blob.Amount,
Currency = blob.Currency,
Description = blob.Description,
Title = blob.Title,
ExpiryDate = blob.ExpiryDate,
Email = blob.Email,
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
EmbeddedCSS = blob.EmbeddedCSS,
CustomCSSLink = blob.CustomCSSLink
};
}
}
}

View File

@ -0,0 +1,76 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.Models;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
public class GreenFieldServerInfoController : Controller
{
private readonly BTCPayServerEnvironment _env;
private readonly NBXplorerDashboard _dashBoard;
private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
public GreenFieldServerInfoController(
BTCPayServerEnvironment env,
NBXplorerDashboard dashBoard,
StoreRepository storeRepository,
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
{
_env = env;
_dashBoard = dashBoard;
_storeRepository = storeRepository;
_userManager = userManager;
_networkProvider = networkProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/info")]
public async Task<ActionResult> ServerInfo()
{
var stores = await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
var supportedPaymentMethods = _paymentMethodHandlerDictionary
.SelectMany(handler => handler.GetSupportedPaymentMethods().Select(id => id.ToString()))
.Distinct();
var syncStatus = _dashBoard.GetAll()
.Where(summary => summary.Network.ShowSyncSummary)
.Select(summary => new ServerInfoSyncStatusData
{
CryptoCode = summary.Network.CryptoCode,
NodeInformation = summary.Status.BitcoinStatus is BitcoinStatus s ? new ServerInfoNodeData()
{
Headers = s.Headers,
Blocks = s.Blocks,
VerificationProgress = s.VerificationProgress
}: null,
ChainHeight = summary.Status.ChainHeight,
SyncHeight = summary.Status.SyncHeight
});
ServerInfoData model = new ServerInfoData
{
FullySynched = _dashBoard.IsFullySynched(),
SyncStatus = syncStatus,
Onion = _env.OnionUrl,
Version = _env.Version,
SupportedPaymentMethods = supportedPaymentMethods
};
return Ok(model);
}
}
}

View File

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenFieldController : ControllerBase
{
private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager;
public GreenFieldController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
{
_storeRepository = storeRepository;
_userManager = userManager;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores")]
public ActionResult<IEnumerable<Client.Models.StoreData>> GetStores()
{
var stores = HttpContext.GetStoresData();
return Ok(stores.Select(FromModel));
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}")]
public ActionResult<Client.Models.StoreData> GetStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
return Ok(FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}")]
public async Task<IActionResult> RemoveStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
if (!_storeRepository.CanDeleteStores())
{
return this.CreateAPIError("unsupported",
"BTCPay Server is using a database server that does not allow you to remove stores.");
}
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
return Ok();
}
[HttpPost("~/api/v1/stores")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var store = new Data.StoreData();
ToModel(request, store);
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
return Ok(FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
ToModel(request, store);
await _storeRepository.UpdateStore(store);
return Ok(FromModel(store));
}
private static Client.Models.StoreData FromModel(Data.StoreData data)
{
var storeBlob = data.GetStoreBlob();
return new Client.Models.StoreData()
{
Id = data.Id,
Name = data.StoreName,
Website = data.StoreWebsite,
SpeedPolicy = data.SpeedPolicy,
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
//blob
//we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
//we do not include ChangellySettings in this model and instead opt to set it in stores/storeid/changelly endpoints
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
NetworkFeeMode = storeBlob.NetworkFeeMode,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
DefaultLang = storeBlob.DefaultLang,
MonitoringExpiration = TimeSpan.FromMinutes(storeBlob.MonitoringExpiration),
InvoiceExpiration = TimeSpan.FromMinutes(storeBlob.InvoiceExpiration),
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
CustomLogo = storeBlob.CustomLogo,
CustomCSS = storeBlob.CustomCSS,
HtmlTitle = storeBlob.HtmlTitle,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
PaymentTolerance = storeBlob.PaymentTolerance,
RedirectAutomatically = storeBlob.RedirectAutomatically,
PayJoinEnabled = storeBlob.PayJoinEnabled,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints
};
}
private static void ToModel(StoreBaseData restModel, Data.StoreData model)
{
var blob = model.GetStoreBlob();
model.StoreName = restModel.Name;
model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website;
model.SpeedPolicy = restModel.SpeedPolicy;
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
//blob
//we do not include DefaultCurrencyPairs;Spread; PreferredExchange; RateScripting; RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
//we do not include ChangellySettings in this model and instead opt to set it in stores/storeid/changelly endpoints
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
blob.DefaultLang = restModel.DefaultLang;
blob.MonitoringExpiration = (int)restModel.MonitoringExpiration.TotalMinutes;
blob.InvoiceExpiration = (int)restModel.InvoiceExpiration.TotalMinutes;
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.CustomLogo = restModel.CustomLogo;
blob.CustomCSS = restModel.CustomCSS;
blob.HtmlTitle = restModel.HtmlTitle;
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice;
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
model.SetStoreBlob(blob);
}
private IActionResult Validate(StoreBaseData request)
{
if (request is null)
{
return BadRequest();
}
if (string.IsNullOrEmpty(request.Name))
ModelState.AddModelError(nameof(request.Name), "Name is missing");
else if(request.Name.Length < 1 || request.Name.Length > 50)
ModelState.AddModelError(nameof(request.Name), "Name can only be between 1 and 50 characters");
if (!string.IsNullOrEmpty(request.Website) && !Uri.TryCreate(request.Website, UriKind.Absolute, out _))
{
ModelState.AddModelError(nameof(request.Website), "Website is not a valid url");
}
if(request.InvoiceExpiration < TimeSpan.FromMinutes(1) && request.InvoiceExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
ModelState.AddModelError(nameof(request.InvoiceExpiration), "InvoiceExpiration can only be between 1 and 34560 mins");
if(request.MonitoringExpiration < TimeSpan.FromMinutes(10) && request.MonitoringExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
ModelState.AddModelError(nameof(request.MonitoringExpiration), "InvoiceExpiration can only be between 10 and 34560 mins");
if(request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
}
}
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NicolasDorier.RateLimits;
using BTCPayServer.Client;
using System.Reflection;
namespace BTCPayServer.Controllers.GreenField
{
@ -62,16 +63,21 @@ namespace BTCPayServer.Controllers.GreenField
[AllowAnonymous]
[HttpPost("~/api/v1/users")]
public async Task<ActionResult<ApplicationUserData>> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
public async Task<IActionResult> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
{
if (request?.Email is null)
return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing"));
if (!Validation.EmailValidator.IsEmail(request.Email))
ModelState.AddModelError(nameof(request.Email), "Email is missing");
if (!string.IsNullOrEmpty(request?.Email) && !Validation.EmailValidator.IsEmail(request.Email))
{
return BadRequest(CreateValidationProblem(nameof(request.Email), "Invalid email"));
ModelState.AddModelError(nameof(request.Email), "Invalid email");
}
if (request?.Password is null)
return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing"));
ModelState.AddModelError(nameof(request.Password), "Password is missing");
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var isAuth = User.Identity.AuthenticationType == GreenFieldConstants.AuthenticationType;
@ -113,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField
{
ModelState.AddModelError(nameof(request.Password), error.Description);
}
return BadRequest(new ValidationProblemDetails(ModelState));
return this.CreateValidationError(ModelState);
}
if (!isAdmin)
{
@ -125,9 +131,12 @@ namespace BTCPayServer.Controllers.GreenField
{
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
if (error.Code == "DuplicateUserName")
ModelState.AddModelError(nameof(request.Email), error.Description);
else
ModelState.AddModelError(string.Empty, error.Description);
}
return BadRequest(new ValidationProblemDetails(ModelState));
return this.CreateValidationError(ModelState);
}
if (request.IsAdministrator is true)
@ -152,13 +161,6 @@ namespace BTCPayServer.Controllers.GreenField
return CreatedAtAction(string.Empty, user);
}
private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage)
{
var modelState = new ModelStateDictionary();
modelState.AddModelError(propertyName, errorMessage);
return new ValidationProblemDetails(modelState);
}
private static ApplicationUserData FromModel(ApplicationUser data)
{
return new ApplicationUserData()

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