Compare commits

...

176 Commits

Author SHA1 Message Date
9b404e330d bump 2019-01-08 23:03:53 +09:00
1667f9b2ef Merge pull request #504 from Kukks/bugfix/general
Fix Coinswitch Issues, Fix LN Node Info Clipboard, Fix Vue-Cloak Styles
2019-01-08 23:02:34 +09:00
8b686f0b12 fix coin switch issues 2019-01-08 11:27:37 +01:00
def8d1e0cb fix ln node clipboard 2019-01-08 10:54:02 +01:00
b15267be4d Merge pull request #453 from 2pac1/master
Update Anyone can enable invoices text so its much more clear
2019-01-08 12:56:04 +09:00
5c074f6f5f Update translations 2019-01-07 22:51:26 +09:00
679942159e bump 2019-01-07 22:37:55 +09:00
63c309bd12 Merge pull request #499 from Kukks/node-info-page
Add Node Info Page
2019-01-07 22:04:08 +09:00
3cefd7bd1e Merge pull request #467 from Kukks/feature/coinswitch
CoinSwitch Integration
2019-01-07 19:11:55 +09:00
12c418d84d Add Node Info Page 2019-01-07 09:52:27 +01:00
4b982f815c Renaming 2019-01-07 15:35:18 +09:00
d4d3346b6d Merge pull request #463 from sipsorcery/455-disablereg
Set disable registration as default true
2019-01-07 15:20:04 +09:00
6010a103e0 Added new disable-registration command line option. 2019-01-06 16:43:55 +01:00
5dc1da2af0 Don't disable user registrations if debug for unit tests. 2019-01-06 14:55:18 +01:00
5fd77d9fcc Merge pull request #492 from Kukks/crowdfund-part-1
Crowdfund Part 1: JS Dependencies
2019-01-06 21:59:52 +09:00
540414d8f5 Merge pull request #495 from Kukks/crowdfund-part-3
Enhance Invoice Events
2019-01-06 18:14:56 +09:00
abcdb8ced0 Merge pull request #494 from Kukks/crowdfund-part-2
Crowdfund Part 2: Expand Invoice Searching
2019-01-06 18:14:23 +09:00
5076d73695 Enhance Invoice Events 2019-01-06 10:12:45 +01:00
d88735f84e Merge remote-tracking branch 'upstream/master' into 455-disablereg 2019-01-06 10:05:33 +01:00
2244f0ab76 Merge pull request #493 from btcpayserver/feature/fastertests
[WIP] Make tests fast to execute
2019-01-06 18:04:38 +09:00
40c85d6104 Expand Invoice Searching 2019-01-06 10:00:55 +01:00
42892e24f4 Remove uneeded database call during derivation scheme registration 2019-01-06 17:58:11 +09:00
c27557826b add vendors 2019-01-06 09:08:05 +01:00
88150b6535 Improve IPN tests 2019-01-06 15:04:30 +09:00
b2aebcc5d3 Merge pull request #480 from britttttk/fix/PaymentButton
Fix payment button size
2019-01-05 22:32:11 +09:00
ae9ad0fa65 Merge pull request #489 from btcpayserver/feature/networkfee
Add support for removing network fee on first payment
2019-01-05 22:09:10 +09:00
a05cd5678b Add support for removing network fee on first payment 2019-01-05 17:45:49 +09:00
0f175174f6 Rename TxFee to NetworkFee and save the Network Fee of each payment under PaymentEntity 2019-01-05 13:31:05 +09:00
fa8993191e Update coinswitch.html 2019-01-03 15:31:56 +01:00
239ce28575 Update coinswitch.html 2019-01-02 21:50:43 +01:00
1e26926350 Fix payment button size 2018-12-30 00:27:22 -07:00
fff39d9879 Merge remote-tracking branch 'origin/feature/coinswitch' into feature/coinswitch 2018-12-28 12:34:01 +01:00
444e761d41 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-28 12:33:55 +01:00
68a3def35a save state for coinswitch started order 2018-12-28 12:33:47 +01:00
3effdf0f4d Update UpdateCoinSwitchSettings.cshtml 2018-12-28 10:49:12 +01:00
3c122bcf53 Merge pull request #478 from mariodian/pos-fix-search-btn
PoS: Fix z-index of search cancel button that overlaps modal confirmation
2018-12-28 16:12:59 +09:00
037ff52f4f Fix z-index of search cancel button that overlaps modal confirmation 2018-12-28 11:13:04 +08:00
fa506b5bf8 bump 2018-12-27 18:52:01 +09:00
e3193a92d0 Add PaidCurrency in the excel export 2018-12-27 16:48:33 +09:00
d76dabdca6 Remove warning 2018-12-27 16:19:00 +09:00
d219f50912 Merge branch 'pos-new-design' 2018-12-27 16:17:07 +09:00
e08710a19c Fix tests 2018-12-27 16:11:58 +09:00
4e167b35be Bug fixes
- fix `tip reset` when cart content changes
- fix negative cart value when deleting empty cart items
2018-12-27 13:19:51 +08:00
16873384a8 - fix cart item removal
- fix empty qty field
- remove tip when total changes
2018-12-27 12:57:31 +08:00
f87339f9fa Change CustomTipPercentages type to int[] 2018-12-27 12:57:31 +08:00
5f5e5e3211 Add missing AppId 2018-12-27 12:57:31 +08:00
f724db8226 Fix cart table widths 2018-12-27 12:57:31 +08:00
c7e90cd7df New PoS design 2018-12-27 12:57:31 +08:00
8c5b00b1a3 Merge pull request #470 from Kukks/feature/bootstrapbump
update bootstrap to 4.2.1
2018-12-27 13:40:42 +09:00
a3b79fbcd8 Merge pull request #462 from rockstardev/master
InvoiceDue field in export
2018-12-26 16:47:38 +09:00
9db77e6351 Rewrite and comment non obvious code for ledger 2018-12-26 15:10:00 +09:00
5bc1eaec9f bump 2018-12-26 15:04:43 +09:00
81c9ce7284 Limit the number of time the wallet need to export the xpub 2018-12-26 15:04:11 +09:00
caa6978d80 Save the KeyPath of the WalletKeyPathRoot of the hardware wallet so we don't have to scan for it 2018-12-26 14:04:00 +09:00
af22d6a4e3 Remove preliminary test to know if the ledger can handle the store. If it can't signing will fail anyway. 2018-12-25 19:33:03 +09:00
0eabb3c37c Remove useless query to ledger xpub in the Add derivation scheme screen 2018-12-25 19:02:11 +09:00
6f896cb096 update bootstrap to 4.2.1 2018-12-22 17:32:03 +01:00
a63ed4d3b4 bump 2018-12-21 16:45:05 +09:00
968c820702 Add turkish translation 2018-12-21 14:15:05 +09:00
3061b4dfd2 Add comments 2018-12-21 13:33:26 +09:00
ed4de612dd Fix layout if customized to an absolute uri 2018-12-21 13:31:02 +09:00
d4bdd5fd9c Do not use absolute link to link theme files on layout.cshtml 2018-12-21 13:24:01 +09:00
8b71556425 Merge branch 'master' into 455-disablereg 2018-12-20 21:58:07 +01:00
ae6e1bfd85 Update UpdateCoinSwitchSettings.cshtml 2018-12-20 21:01:10 +01:00
6d1f3b73ef update link 2018-12-20 20:40:33 +01:00
0dcaf80c7f Changed disable register mechanism to apply policy setting after admin user created rather than using DB user count checks. 2018-12-20 20:39:48 +01:00
fc9cd5bdf0 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-20 17:57:04 +01:00
a434c45196 fix function names 2018-12-20 17:56:57 +01:00
87b316ec23 Merge pull request #439 from Kukks/grs-clightning
Add lightning icon for GRS
2018-12-20 22:43:28 +09:00
9c99ffae57 Lightning charge integration 2018-12-20 22:40:32 +09:00
30a3a84ec9 fix final issues with integration 2018-12-20 14:33:31 +01:00
d0f585df9d fix tests 2018-12-20 21:34:09 +09:00
bac2db5cda Add timeout to lightning tests 2018-12-20 21:27:08 +09:00
c35bf2f483 fix docker compose 2018-12-20 21:23:24 +09:00
2e04c5e39c Update docker-compose test 2018-12-20 21:20:30 +09:00
9dcf16e819 Add xunit diagnostic message 2018-12-20 21:00:06 +09:00
361d494cde Accept cookiefilepath as alternative to cookiefile for spark connection string 2018-12-20 20:24:06 +09:00
4d7015294e Fix configuration of lnd rest 2018-12-20 20:12:36 +09:00
5f16fb4668 report slow tests 2018-12-20 18:44:39 +09:00
4bf2228675 Show test logs in CircleCI 2018-12-20 18:35:32 +09:00
2ba823f192 Merge pull request #465 from Kukks/master
do not add ln payment if amount couldn't be parsed
2018-12-20 18:09:57 +09:00
27fa2d5b69 do not add ln payment if amount couldn't be parsed 2018-12-20 09:21:20 +01:00
47ef7661d8 Fix search for macaroon 2018-12-20 17:08:32 +09:00
3f6ff25322 update es-ES 2018-12-20 17:00:30 +09:00
f56c23009a bump 2018-12-20 16:57:59 +09:00
e80593fb7b Expose LND's other macaroon if possible 2018-12-20 16:52:04 +09:00
57324345ac Remove remaining of RestrictedMacaroon 2018-12-20 16:24:36 +09:00
73e280157d Show the gRPC cypher suites for gRPC consumption 2018-12-20 14:16:23 +09:00
cfaa5766ed Always allow user registration if there are no user records. 2018-12-19 20:03:27 +01:00
8b08db308b Merge branch 'master' into 455-disablereg 2018-12-19 19:40:13 +01:00
3e2ff55954 Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch 2018-12-19 10:41:36 +01:00
70d1d0d230 Remove volumes before running tests 2018-12-19 15:50:20 +09:00
94e0048a3b Redirect users to docs.btcpayserver.org on home page 2018-12-19 15:30:10 +09:00
a34d1641b3 Set disable registration as default true. 2018-12-18 20:16:48 +01:00
40c645e433 coinswitch integration 2018-12-18 20:14:59 +01:00
b2e5415a35 coinswitch integration 2018-12-18 20:00:30 +01:00
365ee4cf0b Fixing CSV test now that we have new field / reorders 2018-12-18 12:35:59 -06:00
2b4603a234 coinswitch integration 2018-12-18 19:01:58 +01:00
ec23eae21d Ensuding that payments are always ordered by time for consistency 2018-12-18 11:56:51 -06:00
7a9229628a InvoiceDue field in export 2018-12-18 11:56:12 -06:00
9db5c0f375 Hack tests to make currency formatting work on linux 2018-12-19 00:28:06 +09:00
2bb24282d2 Clean previous run with dock-compose 2018-12-19 00:15:33 +09:00
998472e463 Fix symbol display on linux 2018-12-19 00:11:15 +09:00
63ff46a768 cache docker on circleCI for tests 2018-12-18 23:27:57 +09:00
660f43e3b7 Add fast test for JPY formatting 2018-12-18 23:26:35 +09:00
0ba96aa4b8 Fix export tests 2018-12-18 23:24:22 +09:00
d85247d2ad Run tests inside container 2018-12-18 22:35:58 +09:00
9ca85ed365 Change column order 2018-12-18 21:44:51 +09:00
93113fd871 Fix payment exports to reflect correctly payment data, rename fields. 2018-12-18 21:35:52 +09:00
d5ae79c38c Add more information about status in the CSV export 2018-12-18 19:33:14 +09:00
7cf07b27e3 Invoice export should not prefix amounts with crypto code 2018-12-18 19:20:10 +09:00
bb0f986b0c Add additional test on euro formatting 2018-12-18 19:09:55 +09:00
2c2a85327f Add test logs 2018-12-18 01:02:27 +09:00
7bf03e497b In cart js, add space to symbol if needed (fix #450) 2018-12-18 00:38:59 +09:00
7a4dee3d38 Point of Sale returns correct currency information (#450) 2018-12-18 00:25:17 +09:00
7b27d6f0bb Merge branch 'mariodian-pos-product-management' 2018-12-15 23:40:04 +09:00
83dc95a0a7 Remove dollar sign in textbox 2018-12-15 23:39:45 +09:00
d60889f952 Merge branch 'pos-product-management' of https://github.com/mariodian/btcpayserver into mariodian-pos-product-management 2018-12-15 23:22:07 +09:00
8c9952973d Merge pull request #449 from sipsorcery/uxpwdreset
HTML formatting fix for issue #448.
2018-12-15 23:20:09 +09:00
00673bdb7f Fix product width on smaller screens 2018-12-14 16:16:08 +08:00
d039890a9b Create js-only product management in PoS 2018-12-14 16:03:02 +08:00
41e88c07fe update languages 2018-12-14 13:14:03 +09:00
67c5027b16 bump 2018-12-14 13:12:27 +09:00
a341d4f800 Show Spark QR Code pairing 2018-12-14 13:12:27 +09:00
e3833914b3 Update AddDerivationScheme.cshtml 2018-12-13 16:18:01 +01:00
49cdec6961 Update PayButtonEnable.cshtml 2018-12-13 12:26:30 +01:00
3ad1834439 Better fix this as well or the user gets a blank page after the reset. 2018-12-12 22:39:37 +01:00
4b492eae85 HTML formatting fix for issue #448. 2018-12-12 22:29:40 +01:00
f0ff47af8d Merge pull request #447 from britttttk/translations/disable
Check for disable flag in Transifex
2018-12-12 23:40:47 +09:00
991826b686 do not show restricted macaroon 2018-12-12 18:52:01 +09:00
22d59a1ed7 Do not leak access key in browser 2018-12-12 18:37:50 +09:00
475ea68696 Can attach external spark 2018-12-12 18:19:13 +09:00
864e84706a check for disable flag 2018-12-11 22:18:17 -07:00
9c93e76eeb Remove temporary nuget down hack 2018-12-12 12:20:17 +09:00
c00c95efcf initial coinswitch work 2018-12-11 12:47:38 +01:00
94be2b46d5 docker build should use right api.nuget.org server 2018-12-10 23:36:54 +09:00
4b4d0d2d19 Adding working server for api.nuget.org 2018-12-10 22:35:43 +09:00
0d06cf63b7 Use enum for invoice status and invoice exception 2018-12-10 21:48:28 +09:00
7b24c02d51 bump 2018-12-10 20:34:34 +09:00
becf488714 Merge remote-tracking branch 'btcpayserver/master' into grs-clightning 2018-12-10 10:42:08 +01:00
e89e8226e4 Fix build 2018-12-10 17:34:27 +09:00
a533a96598 Remove XFrame for PoS 2018-12-10 16:39:21 +09:00
27321c0919 bump 2018-12-10 16:04:28 +09:00
058472d325 Show restricted macaroon for LND 2018-12-10 16:03:58 +09:00
b5c9a03052 Can mark invoice as complete 2018-12-10 15:34:48 +09:00
07dad3affa bump 2018-12-07 19:35:25 +09:00
8afc103ae7 Show REST connection information for LND in a QR Code 2018-12-07 19:31:07 +09:00
591d7b4b80 Can show external service link with BTCPAY_EXTERNALSERVICES 2018-12-07 18:42:39 +09:00
2162afc78e Lightning network warnings 2018-12-07 17:54:10 +09:00
25e226d219 Clarify the code 2018-12-07 14:37:07 +09:00
8472bfe90d Add test for bad bitid signature 2018-12-07 14:34:07 +09:00
93645b2fbe Fix error 500 if token not found 2018-12-07 13:48:39 +09:00
d53c987f2e bump 2018-12-06 17:25:50 +09:00
682693a9f0 Update translations 2018-12-06 17:23:42 +09:00
e836faf792 Stop setting BIP70 link info 2018-12-06 17:12:51 +09:00
6e27233be8 Remove BIP70 support 2018-12-06 17:08:28 +09:00
9209984a2f Remove useless argument from GetInvoice 2018-12-06 17:05:27 +09:00
1477630c78 Remove anonymous access to invoice data 2018-12-06 16:58:04 +09:00
ab670080c7 bump 2018-12-06 12:29:13 +09:00
8198f98376 Code simplification 2018-12-06 12:26:42 +09:00
65b4697229 Properly error 401 if request is not signed correctly 2018-12-06 12:22:05 +09:00
e75a1a8b70 Improve ledger feedback for asking authorization to access xpub 2018-12-04 21:22:27 +09:00
580494fea7 add correct icon 2018-12-04 11:46:01 +01:00
5a958da84d bump 2018-12-04 13:04:56 +09:00
cad602ad14 Fix several issues in cart
* Fix: Only USD currency with 2 decimals were properly handled for tips
* Fix: All PoS apps would were sharing the same basket
* Fix: Currency formatting was not using server side information
* Fix: Various bug of formatting for decimal 0 and more than 2.
2018-12-04 13:04:26 +09:00
1f14bd6188 Add button and qr code to the bitpay translator 2018-12-04 11:53:25 +09:00
156f52b76f Add bitpay translator 2018-12-03 23:59:08 +09:00
d674b8ac71 Merge pull request #430 from dalijolijo/master
bump
2018-12-01 22:40:48 +09:00
861150971f bump 2018-12-01 10:15:57 +00:00
a653421514 Merge pull request #428 from mariodian/fix-pos-cart-currency
Fix currency format for total amount
2018-12-01 14:09:01 +09:00
8f234a02cb Add currency formats for major currencies 2018-12-01 12:59:45 +08:00
92ecf99427 bump 2018-12-01 13:23:56 +09:00
705dbf12d7 Change translation of the expiration screen 2018-12-01 13:19:35 +09:00
fe11b11c13 Add Polski and Srpski 2018-12-01 12:02:53 +09:00
f2a43ad1f3 Escape js properties in html template 2018-11-30 21:14:09 +08:00
cbbe5cfb25 - fix currency format for numbers over 999
- fix cart table
2018-11-30 20:01:47 +08:00
166 changed files with 37450 additions and 8257 deletions

View File

@ -7,23 +7,16 @@ jobs:
- checkout
test:
machine: true
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
lsb_release -a
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1
dotnet --info
dotnet build /p:TreatWarningsAsErrors=true
cd BTCPayServer.Tests
dotnet test --filter Fast=Fast
docker-compose up -d dev
dotnet test --filter Integration=Integration
docker-compose down --v
docker-compose build
docker-compose run tests
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:

View File

@ -34,6 +34,7 @@ using System.Security.Principal;
using System.Text;
using System.Threading;
using Xunit;
using BTCPayServer.Services;
namespace BTCPayServer.Tests
{
@ -121,7 +122,7 @@ namespace BTCPayServer.Tests
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>

View File

@ -0,0 +1,89 @@
using BTCPayServer.Controllers;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class CoinSwitchTests
{
public CoinSwitchTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.CoinSwitchSettings);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
updateModel.MerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
}
}
}
}

View File

@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Channels;
using System.IO;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
namespace BTCPayServer.Tests
{
public class CustomServer : IDisposable
{
TaskCompletionSource<bool> _Evt = null;
IWebHost _Host = null;
CancellationTokenSource _Closed = new CancellationTokenSource();
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
public CustomServer()
{
{
var port = Utils.FreeTcpPort();
_Host = new WebHostBuilder()
.Configure(app =>
{
app.Run(req =>
{
while (_Act == null)
{
Thread.Sleep(10);
_Closed.Token.ThrowIfCancellationRequested();
}
_Act(req);
_Act = null;
_Evt.TrySetResult(true);
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
req.Response.StatusCode = 200;
return Task.CompletedTask;
});
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
}
Action<HttpContext> _Act;
public void ProcessNextRequest(Action<HttpContext> act)
public async Task<JObject> GetNextRequest()
{
var source = new TaskCompletionSource<bool>();
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
cancellation.Token.Register(() => source.TrySetCanceled());
source = new TaskCompletionSource<bool>();
_Evt = source;
_Act = act;
try
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
{
_Evt.Task.GetAwaiter().GetResult();
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
try
{
JObject req = null;
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
!_Requests.Reader.TryRead(out req))
{
}
return req;
}
catch (TaskCanceledException)
{
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
}
}
}

View File

@ -1,12 +1,17 @@
FROM microsoft/dotnet:2.1.403-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
WORKDIR /app/BTCPayServer.Tests
RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
COPY . .
RUN dotnet build
WORKDIR /source/BTCPayServer.Tests
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@ -22,6 +22,7 @@ using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
namespace BTCPayServer.Tests
{
@ -152,6 +153,7 @@ namespace BTCPayServer.Tests
{
get; set;
}
public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose()

View File

@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
namespace BTCPayServer.Tests
{
@ -58,6 +59,21 @@ namespace BTCPayServer.Tests
CreateStoreAsync().GetAwaiter().GetResult();
}
public void SetNetworkFeeMode(NetworkFeeMode mode)
{
ModifyStore((store) =>
{
store.NetworkFeeMode = mode;
});
}
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
@ -83,10 +99,6 @@ namespace BTCPayServer.Tests
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationScheme = DerivationScheme.ToString(),

View File

@ -1,4 +1,4 @@
using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Logging;
using System.Linq;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -48,6 +48,10 @@ using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Security;
using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
using NBitpayClient.Extensions;
using BTCPayServer.Services;
using System.Text.RegularExpressions;
using BTCPayServer.Events;
namespace BTCPayServer.Tests
{
@ -97,7 +101,7 @@ namespace BTCPayServer.Tests
Rate = 10513.44m,
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00000100m),
NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod()
@ -106,7 +110,7 @@ namespace BTCPayServer.Tests
Rate = 216.79m
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
TxFee = Money.Coins(0.00010000m),
NextNetworkFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
@ -114,12 +118,12 @@ namespace BTCPayServer.Tests
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
accounting = btc.Calculate();
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Output = new TxOut() { Value = accounting.Due }
}));
@ -146,7 +150,7 @@ namespace BTCPayServer.Tests
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
@ -154,20 +158,20 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m });
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
@ -186,13 +190,13 @@ namespace BTCPayServer.Tests
{
CryptoCode = "BTC",
Rate = 1000,
TxFee = Money.Coins(0.1m)
NextNetworkFee = Money.Coins(0.1m)
});
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
TxFee = Money.Coins(0.01m)
NextNetworkFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
@ -204,7 +208,7 @@ namespace BTCPayServer.Tests
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -221,7 +225,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
@ -241,7 +245,7 @@ namespace BTCPayServer.Tests
Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true, NetworkFee = 0.1m });
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
@ -271,7 +275,7 @@ namespace BTCPayServer.Tests
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.PaymentTolerance = 0;
@ -339,69 +343,22 @@ namespace BTCPayServer.Tests
{
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)"),
(0.001m, "$0.001 (USD)"),
(0.01m, "$0.01 (USD)"),
(0.1m, "$0.10 (USD)"),
(0.0005m, "$0.0005 (USD)", "USD"),
(0.001m, "$0.001 (USD)", "USD"),
(0.01m, "$0.01 (USD)", "USD"),
(0.1m, "$0.10 (USD)", "USD"),
(0.1m, "0,10 € (EUR)", "EUR"),
(1000m, "¥1,000 (JPY)", "JPY"),
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
})
{
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanPayUsingBIP70()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.False(invoice.Refundable);
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
var request = url.GetPaymentRequest();
var payment = request.CreatePayment();
Transaction tx = new Transaction();
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
var cashCow = tester.ExplorerNode;
tx = cashCow.FundRawTransaction(tx).Transaction;
tx = cashCow.SignRawTransaction(tx);
payment.Transactions.Add(tx);
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
var ack = payment.SubmitPayment();
Assert.NotNull(ack);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable);
});
}
}
[Fact]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetLightningServer()
{
@ -453,7 +410,7 @@ namespace BTCPayServer.Tests
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentLnd()
{
@ -530,7 +487,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public void CanSendIPN()
public async Task CanSendIPN()
{
using (var callbackServer = new CustomServer())
{
@ -540,6 +497,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0m,
@ -552,13 +510,43 @@ namespace BTCPayServer.Tests
ExtendedNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000);
callbackServer.ProcessNextRequest((ctx) =>
bool receivedPayment = false;
bool paid = false;
bool confirmed = false;
bool completed = false;
while (!completed || !confirmed)
{
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
});
var request = await callbackServer.GetNextRequest();
if (request.ContainsKey("event"))
{
var evtName = request["event"]["name"].Value<string>();
switch (evtName)
{
case InvoiceEvent.Created:
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
break;
case InvoiceEvent.ReceivedPayment:
receivedPayment = true;
break;
case InvoiceEvent.PaidInFull:
Assert.True(receivedPayment);
tester.ExplorerNode.Generate(6);
paid = true;
break;
case InvoiceEvent.Confirmed:
Assert.True(paid);
confirmed = true;
break;
case InvoiceEvent.Completed:
Assert.True(paid); //TODO: Fix, out of order event mean we can receive invoice_confirmed after invoice_complete
completed = true;
break;
default:
Assert.False(true, $"{evtName} was not expected");
break;
}
}
}
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
@ -772,6 +760,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
@ -934,6 +923,17 @@ namespace BTCPayServer.Tests
var result = client.SendAsync(message).GetAwaiter().GetResult();
result.EnsureSuccessStatusCode();
/////////////////////
// Have error 403 with bad signature
client = new HttpClient();
HttpRequestMessage mess = new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens");
mess.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
mess.Headers.Add("x-signature", "3045022100caa123193afc22ef93d9c6b358debce6897c09dd9869fe6fe029c9cb43623fac022000b90c65c50ba8bbbc6ebee8878abe5659e17b9f2e1b27d95eda4423da5608fe");
mess.Headers.Add("x-identity", "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99");
mess.Method = HttpMethod.Get;
result = client.SendAsync(mess).GetAwaiter().GetResult();
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode);
//
}
}
@ -1397,7 +1397,7 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
@ -1464,6 +1464,7 @@ namespace BTCPayServer.Tests
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.CustomTipText = "Wanna tip?";
vmpos.CustomTipPercentages = "15,18,20";
vmpos.Template = @"
apple:
price: 5.0
@ -1489,6 +1490,7 @@ donation:
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
//
@ -1506,6 +1508,44 @@ donation:
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);
Assert.Equal("donation", donationInvoice.ItemDesc);
foreach (var test in new[]
{
(Code: "EUR", ExpectedSymbol: "€", ExpectedDecimalSeparator: ",", ExpectedDivisibility: 2, ExpectedThousandSeparator: "\xa0", ExpectedPrefixed: false, ExpectedSymbolSpace: true),
(Code: "INR", ExpectedSymbol: "₹", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 2, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true),
(Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false),
(Code: "BTC", ExpectedSymbol: "BTC", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8, ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true),
})
{
Logs.Tester.LogInformation($"Testing for {test.Code}");
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = test.Item1;
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.CustomTipText = "Wanna tip?";
vmpos.Template = @"
apple:
price: 1000.0
title: good apple
orange:
price: 10.0
donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
publicApps = user.GetController<AppsPublicController>();
vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
Assert.Equal(test.Code, vmview.CurrencyCode);
Assert.Equal(test.ExpectedSymbol, vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well);
Assert.Equal(test.ExpectedSymbol, vmview.CurrencyInfo.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well);
Assert.Equal(test.ExpectedDecimalSeparator, vmview.CurrencyInfo.DecimalSeparator);
Assert.Equal(test.ExpectedThousandSeparator, vmview.CurrencyInfo.ThousandSeparator);
Assert.Equal(test.ExpectedPrefixed, vmview.CurrencyInfo.Prefixed);
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
}
}
}
@ -1586,16 +1626,22 @@ donation:
[Trait("Integration", "Integration")]
public void CanExportInvoicesJson()
{
decimal GetFieldValue(string input, string fieldName)
{
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
Assert.True(match.Success);
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
}
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1603,6 +1649,7 @@ donation:
FullNotifications = true
}, Facade.Merchant);
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
// ensure 0 invoices exported because there are no payments yet
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var result = Assert.IsType<ContentResult>(jsonResult);
@ -1611,47 +1658,123 @@ donation:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
//
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
// look if you can reduce thread sleep, this was min value for me
// should reduce invoice due by 0 USD because payment = network fee
cashCow.SendToAddress(invoiceAddress, networkFee);
Thread.Sleep(1000);
// pay remaining amount
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
Eventually(() =>
{
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
Assert.Equal("application/json", paidresult.ContentType);
Assert.Contains("\"ItemDesc\": \"Some \\\", description\"", paidresult.Content);
Assert.Contains("\"FiatPrice\": 500.0", paidresult.Content);
Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content);
Assert.Contains("\"PaymentDue\": \"0.10020000 BTC\"", paidresult.Content);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content);
});
/*
[
{
"ReceivedDate": "2018-11-30T10:27:13Z",
"StoreId": "FKaSZrXLJ2tcLfCyeiYYfmZp1UM5nZ1LDecQqbwBRuHi",
"OrderId": "orderId",
"InvoiceId": "4XUkgPMaTBzwJGV9P84kPC",
"CreatedDate": "2018-11-30T10:27:06Z",
"ExpirationDate": "2018-11-30T10:42:06Z",
"MonitoringDate": "2018-11-30T11:42:06Z",
"PaymentId": "6e5755c3357b20fd66f5fc478778d81371eab341e7112ab66ed6122c0ec0d9e5-1",
"CryptoCode": "BTC",
"Destination": "mhhSEQuoM993o6vwnBeufJ4TaWov2ZUsPQ",
"PaymentType": "OnChain",
"PaymentDue": "0.10020000 BTC",
"PaymentPaid": "0.10009990 BTC",
"PaymentOverpaid": "0.00000000 BTC",
"ConversionRate": 5000.0,
"FiatPrice": 500.0,
"FiatCurrency": "USD",
"ItemCode": null,
"ItemDesc": "Some \", description",
"Status": "new"
}
]
*/
var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content);
Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
var pay2str = parsedJson[1].ToString();
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
var pay3str = parsedJson[2].ToString();
Assert.Contains("\"InvoiceDue\": 0", pay3str);
});
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanChangeNetworkFeeMode()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
{
Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}");
user.SetNetworkFeeMode(networkFeeMode);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var networkFee = Money.Satoshis(10000).ToDecimal(MoneyUnit.BTC);
var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
// Check that for the first payment, no network fee are included
var due = Money.Parse(invoice.CryptoInfo[0].Due);
var productPartDue = (invoice.Price / invoice.Rate);
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
case NetworkFeeMode.Never:
Assert.Equal(productPartDue, due.ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(productPartDue + networkFee, due.ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
var firstPayment = productPartDue - missingMoney;
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
// Check that for the second payment, network fee are included
due = Money.Parse(invoice.CryptoInfo[0].Due);
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
switch (networkFeeMode)
{
case NetworkFeeMode.MultiplePaymentsOnly:
Assert.Equal(missingMoney + networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Always:
Assert.Equal(missingMoney + 2 * networkFee, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney + 2 * networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
case NetworkFeeMode.Never:
Assert.Equal(missingMoney, due.ToDecimal(MoneyUnit.BTC));
Assert.Equal(firstPayment + missingMoney, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
break;
default:
throw new NotSupportedException(networkFeeMode.ToString());
}
});
cashCow.SendToAddress(invoiceAddress, due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
}
}
}
@ -1665,7 +1788,7 @@ donation:
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
@ -1687,14 +1810,9 @@ donation:
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
Assert.Contains($",\"OnChain\",\"0.10020000 BTC\",\"0.10009990 BTC\",\"0.00000000 BTC\",\"5000.0\",\"500.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new\"", paidresult.Content);
Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
});
/*
ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate,PaymentId,CryptoCode,Destination,PaymentType,PaymentDue,PaymentPaid,PaymentOverpaid,ConversionRate,FiatPrice,FiatCurrency,ItemCode,ItemDesc,Status
"11/30/2018 10:28:42 AM","7AagXzWdWhLLR3Zar25YLiw2uHAJDzVT4oXGKC9bBCis","orderId","GxtJsWbgxxAXXoCurqyeK6","11/30/2018 10:28:40 AM","11/30/2018 10:43:40 AM","11/30/2018 11:43:40 AM","ec0341537f565d213bc64caa352fbbf9e0deb31cab1f91bccf89db0dd1604457-0","BTC","mqWghCp9RVw8fNgQMLjawyKStxpGfWBk1L","OnChain","0.10020000 BTC","0.10009990 BTC","0.00000000 BTC","5000.0","500.0","USD","","Some ``, description","new"
*/
}
}
@ -1801,7 +1919,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate
Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
Assert.Single(invoiceEntity.HistoricalAddresses);
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
@ -1819,7 +1937,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);

View File

@ -28,18 +28,21 @@ namespace BTCPayServer.Tests
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
var client = new TransifexClient(GetTransifexAPIToken());
var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/");
var langs = ((JObject)json["stats"]).Properties().Select(n => n.Name).ToArray();
var langs = new[] { "en" }.Concat(((JObject)json["stats"]).Properties().Select(n => n.Name)).ToArray();
var langsDir = Path.Combine(Services.LanguageService.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
JObject sourceLang = null;
Task.WaitAll(langs.Select(async l =>
{
if (l == "no")
return;
bool isSourceLang = l == "en";
var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/");
if(!isSourceLang)
{
while (sourceLang == null)
await Task.Delay(10);
}
var content = j["content"].Value<string>();
if (l == "en_US")
l = "en";
if (l == "ne_NP")
l = "np_NP";
if (l == "zh_CN")
@ -51,9 +54,24 @@ namespace BTCPayServer.Tests
var langFile = Path.Combine(langsDir, langCode + ".json");
var jobj = JObject.Parse(content);
jobj["code"] = langCode;
if ((string)jobj["currentLanguage"] == "English")
return;
if ((string)jobj["currentLanguage"] == "English" && !isSourceLang)
return; // Not translated
if ((string)jobj["currentLanguage"] == "disable")
return; // Not translated
jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
if (isSourceLang)
{
sourceLang = jobj;
}
else
{
if(jobj["InvoiceExpired_Body_3"].Value<string>() == sourceLang["InvoiceExpired_Body_3"].Value<string>())
{
jobj["InvoiceExpired_Body_3"] = string.Empty;
}
}
content = jobj.ToString(Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(Path.Combine(langsDir, langFile), content);
}).ToArray());

View File

@ -19,10 +19,10 @@ services:
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
expose:
- "80"
@ -36,7 +36,7 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.17.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -53,7 +53,7 @@ services:
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.17.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.1
image: nicolasdorier/nbxplorer:2.0.0.2
restart: unless-stopped
ports:
- "32838:32838"
@ -93,12 +93,13 @@ services:
- bitcoind
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.17.0
restart: unless-stopped
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
@ -106,9 +107,9 @@ services:
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
deprecatedrpc=signrawtransaction
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
@ -118,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6.2-3-dev
image: btcpayserver/lightning:v0.6.2-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -144,7 +145,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.3
image: shesek/lightning-charge:0.4.6-standalone
restart: unless-stopped
environment:
NETWORK: regtest
@ -164,7 +165,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6.2-3-dev
image: btcpayserver/lightning:v0.6.2-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -188,13 +189,13 @@ services:
- bitcoind
litecoind:
image: nicolasdorier/docker-litecoin:0.15.1
restart: unless-stopped
image: nicolasdorier/docker-litecoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
@ -221,13 +222,16 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:0.5-beta-2
image: btcpayserver/lnd:v0.5.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
@ -248,13 +252,16 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.5-beta-2
image: btcpayserver/lnd:v0.5.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
LND_EXPLORERURL: "http://nbxplorer:32838/"
LND_EXTRA_ARGS: |
restlisten=0.0.0.0:8080
rpclisten=127.0.0.1:10008
rpclisten=0.0.0.0:10009
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpubrawblock=tcp://bitcoind:28332

View File

@ -0,0 +1,5 @@
#!/bin/sh
set -e
dotnet test --filter Fast=Fast --no-build
dotnet test --filter Integration=Integration --no-build -v n

View File

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

View File

@ -242,6 +242,8 @@ namespace BTCPayServer.Authentication
using (var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
if (token == null)
return null;
return CreateTokenEntity(token);
}
}

View File

@ -26,7 +26,7 @@ namespace BTCPayServer
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
});

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.20</Version>
<Version>1.0.3.39</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,7 +33,7 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.4" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.20" />
@ -46,10 +46,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.71" />
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
@ -133,7 +133,13 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndRestServices.cshtml">
<Content Update="Views\Home\BitpayTranslator.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
@ -151,7 +157,7 @@
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndGrpcServices.cshtml">
<Content Update="Views\Server\LndServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">

View File

@ -37,8 +37,8 @@ namespace BTCPayServer.Configuration
{
get;
private set;
}
}
public string LogFile
{
get;
@ -61,6 +61,12 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public bool DisableRegistration
{
get;
private set;
}
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
@ -68,7 +74,7 @@ namespace BTCPayServer.Configuration
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf)
@ -137,10 +143,49 @@ namespace BTCPayServer.Configuration
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
if (charge.Length != 0)
{
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
chargeError ?? string.Empty);
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
var services = conf.GetOrDefault<string>("externalservices", null);
if (services != null)
{
foreach (var service in services.Split(new[] { ';', ',' })
.Select(p => p.Split(':'))
.Where(p => p.Length == 2)
.Select(p => (Name: p[0], Link: p[1])))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
}
}
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
@ -198,6 +243,8 @@ namespace BTCPayServer.Configuration
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
@ -248,6 +295,8 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }

View File

@ -33,6 +33,7 @@ namespace BTCPayServer.Configuration
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
@ -42,6 +43,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("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -49,6 +51,8 @@ namespace BTCPayServer.Configuration
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
}
return app;
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalCharge : ExternalService
{
public ExternalCharge(LightningConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; }
}
}

View File

@ -8,28 +8,23 @@ namespace BTCPayServer.Configuration.External
{
public abstract class ExternalLnd : ExternalService
{
public ExternalLnd(LightningConnectionString connectionString, LndTypes type)
public ExternalLnd(LightningConnectionString connectionString, string type)
{
ConnectionString = connectionString;
Type = type;
}
public LndTypes Type { get; set; }
public string Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public enum LndTypes
{
gRPC, Rest
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { }
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { }
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
{
public SparkConnectionString ConnectionString { get; }
public ExternalSpark(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class SparkConnectionString
{
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
return false;
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
return false;
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
return false;
resultTemp.CookeFile = kv[1];
break;
default:
return false;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -32,6 +32,7 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
Configuration.BTCPayServerOptions _Options;
ILogger _logger;
public AccountController(
@ -40,7 +41,8 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository)
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
@ -48,6 +50,7 @@ namespace BTCPayServer.Controllers
_emailSender = emailSender;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_logger = Logs.PayServer;
}
@ -271,6 +274,13 @@ namespace BTCPayServer.Controllers
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if(_Options.DisableRegistration)
{
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
policies.LockSubscription = true;
await _SettingsRepository.UpdateSetting(policies);
}
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

View File

@ -1,5 +1,8 @@
using System.Text;
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
@ -65,6 +68,9 @@ namespace BTCPayServer.Controllers
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
public string CustomCSSLink { get; set; }
}
@ -87,6 +93,7 @@ namespace BTCPayServer.Controllers
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
@ -157,6 +164,7 @@ namespace BTCPayServer.Controllers
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);
@ -174,5 +182,21 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
}
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
else
{
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, "");
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
@ -32,14 +33,16 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
return View(new ViewPointOfSaleViewModel()
{
@ -47,12 +50,24 @@ namespace BTCPayServer.Controllers
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
CurrencySymbol = currency.Symbol,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomCSSLink = settings.CustomCSSLink
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
AppId = appId
});
}
@ -123,7 +138,7 @@ namespace BTCPayServer.Controllers
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
public CurrencyNameTable Currencies => _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
{
_ContextFactory = contextFactory;

View File

@ -6,16 +6,85 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
public IHttpClientFactory HttpClientFactory { get; }
public HomeController(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
}
public IActionResult Index()
{
return View("Home");
}
[Route("translate")]
public IActionResult BitpayTranslator()
{
return View(new BitpayTranslatorViewModel());
}
[HttpPost]
[Route("translate")]
public async Task<IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm)
{
if (!ModelState.IsValid)
return View(vm);
vm.BitpayLink = vm.BitpayLink ?? string.Empty;
vm.BitpayLink = vm.BitpayLink.Trim();
if (!vm.BitpayLink.StartsWith("bitcoin:", StringComparison.OrdinalIgnoreCase))
{
var invoiceId = vm.BitpayLink.Substring(vm.BitpayLink.LastIndexOf("=", StringComparison.OrdinalIgnoreCase) + 1);
vm.BitpayLink = $"bitcoin:?r=https://bitpay.com/i/{invoiceId}";
}
try
{
BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink);
#pragma warning disable CS0618 // Type or member is obsolete
if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase))
{
throw new Exception("This tool only work with bitpay");
}
var client = HttpClientFactory.CreateClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl);
#pragma warning restore CS0618 // Type or member is obsolete
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request"));
var result = await client.SendAsync(request);
// {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"}
var str = await result.Content.ReadAsStringAsync();
try
{
var jobj = JObject.Parse(str);
vm.Address = ((JArray)jobj["outputs"])[0]["address"].Value<string>();
var amount = Money.Satoshis(((JArray)jobj["outputs"])[0]["amount"].Value<long>());
vm.Amount = amount.ToString();
vm.BitcoinUri = $"bitcoin:{vm.Address}?amount={amount.ToString()}";
}
catch (JsonReaderException)
{
ModelState.AddModelError(nameof(vm.BitpayLink), $"Invalid or expired bitpay invoice");
return View(vm);
}
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.BitpayLink), $"Error while requesting {ex.Message}");
return View(vm);
}
return View(vm);
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";

View File

@ -40,16 +40,18 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{id}")]
[AllowAnonymous]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
{
var invoice = await _InvoiceRepository.GetInvoice(null, id);
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
InvoiceId = id,
StoreId = new[] { HttpContext.GetStoreData().Id }
})).FirstOrDefault();
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp);
}
[HttpGet]
[Route("invoices")]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
@ -71,8 +73,8 @@ namespace BTCPayServer.Controllers
Skip = offset,
EndDate = dateEnd,
StartDate = dateStart,
OrderId = orderId,
ItemCode = itemCode,
OrderId = orderId == null ? null : new[] { orderId },
ItemCode = itemCode == null ? null : new[] { itemCode },
Status = status == null ? null : new[] { status },
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};

View File

@ -1,117 +0,0 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}/{cryptoCode?}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var network = _NetworkProvider.GetNetwork(cryptoCode);
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (store == null)
throw new BitpayHttpException(401, "Unknown store");
if (store.StoreCertificate != null)
{
try
{
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
}
}
return new PaymentRequestActionResult(request);
}
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
return NotFound();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
return NotFound();
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View File

@ -9,9 +9,11 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
@ -30,11 +32,13 @@ namespace BTCPayServer.Controllers
{
[HttpGet]
[Route("invoices/{invoiceId}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
InvoiceId = invoiceId,
UserId = GetUserId(),
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
@ -48,7 +52,7 @@ namespace BTCPayServer.Controllers
StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
State = invoice.GetInvoiceState().ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
@ -100,7 +104,7 @@ namespace BTCPayServer.Controllers
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
@ -209,7 +213,7 @@ namespace BTCPayServer.Controllers
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
@ -255,6 +259,11 @@ namespace BTCPayServer.Controllers
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
: null;
var changellyAmountDue = changelly != null
@ -298,12 +307,17 @@ namespace BTCPayServer.Controllers
throw new NotSupportedException(),
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
NetworkFee = paymentMethodDetails.GetTxFee(),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
@ -369,8 +383,8 @@ namespace BTCPayServer.Controllers
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
@ -437,15 +451,18 @@ namespace BTCPayServer.Controllers
var list = await ListInvoicesProcess(searchTerm, skip, count);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "new",
Status = state.ToString(),
ShowCheckout = invoice.Status == InvoiceStatus.New,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete()
});
}
return View(model);
@ -476,7 +493,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport();
var model = new InvoiceExport(_NetworkProvider);
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
var res = model.Process(invoices, format);
@ -585,17 +602,60 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost]
[Route("invoices/invalidatepaid")]
[HttpGet]
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (newState == "invalid")
{
return View("Confirm", new ConfirmModel()
{
Action = "Make invoice invalid",
Title = "Change invoice state",
Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?",
});
}
else if (newState == "complete")
{
return View("Confirm", new ConfirmModel()
{
Action = "Make invoice complete",
Title = "Change invoice state",
Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?",
ButtonClass = "btn-primary"
});
}
else
return NotFound();
}
[HttpPost]
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ChangeInvoiceStateConfirm(string invoiceId, string newState)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
InvoiceId = invoiceId,
UserId = GetUserId()
})).FirstOrDefault();
if (invoice == null)
return NotFound();
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
StatusMessage = "Invoice marked invalid";
}
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
StatusMessage = "Invoice marked complete";
}
return RedirectToAction(nameof(ListInvoices));
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
@ -99,7 +100,7 @@ namespace BTCPayServer.Controllers
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.Status = "new";
entity.Status = InvoiceStatus.New;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
@ -159,7 +160,7 @@ namespace BTCPayServer.Controllers
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -200,8 +201,6 @@ namespace BTCPayServer.Controllers
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null;
@ -241,7 +240,7 @@ namespace BTCPayServer.Controllers
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.TxFee;
entity.TxFee = paymentMethod.NextNetworkFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class Macaroons
{
public class Macaroon
{
public Macaroon(byte[] bytes)
{
Bytes = bytes;
Hex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(bytes);
}
public string Hex { get; set; }
public byte[] Bytes { get; set; }
}
public static async Task<Macaroons> GetFromDirectoryAsync(string directoryPath)
{
if (directoryPath == null)
throw new ArgumentNullException(nameof(directoryPath));
Macaroons macaroons = new Macaroons();
if (!Directory.Exists(directoryPath))
return macaroons;
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
{
try
{
switch (Path.GetFileName(file))
{
case "admin.macaroon":
macaroons.AdminMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "readonly.macaroon":
macaroons.ReadonlyMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
case "invoice.macaroon":
macaroons.InvoiceMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
break;
default:
break;
}
}
catch { }
}
return macaroons;
}
public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; }
public Macaroon AdminMacaroon { get; set; }
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Controllers
{
[Route("embed/{storeId}/{cryptoCode}/ln")]
[AllowAnonymous]
public class PublicLightningNodeInfoController : Controller
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
private readonly StoreRepository _StoreRepository;
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
{
_BtcPayNetworkProvider = btcPayNetworkProvider;
_LightningLikePaymentHandler = lightningLikePaymentHandler;
_StoreRepository = storeRepository;
}
[HttpGet]
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
{
var store = await _StoreRepository.FindStore(storeId);
if (store == null)
return NotFound();
try
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()
{
Available = true,
NodeInfo = nodeInfo.ToString(),
CryptoCode = cryptoCode,
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network)
});
}
catch (Exception)
{
return View(new ShowLightningNodeInfoViewModel() {Available = false, CryptoCode = cryptoCode});
}
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
}
}
public class ShowLightningNodeInfoViewModel
{
public string NodeInfo { get; set; }
public bool Available { get; set; }
public string CryptoCode { get; set; }
public string CryptoImage { get; set; }
}
}

View File

@ -46,6 +46,7 @@ namespace BTCPayServer.Controllers
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
Services.Stores.StoreRepository storeRepository)
{
@ -53,6 +54,7 @@ namespace BTCPayServer.Controllers
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_dashBoard = dashBoard;
HttpClientFactory = httpClientFactory;
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
@ -395,6 +397,7 @@ namespace BTCPayServer.Controllers
{
get; set;
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/emails")]
public async Task<IActionResult> Emails()
@ -431,16 +434,126 @@ namespace BTCPayServer.Controllers
{
Crypto = cryptoCode,
Type = grpcService.Type,
Action = nameof(LndServices),
Index = i++,
});
}
i = 0;
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Index = i++,
});
}
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Lightning charge server",
Action = nameof(LightningChargeServices),
Index = i++,
});
}
}
result.HasSSH = _Options.SSHSettings != null;
foreach(var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePath(externalService.Value)
});
}
if(_Options.SSHSettings != null)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = "SSH",
Link = this.Url.Action(nameof(SSHService))
});
}
return View(result);
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce)
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if (lightningCharge == null)
{
return NotFound();
}
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
vm.APIToken = lightningCharge.Password;
try
{
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
{
if (lightningCharge.CookieFilePath != "fake")
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
else
vm.APIToken = "fake";
}
var builder = new UriBuilder(lightningCharge.ToUri(false));
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if(spark == null)
{
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
vm.ShowQR = showQR;
try
{
var cookie = (spark.CookeFile == "fake"
? "fake:fake:fake" // If we are testing, it should not crash
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
}
}
catch(Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
@ -451,9 +564,19 @@ namespace BTCPayServer.Controllers
if (external == null)
return NotFound();
var model = new LndGrpcServicesViewModel();
if (external.ConnectionType == LightningConnectionType.LndGRPC)
{
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if(external.ConnectionType == LightningConnectionType.LndREST)
{
model.Uri = external.BaseUri.AbsoluteUri;
model.ConnectionType = "REST";
}
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
if (external.CertificateThumbprint != null)
{
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
@ -462,10 +585,14 @@ namespace BTCPayServer.Controllers
{
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
}
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null)
{
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
@ -492,29 +619,46 @@ namespace BTCPayServer.Controllers
return Json(conf);
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
[Route("server/services/lnd/{cryptoCode}/{index}")]
[HttpPost]
public IActionResult LndGrpcServicesPost(string cryptoCode, int index)
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
{
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
LightningConfigurations confs = new LightningConfigurations();
LightningConfiguration conf = new LightningConfiguration();
conf.Type = "grpc";
conf.ChainType = _Options.NetworkType.ToString();
conf.CryptoCode = cryptoCode;
conf.Host = external.BaseUri.DnsSafeHost;
conf.Port = external.BaseUri.Port;
conf.SSL = external.BaseUri.Scheme == "https";
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
confs.Configurations.Add(conf);
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
if (external.ConnectionType == LightningConnectionType.LndGRPC)
{
LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc";
grpcConf.Host = external.BaseUri.DnsSafeHost;
grpcConf.Port = external.BaseUri.Port;
grpcConf.SSL = external.BaseUri.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (external.ConnectionType == LightningConnectionType.LndREST)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Uri = external.BaseUri.AbsoluteUri;
confs.Configurations.Add(restconf);
}
else
throw new NotSupportedException(external.ConnectionType.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce });
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
@ -539,28 +683,6 @@ namespace BTCPayServer.Controllers
return connectionString;
}
[Route("server/services/lnd-rest/{cryptoCode}/{index}")]
public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndRestServicesViewModel();
model.BaseApiUrl = external.BaseUri.ToString();
if (external.CertificateThumbprint != null)
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
if (external.Macaroon != null)
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
return View(model);
}
[Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false)
{

View File

@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
string storeId,
string cryptoCode,
string command,
int account = 0)
string keyPath = "")
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
@ -67,7 +67,10 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
var k = KeyPath.Parse(keyPath);
if (k.Indexes.Length == 0)
throw new FormatException("Invalid key path");
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
result = getxpubResult;
}
}
@ -171,7 +174,8 @@ namespace BTCPayServer.Controllers
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
store.SetStoreBlob(storeBlob);
}
catch

View File

@ -0,0 +1,72 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/coinswitch")]
public IActionResult UpdateCoinSwitchSettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
{
var existing = store.GetStoreBlob().CoinSwitchSettings;
if (existing == null) return;
vm.MerchantId = existing.MerchantId;
vm.Enabled = existing.Enabled;
vm.Mode = existing.Mode;
}
[HttpPost]
[Route("{storeId}/coinswitch")]
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var coinSwitchSettings = new CoinSwitchSettings()
{
MerchantId = vm.MerchantId,
Enabled = vm.Enabled,
Mode = vm.Mode
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.CoinSwitchSettings = coinSwitchSettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "CoinSwitch settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
default:
return View(vm);
}
}
}
}

View File

@ -27,7 +27,8 @@ namespace BTCPayServer.Controllers
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
StoreId = storeId
};
SetExistingValues(store, vm);
return View(vm);
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(paymentMethod, network);
var info = await handler.GetNodeInfo(paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))

View File

@ -10,7 +10,9 @@ using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -406,7 +408,7 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
@ -464,6 +466,14 @@ namespace BTCPayServer.Controllers
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = coinSwitchEnabled,
Action = nameof(UpdateCoinSwitchSettings),
Provider = "CoinSwitch"
});
}
[HttpPost]
@ -489,7 +499,7 @@ namespace BTCPayServer.Controllers
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;

View File

@ -410,8 +410,8 @@ namespace BTCPayServer.Controllers
return NotFound();
var cryptoCode = walletId.CryptoCode;
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
@ -476,15 +476,6 @@ namespace BTCPayServer.Controllers
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
result = new GetInfoResult();
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
@ -514,10 +505,20 @@ namespace BTCPayServer.Controllers
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
// Some deployment have the wallet root key path saved in the store blob
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
throw new HardwareWalletException($"This store is not configured to use this ledger");
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();

View File

@ -81,5 +81,10 @@ namespace BTCPayServer.Data
get; set;
}
public List<PendingInvoiceData> PendingInvoices { get; set; }
public Services.Invoices.InvoiceState GetInvoiceState()
{
return new Services.Invoices.InvoiceState(Status, ExceptionStatus);
}
}
}

View File

@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
@ -249,6 +250,12 @@ namespace BTCPayServer.Data
}
}
public enum NetworkFeeMode
{
MultiplePaymentsOnly,
Always,
Never
}
public class StoreBlob
{
public StoreBlob()
@ -258,10 +265,21 @@ namespace BTCPayServer.Data
PaymentTolerance = 0;
RequiresRefundEmail = true;
}
public bool NetworkFeeDisabled
[Obsolete("Use NetworkFeeMode instead")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? NetworkFeeDisabled
{
get; set;
}
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode
{
get;
set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
@ -305,6 +323,7 @@ namespace BTCPayServer.Data
public bool AnyoneCanInvoice { get; set; }
public ChangellySettings ChangellySettings { get; set; }
public CoinSwitchSettings CoinSwitchSettings { get; set; }
string _LightningDescriptionTemplate;
@ -366,6 +385,23 @@ namespace BTCPayServer.Data
[Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; }
#pragma warning disable CS0618 // Type or member is obsolete
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
{
if (keyPath == null)
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
else
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
}
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
{
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
return KeyPath.Parse(k);
return null;
}
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@ -11,23 +11,14 @@ namespace BTCPayServer.Events
public InvoiceDataChangedEvent(InvoiceEntity invoice)
{
InvoiceId = invoice.Id;
Status = invoice.Status;
ExceptionStatus = invoice.ExceptionStatus;
State = invoice.GetInvoiceState();
}
public string InvoiceId { get; set; }
public string Status { get; internal set; }
public string ExceptionStatus { get; internal set; }
public string InvoiceId { get; }
public InvoiceState State { get; }
public override string ToString()
{
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
{
return $"Invoice status is {Status}";
}
else
{
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
}
return $"Invoice status is {State}";
}
}
}

View File

@ -8,6 +8,18 @@ namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public const string Created = "invoice_created";
public const string ReceivedPayment = "invoice_receivedPayment";
public const string MarkedCompleted = "invoice_markedComplete";
public const string MarkedInvalid= "invoice_markedInvalid";
public const string Expired= "invoice_expired";
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
public const string PaidInFull= "invoice_paidInFull";
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
public const string FailedToConfirm= "invoice_failedToConfirm";
public const string Confirmed= "invoice_confirmed";
public const string Completed= "invoice_completed";
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
{
Invoice = invoice;
@ -19,6 +31,8 @@ namespace BTCPayServer.Events
public int EventCode { get; set; }
public string Name { get; set; }
public PaymentEntity Payment { get; set; }
public override string ToString()
{
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";

View File

@ -168,6 +168,41 @@ namespace BTCPayServer
request.Path.ToUriComponent());
}
/// <summary>
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePath(this HttpRequest request, string path)
{
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
request.PathBase.ToUriComponent(),
path);
}
/// <summary>
/// If 'https://example.com/toto' returns 'https://example.com/toto'
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
/// If 'toto' and RootPath is empty returns '/toto'
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
request.PathBase.ToUriComponent(),
path);
}
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =

View File

@ -330,7 +330,7 @@ namespace BTCPayServer.HostedServices
{
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id);
if (invoice == null)
return;
List<Task> tasks = new List<Task>();
@ -341,13 +341,14 @@ namespace BTCPayServer.HostedServices
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed" ||
e.Name == "invoice_expiredPaidPartial"
if (e.Name == InvoiceEvent.Expired ||
e.Name == InvoiceEvent.PaidInFull ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.MarkedInvalid ||
e.Name == InvoiceEvent.MarkedCompleted ||
e.Name == InvoiceEvent.FailedToConfirm ||
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
}

View File

@ -61,15 +61,15 @@ namespace BTCPayServer.HostedServices
private async Task UpdateInvoice(UpdateInvoiceContext context)
{
var invoice = context.Invoice;
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
invoice.Status = "expired";
if(invoice.ExceptionStatus == "paidPartial")
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
invoice.Status = InvoiceStatus.Expired;
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -78,57 +78,57 @@ namespace BTCPayServer.HostedServices
if (paymentMethod == null)
return;
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
if (invoice.Status == "new" || invoice.Status == "expired")
if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired)
{
if (accounting.Paid >= accounting.MinimumTotalDue)
{
if (invoice.Status == "new")
if (invoice.Status == InvoiceStatus.New)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
invoice.Status = InvoiceStatus.Paid;
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
context.MarkDirty();
}
}
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
{
invoice.ExceptionStatus = "paidPartial";
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
}
}
// Just make sure RBF did not cancelled a payment
if (invoice.Status == "paid")
if (invoice.Status == InvoiceStatus.Paid)
{
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
{
invoice.ExceptionStatus = null;
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
context.MarkDirty();
}
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
{
invoice.ExceptionStatus = "paidOver";
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
context.MarkDirty();
}
if (accounting.Paid < accounting.MinimumTotalDue)
{
invoice.Status = "new";
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
invoice.Status = InvoiceStatus.New;
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
}
}
if (invoice.Status == "paid")
if (invoice.Status == InvoiceStatus.Paid)
{
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
@ -139,26 +139,26 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatus.Invalid;
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
invoice.Status = InvoiceStatus.Confirmed;
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
if (invoice.Status == InvoiceStatus.Confirmed)
{
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
invoice.Status = "complete";
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
invoice.Status = InvoiceStatus.Complete;
context.MarkDirty();
}
}
@ -208,7 +208,7 @@ namespace BTCPayServer.HostedServices
private async Task Wait(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
try
{
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
@ -247,13 +247,13 @@ namespace BTCPayServer.HostedServices
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
if (b.Name == InvoiceEvent.Created)
{
Watch(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == "invoice_receivedPayment")
if (b.Name == InvoiceEvent.ReceivedPayment)
{
Watch(b.Invoice.Id);
}
@ -283,14 +283,14 @@ namespace BTCPayServer.HostedServices
loopCount++;
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
@ -299,8 +299,8 @@ namespace BTCPayServer.HostedServices
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));

View File

@ -59,6 +59,12 @@ namespace BTCPayServer.HostedServices
settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertNetworkFeeProperty)
{
await ConvertNetworkFeeProperty();
settings.ConvertNetworkFeeProperty = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -67,6 +73,26 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertNetworkFeeProperty()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0618 // Type or member is obsolete
if (blob.NetworkFeeDisabled != null)
{
blob.NetworkFeeMode = blob.NetworkFeeDisabled.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always;
blob.NetworkFeeDisabled = null;
store.SetStoreBlob(blob);
}
#pragma warning restore CS0618 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertMultiplierToSpread()
{
using (var ctx = _DBContextFactory.CreateContext())

View File

@ -39,6 +39,7 @@ using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
@ -132,6 +133,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<ChangellyClientProvider>();

View File

@ -36,8 +36,11 @@ namespace BTCPayServer.Models.AppViewModels
public string CustomButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Do you want to leave a tip?")]
[Display(Name = "Text to display in the tip input")]
public string CustomTipText { get; set; }
[MaxLength(30)]
[Display(Name = "Tip percentage amounts (comma separated)")]
public string CustomTipPercentages { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]

View File

@ -22,16 +22,30 @@ namespace BTCPayServer.Models.AppViewModels
public bool Custom { get; set; }
}
public class CurrencyInfoData
{
public bool Prefixed { get; set; }
public string CurrencySymbol { get; set; }
public string ThousandSeparator { get; set; }
public string DecimalSeparator { get; set; }
public int Divisibility { get; set; }
public bool SymbolSpace { get; set; }
}
public CurrencyInfoData CurrencyInfo { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }
public string CurrencyCode { get; set; }
public string CurrencySymbol { get; set; }
public string AppId { get; set; }
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomTipText { get; set; }
public int[] CustomTipPercentages { get; set; }
public string CustomCSSLink { get; set; }
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models
{
public class BitpayTranslatorViewModel
{
[Display(Name = "Bitpay's invoice URL or obsolete invoice url")]
public string BitpayLink { get; set; }
public string Address { get; set; }
public string Amount { get; set; }
public string BitcoinUri { get; set; }
}
}

View File

@ -40,6 +40,12 @@ namespace BTCPayServer.Models
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
[JsonIgnore]
public string StoreId
{
get; set;
}
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
public string Url

View File

@ -81,11 +81,11 @@ namespace BTCPayServer.Models.InvoicingModels
public string BOLT11 { get; set; }
}
public string Status
public string State
{
get; set;
}
public string StatusException { get; set; }
public InvoiceExceptionStatus StatusException { get; set; }
public DateTimeOffset CreatedDate
{
get; set;

View File

@ -46,6 +46,9 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public bool CanMarkComplete { get; set; }
public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
public bool ShowCheckout { get; set; }
public string ExceptionStatus { get; set; }
public string AmountCurrency

View File

@ -61,5 +61,9 @@ namespace BTCPayServer.Models.InvoicingModels
public string PeerInfo { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal? ChangellyAmountDue { get; set; }
public bool CoinSwitchEnabled { get; set; }
public string CoinSwitchMode { get; set; }
public string CoinSwitchMerchantId { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class ChargeServiceViewModel
{
public string Uri { get; set; }
public string APIToken { get; set; }
public string AuthenticatedUri { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@ -10,8 +11,16 @@ namespace BTCPayServer.Models.ServerViewModels
public string Host { get; set; }
public bool SSL { get; set; }
public string Macaroon { get; set; }
public string AdminMacaroon { get; set; }
public string ReadonlyMacaroon { get; set; }
public string InvoiceMacaroon { get; set; }
public string CertificateThumbprint { get; set; }
[Display(Name = "GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)")]
public string GRPCSSLCipherSuites { get; set; }
public string QRCode { get; set; }
public string QRCodeLink { get; set; }
[Display(Name = "REST Uri")]
public string Uri { get; set; }
public string ConnectionType { get; internal set; }
}
}

View File

@ -11,11 +11,18 @@ namespace BTCPayServer.Models.ServerViewModels
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public LndTypes Type { get; set; }
public string Type { get; set; }
public int Index { get; set; }
public string Action { get; internal set; }
}
public class ExternalService
{
public string Name { get; set; }
public string Link { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public bool HasSSH { get; set; }
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class SparkServicesViewModel
{
public string SparkLink { get; set; }
public bool ShowQR { get; set; }
}
}

View File

@ -24,6 +24,7 @@ namespace BTCPayServer.Models.StoreViewModels
} = new List<(string KeyPath, string Address)>();
public string CryptoCode { get; set; }
public string KeyPath { get; set; }
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }

View File

@ -25,5 +25,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
public bool Enabled { get; set; } = true;
public string StoreId { get; set; }
}
}

View File

@ -82,8 +82,8 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Add network fee to invoice (vary with mining fees)")]
public bool NetworkFee
[Display(Name = "Add additional fee (network fee) to invoice...")]
public Data.NetworkFeeMode NetworkFeeMode
{
get; set;
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Payments.CoinSwitch;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class UpdateCoinSwitchSettingsViewModel
{
public string MerchantId { get; set; }
public bool Enabled { get; set; }
[Display(Name = "Integration Mode")]
public string Mode { get; set; } = "inline";
public List<SelectListItem> Modes { get; } = new List<SelectListItem>
{
new SelectListItem { Value = "popup", Text = "Open in a popup" },
new SelectListItem { Value = "inline", Text = "Embed inside Checkout UI " },
};
public string StatusMessage { get; set; }
}
}

View File

@ -20,27 +20,21 @@ namespace BTCPayServer.Payments.Bitcoin
return DepositAddress;
}
public decimal GetTxFee()
public decimal GetNextNetworkFee()
{
return TxFee.ToDecimal(MoneyUnit.BTC);
return NextNetworkFee.ToDecimal(MoneyUnit.BTC);
}
public void SetNoTxFee()
{
TxFee = Money.Zero;
}
public void SetPaymentDestination(string newPaymentDestination)
{
DepositAddress = newPaymentDestination;
}
public Data.NetworkFeeMode NetworkFeeMode { get; set; }
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
[JsonIgnore]
public FeeRate FeeRate { get; set; }
[JsonIgnore]
public Money TxFee { get; set; }
public Money NextNetworkFee { get; set; }
[JsonIgnore]
public String DepositAddress { get; set; }
public BitcoinAddress GetDepositAddress(Network network)

View File

@ -32,6 +32,7 @@ namespace BTCPayServer.Payments.Bitcoin
public TxOut Output { get; set; }
public int ConfirmationCount { get; set; }
public bool RBF { get; set; }
public decimal NetworkFee { get; set; }
/// <summary>
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
@ -78,5 +79,15 @@ namespace BTCPayServer.Payments.Bitcoin
}
return false;
}
public BitcoinAddress GetDestination(BTCPayNetwork network)
{
return Output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
}
string CryptoPaymentData.GetDestination(BTCPayNetwork network)
{
return GetDestination(network).ToString();
}
}
}

View File

@ -48,8 +48,18 @@ namespace BTCPayServer.Payments.Bitcoin
throw new PaymentMethodUnavailableException($"Full node not available");
var prepare = (Prepare)preparePaymentObject;
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.NetworkFeeMode = store.GetStoreBlob().NetworkFeeMode;
onchainMethod.FeeRate = await prepare.GetFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
switch (onchainMethod.NetworkFeeMode)
{
case NetworkFeeMode.Always:
onchainMethod.NextNetworkFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
break;
case NetworkFeeMode.Never:
case NetworkFeeMode.MultiplePaymentsOnly:
onchainMethod.NextNetworkFee = Money.Zero;
break;
}
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
return onchainMethod;
}

View File

@ -158,7 +158,7 @@ namespace BTCPayServer.Payments.Bitcoin
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
if(payment != null)
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
}
@ -205,7 +205,7 @@ namespace BTCPayServer.Payments.Bitcoin
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, false);
if (invoice == null)
return null;
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
@ -315,7 +315,7 @@ namespace BTCPayServer.Payments.Bitcoin
var invoices = await _InvoiceRepository.GetPendingInvoices();
foreach (var invoiceId in invoices)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
continue;
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null)
{
@ -373,7 +373,7 @@ namespace BTCPayServer.Payments.Bitcoin
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

View File

@ -0,0 +1,15 @@
namespace BTCPayServer.Payments.CoinSwitch
{
public class CoinSwitchSettings
{
public string MerchantId { get; set; }
public string Mode { get; set; }
public bool Enabled { get; set; }
public bool IsConfigured()
{
return
!string.IsNullOrEmpty(MerchantId);
}
}
}

View File

@ -18,11 +18,10 @@ namespace BTCPayServer.Payments
string GetPaymentDestination();
PaymentTypes GetPaymentType();
/// <summary>
/// Returns what a merchant would need to pay to cashout this payment
/// Returns fee that the merchant charge to the customer for the next payment
/// </summary>
/// <returns></returns>
decimal GetTxFee();
void SetNoTxFee();
decimal GetNextNetworkFee();
/// <summary>
/// Change the payment destination (internal plumbing)
/// </summary>

View File

@ -16,6 +16,14 @@ namespace BTCPayServer.Payments.Lightning
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
public string GetDestination(BTCPayNetwork network)
{
return GetPaymentId();
}
public decimal NetworkFee { get; set; }
public string GetPaymentId()
{
return BOLT11;

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Payments.Lightning
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network);
var test = GetNodeInfo(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = supportedPaymentMethod.CreateClient(network);
@ -63,7 +63,7 @@ namespace BTCPayServer.Payments.Lightning
};
}
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
}
catch (Exception ex)
{

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning
{
@ -21,15 +22,10 @@ namespace BTCPayServer.Payments.Lightning
return PaymentTypes.LightningLike;
}
public decimal GetTxFee()
public decimal GetNextNetworkFee()
{
return 0.0m;
}
public void SetNoTxFee()
{
}
public void SetPaymentDestination(string newPaymentDestination)
{
BOLT11 = newPaymentDestination;

View File

@ -42,7 +42,7 @@ namespace BTCPayServer.Payments.Lightning
{
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
if (inv.Name == InvoiceEvent.Created)
{
await EnsureListening(inv.Invoice.Id, false);
}
@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning
{
if (Listening(invoiceId))
return;
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
{
@ -156,7 +156,8 @@ namespace BTCPayServer.Payments.Lightning
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == LightningInvoiceStatus.Paid && notification.PaidAt.HasValue)
if (notification.Status == LightningInvoiceStatus.Paid &&
notification.PaidAt.HasValue && notification.Amount != null)
{
await AddPayment(network, notification, listenedInvoice);
if (DoneListening(listenedInvoice))
@ -191,12 +192,12 @@ namespace BTCPayServer.Payments.Lightning
{
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
}, network, accounted: true);
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
}
}

View File

@ -5,7 +5,7 @@
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
@ -24,15 +24,18 @@
"BTCPAY_NETWORK": "regtest",
"BTCPAY_PORT": "14142",
"BTCPAY_HttpsUseDefaultCertificate": "true",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;"
},
"applicationUrl": "https://localhost:14142/"
}

View File

@ -57,43 +57,37 @@ namespace BTCPayServer.Security
List<Claim> claims = new List<Claim>();
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
string storeId = null;
// Careful, those are not the opposite. failedAuth says if a the tentative failed.
// successAuth, ensure that at least one succeed.
var failedAuth = false;
var successAuth = false;
bool? success = null;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
storeId = result.StoreId;
failedAuth = !result.SuccessAuth;
successAuth = result.SuccessAuth;
success = result.SuccessAuth;
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
successAuth = storeId != null;
success = storeId != null;
}
if (failedAuth)
if (success.HasValue)
{
return AuthenticateResult.Fail("Invalid credentials");
}
if (successAuth)
{
if (storeId != null)
if (success.Value)
{
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
if (storeId != null)
{
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
else
{
return AuthenticateResult.Fail("Invalid credentials");
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
}
return AuthenticateResult.NoResult();
@ -148,6 +142,10 @@ namespace BTCPayServer.Security
storeId = bitToken.StoreId;
}
}
else
{
return (storeId, false);
}
}
catch (FormatException) { }
return (storeId, true);

View File

@ -89,20 +89,19 @@ namespace BTCPayServer.Services
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation)
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation);
var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = segwit,
Legacy = !segwit
});
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
@ -129,7 +128,13 @@ namespace BTCPayServer.Services
}
}
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
public async Task<bool> CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation)
{
var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation);
return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey;
}
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
{
List<KeyPath> derivations = new List<KeyPath>();
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
@ -164,7 +169,17 @@ namespace BTCPayServer.Services
KeyPath changeKeyPath,
CancellationToken cancellationToken)
{
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
try
{
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
if (signedTransaction == null)
throw new Exception("The ledger failed to sign the transaction");
return signedTransaction;
}
catch (Exception ex)
{
throw new Exception("The ledger failed to sign the transaction", ex);
}
}
public void Dispose()

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Bitcoin;
@ -10,6 +11,12 @@ namespace BTCPayServer.Services.Invoices.Export
{
public class InvoiceExport
{
public BTCPayNetworkProvider Networks { get; }
public InvoiceExport(BTCPayNetworkProvider networks)
{
Networks = networks;
}
public string Process(InvoiceEntity[] invoices, string fileFormat)
{
var csvInvoices = new List<ExportInvoiceHolder>();
@ -45,6 +52,8 @@ namespace BTCPayServer.Services.Invoices.Export
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
{
var exportList = new List<ExportInvoiceHolder>();
var invoiceDue = invoice.ProductInformation.Price;
// in this first version we are only exporting invoices that were paid
foreach (var payment in invoice.GetPayments())
{
@ -55,9 +64,9 @@ namespace BTCPayServer.Services.Invoices.Export
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData();
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), null);
var accounting = pmethod.Calculate();
var details = pmethod.GetPaymentMethodDetails();
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
var target = new ExportInvoiceHolder
{
@ -65,23 +74,31 @@ namespace BTCPayServer.Services.Invoices.Export
PaymentId = pdata.GetPaymentId(),
CryptoCode = cryptoCode,
ConversionRate = pmethod.Rate,
PaymentType = details.GetPaymentType() == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
Destination = details.GetPaymentDestination(),
PaymentDue = $"{accounting.MinimumTotalDue} {cryptoCode}",
PaymentPaid = $"{accounting.CryptoPaid} {cryptoCode}",
PaymentOverpaid = $"{accounting.OverpaidHelper} {cryptoCode}",
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture),
// Adding NetworkFee because Paid doesn't take into account network fees
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
// while looking just at export you could sum Paid and assume merchant "received payments"
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
InvoiceDue = invoiceDue,
OrderId = invoice.OrderId,
StoreId = invoice.StoreId,
InvoiceId = invoice.Id,
CreatedDate = invoice.InvoiceTime.UtcDateTime,
ExpirationDate = invoice.ExpirationTime.UtcDateTime,
MonitoringDate = invoice.MonitoringExpiration.UtcDateTime,
Status = invoice.Status,
ItemCode = invoice.ProductInformation?.ItemCode,
ItemDesc = invoice.ProductInformation?.ItemDesc,
FiatPrice = invoice.ProductInformation?.Price ?? 0,
FiatCurrency = invoice.ProductInformation?.Currency,
InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime,
InvoiceExpirationDate = invoice.ExpirationTime.UtcDateTime,
InvoiceMonitoringDate = invoice.MonitoringExpiration.UtcDateTime,
#pragma warning disable CS0618 // Type or member is obsolete
InvoiceFullStatus = invoice.GetInvoiceState().ToString(),
InvoiceStatus = invoice.StatusString,
InvoiceExceptionStatus = invoice.ExceptionStatusString,
#pragma warning restore CS0618 // Type or member is obsolete
InvoiceItemCode = invoice.ProductInformation.ItemCode,
InvoiceItemDesc = invoice.ProductInformation.ItemDesc,
InvoicePrice = invoice.ProductInformation.Price,
InvoiceCurrency = invoice.ProductInformation.Currency,
};
exportList.Add(target);
@ -99,23 +116,25 @@ namespace BTCPayServer.Services.Invoices.Export
public string StoreId { get; set; }
public string OrderId { get; set; }
public string InvoiceId { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ExpirationDate { get; set; }
public DateTime MonitoringDate { get; set; }
public DateTime InvoiceCreatedDate { get; set; }
public DateTime InvoiceExpirationDate { get; set; }
public DateTime InvoiceMonitoringDate { get; set; }
public string PaymentId { get; set; }
public string CryptoCode { get; set; }
public string Destination { get; set; }
public string PaymentType { get; set; }
public string PaymentDue { get; set; }
public string PaymentPaid { get; set; }
public string PaymentOverpaid { get; set; }
public string CryptoCode { get; set; }
public string Paid { get; set; }
public string NetworkFee { get; set; }
public decimal ConversionRate { get; set; }
public decimal FiatPrice { get; set; }
public string FiatCurrency { get; set; }
public string ItemCode { get; set; }
public string ItemDesc { get; set; }
public string Status { get; set; }
public string PaidCurrency { get; set; }
public string InvoiceCurrency { get; set; }
public decimal InvoiceDue { get; set; }
public decimal InvoicePrice { get; set; }
public string InvoiceItemCode { get; set; }
public string InvoiceItemDesc { get; set; }
public string InvoiceFullStatus { get; set; }
public string InvoiceStatus { get; set; }
public string InvoiceExceptionStatus { get; set; }
}
}

View File

@ -226,15 +226,23 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618
}
public string Status
[JsonIgnore]
public InvoiceStatus Status
{
get;
set;
}
public string ExceptionStatus
[JsonProperty(PropertyName = "status")]
[Obsolete("Use Status instead")]
public string StatusString => InvoiceState.ToString(Status);
[JsonIgnore]
public InvoiceExceptionStatus ExceptionStatus
{
get; set;
}
[JsonProperty(PropertyName = "exceptionStatus")]
[Obsolete("Use ExceptionStatus instead")]
public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus);
[Obsolete("Use GetPayments instead")]
public List<PaymentEntity> Payments
@ -341,7 +349,10 @@ namespace BTCPayServer.Services.Invoices
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime,
Status = Status,
#pragma warning disable CS0618 // Type or member is obsolete
Status = StatusString,
ExceptionStatus = ExceptionStatus == InvoiceExceptionStatus.None ? new JValue(false) : new JValue(ExceptionStatusString),
#pragma warning restore CS0618 // Type or member is obsolete
Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable },
PaymentSubtotals = new Dictionary<string, long>(),
@ -395,9 +406,6 @@ namespace BTCPayServer.Services.Invoices
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
@ -450,7 +458,6 @@ namespace BTCPayServer.Services.Invoices
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
return dto;
}
@ -478,7 +485,7 @@ namespace BTCPayServer.Services.Invoices
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
{
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
if (PaymentMethod != null)
@ -492,11 +499,11 @@ namespace BTCPayServer.Services.Invoices
r.ParentEntity = this;
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
if (r.Network != null || networkProvider == null)
rates.Add(r);
paymentMethods.Add(r);
}
}
#pragma warning restore CS0618
return rates;
return paymentMethods;
}
Network Dummy = Network.Main;
@ -510,8 +517,6 @@ namespace BTCPayServer.Services.Invoices
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
{
if (paymentMethods.NetworkProvider != null)
throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null");
var obj = new JObject();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
@ -529,6 +534,108 @@ namespace BTCPayServer.Services.Invoices
}
#pragma warning restore CS0618
}
public InvoiceState GetInvoiceState()
{
return new InvoiceState(Status, ExceptionStatus);
}
}
public enum InvoiceStatus
{
New,
Paid,
Expired,
Invalid,
Complete,
Confirmed
}
public enum InvoiceExceptionStatus
{
None,
PaidLate,
PaidPartial,
Marked,
Invalid,
PaidOver
}
public class InvoiceState
{
static Dictionary<string, InvoiceStatus> _StringToInvoiceStatus;
static Dictionary<InvoiceStatus, string> _InvoiceStatusToString;
static Dictionary<string, InvoiceExceptionStatus> _StringToExceptionStatus;
static Dictionary<InvoiceExceptionStatus, string> _ExceptionStatusToString;
static InvoiceState()
{
_StringToInvoiceStatus = new Dictionary<string, InvoiceStatus>();
_StringToInvoiceStatus.Add("paid", InvoiceStatus.Paid);
_StringToInvoiceStatus.Add("expired", InvoiceStatus.Expired);
_StringToInvoiceStatus.Add("invalid", InvoiceStatus.Invalid);
_StringToInvoiceStatus.Add("complete", InvoiceStatus.Complete);
_StringToInvoiceStatus.Add("new", InvoiceStatus.New);
_StringToInvoiceStatus.Add("confirmed", InvoiceStatus.Confirmed);
_InvoiceStatusToString = _StringToInvoiceStatus.ToDictionary(kv => kv.Value, kv => kv.Key);
_StringToExceptionStatus = new Dictionary<string, InvoiceExceptionStatus>();
_StringToExceptionStatus.Add(string.Empty, InvoiceExceptionStatus.None);
_StringToExceptionStatus.Add("paidPartial", InvoiceExceptionStatus.PaidPartial);
_StringToExceptionStatus.Add("paidLate", InvoiceExceptionStatus.PaidLate);
_StringToExceptionStatus.Add("paidOver", InvoiceExceptionStatus.PaidOver);
_StringToExceptionStatus.Add("marked", InvoiceExceptionStatus.Marked);
_ExceptionStatusToString = _StringToExceptionStatus.ToDictionary(kv => kv.Value, kv => kv.Key);
_StringToExceptionStatus.Add("false", InvoiceExceptionStatus.None);
}
public InvoiceState(string status, string exceptionStatus)
{
Status = _StringToInvoiceStatus[status];
ExceptionStatus = _StringToExceptionStatus[exceptionStatus ?? string.Empty];
}
public InvoiceState(InvoiceStatus status, InvoiceExceptionStatus exceptionStatus)
{
Status = status;
ExceptionStatus = exceptionStatus;
}
public InvoiceStatus Status { get; }
public InvoiceExceptionStatus ExceptionStatus { get; }
public static string ToString(InvoiceStatus status)
{
return _InvoiceStatusToString[status];
}
public static string ToString(InvoiceExceptionStatus exceptionStatus)
{
return _ExceptionStatusToString[exceptionStatus];
}
public bool CanMarkComplete()
{
return (Status == InvoiceStatus.Paid) ||
#pragma warning disable CA1305 // Specify IFormatProvider
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
(Status != InvoiceStatus.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked) ||
(Status == InvoiceStatus.Invalid);
#pragma warning restore CA1305 // Specify IFormatProvider
}
public bool CanMarkInvalid()
{
return (Status == InvoiceStatus.Paid) ||
(Status == InvoiceStatus.New) ||
#pragma warning disable CA1305 // Specify IFormatProvider
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
(Status != InvoiceStatus.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
#pragma warning restore CA1305 // Specify IFormatProvider;
}
public override string ToString()
{
return ToString(Status) + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
}
}
public class PaymentMethodAccounting
@ -572,7 +679,7 @@ namespace BTCPayServer.Services.Invoices
/// </summary>
public Money NetworkFee { get; set; }
/// <summary>
/// Minimum required to be paid in order to accept invocie as paid
/// Minimum required to be paid in order to accept invoice as paid
/// </summary>
public Money MinimumTotalDue { get; set; }
}
@ -622,7 +729,7 @@ namespace BTCPayServer.Services.Invoices
{
FeeRate = FeeRate,
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
TxFee = TxFee
NextNetworkFee = NextNetworkFee
};
}
else
@ -630,7 +737,7 @@ namespace BTCPayServer.Services.Invoices
var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails);
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
{
btcLike.TxFee = TxFee;
btcLike.NextNetworkFee = NextNetworkFee;
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
btcLike.FeeRate = FeeRate;
}
@ -652,7 +759,7 @@ namespace BTCPayServer.Services.Invoices
if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod)
{
TxFee = bitcoinPaymentMethod.TxFee;
NextNetworkFee = bitcoinPaymentMethod.NextNetworkFee;
FeeRate = bitcoinPaymentMethod.FeeRate;
DepositAddress = bitcoinPaymentMethod.DepositAddress;
}
@ -667,8 +774,8 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")]
public FeeRate FeeRate { get; set; }
[JsonProperty(PropertyName = "txFee")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")]
public Money TxFee { get; set; }
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).NextNetworkFee")]
public Money NextNetworkFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
public string DepositAddress { get; set; }
@ -692,7 +799,7 @@ namespace BTCPayServer.Services.Invoices
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee());
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
paid += _.GetValue(paymentMethods, GetId());
if (!paidEnough)
{
@ -733,17 +840,18 @@ namespace BTCPayServer.Services.Invoices
var method = GetPaymentMethodDetails();
if (method == null)
return 0.0m;
return method.GetTxFee();
return method.GetNextNetworkFee();
}
}
public class PaymentEntity
{
public int Version { get; set; }
public DateTimeOffset ReceivedTime
{
get; set;
}
public decimal NetworkFee { get; set; }
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
public OutPoint Outpoint
{
@ -780,7 +888,7 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
// In case this is a payment done before this update, consider it unconfirmed with RBF for safety
// For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
paymentData.Outpoint = Outpoint;
paymentData.Output = Output;
@ -873,5 +981,6 @@ namespace BTCPayServer.Services.Invoices
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
PaymentTypes GetPaymentType();
string GetDestination(BTCPayNetwork network);
}
}

View File

@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Invoices
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
{
int retryCount = 0;
retry:
retry:
try
{
_Engine = new DBreezeEngine(dbreezePath);
@ -126,7 +126,9 @@ namespace BTCPayServer.Services.Invoices
Created = invoice.InvoiceTime,
Blob = ToBytes(invoice, null),
OrderId = invoice.OrderId,
Status = invoice.Status,
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
});
@ -191,7 +193,7 @@ namespace BTCPayServer.Services.Invoices
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
}
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network)
public async Task<bool> NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetwork network)
{
using (var context = _ContextFactory.CreateContext())
{
@ -204,14 +206,13 @@ namespace BTCPayServer.Services.Invoices
if (currencyData == null)
return false;
var existingPaymentMethod = currencyData.GetPaymentMethodDetails();
var existingPaymentMethod = (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)currencyData.GetPaymentMethodDetails();
if (existingPaymentMethod.GetPaymentDestination() != null)
{
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
}
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
currencyData.SetPaymentMethodDetails(existingPaymentMethod);
#pragma warning disable CS0618
if (network.IsBTC)
@ -316,15 +317,15 @@ namespace BTCPayServer.Services.Invoices
});
}
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
invoiceData.Status = status;
invoiceData.ExceptionStatus = exceptionStatus;
invoiceData.Status = InvoiceState.ToString(invoiceState.Status);
invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus);
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
@ -334,13 +335,26 @@ namespace BTCPayServer.Services.Invoices
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData?.Status != "paid")
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid())
return;
invoiceData.Status = "invalid";
invoiceData.ExceptionStatus = "marked";
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool inludeAddressData = false)
public async Task UpdatePaidInvoiceToComplete(string invoiceId)
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete())
return;
invoiceData.Status = "complete";
invoiceData.ExceptionStatus = "marked";
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<InvoiceEntity> GetInvoice(string id, bool inludeAddressData = false)
{
using (var context = _ContextFactory.CreateContext())
{
@ -353,9 +367,6 @@ namespace BTCPayServer.Services.Invoices
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
query = query.Where(i => i.Id == id);
if (storeId != null)
query = query.Where(i => i.StoreDataId == storeId);
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
if (invoice == null)
return null;
@ -367,16 +378,31 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
PaymentMethodDictionary paymentMethods = null;
#pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
paymentEntity.Accounted = p.Accounted;
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee.
// We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity.
if (paymentEntity.Version == 0)
{
if (paymentMethods == null)
paymentMethods = entity.GetPaymentMethods(null);
var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails();
if (paymentMethodDetails != null) // == null should never happen, but we never know.
paymentEntity.NetworkFee = paymentMethodDetails.GetNextNetworkFee();
}
return paymentEntity;
}).ToList();
})
.OrderBy(a => a.ReceivedTime).ToList();
#pragma warning restore CS0618
entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status;
var state = invoice.GetInvoiceState();
entity.ExceptionStatus = state.ExceptionStatus;
entity.Status = state.Status;
entity.RefundMail = invoice.CustomerEmail;
entity.Refundable = invoice.RefundAddresses.Count != 0;
if (invoice.HistoricalAddressInvoices != null)
@ -437,11 +463,16 @@ namespace BTCPayServer.Services.Invoices
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.ItemCode != null)
query = query.Where(i => i.ItemCode == queryObject.ItemCode);
if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
@ -533,21 +564,37 @@ namespace BTCPayServer.Services.Invoices
/// <param name="cryptoCode"></param>
/// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetwork network, bool accounted = false)
{
using (var context = _ContextFactory.CreateContext())
{
var invoice = context.Invoices.Find(invoiceId);
if (invoice == null)
return null;
InvoiceEntity invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null);
IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
PaymentEntity entity = new PaymentEntity
{
Version = 1,
#pragma warning disable CS0618
CryptoCode = cryptoCode,
CryptoCode = network.CryptoCode,
#pragma warning restore CS0618
ReceivedTime = date.UtcDateTime,
Accounted = accounted
Accounted = accounted,
NetworkFee = paymentMethodDetails.GetNextNetworkFee()
};
entity.SetCryptoPaymentData(paymentData);
if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod &&
bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly &&
bitcoinPaymentMethod.NextNetworkFee == Money.Zero)
{
bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.FeeRate.GetFee(100); // assume price for 100 bytes
paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod);
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
}
PaymentData data = new PaymentData
{
Id = paymentData.GetPaymentId(),
@ -652,12 +699,12 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public string OrderId
public string[] OrderId
{
get; set;
}
public string ItemCode
public string[] ItemCode
{
get; set;
}

View File

@ -15,13 +15,6 @@ namespace BTCPayServer.Services.Invoices
}
public PaymentMethodDictionary(BTCPayNetworkProvider networkProvider)
{
NetworkProvider = networkProvider;
}
public BTCPayNetworkProvider NetworkProvider { get; set; }
public PaymentMethod this[PaymentMethodId index]
{
get

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Services
{
@ -27,9 +28,9 @@ namespace BTCPayServer.Services
private void CleanExpired()
{
foreach(var item in _Map)
foreach (var item in _Map)
{
if(item.Value.expiration < DateTimeOffset.UtcNow)
if (item.Value.expiration < DateTimeOffset.UtcNow)
{
_Map.TryRemove(item.Key, out var unused);
}
@ -39,17 +40,28 @@ namespace BTCPayServer.Services
public class LightningConfigurations
{
public List<LightningConfiguration> Configurations { get; set; } = new List<LightningConfiguration>();
public List<object> Configurations { get; set; } = new List<object>();
}
public class LightningConfiguration
public class LNDConfiguration
{
public string ChainType { get; set; }
public string Type { get; set; }
public string CryptoCode { get; set; }
public string CertificateThumbprint { get; set; }
public string Macaroon { get; set; }
public string AdminMacaroon { get; set; }
public string ReadonlyMacaroon { get; set; }
public string InvoiceMacaroon { get; set; }
}
public class LightningConfiguration : LNDConfiguration
{
public string Host { get; set; }
public int Port { get; set; }
public bool SSL { get; set; }
public string CertificateThumbprint { get; set; }
public string Macaroon { get; set; }
}
public class LNDRestConfiguration : LNDConfiguration
{
public string Uri { get; set; }
}
}

View File

@ -10,5 +10,6 @@ namespace BTCPayServer.Services
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
public bool ConvertNetworkFeeProperty { get; set; }
}
}

View File

@ -61,7 +61,15 @@ namespace BTCPayServer.Services.Rates
currencyInfo.CurrencySymbol = currency;
return currencyInfo;
}
public NumberFormatInfo GetNumberFormatInfo(string currency)
{
var curr = GetCurrencyProvider(currency);
if (curr is CultureInfo cu)
return cu.NumberFormat;
if (curr is NumberFormatInfo ni)
return ni;
return null;
}
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)

View File

@ -3,33 +3,39 @@
ViewData["Title"] = "Reset password";
}
<h2>@ViewData["Title"]</h2>
<h4>Reset your password.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Code" type="hidden" />
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Code" type="hidden" />
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")

View File

@ -3,6 +3,12 @@
}
<h2>@ViewData["Title"]</h2>
<p>
Your password has been reset. Please <a asp-action="Login">click here to log in</a>.
</p>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
Your password has been reset. Please <a asp-action="Login">click here to log in</a>.
</div>
</div>
</div>
</section>

View File

@ -3,6 +3,26 @@
ViewData["Title"] = "Update Point of Sale";
}
<section>
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Product management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@ -52,15 +72,28 @@
<input asp-for="CustomTipText" class="form-control" />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomTipPercentages" class="control-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSSLink" class="control-label"></label>
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label">Products</label>*
<div class="mb-3">
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Product</a>
</div>
<div class="js-products bg-light row p-3">
</div>
</div>
<div class="form-group">
<label asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
@ -101,5 +134,56 @@
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
{image}
<div class="card-body">
<h6 class="card-title">{title}</h6>
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
</div>
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
<input class="js-product-index" type="hidden" name="index" value="{index}">
<div class="form-row">
<div class="col-sm-6">
<label>Title</label>*
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
<select class="js-product-custom form-control">
{custom}
</select>
</div>
</div>
<div class="form-row">
<div class="col">
<label>Image</label>
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
</div>
</div>
<div class="form-row">
<div class="col">
<label>Description</label>
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
</div>
</div>
</div>
</script>
<script src="~/products/js/products.js"></script>
<script src="~/products/js/products.jquery.js"></script>
}

View File

@ -5,6 +5,7 @@
@{
ViewData["Title"] = Model.Title;
Layout = null;
int[] CustomTipPercentages = Model.CustomTipPercentages;
}
<!DOCTYPE html>
@ -14,6 +15,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
<link rel="apple-touch-startup-image" href="~/img/splash.png">
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
@ -23,125 +29,330 @@
@if (Model.EnableShoppingCart)
{
<link rel="stylesheet" href="~/cart/css/style.css">
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}
</head>
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
{image}
<td class="align-middle pr-0 pl-2"><b>{title}</b></td>
<td class="align-middle px-0" align="right">
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
</td>
<td class="align-middle px-0" align="right">
<div class="input-group">
<div class="input-group-prepend">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
</div>
<input class="js-cart-item-count form-control form-control-sm pull-left" type="text"name="count" placeholder="Qty" value="{count}" data-prev="{count}">
<div class="input-group-append"><a class="js-cart-item-plus btn btn-link px-2" href="#">
<i class="fa fa-plus-circle fa-fw text-success"></i></a>
</div>
</div>
</td>
<td class="align-middle" align="right">{price}</td>
</tr>
</script>
<script id="template-cart-item-image" type="text/template">
<td class="align-middle pr-0" width="1%"><img src="{image}" width="50"></td>
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<td colspan="5">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-extra" type="text/template">
<tr>
<td colspan="5" class="border-0 pb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
<tr>
<td colspan="5" class="border-top-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
</div>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<div class="input-group-append">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-tip" type="text/template">
<tr class="h5">
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
</tr>
<tr>
<td colspan="5" class="border-0">
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
</div>
<input class="js-cart-tip form-control" type="number" min="0" step="@Model.Step" value="{tip}" name="tip" placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)">
<div class="input-group-append">
<a class="js-cart-tip-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="row mb-1">
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
var percentage = CustomTipPercentages[i];
<div class="col">
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</div>
}
}
</div>
</td>
</tr>
</script>
<script id="template-cart-total" type="text/template">
<tr class="h4 table-light">
<td colspan="1" class="pb-4">Total</td>
<td colspan="4" align="right" class="pb-4">
<span class="js-cart-total">{total}</span>
</td>
</tr>
</script>
<body class="h-100">
@if (Model.EnableShoppingCart)
{
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Shopping cart</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
</button>
</div>
<div class="modal-body p-0">
<table id="js-cart-summary" class="table m-0">
<tbody class="my-3">
<tr class="h5">
<td colspan="2" class="border-top-0">Summary</td>
</tr>
<tr class="h6">
<td class="border-0 pb-0">Total products</td>
<td align="right" class="border-0 pb-0">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
<tr class="h6">
<td class="border-0 pb-y">Discount</td>
<td align="right" class="border-0 pb-y">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
<tr class="h6">
<td class="border-top-0 pt-0">Tip</td>
<td align="right" class="border-top-0 pt-0">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
<tr class="h3 table-light">
<td>Total</td>
<td align="right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer bg-light">
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>
</div>
</div>
<div class="modal-body">
<table id="js-cart-list" class="table mt-2 mb-3">
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-3">
<div class="row">
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
</div>
<div class="col-sm-8 col-lg-9 mb-2">
<div class="input-group mb-2">
<input type="text" class="js-search form-control" placeholder="Find product">
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
</div>
</div>
</div>
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark">
<div class="bg-warning p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
<thead class="thead-dark">
<tr>
<th colspan="2">Product</th>
<th class="text-right" width="80">Quantity</th>
<th class="text-right" width="25%">Price</th>
<th colspan="3" width="55%">Product</th>
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<button id="js-cart-pay" class="btn btn-primary" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>
</div>
</div>
</div>
}
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@if (Model.EnableShoppingCart)
{
<a id="js-cart" class="btn btn-warning text-white text-right" href="#" data-toggle="modal" data-target="#cartModal"><i class="fa fa-shopping-basket"></i>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
}
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
<tbody></tbody>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
<div class="text-center mb-5 pb-5">
<img src="~/img/logo-white.png" height="40">
</div>
</nav>
</div>
} else {
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<p class="card-text">@description</p>
<img class="card-img-top" src="@image" alt="Card image cap">
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
</div>
}
</div>
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
</div>
</div>
}
</div>
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
}
</div>
</div>
}
</body>
</html>

View File

@ -0,0 +1,61 @@
@model BitpayTranslatorViewModel
<section>
<div class="container">
@if (Model.Address != null)
{
<div class="row">
<div class="col-lg-12 text-center">
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>You need to pay <b>@Model.Amount</b> to <b>@Model.Address</b></p>
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.BitcoinUri)" style="margin-bottom:20px;"></div>
<p>
<a class="btn btn-primary" href="@Model.BitcoinUri">
<span>Open in wallet</span>
</a>
</p>
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">The Bitpay Translator</h2>
<hr class="primary">
<p>Bitpay is using deprecated standard in their invoices which multiple wallet do not support, use this transform their invoices to regular address/amount.</p>
</div>
</div>
<div class="row">
<div class="col-lg-4 text-center">&nbsp;</div>
<div class="col-lg-4 text-center">
<form method="post">
<div class="form-group">
<label asp-for="BitpayLink" class="control-label"></label>*
<input asp-for="BitpayLink" class="form-control" />
<span asp-validation-for="BitpayLink" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-secondary" title="Continue">Translate to address</button>
</div>
</form>
</div>
<div class="col-lg-4 text-center">&nbsp;</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.BitcoinUri)",
width: 150,
height: 150
});
$("#qrCode > img").css({ "margin": "auto" });
</script>
}

View File

@ -9,8 +9,8 @@
<div class="header-content-inner text-white">
<h1>Welcome to BTCPay Server</h1>
<hr />
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business. The API is compatible with Bitpay service to allow seamless migration.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Getting started</a>
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://docs.btcpayserver.org">Getting started</a>
</div>
</div>
</header>

View File

@ -151,7 +151,7 @@
<div class="payment-tabs__tab" id="copy-tab">
<span>{{$t("Copy")}}</span>
</div>
@if (Model.ChangellyEnabled)
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span>{{$t("Conversion")}}</span>
@ -256,7 +256,7 @@
</div>
</nav>
</div>
@if (Model.ChangellyEnabled)
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div id="altcoins" class="bp-view payment manual-flow">
<nav v-if="srvModel.isLightning">
@ -274,42 +274,99 @@
{{$t("ConversionTab_BodyDesc", srvModel)}}
</span>
</div>
<center>
<changelly inline-template
:merchant-id="srvModel.changellyMerchantId"
:store-id="srvModel.storeId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.changellyAmountDue"
:to-currency-address="srvModel.btcAddress">
<div class="changelly-component">
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
<select
<center>
@if (Model.CoinSwitchEnabled && Model.ChangellyEnabled)
{
<template v-if="!selectedThirdPartyProcessor">
<button v-on:click="selectedThirdPartyProcessor = 'coinswitch'" class="action-button">
{{$t("Pay with CoinSwitch")}}
</button>
<button v-on:click="selectedThirdPartyProcessor = 'changelly'" class="action-button">
{{$t("Pay with Changelly")}}
</button>
</template>
}
@if (Model.CoinSwitchEnabled)
{
<coinswitch inline-template
v-if="!srvModel.changellyEnabled || selectedThirdPartyProcessor === 'coinswitch'"
:mode="srvModel.coinSwitchMode"
:merchant-id="srvModel.coinSwitchMerchantId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.btcDue"
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
:to-currency-address="srvModel.btcAddress">
<div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">
{{$t("Pay with CoinSwitch")}}
</a>
@if (Model.ChangellyEnabled)
{
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">
{{$t("Pay with Changelly")}}
</button>
}
<iframe
v-if="showInlineIFrame"
v-on:load="onLoadIframe"
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
:src="url"></iframe>
</div>
</coinswitch>
}
@if(Model.ChangellyEnabled){
<changelly inline-template
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
:merchant-id="srvModel.changellyMerchantId"
:store-id="srvModel.storeId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.changellyAmountDue"
:to-currency-address="srvModel.btcAddress">
<div class="changelly-component">
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
<select
v-model="selectedFromCurrency"
:disabled="isLoading"
v-on:change="onCurrencyChange($event)"
ref="changellyCurrenciesDropdown">
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">
{{currency.fullName}}
</option>
</select>
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">
{{currency.fullName}}
</option>
</select>
</div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">
{{$t("Pay with Changelly")}}
</a>
@if (Model.CoinSwitchEnabled)
{
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">
{{$t("Pay with CoinSwitch")}}
</button>
}
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
<a v-on:click="openDialog($event)" :href="url" class="btn btn-primary retry-button changelly-component-button" v-show="url">
Pay with Changelly
</a>
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
</changelly>
</changelly>
}
</center>
</nav>
</div>

View File

@ -170,14 +170,16 @@
el: '#checkoutCtrl',
components: {
qrcode: VueQr,
changelly: ChangellyComponent
changelly: ChangellyComponent,
coinswitch: CoinSwitchComponent
},
data: {
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal
isModal: srvModel.isModal,
selectedThirdPartyProcessor: ""
}
});
</script>

View File

@ -54,6 +54,10 @@
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>State</th>
<td>@Model.State</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
@ -70,14 +74,6 @@
<th>Transaction speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Status Exception</th>
<td>@Model.StatusException</td>
</tr>
<tr>
<th>Refund email</th>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
@ -221,7 +217,7 @@
<th class="text-right">Rate</th>
<th class="text-right">Paid</th>
<th class="text-right">Due</th>
@if (Model.StatusException == "paidOver")
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
{
<th class="text-right">Overpaid</th>
}
@ -236,7 +232,7 @@
<td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td>
@if (Model.StatusException == "paidOver")
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
{
<td class="text-right">@payment.Overpaid</td>
}

View File

@ -29,6 +29,8 @@
</p>
<ul>
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
@ -99,23 +101,7 @@
}
</td>
<td>@invoice.InvoiceId</td>
@if (invoice.Status == "paid")
{
<td>
<div class="btn-group">
<a class="dropdown-toggle dropdown-toggle-split" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status <span class="sr-only">Toggle Dropdown</span>
</a>
<div class="dropdown-menu pull-right">
<button class="dropdown-item small" data-toggle="modal" data-target="#myModal" onclick="$('#invoiceId').val('@invoice.InvoiceId')">Make Invalid</button>
</div>
</div>
</td>
}
else
{
<td>@invoice.Status</td>
}
<td>@invoice.Status</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if (invoice.ShowCheckout)
@ -123,9 +109,32 @@
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
-
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
@if (invoice.CanMarkStatus)
{
<a class="dropdown-toggle dropdown-toggle-split" href="#" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Change status <span class="sr-only">Toggle Dropdown</span>
</a>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<form method="get" asp-action="ChangeInvoiceState" asp-route-invoiceId="@invoice.InvoiceId" asp-route-newState="invalid">
<button class="dropdown-item small">Mark as invalid <span class="fa fa-times"></span></button>
</form>
}
@if (invoice.CanMarkComplete)
{
<form method="get" asp-action="ChangeInvoiceState" asp-route-invoiceId="@invoice.InvoiceId" asp-route-newState="complete">
<button class="dropdown-item small">Mark as complete <span class="fa fa-check-circle"></span></button>
</form>
}
</div>
}
<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</td>
</tr>
@ -153,28 +162,3 @@
</div>
</section>
<!-- Modal -->
<div id="myModal" class="modal fade" role="dialog">
<form method="post" action="/invoices/invalidatepaid">
<input id="invoiceId" name="invoiceId" type="hidden" />
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Set Invoice status to Invalid</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to invalidate this transaction? This action is NOT undoable!</p>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</form>
</div>

View File

@ -0,0 +1 @@
@using BTCPayServer.Services.Invoices

View File

@ -0,0 +1,140 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>@Model.CryptoCode LN Node Info</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
<link rel="apple-touch-startup-image" href="~/img/splash.png">
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
window.onload = function() {
Vue.use(Toasted);
var app = new Vue({
el: '#app',
components: {
qrcode: VueQr
},
data: {
srvModel: srvModel
},
mounted: function() {
this.$nextTick(function() {
var copyInput = new Clipboard('.copy');
copyInput.on("success",
function(e) {
Vue.toasted.show('Copied',
{
iconPack: "fontawesome",
icon: "copy",
duration: 5000
});
});
});
}
});
}
</script>
<style>
.qr-icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.qr-container {
position: relative;
text-align: center;
}
.copy {
cursor: copy;
}
</style>
</head>
<body >
<noscript>
<div class="container">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
@Model.CryptoCode Lightning Node - @(Model.Available? "Online" : "Unavailable")
<small class="@(Model.Available? "text-success" : "text-danger")">
<span class="fa fa-circle "></span>
</small>
</h1>
@if (Model.Available)
{
<div class="card-body m-sm-0 p-sm-0" >
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
}
</div>
</div>
</div>
</noscript>
<div id="app" class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
{{srvModel.cryptoCode}} Lightning Node
- {{srvModel.available? "Online" : "Unavailable"}}
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<span class="fa fa-circle "></span>
</small>
</h1>
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
<qrcode v-bind:val="srvModel.nodeInfo" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,60 @@
@model ChargeServiceViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Lightning charge service</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>Lightning charge is a simple API for invoicing on lightning network, you can use it with several plugins:</span>
<ul>
<li><a href="https://github.com/ElementsProject/woocommerce-gateway-lightning" target="_blank">WooCommerce Lightning Gateway</a>: A comprehensive e-commerce application that integrates with stock-management and order-tracking systems</li>
<li><a href="https://github.com/ElementsProject/nanopos" target="_blank">Nanopos</a>: A simple point-of-sale system for fixed-price goods</li>
<li><a href="https://github.com/ElementsProject/filebazaar" target="_blank">FileBazaar</a>: A system for selling files such as documents, images, and videos</li>
<li><a href="https://github.com/ElementsProject/wordpress-lightning-publisher" target="_blank">Lightning Publisher for WordPress</a>: A patronage model for unlocking WordPress blog entries</li>
<li><a href="https://github.com/ElementsProject/paypercall" target="_blank">Paypercall</a>: A programmers toolkit for Lightning that enables micropayments for individual API calls</li>
<li><a href="https://github.com/ElementsProject/ifpaytt" target="_blank">Ifpaytt</a>: An extension of paypercall that allows web developers using IFTTT to request payments for service usage</li>
<li><a href="https://github.com/ElementsProject/lightning-jukebox" target="_blank">Lightning Jukebox</a>: A fun demo that reimagines a classic technology for the Lightning Network</li>
<li><a href="https://github.com/ElementsProject/nanotip" target="_blank">Nanotip</a>: The simple tip jar, rebuilt to issue Lightning Network invoices</li>
</ul>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<h4>Credentials</h4>
@if (Model.Uri != null)
{
<div class="form-group">
<label asp-for="Uri"></label>
<input asp-for="Uri" readonly class="form-control" />
</div>
}
@if (Model.APIToken != null)
{
<div class="form-group">
<label asp-for="APIToken"></label>
<input asp-for="APIToken" readonly class="form-control" />
</div>
}
@if (Model.AuthenticatedUri != null)
{
<div class="form-group">
<label asp-for="AuthenticatedUri"></label>
<input asp-for="AuthenticatedUri" readonly class="form-control" />
</div>
}
</div>
</div>

View File

@ -1,41 +0,0 @@
@model LndRestServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>LND REST</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>BTCPay exposes LND Rest services for outside consumption. See connection information below</p>
</div>
<div class="form-group">
<label asp-for="BaseApiUrl">Base API Url</label>
<input asp-for="BaseApiUrl" readonly class="form-control" />
</div>
@if (Model.Macaroon != null)
{
<div class="form-group">
<label asp-for="Macaroon"></label>
<input asp-for="Macaroon" readonly class="form-control" />
</div>
}
@if (Model.CertificateThumbprint != null)
{
<div class="form-group">
<label asp-for="CertificateThumbprint"></label>
<input asp-for="CertificateThumbprint" readonly class="form-control" />
</div>
}
</div>
</div>

View File

@ -4,7 +4,7 @@
}
<h4>LND GRPC</h4>
<h4>LND @Model.ConnectionType</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
@ -17,14 +17,14 @@
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection information here.<br /></span>
<span>BTCPay exposes LND's @Model.ConnectionType service for outside consumption, you will find connection information here.<br /></span>
</p>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use this QR Code to connect your Zap wallet to your LND instance.<br /></span>
<span>You can use this QR Code to connect external software to your LND instance.<br /></span>
<span>This QR Code is only valid for 10 minutes</span>
</p>
</div>
@ -61,22 +61,60 @@
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
</div>
<div id="details" class="collapse">
<div class="form-group">
<label asp-for="Host"></label>
<input asp-for="Host" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="SSL"></label>
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
</div>
@if(Model.Macaroon != null)
@if (Model.Uri == null)
{
<div class="form-group">
<label asp-for="Host"></label>
<input asp-for="Host" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="SSL"></label>
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
</div>
}
else
{
<div class="form-group">
<label asp-for="Uri"></label>
<input asp-for="Uri" readonly class="form-control" />
</div>
}
@if (Model.Macaroon != null)
{
<div class="form-group">
<label asp-for="Macaroon"></label>
<input asp-for="Macaroon" readonly class="form-control" />
</div>
}
@if(Model.CertificateThumbprint != null)
@if (Model.AdminMacaroon != null)
{
<div class="form-group">
<label asp-for="AdminMacaroon"></label>
<input asp-for="AdminMacaroon" readonly class="form-control" />
</div>
}
@if (Model.InvoiceMacaroon != null)
{
<div class="form-group">
<label asp-for="InvoiceMacaroon"></label>
<input asp-for="InvoiceMacaroon" readonly class="form-control" />
</div>
}
@if (Model.ReadonlyMacaroon != null)
{
<div class="form-group">
<label asp-for="ReadonlyMacaroon"></label>
<input asp-for="ReadonlyMacaroon" readonly class="form-control" />
</div>
}
@if (Model.GRPCSSLCipherSuites != null)
{
<div class="form-group">
<label asp-for="GRPCSSLCipherSuites"></label>
<input asp-for="GRPCSSLCipherSuites" readonly class="form-control" />
</div>
}
@if (Model.CertificateThumbprint != null)
{
<div class="form-group">
<label asp-for="CertificateThumbprint"></label>

View File

@ -15,8 +15,9 @@
<div class="row">
<div class="col-md-8">
<h4>Crypto services</h4>
<div class="form-group">
<span>You can get access here to LND (gRPC, Rest) or SSH services exposed by your server</span>
<span>You can get access here to LND (gRPC, Rest) services exposed by your server</span>
</div>
<div class="form-group">
@ -33,26 +34,9 @@
{
<tr>
<td>@lnd.Crypto</td>
<td>LND @lnd.Type.ToString()</td>
<td>@lnd.Type</td>
<td style="text-align:right">
@if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC)
{
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest)
{
<a asp-action="LndRestServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
</td>
</tr>
}
@if (Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
<a asp-action="@lnd.Action" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
@ -62,6 +46,39 @@
</div>
</div>
@if (Model.ExternalServices.Count != 0)
{
<div class="row">
<div class="col-md-8">
<h4>Other services</h4>
<div class="form-group">
<span>Other external services</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.ExternalServices)
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<a href="@s.Link" target="_blank">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -0,0 +1,79 @@
@model SparkServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Spark service</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
@if (Model.ShowQR)
{
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<div>
<span><b>CONFIDENTIAL:</b> This QR Code is confidential, close this window as soon as you don't need it anymore.<br /></span>
<span>A malicious actor with access to this QR Code could steal the funds on your lightning wallet.</span>
</div>
</div>
}
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<h5>Browser connection</h5>
<p>
<span>You can go to spark from your browser by <a href="@Model.SparkLink" target="_blank">clicking here</a><br /></span>
</p>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use QR Code to connect to your clightning from your mobile.<br /></span>
</p>
</div>
<div class="form-group">
@if (!Model.ShowQR)
{
<div class="form-group">
<form method="get">
<input type="hidden" asp-for="ShowQR" value="true" />
<button type="submit" class="btn btn-primary">Show Confidential QR Code</button>
</form>
</div>
}
else
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.SparkLink)"></div>
</div>
}
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
@if (Model.ShowQR)
{
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.SparkLink)",
width: 150,
height: 150
});
</script>
}
}

View File

@ -19,8 +19,8 @@
<title>BTCPay Server</title>
@* CSS *@
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.CreativeStartUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.CreativeStartUri)" rel="stylesheet" />
<bundle name="wwwroot/bundles/main-bundle.min.css" />

View File

@ -15,26 +15,33 @@
<div class="row">
<div class="col-md-8">
<form method="post">
@if(!Model.Confirmation)
@if (!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<div id="ledger-loading" class="form-text text-muted">
Checking if a ledger wallet is connected...
</div>
<div id="ledger-validate" class="form-text text-muted" style="display: none;">
Please validate access on your screen...
</div>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
<ul>
@for(int i = 0; i < 4; i++)
@for (int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
<li><a class="ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
</ul>
</div>
@ -89,6 +96,7 @@
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="Confirmation" />
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="Enabled" />
<div class="form-group">
@ -100,7 +108,7 @@
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
@foreach (var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>

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