Compare commits

...

115 Commits

Author SHA1 Message Date
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
0eccc6085b bump 2018-11-30 04:34:38 -06:00
a89da1f705 Recoding test to respect new ordering in CSV 2018-11-30 04:34:18 -06:00
5b297e539a Additional fields and ordering based on feedback 2018-11-30 04:18:37 -06:00
1d932c3753 Improve invoice script if no PoS data available 2018-11-30 04:17:57 -06:00
5a77fc74ba quickly fix changelly button style (#423)
Fix the button for now so it doesn't appear broken.
2018-11-30 04:17:44 -06:00
7b47b96252 Always using quotes for CSV export 2018-11-30 03:15:23 -06:00
a4bec83ecc Fixing warnings on invariant culture, hate this for being so verbose 2018-11-30 02:51:23 -06:00
8509a0de18 Basic export CSV and JSON tests 2018-11-30 02:34:43 -06:00
8e30b7430d Adding PaymentType and destination, CSV export 2018-11-30 02:04:26 -06:00
9235d32a45 Export of payments made on invoices 2018-11-30 01:22:39 -06:00
dd503570ac bump 2018-11-30 11:30:30 +09:00
613281a1e7 Fix form processing when cart is enabled (#424) 2018-11-30 11:29:27 +09:00
bab7bf6633 bump 2018-11-27 15:17:32 +09:00
1831692761 Enable shopping cart, add items to cart, enable tips (#410)
Modal cart, remove items, checkout

Fix removal and adding of cart items

Improve cart UI

Add cart bundle, remove unused js files from the view when cart isn't used

Do not enable cart by default

Do not put modal into the view when the cart is disabled

Escape js properties

Work with amounts as cents

Make animation speed look constant

Enable tips in the cart

Fix cart UI
2018-11-27 15:14:32 +09:00
e144d2479b Add POS Data to Invoice UI (#409)
* Add POS Data in Invoice UI

* fix build

* extract in helper and add UTs

* add in unit test coverage through mvc view too
2018-11-27 15:13:09 +09:00
c25831316e bump nbx 2018-11-26 12:02:50 +09:00
60b72aabe8 fix test 2018-11-24 13:38:23 +09:00
c8fcb0ab18 Use framework dependent build for ARM 2018-11-23 16:14:13 +09:00
9911d18390 Do not push latest images to dockerhub 2018-11-23 14:12:47 +09:00
e24630ac1e Remove qemu install requirement for the host 2018-11-23 14:08:14 +09:00
4c1fd3edae More comment on ARM build 2018-11-23 14:02:44 +09:00
f65492dd66 Use stretch slim for arm 2018-11-23 14:00:33 +09:00
5d978c7670 Use manifest image for building arm images 2018-11-23 13:58:21 +09:00
11788cece9 No need to create latest tag 2018-11-23 13:18:35 +09:00
1aaa55dc62 Make test less flaky 2018-11-23 13:09:30 +09:00
ce57a2b8fb Do not tag latest 2018-11-23 12:59:48 +09:00
0604cc5bd0 bump 2018-11-23 11:37:05 +09:00
3d2c0bcc6c Use specific sdk and runtime version for arm 2018-11-23 11:23:27 +09:00
0f222979a6 CircleCI multiarch Docker images (#416)
* Preparing final version of CircleCI docker building

* Removing test job requirement for building Docker images

We'll already monitor build before tagging, would be too many checks

* Adding pushing of manifest for tag

* Easy access to docker/circle config files for edit

* Generalizing script with $DOCKERHUB_REPO variable
2018-11-23 11:21:01 +09:00
a1eb6a14f5 Fix all script because of docker-compose team screwing up (https://github.com/docker/compose/issues/6316) 2018-11-22 16:16:10 +09:00
186ce01022 add pairing code to tokens page after authorize (#412) 2018-11-22 15:13:35 +09:00
0096ec1d12 bump nbxplorer 2018-11-21 20:41:51 +09:00
2929d7bf51 Fixing MONA_BTC rate breaking tests and CircleCI because zaif is down 2018-11-20 15:42:45 -06:00
d90fb5764d Add noindex,nofollow to invoices, checkouts and fix create invoice ui bug (#407)
* add noindex,nofollow on invoices

* fix create invoice button and add noindex,nofollow to checkout and invoice pages
2018-11-19 13:20:48 +09:00
4dccd0c733 Add better instruction on how to customize the theme 2018-11-17 12:43:11 +09:00
300d912331 bump 2018-11-17 11:43:41 +09:00
9d21c89151 Preserving title with custom amount (#403)
* Preserving title with custom amount

* Custom button texts for complete localization

* Update tests, now checking custom amount description and button text

* Support for Custom CSS in POS
2018-11-17 11:39:43 +09:00
24a8c4015c Bump 2018-11-17 11:35:20 +09:00
5eb40d6b7f Bugfixing redirect button (#405) 2018-11-17 11:32:31 +09:00
36f486e91b Add test for the parser 2018-11-17 01:45:59 +09:00
5b684ac26e Make really sure we don't generate segwit addresses for non segwit coins 2018-11-17 01:39:32 +09:00
85062725bd bump 2018-11-17 01:21:56 +09:00
401d9c8565 DerivationSchemeParser should not override a label 2018-11-17 01:21:34 +09:00
6f276ac1bc Do not crash if derivation strategy is empty 2018-11-17 01:09:28 +09:00
4350785cef Remove double slash 2018-11-17 00:23:51 +09:00
9a2a85ac3d Update translations 2018-11-17 00:22:18 +09:00
d030a61322 bump 2018-11-17 00:16:31 +09:00
dacb6dca41 bump .net core 2018-11-17 00:13:22 +09:00
c40fc69087 Use the choiceKey of PoS item as ItemCode 2018-11-16 23:16:44 +09:00
eff983135c showcase the custom field in PoS template 2018-11-16 18:36:18 +09:00
479303dd9e Tweaking UI for custom amounts (#398)
* Tweaking appearance of custom amount card

* Allowing POS items to have custom amounts, good for donations/tips

* Prepending currency symbol in POS

* Fixing regression, thanks unit test
2018-11-16 12:31:38 +09:00
e9b2088f7d change default title for pointofsale 2018-11-14 17:45:46 +09:00
4af5b94013 Add tooling which pull transifex translation automatically, add Slovenčina. (Close #386) 2018-11-14 16:48:25 +09:00
441398402d Remove global.json because .403 became suddenly unavailable 2018-11-13 16:41:49 +09:00
258d4fda3f bump 2018-11-13 16:37:43 +09:00
8e667f6c3f Allow empty template (Fix #303) 2018-11-13 16:32:13 +09:00
a996cc2e6d Fix margins, change template (#397) 2018-11-13 16:29:18 +09:00
9b8a8690e7 Change links to gitbook 2018-11-13 16:21:58 +09:00
f2387fd6b5 Workaround to compile on circle 2018-11-13 16:16:57 +09:00
888036a99d use docker on Circle CI 2018-11-13 15:55:10 +09:00
539c0ed7f0 show dotnet info on CI 2018-11-13 15:47:25 +09:00
95e065a462 Add tooltip to update store (#382) 2018-11-13 15:36:07 +09:00
087f20cb6c Fix small view error in logs (#392) 2018-11-12 22:25:39 +09:00
7adf321956 Checkout Experience Language Setting (#393)
* fix check out experience default language validation of preset value not found

* Update CheckoutExperienceViewModel.cs
2018-11-12 22:17:00 +09:00
dc749462ec automatically detect the btcpay server url in btcpay.js 2018-11-10 23:43:48 +09:00
16b57f24a2 Fix #383 2018-11-10 23:25:11 +09:00
b16b1c3e8b - add item image and description (#391)
- fix margins
2018-11-10 15:38:26 +09:00
fee56873b5 Handle exception if log file do not exists. 2018-11-09 21:43:10 +09:00
e1b2b72cd2 bump 2018-11-09 21:16:09 +09:00
daf4e5ce6c I am sorry for so many prs <3 (#389)
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes

* make sure language files are named correctly

* fix dropdown width issue when in modal form

* fix issue from jquery hell
2018-11-09 21:13:00 +09:00
2ec2c7263f Make language loading more efficient and solid (#388)
* make language loading more solid

* disable browser lang preferences

* pr fix

* pr fixes

* pr fixes
2018-11-09 19:02:53 +09:00
abfcab552f bump (#384) 2018-11-09 17:34:30 +09:00
cfdf8b1670 Example modal in invoice list (#387) 2018-11-09 17:13:45 +09:00
f23e2a3ec4 async i18n and json translation format (#369)
* start working on loading locales async and as json

* finish off langs and UI

* fix path

* fix tests
2018-11-09 16:48:38 +09:00
aa1ac3da50 Modal invoice through btcpay.js (#381)
* Modal through btcpay.js

* Handling close action depending on whether is modal or not

* Tweaking button position

* Stripping trailing slashes if present when setting site root
2018-11-09 16:09:09 +09:00
c9c7316b7d Logs UI in Server Admin (#374)
* add in ui

* add in logging viewer

* Revert "add in ui"

This reverts commit 9614721fa8a439f7097adca69772b5d41f6585d6.

* finish basic feature

* clean up

* improve and fix build

* add in debug log level command option

* use paging for log file list, use extension to select log files, show message for setting up logging

* make paging a little better

* add very basic UT for logs

* Update ServerController.cs
2018-11-07 22:29:35 +09:00
d152d5cd90 fix build 2018-11-06 16:08:42 +09:00
6fd37710e1 Rename validators namespace 2018-11-06 15:38:07 +09:00
0419a3c19a do not affect Buyer for every paymentid 2018-11-05 17:37:55 +09:00
0c382da561 Show unconf transactions with low opacity 2018-11-05 17:26:49 +09:00
9fc7f287d2 Expose buyer object to conform to bitpay API 2018-11-05 17:02:12 +09:00
dd7c4850f0 bump nbxplorer 2018-11-05 14:12:05 +09:00
93992ec3ed bump 2018-11-05 12:15:05 +09:00
15d9adfbf1 Fix rate fetching for kraken doge and dash 2018-11-05 12:14:39 +09:00
676a914c40 Fix, allow rescan if other crypto nodes are not synched 2018-11-04 22:46:27 +09:00
b423b4eec1 Do not allow rescan of wallet which are not segwit 2018-11-04 14:59:28 +09:00
9784a89112 limit apdu size to ledger 2018-11-04 00:36:48 +09:00
7b596e6d9c Add Bitcore BTX support (#259) 2018-11-03 13:42:17 +09:00
76febcf238 bump NBXplorer.Client (#378)
NBXplorer.Client Version 1.0.3.5 is available: da7df86019
2018-11-02 21:38:41 +09:00
a57a72de88 bump clightning 2018-11-02 19:02:07 +09:00
235b307b06 bump deps 2018-11-02 18:05:48 +09:00
05b0f6d0f7 Fix invoice search not working on transaction id 2018-11-02 14:26:13 +09:00
1d7081d8b8 bump 2018-11-01 21:51:16 +09:00
c0174c0c2c inverse DASH rate 2018-11-01 16:33:53 +09:00
fa8324c1f9 Fix DASH rate for kraken 2018-11-01 14:48:46 +09:00
4b0951caec trim destination in WalletSend 2018-11-01 12:54:25 +09:00
0d51c99717 Properly configure the logger to log what happen in ConfigureServices, add https profile adapted for debugging ledger wallet. 2018-11-01 12:52:32 +09:00
24623c59d7 Adjusted mechanism for setting https binding configuration option (#372)
* Adjusted mechanism for setting https binding configuration option.

* Modified the https binding logic to use default bind and port options.

* Removed dedicated https certification config properties and instead used direct access via setting name.
2018-11-01 12:07:28 +09:00
88044f6b76 Decouple Wallet Send screen from Ledger Wallet 2018-11-01 00:19:25 +09:00
38edbf8362 Improve token UX (Fix #353) 2018-10-31 17:59:09 +09:00
bc0acf5701 make test more reliable 2018-10-31 16:57:31 +09:00
a82f181126 Reactivate cryptopia 2018-10-31 13:31:03 +09:00
be0139a46f bump 2018-10-31 13:06:36 +09:00
4db5b4f2b1 Wait for the nodes to be fully synched before starting tests 2018-10-31 13:06:17 +09:00
93cefced80 bump .NET core and dependencies 2018-10-31 13:03:12 +09:00
85f586f623 bump dependencies 2018-10-31 11:56:21 +09:00
2be1f97419 Remove cryptopedia as direct provider, add estimated time to wallet rescan page, bump nbx 2018-10-30 15:40:27 +09:00
63014231ab Revert "Added configuration options for BtcPayServer https binding. (#360)"
This reverts commit 3ac37497ab9b5ff2c28eaab54c7f2a12356659dd.
2018-10-30 00:25:05 +09:00
3ac37497ab Added configuration options for BtcPayServer https binding. (#360) 2018-10-30 00:11:02 +09:00
d0cafb020f Add an invoices list to store list 2018-10-29 12:44:20 +09:00
d3b3198b68 For lightning payments tests, add small delay after creating the invoice before sending the payment 2018-10-29 00:22:30 +09:00
c1f17ff63b Add some test logs to flaky test 2018-10-28 23:43:48 +09:00
148 changed files with 4320 additions and 1738 deletions

View File

@ -5,6 +5,7 @@ jobs:
docker_layer_caching: true
steps:
- checkout
test:
machine: true
steps:
@ -17,13 +18,88 @@ jobs:
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
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
publish_docker_linuxarm:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
publish_docker_multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
workflows:
version: 2
build_and_test:
jobs:
- test
publish:
jobs:
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/

View File

@ -6,12 +6,13 @@
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>7.2</LangVersion>
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -1,4 +1,5 @@
using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
@ -139,6 +140,11 @@ namespace BTCPayServer.Tests
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
while(!dashBoard.IsFullySynched())
{
Thread.Sleep(10);
}
if (MockRates)
{

View File

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
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

View File

@ -29,12 +29,6 @@ If you want to stop, and remove all existing data
docker-compose down --v
```
You can run the tests inside a container by running
```
docker-compose run --rm tests
```
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
## How to manually test payments

View File

@ -73,16 +73,16 @@ namespace BTCPayServer.Tests
public BTCPayNetwork SupportedNetwork { get; set; }
public WalletId RegisterDerivationScheme(string crytoCode)
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
{
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
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);

View File

@ -43,7 +43,11 @@ using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
using BTCPayServer.Models.WalletViewModels;
using System.Security.Claims;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Security;
using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
{
@ -480,10 +484,6 @@ namespace BTCPayServer.Tests
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
// TODO: If this parameter is less than 1 second we start having concurrency problems
await Task.Delay(TimeSpan.FromMilliseconds(1000));
//
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01m,
@ -492,6 +492,7 @@ namespace BTCPayServer.Tests
OrderId = "orderId",
ItemDesc = "Some description"
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
{
@ -586,6 +587,23 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanSolveTheDogesRatesOnKraken()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanRescanWallet()
@ -595,15 +613,16 @@ namespace BTCPayServer.Tests
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
acc.RegisterDerivationScheme("BTC", true);
var btcDerivationScheme = acc.DerivationScheme;
acc.RegisterDerivationScheme("LTC");
acc.RegisterDerivationScheme("LTC", true);
var walletController = tester.PayTester.GetController<WalletsController>(acc.UserId);
WalletId walletId = new WalletId(acc.StoreId, "LTC");
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.False(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSegwit);
Assert.False(rescan.IsSupportedByCurrency);
Assert.False(rescan.IsServerAdmin);
@ -626,10 +645,10 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
while(true)
while (true)
{
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
if(rescan.Progress == null && rescan.LastSuccess != null)
if (rescan.Progress == null && rescan.LastSuccess != null)
{
if (rescan.LastSuccess.Found == 0)
continue;
@ -760,6 +779,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
var payment2 = invoice.BtcDue;
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress,
@ -769,8 +789,10 @@ namespace BTCPayServer.Tests
false, //subtractfeefromamount
true, //replaceable
}).ResultString);
Logs.Tester.LogInformation($"Let's send a first payment of {payment1} for the {invoice.BtcDue} invoice ({tx1})");
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
Logs.Tester.LogInformation($"The invoice should be paidOver");
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -788,9 +810,18 @@ namespace BTCPayServer.Tests
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
using (var cts = new CancellationTokenSource(10000))
using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession())
{
listener.ListenAllDerivationSchemes();
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
Thread.Sleep(1000); // Make sure the replacement has a different timestamp
var tx2 = tester.ExplorerNode.SendRawTransaction(replaced);
Logs.Tester.LogInformation($"Let's RBF with a payment of {payment2} ({tx2}), waiting for NBXplorer to pick it up");
Assert.Equal(tx2, ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash);
}
Logs.Tester.LogInformation($"The invoice should now not be paidOver anymore");
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
@ -1292,6 +1323,19 @@ namespace BTCPayServer.Tests
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
parser = new DerivationSchemeParser(Network.RegTest);
var parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString());
// Let's make sure we can't generate segwit with dogecoin
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
}
[Fact]
@ -1417,12 +1461,19 @@ namespace BTCPayServer.Tests
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "CAD";
vmpos.Template =
"apple:\n" +
" price: 5.0\n" +
" title: good apple\n" +
"orange:\n" +
" price: 10.0\n";
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.CustomTipText = "Wanna tip?";
vmpos.Template = @"
apple:
price: 5.0
title: good apple
orange:
price: 10.0
donation:
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal("hello", vmpos.Title);
@ -1430,19 +1481,225 @@ namespace BTCPayServer.Tests
var publicApps = user.GetController<AppsPublicController>();
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(2, vmview.Items.Length);
Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
//
var invoices = user.BitPay.GetInvoices();
var orangeInvoice = invoices.First();
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 5, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 5m);
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);
Assert.Equal("donation", donationInvoice.ItemDesc);
}
}
[Fact]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
};
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, InvoiceController.PosDataParser.ParsePosData(tuple.input));
});
}
[Fact]
[Trait("Integration", "Integration")]
public async Task PosDataParser_ParsesCorrectly_Slower()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var controller = tester.PayTester.GetController<InvoiceController>(null);
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
};
var tasks = new List<Task>();
foreach (var valueTuple in testCases)
{
tasks.Add(user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC")
{
PosData = valueTuple.input
}).ContinueWith(async task =>
{
var result = await controller.Invoice(task.Result.Id);
var viewModel =
Assert.IsType<InvoiceDetailsModel>(
Assert.IsType<ViewResult>(result).Model);
Assert.Equal(valueTuple.expectedOutput, viewModel.PosData);
}));
}
await Task.WhenAll(tasks);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanExportInvoicesJson()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
// 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);
Assert.Equal("application/json", result.ContentType);
Assert.Equal("[]", result.Content);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
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"
}
]
*/
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanExportInvoicesCsv()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
var exportResultPaid = user.GetController<InvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
Assert.Contains($",\"OnChain\",\"0.10020000 BTC\",\"0.10009990 BTC\",\"0.00000000 BTC\",\"5000.0\",\"500.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new\"", 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"
*/
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteApps()
@ -1503,7 +1760,7 @@ namespace BTCPayServer.Tests
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -1615,7 +1872,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() =>
{
@ -1623,6 +1880,13 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = txId.ToString()
}).GetAwaiter().GetResult();
Assert.Single(textSearchResult);
});
cashCow.Generate(1);
@ -1699,11 +1963,12 @@ namespace BTCPayServer.Tests
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
Assert.NotNull(rateResult.BidAsk);
Logs.Tester.LogInformation($"Testing {value.Key.ToString()}");
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
public static RateProviderFactory CreateBTCPayRateFactory()
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
@ -1736,6 +2001,22 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CheckLogsRoute()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<ServerController>();
var vm = Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
}
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()

View File

@ -0,0 +1,109 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using NBitcoin.DataEncoders;
using Newtonsoft.Json.Linq;
using Xunit;
using System.IO;
namespace BTCPayServer.Tests
{
/// <summary>
/// This class hold easy to run utilities for dev time
/// </summary>
public class UtilitiesTests
{
/// <summary>
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
/// </summary>
[Trait("Utilities", "Utilities")]
[Fact]
public async Task PullTransifexTranslations()
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 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 = 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 =>
{
bool isSourceLang = l == "en";
if (l == "no")
return;
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 == "ne_NP")
l = "np_NP";
if (l == "zh_CN")
l = "zh-SP";
if (l == "kk")
l = "kk-KZ";
var langCode = l.Replace("_", "-");
var langFile = Path.Combine(langsDir, langCode + ".json");
var jobj = JObject.Parse(content);
jobj["code"] = langCode;
if ((string)jobj["currentLanguage"] == "English" && !isSourceLang)
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());
}
private static string GetTransifexAPIToken()
{
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config["TransifexAPIToken"];
Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken <youapitoken>\"");
return token;
}
}
public class TransifexClient
{
public TransifexClient(string apiToken)
{
Client = new HttpClient();
APIToken = apiToken;
}
public HttpClient Client { get; }
public string APIToken { get; }
public async Task<JObject> GetTransifexAsync(string uri)
{
var message = new HttpRequestMessage(HttpMethod.Get, uri);
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes($"api:{APIToken}")));
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
var response = await Client.SendAsync(message);
return await response.Content.ReadAsAsync<JObject>();
}
}
}

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
docker exec -ti "$bitcoind_container_id" bitcoin-cli -datadir="/data" "$@"

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:1.1.0.4
image: nicolasdorier/nbxplorer:2.0.0.1
restart: unless-stopped
ports:
- "32838:32838"
@ -118,7 +118,8 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6.1-1-dev
image: nicolasdorier/clightning:v0.6.2-3-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
@ -163,7 +164,8 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6.1-1-dev
image: nicolasdorier/clightning:v0.6.2-3-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -219,7 +221,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:0.5-beta
image: btcpayserver/lnd:0.5-beta-2
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -246,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.5-beta
image: btcpayserver/lnd:0.5-beta-2
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

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

View File

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

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"

View File

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

View File

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

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcore()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTX");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = cryptopia(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
});
}
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = zaif(MONA_BTC)"
"MONA_BTC = bittrex(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",

View File

@ -47,6 +47,7 @@ namespace BTCPayServer
NetworkType = networkType;
InitBitcoin();
InitLitecoin();
InitBitcore();
InitDogecoin();
InitBitcoinGold();
InitMonacoin();

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.2</Version>
<Version>1.0.3.21</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,37 +33,42 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.2" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.66" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.71" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.3.3" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
@ -134,6 +139,9 @@
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\ShowToken.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonEnable.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -158,6 +166,9 @@
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -171,10 +182,4 @@
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -16,6 +16,7 @@ using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration
{
@ -33,6 +34,12 @@ namespace BTCPayServer.Configuration
get; set;
}
public string ConfigurationFile
{
get;
private set;
}
public string LogFile
{
get;
private set;
@ -54,6 +61,16 @@ namespace BTCPayServer.Configuration
set;
} = new List<NBXplorerConnectionSetting>();
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
}
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf)
{
NetworkType = DefaultConfiguration.GetNetworkType(conf);
@ -174,6 +191,13 @@ namespace BTCPayServer.Configuration
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))
{
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)

View File

@ -37,6 +37,8 @@ namespace BTCPayServer.Configuration
}
else if (typeof(T) == typeof(string))
return (T)(object)str;
else if (typeof(T) == typeof(IPAddress))
return (T)(object)IPAddress.Parse(str);
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);

View File

@ -41,6 +41,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
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);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -106,6 +107,8 @@ namespace BTCPayServer.Configuration
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine("#httpscertificatefilepath=devtest.pfx");
builder.AppendLine("#httpscertificatefilepassword=toto");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");

View File

@ -15,28 +15,58 @@ namespace BTCPayServer.Controllers
{
public PointOfSaleSettings()
{
Title = "My awesome Point of Sale";
Title = "Tea shop";
Currency = "USD";
Template =
"tea:\n" +
" price: 0.02\n" +
" title: Green Tea # title is optional, defaults to the keys\n\n" +
"coffee:\n" +
" price: 1\n\n" +
"bamba:\n" +
" price: 3\n\n" +
"beer:\n" +
" price: 7\n\n" +
"hat:\n" +
" price: 15\n\n" +
"tshirt:\n" +
" price: 25";
"green tea:\n" +
" price: 1\n" +
" title: Green Tea\n" +
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
" image: https://cdn.pixabay.com/photo/2015/03/26/11/03/green-tea-692339__480.jpg\n\n" +
"black tea:\n" +
" price: 1\n" +
" title: Black Tea\n" +
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
" image: https://cdn.pixabay.com/photo/2016/11/29/13/04/beverage-1869716__480.jpg\n\n" +
"rooibos:\n" +
" price: 1.2\n" +
" title: Rooibos\n" +
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
" image: https://cdn.pixabay.com/photo/2017/01/08/08/14/water-1962388__480.jpg\n\n" +
"pu erh:\n" +
" price: 2\n" +
" title: Pu Erh\n" +
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
" image: https://cdn.pixabay.com/photo/2018/07/21/16/56/tea-cup-3552917__480.jpg\n\n" +
"herbal tea:\n" +
" price: 1.8\n" +
" title: Herbal Tea\n" +
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2015/07/02/20/57/chamomile-829538__480.jpg\n" +
" custom: true\n\n" +
"fruit tea:\n" +
" price: 1.5\n" +
" title: Fruit Tea\n" +
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
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 string CustomCSSLink { get; set; }
}
[HttpGet]
@ -50,9 +80,14 @@ namespace BTCPayServer.Controllers
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
{
@ -115,9 +150,14 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);
StatusMessage = "App updated";

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@ -44,8 +45,14 @@ namespace BTCPayServer.Controllers
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
Items = _AppsHelper.Parse(settings.Template, settings.Currency)
CurrencySymbol = currency.Symbol,
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomCSSLink = settings.CustomCSSLink
});
}
@ -69,7 +76,7 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
@ -83,10 +90,12 @@ namespace BTCPayServer.Controllers
return NotFound();
title = choice.Title;
price = choice.Price.Value;
if (amount > price)
price = amount;
}
else
{
if (!settings.ShowCustomAmount)
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
return NotFound();
price = amount;
title = settings.Title;
@ -95,6 +104,7 @@ namespace BTCPayServer.Controllers
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemCode = choiceKey ?? string.Empty,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
@ -104,7 +114,7 @@ namespace BTCPayServer.Controllers
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
}
@ -140,38 +150,61 @@ namespace BTCPayServer.Controllers
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = c.GetDetailString("description"),
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
@ -14,13 +15,14 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -41,7 +43,6 @@ namespace BTCPayServer.Controllers
var dto = invoice.EntityToDTO(_NetworkProvider);
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
@ -58,13 +59,14 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(dto.PosData)
};
foreach (var data in invoice.GetPaymentMethods(null))
@ -74,9 +76,9 @@ namespace BTCPayServer.Controllers
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Due = $"{accounting.Due} {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = $"{accounting.CryptoPaid} {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = $"{accounting.OverpaidHelper} {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
@ -174,7 +176,8 @@ namespace BTCPayServer.Controllers
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
[ReferrerPolicyAttribute("origin")]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null,
[FromQuery]string view = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
@ -185,6 +188,8 @@ namespace BTCPayServer.Controllers
if (model == null)
return NotFound();
if (view == "modal")
model.IsModal = true;
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
@ -267,7 +272,7 @@ namespace BTCPayServer.Controllers
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
DefaultLang = storeBlob.DefaultLang ?? "en",
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
@ -466,6 +471,28 @@ namespace BTCPayServer.Controllers
return list;
}
[HttpGet]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport();
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
{
FileName = $"btcpay-export-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
Inline = true
};
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("X-Content-Type-Options", "nosniff");
return Content(res, "application/" + format);
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
@ -583,5 +610,43 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
if (string.IsNullOrEmpty(posData))
{
return result;
}
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
break;
default:
result.Add(item.Key, item.Value.ToString());
break;
}
}
}
catch
{
result.Add(string.Empty, posData);
}
return result;
}
}
}
}

View File

@ -14,7 +14,7 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;

View File

@ -443,7 +443,7 @@ namespace BTCPayServer.Controllers
if (!is2faTokenValid)
{
ModelState.AddModelError("model.Code", "Verification code is invalid.");
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
return View(model);
}

View File

@ -8,7 +8,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -167,6 +168,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@ -625,5 +627,66 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
{
if (offset < 0)
{
offset = 0;
}
var vm = new LogsViewModel();
if (string.IsNullOrEmpty(_Options.LogFile))
{
vm.StatusMessage = "Error: File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments";
}
else
{
var di = Directory.GetParent(_Options.LogFile);
if (di == null)
{
vm.StatusMessage = "Error: Could not load log files";
}
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
vm.LogFileCount = logFiles.Length;
vm.LogFiles = logFiles
.OrderBy(info => info.LastWriteTime)
.Skip(offset)
.Take(5)
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try
{
using (var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
{
using (var reader = new StreamReader(fileStream))
{
vm.Log = await reader.ReadToEndAsync();
}
}
}
catch
{
return NotFound();
}
}
return View("Logs", vm);
}
}
}

View File

@ -34,13 +34,66 @@ namespace BTCPayServer.Controllers
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
public async Task<IActionResult> AddDerivationSchemeLedger(
string storeId,
string cryptoCode,
string command,
int account = 0)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
var network = _NetworkProvider.GetNetwork(cryptoCode);
using (var normalOperationTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
try
{
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
@ -60,7 +113,6 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
@ -109,8 +161,9 @@ namespace BTCPayServer.Controllers
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
(!vm.Confirmation && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try

View File

@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -51,6 +52,7 @@ namespace BTCPayServer.Controllers
IFeeProviderFactory feeRateProvider,
LanguageService langService,
ChangellyClientProvider changellyClientProvider,
IOptions<MvcJsonOptions> mvcJsonOptions,
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
{
_RateFactory = rateFactory;
@ -59,6 +61,7 @@ namespace BTCPayServer.Controllers
_UserManager = userManager;
_LangService = langService;
_changellyClientProvider = changellyClientProvider;
MvcJsonOptions = mvcJsonOptions;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
@ -570,6 +573,45 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Action = "Revoke the token",
Title = "Revoke the token",
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}")]
public async Task<IActionResult> ShowToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != StoreData.Id)
return NotFound();
return View(token);
}
[HttpPost]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]
@ -634,6 +676,7 @@ namespace BTCPayServer.Controllers
}
public string GeneratedPairingCode { get; set; }
public IOptions<MvcJsonOptions> MvcJsonOptions { get; }
[HttpGet]
[Route("/api-tokens")]
@ -671,21 +714,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey()
@ -761,7 +789,8 @@ namespace BTCPayServer.Controllers
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id
storeId = store.Id,
pairingCode = pairingCode
});
}
else

View File

@ -46,6 +46,9 @@ namespace BTCPayServer.Controllers
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
public RateFetcher RateFetcher { get; }
[TempData]
public string StatusMessage { get; set; }
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -132,6 +135,7 @@ namespace BTCPayServer.Controllers
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
vm.IsConfirmed = tx.Confirmations != 0;
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
@ -150,18 +154,27 @@ namespace BTCPayServer.Controllers
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel()
WalletSendModel model = new WalletSendModel()
{
DefaultAddress = defaultDestination,
DefaultAmount = defaultAmount,
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
CryptoCurrency = walletId.CryptoCode
Destination = defaultDestination,
CryptoCode = walletId.CryptoCode
};
if (double.TryParse(defaultAmount, out var amount))
model.Amount = (decimal)amount;
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
@ -185,6 +198,74 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
if (vm.Amount.HasValue)
{
if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
if (vm.CurrentBalance < vm.Amount.Value)
ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
}
if (!ModelState.IsValid)
return View(vm);
return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
{
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
});
}
[HttpGet]
[Route("{walletId}/send/ledger")]
public async Task<IActionResult> WalletSendLedger(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendLedgerModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
return View(vm);
}
private IDestination[] ParseDestination(string destination, Network network)
{
try
{
destination = destination?.Trim();
return new IDestination[] { BitcoinAddress.Create(destination, network) };
}
catch
{
return null;
}
}
[HttpGet]
[Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan(
@ -199,12 +280,15 @@ namespace BTCPayServer.Controllers
return NotFound();
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
// We need to ensure it is segwit,
// because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
vm.IsSegwit = paymentMethod.DerivationStrategyBase.IsSegwit();
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
if(scanProgress != null)
if (scanProgress != null)
{
vm.PreviousError = scanProgress.Error;
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
@ -298,20 +382,25 @@ namespace BTCPayServer.Controllers
return _userManager.GetUserId(User);
}
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
[HttpGet]
[Route("{walletId}/send/ledger/success")]
public IActionResult WalletSendLedgerSuccess(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string txid)
{
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
StatusMessage = $"Transaction broadcasted ({txid})";
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
}
[HttpGet]
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
[Route("{walletId}/send/ledger/ws")]
public async Task<IActionResult> LedgerConnection(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string command,
// getinfo
string cryptoCode = null,
// getxpub
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
DerivationStrategyBase derivationScheme = null,
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
@ -319,6 +408,11 @@ namespace BTCPayServer.Controllers
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var cryptoCode = walletId.CryptoCode;
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
@ -382,15 +476,6 @@ namespace BTCPayServer.Controllers
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
@ -398,13 +483,12 @@ namespace BTCPayServer.Controllers
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
result = new GetInfoResult();
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "sendtoaddress")
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
@ -549,8 +633,6 @@ namespace BTCPayServer.Controllers
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;

View File

@ -63,10 +63,18 @@ namespace BTCPayServer
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
bool hasLabel = false;
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
if (!hasLabel)
{
hintedLabels.Clear();
if (!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
}
hasLabel = true;
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}

View File

@ -32,6 +32,7 @@ using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
@ -128,6 +129,19 @@ namespace BTCPayServer
resp.Headers[name] = value;
}
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
{
if (IsSegwitCore(derivationStrategyBase))
return true;
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
}
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
{
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(

View File

@ -57,19 +57,22 @@ namespace BTCPayServer.Hosting
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env)
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
_Env = env;
LoggerFactory = loggerFactory;
}
IHostingEnvironment _Env;
public IConfiguration Configuration
{
get; set;
}
public ILoggerFactory LoggerFactory { get; }
public void ConfigureServices(IServiceCollection services)
{
Logs.Configure(LoggerFactory);
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, IdentityRole>()
@ -119,14 +122,42 @@ namespace BTCPayServer.Hosting
});
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
bool hasCertPath = !String.IsNullOrEmpty(httpsCertificateFilePath);
if (hasCertPath || useDefaultCertificate)
{
var bindAddress = Configuration.GetOrDefault<IPAddress>("bind", IPAddress.Any);
int bindPort = Configuration.GetOrDefault<int>("port", 443);
services.Configure<KestrelServerOptions>(kestrel =>
{
if (hasCertPath && !File.Exists(httpsCertificateFilePath))
{
// Note that by design this is a fatal error condition that will cause the process to exit.
throw new ConfigException($"The https certificate file could not be found at {httpsCertificateFilePath}.");
}
if(hasCertPath && useDefaultCertificate)
{
throw new ConfigException($"Conflicting settings: if HttpsUseDefaultCertificate is true, HttpsCertificateFilePath should not be used");
}
kestrel.Listen(bindAddress, bindPort, l =>
{
if (hasCertPath)
{
Logs.Configuration.LogInformation($"Using HTTPS with the certificate located in {httpsCertificateFilePath}.");
l.UseHttps(httpsCertificateFilePath, Configuration.GetOrDefault<string>("HttpsCertificateFilePassword", null));
}
else
{
Logs.Configuration.LogInformation($"Using HTTPS with the default certificate");
l.UseHttps();
}
});
});
}
}
public void Configure(
@ -136,7 +167,6 @@ namespace BTCPayServer.Hosting
BTCPayServerOptions options,
ILoggerFactory loggerFactory)
{
Logs.Configure(loggerFactory);
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
{

View File

@ -14,15 +14,33 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[Required]
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "Enable shopping cart")]
public bool EnableShoppingCart { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
[Required]
[MaxLength(30)]
[Display(Name = "Text to display on each buttons for items with a specific price")]
public string ButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Text to display on buttons next to the input allowing the user to enter a custom amount")]
public string CustomButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Do you want to leave a tip?")]
public string CustomTipText { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
}
}

View File

@ -14,13 +14,25 @@ namespace BTCPayServer.Models.AppViewModels
public string Formatted { get; set; }
public decimal Value { get; set; }
}
public string Description { get; set; }
public string Id { get; set; }
public string Image { get; set; }
public ItemPrice Price { get; set; }
public string Title { get; set; }
public bool Custom { 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 CurrencySymbol { get; set; }
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomTipText { get; set; }
public string CustomCSSLink { get; set; }
}
}

View File

@ -247,6 +247,8 @@ namespace BTCPayServer.Models
public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
[JsonProperty("buyer")]
public JObject Buyer { get; set; }
}
public class Flags
{

View File

@ -143,5 +143,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
}
}

View File

@ -21,6 +21,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }

View File

@ -1,4 +1,4 @@
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.IO;
namespace BTCPayServer.Models.ServerViewModels
{
public class LogsViewModel
{
public string StatusMessage
{
get; set;
}
public List<FileInfo> LogFiles { get; set; } = new List<FileInfo>();
public string Log { get; set; }
public int LogFileCount { get; set; }
public int LogFileOffset{ get; set; }
}
}

View File

@ -14,6 +14,7 @@ namespace BTCPayServer.Models.ServerViewModels
public LndTypes Type { get; set; }
public int Index { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public bool HasSSH { get; set; }
}

View File

@ -58,7 +58,7 @@ namespace BTCPayServer.Models.StoreViewModels
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);

View File

@ -29,7 +29,6 @@ namespace BTCPayServer.Models.StoreViewModels
public bool Confirmation { get; set; }
public bool Enabled { get; set; } = true;
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
public KeyPath RootKeyPath { get; set; }
}

View File

@ -2,7 +2,6 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Validation;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;

View File

@ -1,4 +1,4 @@
using BTCPayServer.Validations;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.WalletViewModels
public class TransactionViewModel
{
public DateTimeOffset Timestamp { get; set; }
public bool IsConfirmed { get; set; }
public string Id { get; set; }
public string Link { get; set; }
public bool Positive { get; set; }

View File

@ -12,7 +12,8 @@ namespace BTCPayServer.Models.WalletViewModels
public bool IsServerAdmin { get; set; }
public bool IsSupportedByCurrency { get; set; }
public bool IsFullySync { get; set; }
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync;
public bool IsSegwit { get; set; }
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync && IsSegwit;
[Range(1000, 10_000)]
public int BatchSize { get; set; } = 3000;

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSendLedgerModel
{
public int FeeSatoshiPerByte { get; set; }
public bool SubstractFees { get; set; }
public decimal Amount { get; set; }
public string Destination { get; set; }
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSendModel
{
[Required]
public string Destination { get; set; }
[Range(0.0, double.MaxValue)]
[Required]
public decimal? Amount { get; set; }
public decimal CurrentBalance { get; set; }
public string CryptoCode { get; set; }
public int RecommendedSatoshiPerByte { get; set; }
[Display(Name = "Subtract fees from amount")]
public bool SubstractFees { get; set; }
[Range(1, int.MaxValue)]
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }
public string RateError { get; set; }
}
}

View File

@ -51,7 +51,7 @@ namespace BTCPayServer.Payments.Bitcoin
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, NotificationSession>();
ConcurrentDictionary<string, WebsocketNotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, WebsocketNotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
@ -114,7 +114,7 @@ namespace BTCPayServer.Payments.Bitcoin
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
@ -149,8 +149,7 @@ namespace BTCPayServer.Payments.Bitcoin
foreach (var output in evt.Outputs)
{
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
.Where(o => o.ScriptPubKey == output.ScriptPubKey))
{
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
@ -188,7 +187,7 @@ namespace BTCPayServer.Payments.Bitcoin
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out NotificationSession unused);
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out WebsocketNotificationSession unused);
if (_SessionsByCryptoCode.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);

View File

@ -55,18 +55,15 @@ namespace BTCPayServer
l.AddProvider(new CustomConsoleLogProvider(processor));
// Use Serilog for debug log file.
string debugLogFile = conf.GetOrDefault<string>("debuglog", null);
if (String.IsNullOrEmpty(debugLogFile) == false)
{
Serilog.Log.Logger = new LoggerConfiguration()
var debugLogFile = BTCPayServerOptions.GetDebugLog(conf);
if (string.IsNullOrEmpty(debugLogFile) != false) return;
Serilog.Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug()
.MinimumLevel.Is(BTCPayServerOptions.GetDebugLogLevel(conf))
.WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1)
.CreateLogger();
l.AddSerilog(Serilog.Log.Logger);
logger.LogDebug($"Debug log file configured for {debugLogFile}.");
}
l.AddSerilog(Serilog.Log.Logger);
})
.UseStartup<Startup>()
.Build();

View File

@ -1,22 +1,40 @@
{
"profiles": {
"Docker-Regtest": {
"commandName": "Project",
"commandLineArgs": "--debuglog debug.log",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
"profiles": {
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://127.0.0.1:14142/"
},
"applicationUrl": "http://127.0.0.1:14142/"
"Docker-Regtest-https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_PORT": "14142",
"BTCPAY_HttpsUseDefaultCertificate": "true",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "https://localhost:14142/"
}
}
}
}

View File

@ -80,6 +80,7 @@ namespace BTCPayServer.Services
throw new ArgumentNullException(nameof(ledgerWallet));
_Transport = new WebSocketTransport(ledgerWallet);
_Ledger = new LedgerClient(_Transport);
_Ledger.MaxAPDUSize = 90;
}
public async Task<LedgerTestResult> Test(CancellationToken cancellation)

View File

@ -0,0 +1,146 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
// Ref: https://www.codeproject.com/Articles/566656/CSV-Serializer-for-NET
namespace BTCPayServer.Services.Invoices.Export
{
/// <summary>
/// Serialize and Deserialize Lists of any object type to CSV.
/// </summary>
public class CsvSerializer<T> where T : class, new()
{
private List<PropertyInfo> _properties;
public bool IgnoreEmptyLines { get; set; } = true;
public bool IgnoreReferenceTypesExceptString { get; set; } = true;
public string NewlineReplacement { get; set; } = ((char)0x254).ToString(CultureInfo.InvariantCulture);
public char Separator { get; set; } = ',';
public string RowNumberColumnTitle { get; set; } = "RowNumber";
public bool UseLineNumbers { get; set; } = false;
public bool UseEofLiteral { get; set; } = false;
/// <summary>
/// Csv Serializer
/// Initialize by selected properties from the type to be de/serialized
/// </summary>
public CsvSerializer()
{
var type = typeof(T);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance
| BindingFlags.GetProperty | BindingFlags.SetProperty);
var q = properties.AsQueryable();
if (IgnoreReferenceTypesExceptString)
{
q = q.Where(a => a.PropertyType.IsValueType || a.PropertyType.Name == "String");
}
var r = from a in q
where a.GetCustomAttribute<CsvIgnoreAttribute>() == null
select a;
_properties = r.ToList();
}
/// <summary>
/// Serialize
/// </summary>
/// <param name="stream">stream</param>
/// <param name="data">data</param>
public string Serialize(IList<T> data)
{
var sb = new StringBuilder();
var values = new List<string>();
sb.AppendLine(GetHeader());
var row = 1;
foreach (var item in data)
{
values.Clear();
if (UseLineNumbers)
{
values.Add(row++.ToString(CultureInfo.InvariantCulture));
}
foreach (var p in _properties)
{
var raw = p.GetValue(item);
var value = raw == null ? "" :
raw.ToString()
.Replace("\"", "``", StringComparison.OrdinalIgnoreCase)
.Replace(Environment.NewLine, NewlineReplacement, StringComparison.OrdinalIgnoreCase);
value = String.Format(CultureInfo.InvariantCulture, "\"{0}\"", value);
values.Add(value);
}
sb.AppendLine(String.Join(Separator.ToString(CultureInfo.InvariantCulture), values.ToArray()));
}
if (UseEofLiteral)
{
values.Clear();
if (UseLineNumbers)
{
values.Add(row++.ToString(CultureInfo.InvariantCulture));
}
values.Add("EOF");
sb.AppendLine(string.Join(Separator.ToString(CultureInfo.InvariantCulture), values.ToArray()));
}
return sb.ToString();
}
/// <summary>
/// Get Header
/// </summary>
/// <returns></returns>
private string GetHeader()
{
var header = _properties.Select(a => a.Name);
if (UseLineNumbers)
{
header = new string[] { RowNumberColumnTitle }.Union(header);
}
return string.Join(Separator.ToString(CultureInfo.InvariantCulture), header.ToArray());
}
}
public class CsvIgnoreAttribute : Attribute { }
public class InvalidCsvFormatException : Exception
{
/// <summary>
/// Invalid Csv Format Exception
/// </summary>
/// <param name="message">message</param>
public InvalidCsvFormatException(string message)
: base(message)
{
}
public InvalidCsvFormatException(string message, Exception ex)
: base(message, ex)
{
}
}
}

View File

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Invoices.Export
{
public class InvoiceExport
{
public string Process(InvoiceEntity[] invoices, string fileFormat)
{
var csvInvoices = new List<ExportInvoiceHolder>();
foreach (var i in invoices)
{
csvInvoices.AddRange(convertFromDb(i));
}
if (String.Equals(fileFormat, "json", StringComparison.OrdinalIgnoreCase))
return processJson(csvInvoices);
else if (String.Equals(fileFormat, "csv", StringComparison.OrdinalIgnoreCase))
return processCsv(csvInvoices);
else
throw new Exception("Export format not supported");
}
private string processJson(List<ExportInvoiceHolder> invoices)
{
var serializerSett = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var json = JsonConvert.SerializeObject(invoices, Formatting.Indented, serializerSett);
return json;
}
private string processCsv(List<ExportInvoiceHolder> invoices)
{
var serializer = new CsvSerializer<ExportInvoiceHolder>();
var csv = serializer.Serialize(invoices);
return csv;
}
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
{
var exportList = new List<ExportInvoiceHolder>();
// in this first version we are only exporting invoices that were paid
foreach (var payment in invoice.GetPayments())
{
// not accounted payments are payments which got double spent like RBfed
if (!payment.Accounted)
continue;
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 target = new ExportInvoiceHolder
{
ReceivedDate = payment.ReceivedTime.UtcDateTime,
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}",
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,
};
exportList.Add(target);
}
exportList = exportList.OrderBy(a => a.ReceivedDate).ToList();
return exportList;
}
}
public class ExportInvoiceHolder
{
public DateTime ReceivedDate { get; set; }
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 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 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; }
}
}

View File

@ -423,7 +423,6 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618
dto.CryptoInfo.Add(cryptoInfo);
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
@ -438,7 +437,16 @@ namespace BTCPayServer.Services.Invoices
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
Populate(ProductInformation, dto);
Populate(BuyerInformation, dto);
dto.Buyer = new JObject();
dto.Buyer.Add(new JProperty("name", BuyerInformation.BuyerName));
dto.Buyer.Add(new JProperty("address1", BuyerInformation.BuyerAddress1));
dto.Buyer.Add(new JProperty("address2", BuyerInformation.BuyerAddress2));
dto.Buyer.Add(new JProperty("locality", BuyerInformation.BuyerCity));
dto.Buyer.Add(new JProperty("region", BuyerInformation.BuyerState));
dto.Buyer.Add(new JProperty("postalCode", BuyerInformation.BuyerZip));
dto.Buyer.Add(new JProperty("country", BuyerInformation.BuyerCountry));
dto.Buyer.Add(new JProperty("phone", BuyerInformation.BuyerPhone));
dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(BuyerInformation.BuyerEmail) ? RefundMail : BuyerInformation.BuyerEmail));
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
@ -525,20 +533,21 @@ namespace BTCPayServer.Services.Invoices
public class PaymentMethodAccounting
{
/// <summary>
/// Total amount of this invoice
/// </summary>
/// <summary>Total amount of this invoice</summary>
public Money TotalDue { get; set; }
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
/// <summary>Amount of crypto remaining to pay this invoice</summary>
public Money Due { get; set; }
/// <summary>
/// Same as Due, can be negative
/// </summary>
/// <summary>Same as Due, can be negative</summary>
public Money DueUncapped { get; set; }
/// <summary>If DueUncapped is negative, that means user overpaid invoice</summary>
public Money OverpaidHelper
{
get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; }
}
/// <summary>
/// Total amount of the invoice paid after conversion to this crypto currency
/// </summary>

View File

@ -295,6 +295,8 @@ namespace BTCPayServer.Services.Invoices
{
using (var tx = _Engine.GetTransaction())
{
var terms = searchTerms.Split(null);
searchTerms = string.Join(' ', terms.Select(t => t.Length > 50 ? t.Substring(0, 50) : t).ToArray());
return tx.TextSearch("InvoiceSearch").Block(searchTerms)
.GetDocumentIDs()
.Select(id => Encoders.Base58.EncodeData(id))

View File

@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Services
{
@ -12,34 +16,53 @@ namespace BTCPayServer.Services
DisplayName = displayName;
Code = code;
}
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("currentLanguage")]
public string DisplayName { get; set; }
}
public class LanguageService
{
private readonly Language[] _languages;
public LanguageService(IHostingEnvironment environment)
{
var path = (environment as HostingEnvironment)?.WebRootPath;
if (string.IsNullOrEmpty(path))
{
//test environment
path = Path.Combine(TryGetSolutionDirectoryInfo().FullName,"BTCPayServer", "wwwroot");
}
path = Path.Combine(path, "locales");
var files = Directory.GetFiles(path, "*.json");
var result = new List<Language>();
foreach (var file in files)
{
using (var stream = new StreamReader(file))
{
var json = stream.ReadToEnd();
result.Add(JObject.Parse(json).ToObject<Language>());
}
}
_languages = result.ToArray();
}
public static DirectoryInfo TryGetSolutionDirectoryInfo(string currentPath = null)
{
var directory = new DirectoryInfo(
currentPath ?? Directory.GetCurrentDirectory());
while (directory != null && !directory.GetFiles("*.sln").Any())
{
directory = directory.Parent;
}
return directory;
}
public Language[] GetLanguages()
{
return new[]
{
new Language("en-US", "English"),
new Language("de-DE", "Deutsch"),
new Language("ja-JP", "日本語"),
new Language("fr-FR", "Français"),
new Language("es-ES", "Spanish"),
new Language("pt-PT", "Portuguese"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("np-NP", "नेपाली"),
new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"),
new Language("hr-HR", "Croatian"),
new Language("it-IT", "Italiano"),
new Language("kk-KZ", "Қазақша"),
new Language("ru-RU", "русский"),
new Language("uk-UA", "Українська"),
new Language("vi-VN", "Tiếng Việt"),
new Language("zh-SP", "中文(简体)"),
};
return _languages;
}
}
}

View File

@ -42,6 +42,19 @@ namespace BTCPayServer.Services.Rates
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;
Dictionary<string, string> _TickerMapping = new Dictionary<string, string>()
{
{ "XXDG", "DOGE" },
{ "XXBT", "BTC" },
{ "XBT", "BTC" },
{ "DASH", "DASH" },
{ "ZUSD", "USD" },
{ "ZEUR", "EUR" },
{ "ZJPY", "JPY" },
{ "ZCAD", "CAD" },
};
public async Task<ExchangeRates> GetRatesAsync()
{
var result = new ExchangeRates();
@ -57,7 +70,20 @@ namespace BTCPayServer.Services.Rates
{
try
{
var global = _Helper.ExchangeSymbolToGlobalSymbol(symbol);
string global = null;
var mapped1 = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).SingleOrDefault();
if (mapped1 != null)
{
var p2 = symbol.Substring(mapped1.KrakenTicker.Length);
if (_TickerMapping.TryGetValue(p2, out var mapped2))
p2 = mapped2;
global = $"{p2}_{mapped1.PayTicker}";
}
else
{
global = _Helper.ExchangeSymbolToGlobalSymbol(symbol);
}
if (CurrencyPair.TryParse(global, out var pair))
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
else

View File

@ -102,6 +102,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
// Cryptopia is often not available
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
@ -118,6 +120,8 @@ namespace BTCPayServer.Services.Rates
foreach (var provider in Providers.ToArray())
{
if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs
continue;
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
if(provider.Key == CoinAverageRateProvider.CoinAverageName)
{

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
@ -9,6 +10,8 @@ namespace BTCPayServer.Services
public class ThemeSettings
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string BootstrapCssUri { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]

View File

@ -122,7 +122,7 @@ namespace BTCPayServer.Services.Wallets
UTXOChanges result = null;
try
{
result = await _Client.GetUTXOsAsync(strategy, null, false, cancellation).ConfigureAwait(false);
result = await _Client.GetUTXOsAsync(strategy, cancellation).ConfigureAwait(false);
}
catch
{
@ -153,7 +153,7 @@ namespace BTCPayServer.Services.Wallets
public Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Client.GetTransactionsAsync(derivationStrategyBase, null, false);
return _Client.GetTransactionsAsync(derivationStrategyBase);
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BTCPayServer.Validations
namespace BTCPayServer.Validation
{
public class EmailValidator
{

View File

@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
namespace BTCPayServer.Validation
{
public class PubKeyValidatorAttribute : ValidationAttribute
{

View File

@ -29,10 +29,35 @@
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnableShoppingCart"></label>
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ButtonText" class="control-label"></label>*
<input asp-for="ButtonText" class="form-control" />
<span asp-validation-for="ButtonText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomButtonText" class="control-label"></label>*
<input asp-for="CustomButtonText" class="form-control" />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomTipText" class="control-label"></label>*
<input asp-for="CustomTipText" class="form-control" />
<span asp-validation-for="CustomTipText" 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 asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
@ -41,12 +66,12 @@
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if(Model.Example1 != null)
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if(Model.Example2 != null)
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
@ -63,7 +88,7 @@
</p>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" />
<input type="submit" class="btn btn-primary" value="Save Settings" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>

View File

@ -1,4 +1,5 @@
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
@ -14,43 +15,133 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
@if (Model.EnableShoppingCart)
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}
</head>
<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>
<div class="modal-body">
<table id="js-cart-list" class="table mt-2 mb-3">
<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>
</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" style="margin: auto;">
<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>
<form method="post" asp-antiforgery="false">
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
var item = Model.Items[i];
<div class="@className">
<h3>@item.Title</h3>
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
@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))
{
<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>
</form>
</div>
}
</div>
@if (Model.ShowCustomAmount)
{
<div class="row mt-4">
<div class="col-sm-3">&nbsp;</div>
<div class="col-sm-6">
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<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">Pay</button>
</div>
<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>
</form>
</div>
</div>
<div class="col-sm-3">&nbsp;</div>
</div>
}
</div>
</div>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
</body>
</html>

View File

@ -12,6 +12,9 @@
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
}
</div>
<div class="close-icon close-action">
&#10006;
</div>
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
@ -285,7 +288,7 @@
:disabled="isLoading"
v-on:change="onCurrencyChange($event)"
ref="changellyCurrenciesDropdown">
<option value="">Select a currency to convert from</option>
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">
@ -293,8 +296,8 @@
</option>
</select>
</div>
<a v-on:click="openDialog($event)" :href="url" class="changelly-component-button">
<img src="https://changelly.com/pay_button.png" alt="Changelly" v-show="url"/>
<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")}}
@ -326,9 +329,12 @@
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="srvModel.merchantRefLink">
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</a>
<button class="action-button close-action" v-show="isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
@ -351,7 +357,7 @@
<div class="bp-view expired" id="expired">
<div>
<div class="expired__body">
<div class="expired__body" style="margin-bottom: 20px;">
<div class="expired__header">{{$t("What happened?")}}</div>
<div class="expired__text" i18n="">
{{$t("InvoiceExpired_Body_1", {storeName: srvModel.storeName, maxTimeMinutes: @Model.MaxTimeMinutes})}}
@ -370,10 +376,12 @@
{{srvModel.orderId}}
</div>
</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="srvModel.merchantRefLink"
style="margin-top: 20px;">
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</a>
<button class="action-button close-action" v-show="isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>
</div>

View File

@ -1,5 +1,7 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.Services.LanguageService langService
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@model PaymentModel
@{
Layout = null;
@ -11,6 +13,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<META NAME="robots" CONTENT="noindex,nofollow">
<title>@Model.HtmlTitle</title>
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
@ -27,10 +30,20 @@
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<style type="text/css">
</style>
@if (Model.IsModal)
{
<style type="text/css">
body {
background: rgba(55, 58, 60, 0.4);
}
.close-icon {
display: flex;
}
</style>
}
</head>
<body style="background: #E4E4E4">
<body>
<noscript>
<center style="padding: 2em">
<h2>Javascript is currently disabled in your browser.</h2>
@ -53,97 +66,95 @@
</center>
<![endif]-->
<invoice>
<div class="no-bounce" id="checkoutCtrl" v-cloak>
<div class="modal page">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<div class="modal-content long">
<div class="content">
<div class="invoice">
<partial name="Checkout-Body" />
<invoice>
<div class="no-bounce" id="checkoutCtrl" v-cloak>
<div class="modal page">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<div class="modal-content long">
<div class="content">
<div class="invoice">
<partial name="Checkout-Body" />
</div>
</div>
</div>
</div>
<div style="margin-top: 10px; text-align: center;">
@* Not working because of nsSeparator: false, keySeparator: false,
<div style="margin-top: 10px; text-align: center;">
@* Not working because of nsSeparator: false, keySeparator: false,
{{$t("nested.lang")}} >>
*@
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
@foreach (var lang in langService.GetLanguages())
{
<option value="@lang.Code">@lang.DisplayName</option>
}
</select>
<script>
$(function() {
var storeDefaultLang = '@Model.DefaultLang';
if (urlParams.lang) {
$(".cmblang").val(urlParams.lang);
} else if (storeDefaultLang) {
$(".cmblang").val(storeDefaultLang);
}
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
initDropdown(".cmblang");
});
function initDropdown(selector) {
return $(selector).prettyDropdown({
classic: false,
height: 32,
reverse: true,
hoverIntent: 5000
<select asp-for="DefaultLang"
class="cmblang reverse invisible"
onchange="changeLanguage($(this).val())"
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
<script>
var languageSelectorPrettyDropdown;
$(function() {
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
$("#DefaultLang").val(startingLanguage);
languageSelectorPrettyDropdown = initDropdown("#DefaultLang");
});
}
</script>
</div>
<div style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
function initDropdown(selector) {
return $(selector).prettyDropdown({
classic: false,
height: 32,
reverse: true,
hoverIntent: 5000
});
}
</script>
</div>
<div style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
</div>
</div>
</div>
</div>
</invoice>
<script type="text/javascript">
var storeDefaultLang = '@Model.DefaultLang';
// initialization
i18next.init({
lng: storeDefaultLang,
fallbackLng: 'en-US',
nsSeparator: false,
keySeparator: false,
resources: {
'en-US': { translation: locales_en },
'de-DE': { translation: locales_de },
'es-ES': { translation: locales_es },
'ja-JP': { translation: locales_ja },
'fr-FR': { translation: locales_fr },
'pt': { translation: locales_pt },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'np': { translation: locales_np },
'cs-CZ': { translation: locales_cs },
'is-IS': { translation: locales_is },
'it-IT': { translation: locales_it },
'hr-HR': { translation: locales_hr },
'kk-KZ': { translation: locales_kk },
'ru-RU': { translation: locales_ru },
'uk-UA': { translation: locales_uk },
'vi-VN': { translation: locales_vi },
'zh-SP': { translation: locales_zh_sp }
<script type="text/javascript">
var availableLanguages = @Html.Raw(Json.Serialize(
langService
.GetLanguages()
.Select((language) => language.Code)));;
var storeDefaultLang = "@Model.DefaultLang";
var fallbackLanguage = "en";
startingLanguage = computeStartingLanguage();
// initialization
i18next
.use(window.i18nextXHRBackend)
.init({
backend: {
loadPath: '/locales/{{lng}}.json'
},
lng: startingLanguage,
fallbackLng: fallbackLanguage,
nsSeparator: false,
keySeparator: false
});
function computeStartingLanguage() {
if (urlParams.lang && isLanguageAvailable(urlParams.lang)) {
return urlParams.lang;
}
else if (isLanguageAvailable(storeDefaultLang)) {
return storeDefaultLang;
} else {
return fallbackLanguage;
}
}
function changeLanguage(lang) {
i18next.changeLanguage(lang);
if (isLanguageAvailable(lang)) {
i18next.changeLanguage(lang);
}
}
if (urlParams.lang) {
changeLanguage(urlParams.lang);
} else if (storeDefaultLang) {
changeLanguage(storeDefaultLang);
function isLanguageAvailable(languageCode) {
return availableLanguages.indexOf(languageCode) >= 0;
}
const i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
@ -153,7 +164,7 @@
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
i18n: i18n,
el: '#checkoutCtrl',
@ -162,10 +173,11 @@
changelly: ChangellyComponent
},
data: {
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal
}
});
</script>

View File

@ -2,6 +2,14 @@
@{
ViewData["Title"] = "Create an invoice";
}
<script>
$(function() {
$("#create-invoice-form").on("submit",
function() {
$(this).find("input[type='submit']").prop('disabled', true);
});
})
</script>
<section>
<div class="container">
<div class="row">
@ -12,7 +20,7 @@
</div>
<div class="row">
<div class="col-lg-12">
<form asp-action="CreateInvoice" method="post">
<form asp-action="CreateInvoice" method="post" id="create-invoice-form">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Amount" class="control-label"></label>*

View File

@ -2,6 +2,9 @@
@{
ViewData["Title"] = "Invoice " + Model.Id;
}
@section HeaderContent{
<META NAME="robots" CONTENT="noindex,nofollow">
}
<style type="text/css">
.linethrough {
@ -39,110 +42,134 @@
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
</tr>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<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>
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
</tr>
<tr>
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
</tr>
</table>
@if (Model.PosData.Count == 0)
{
<h3>Product information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
</tr>
</table>
}
</div>
</div>
@if (Model.PosData.Count != 0)
{
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
</tr>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<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>
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
</tr>
<tr>
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
</tr>
</table>
<h3>Product information</h3>
<table class="table table-sm table-responsive-md">
<tr>
@ -159,8 +186,31 @@
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Point of Sale Data</h3>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model.PosData)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>@posDataItem.Value</td>
}
else
{
<td colspan="2">@posDataItem.Value</td>
}
</tr>
}
</table>
</div>
</div>
<div class="row">
}
<div class="row">
<div class="col-md-12">
<h3>Paid summary</h3>
<table class="table table-sm table-responsive-md">
@ -255,29 +305,6 @@
</div>
</div>
}
<div class="row">
<div class="col-md-12">
<h3>Addresses</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th class="firstCol">Payment method</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight-bold" : "";
<tr>
<td>@address.PaymentMethod</td>
<td class="smMaxWidth text-truncate @current">@address.Destination</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">

View File

@ -1,6 +1,11 @@
@model InvoicesModel
@{
ViewData["Title"] = "Invoices";
var rootUrl = Context.Request.GetAbsoluteRoot();
}
@section HeadScripts {
<script src="~/modal/btcpay.js"></script>
}
<section>
@ -36,11 +41,19 @@
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-4">
<div class="col-lg-6">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
</div>
</div>
<div class="col-lg-8">
<div class="col-lg-6">
<div class="form-group">
<form asp-action="SearchInvoice" method="post" style="float:right;">
<div class="input-group">
@ -107,8 +120,13 @@
<td style="text-align:right">
@if (invoice.ShowCheckout)
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
-
</span>
}
<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</td>
</tr>
}

View File

@ -32,6 +32,8 @@
<div class="row">
<div class="col-md-6">
<form method="post">
<input asp-for="AuthenticatorUri" type="hidden" />
<input asp-for="SharedKey" type="hidden" />
<div class="form-group">
<label asp-for="Code" class="control-label">Verification Code</label>
<input asp-for="Code" class="form-control" autocomplete="off" />

View File

@ -0,0 +1,40 @@
@model BTCPayServer.Models.ServerViewModels.LogsViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Logs);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">
<ul>
@foreach (var file in Model.LogFiles)
{
<li>
<a asp-action="LogsView" asp-route-file="@file.Name">@file.Name</a>
</li>
}
<li>
@if (Model.LogFileOffset > 0)
{
<a asp-action="LogsView" asp-route-offset="@(Model.LogFileOffset - 5)"><<</a>
}
Showing @Model.LogFileOffset - (@(Model.LogFileOffset+Model.LogFiles.Count)) of @Model.LogFileCount
@if ((Model.LogFileOffset+ Model.LogFiles.Count) < Model.LogFileCount)
{
<a asp-action="LogsView" asp-route-offset="@(Model.LogFileOffset + Model.LogFiles.Count)">>></a>
}
</li>
</ul>
@if (!string.IsNullOrEmpty(Model.Log))
{
<pre>
@Model.Log
</pre>
}
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance, Logs
}
}

View File

@ -16,6 +16,7 @@
<form method="post">
<div class="form-group">
<label asp-for="BootstrapCssUri"></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="BootstrapCssUri" class="form-control" />
<span asp-validation-for="BootstrapCssUri" class="text-danger"></span>
<p class="form-text text-muted">

View File

@ -6,6 +6,7 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

View File

@ -28,6 +28,7 @@
<bundle name="wwwroot/bundles/main-bundle.min.js" />
@RenderSection("HeadScripts", required: false)
@RenderSection("HeaderContent", false)
</head>
<body id="page-top">

View File

@ -128,9 +128,6 @@
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model.ServerUrl));
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
}

View File

@ -18,9 +18,8 @@
<thead>
<tr>
<th>Label</th>
<th>SIN</th>
<th>Facade</th>
<th>Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -28,13 +27,9 @@
{
<tr>
<td>@token.Label</td>
<td>@token.SIN</td>
<td>@token.Facade</td>
<td>
<form asp-action="DeleteToken" method="post">
<input type="hidden" name="tokenId" value="@token.Id">
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
</form>
<td class="text-right">
<a asp-action="ShowToken" asp-route-tokenId="@token.Id">See information</a> - <a asp-action="RevokeToken" asp-route-tokenId="@token.Id">Revoke</a>
</td>
</tr>
}

View File

@ -0,0 +1,29 @@
@model BTCPayServer.Authentication.BitTokenEntity
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Tokens, "Access Tokens");
}
<div class="row">
<div class="col-md-8">
<h5>Token information</h5>
<table class="table table-sm">
<tr>
<th>Label</th>
<td>@Model.Label</td>
</tr>
<tr>
<th>SIN</th>
<td>@Model.SIN</td>
</tr>
<tr>
<th>Token</th>
<td>@Model.Value</td>
</tr>
<tr>
<th>Facade</th>
<td>@Model.Facade</td>
</tr>
</table>
</div>
</div>

View File

@ -44,29 +44,35 @@
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="AnyoneCanCreateInvoice"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#allow-anyone-to-create-invoice" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="InvoiceExpiration" class="form-control" />
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="MonitoringExpiration" class="form-control" />
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PaymentTolerance"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="PaymentTolerance" class="form-control" />
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy"></label>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<select asp-for="SpeedPolicy" class="form-control">
<option value="0">Is unconfirmed</option>
<option value="1">Has at least 1 confirmation</option>

View File

@ -42,6 +42,7 @@
}
</td>
<td style="text-align:right">
<a asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchTerm="storeid:@store.Id">Invoices</a><span> - </span>
@if (store.IsOwner)
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">Settings</a><span> - </span>

View File

@ -36,6 +36,14 @@
{
<p><span class="fa fa-times-circle" style="color:red;"></span> <span>This full node do not support rescan of the UTXO set</span></p>
}
@if (Model.IsSegwit)
{
<p><span class="fa fa-check-circle" style="color:green;"></span> <span>This wallet is compatible with segwit</span></p>
}
else
{
<p><span class="fa fa-times-circle" style="color:red;"></span> <span>This wallet is not compatible with segwit</span></p>
}
</div>
</div>
}
@ -92,7 +100,7 @@ else
{
<div class="row">
<div class="col-md-10">
<p>Scanning in progress, refresh the page to see the progress...</p>
<p>Scanning in progress, refresh the page to see the progress... (Estimated time: @Model.RemainingTime)</p>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@(Model.Progress.Value)"
aria-valuemin="0" aria-valuemax="100" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(Model.Progress.Value)%;">

View File

@ -1,4 +1,4 @@
@model WalletModel
@model WalletSendModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
@ -6,75 +6,60 @@
}
<h4>@ViewData["Title"]</h4>
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div class="col-md-10">
<p>
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />
If your Ledger wallet is not detected:
Send funds to a destination address.
</p>
<ul>
<li>Make sure you are running the Ledger app with version superior or equal to 1.2.4</li>
<li>Use Google Chrome browser and open the coin app on your Ledger</li>
</ul>
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="fa fa-question-circle" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form id="sendform" style="display:none;">
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
<form method="post">
<input type="hidden" asp-for="Divisibility" />
<input type="hidden" asp-for="Fiat" />
<input type="hidden" asp-for="Rate" />
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
<input type="hidden" asp-for="CryptoCode" />
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
<label asp-for="Destination"></label>
<input asp-for="Destination" class="form-control" />
<span asp-validation-for="Destination" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<label asp-for="Amount"></label>
<div class="input-group">
<input id="amount-textbox" name="Amount" class="form-control" type="text" onkeyup='updateFiatValue();' />
<input asp-for="Amount" class="form-control" onkeyup='updateFiatValue();' />
<div class="input-group-prepend">
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
</div>
</div>
@*<span class="text-muted" id="fiatValue"></span>*@
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
<span asp-validation-for="Amount" class="text-danger"></span>
<p class="form-text text-muted crypto-info">
Your current balance is <a id="crypto-balance-link" href="#"><span>@Model.CurrentBalance</span></a> <span>@Model.CryptoCode</span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<label asp-for="FeeSatoshiPerByte"></label>
<input asp-for="FeeSatoshiPerByte" class="form-control" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
<p class="form-text text-muted crypto-info">
The recommended value is <a id="crypto-fee-link" href="#"><span>@Model.RecommendedSatoshiPerByte</span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
<label asp-for="SubstractFees"></label>
<input asp-for="SubstractFees" class="form-check" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-primary">Confirm</button>
<button type="submit" class="btn btn-primary">Confirm</button>
</form>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
{
<script src="~/js/WalletSend.js" type="text/javascript" defer="defer"></script>
}

View File

@ -0,0 +1,43 @@
@model WalletSendLedgerModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
<h4>Sign the transaction with Ledger</h4>
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div class="col-md-10">
<input type="hidden" asp-for="Destination" />
<input type="hidden" asp-for="Amount" />
<input type="hidden" asp-for="FeeSatoshiPerByte" />
<input type="hidden" asp-for="SubstractFees" />
<p>
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />
If your Ledger wallet is not detected:
</p>
<ul>
<li>Make sure you are running the Ledger app with version superior or equal to 1.2.4</li>
<li>Use Google Chrome browser and open the coin app on your Ledger</li>
</ul>
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="fa fa-question-circle" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
@section Scripts
{
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/WalletSendLedger.js" type="text/javascript" defer="defer"></script>
}

View File

@ -15,8 +15,17 @@
max-width: 400px;
}
}
.unconf {
opacity: 0.5;
}
</style>
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
@ -36,8 +45,8 @@
<tbody>
@foreach (var transaction in Model.Transactions)
{
<tr>
<td>@transaction.Timestamp.ToBrowserDate()</td>
<tr class="@(transaction.IsConfirmed ? "" : "unconf")">
<td>@transaction.Timestamp.ToTimeAgo()</td>
<td class="smMaxWidth text-truncate">
<a href="@transaction.Link" target="_blank">
@transaction.Id

View File

@ -46,10 +46,20 @@
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/i18next/i18next.js",
"wwwroot/vendor/i18next/i18nextXHRBackend.js",
"wwwroot/vendor/i18next/vue-i18next.js",
"wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js",
"wwwroot/vendor/vex/js/vex.combined.min.js",
"wwwroot/checkout/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/bootstrap4/js/bootstrap.js",
"wwwroot/cart/js/cart.js",
"wwwroot/cart/js/cart.jquery.js"
]
}
]

View File

@ -0,0 +1,57 @@
$.fn.addAnimate = function(completeCallback) {
var documentHeight = $(document).height(),
itemPos = $(this).offset(),
itemY = itemPos.top,
cartPos = $('#js-cart').find('.badge').position();
tempItem = '<span id="js-cart-temp-item" class="badge badge-primary text-white badge-pill " style="' +
'position: absolute;' +
'top: ' + itemPos.top + 'px;' +
'left: ' + (itemPos.left + 50) + 'px;">'+
'<i class="fa fa-shopping-basket"></i></span>';
// Make animation speed look constant regardless of how far the object is from the cart
var animationSpeed = (Math.log(itemY) * (documentHeight / Math.log2(documentHeight - itemY))) / 2;
// Add the cart item badge and animate it
$('body').after(tempItem);
$('#js-cart-temp-item').animate({
easing: 'swing',
top: cartPos.top,
left: cartPos.left
}, animationSpeed, function() {
$(this).remove();
completeCallback && completeCallback();
});
};
$(document).ready(function(){
var cart = new Cart();
$('.js-add-cart').click(function(event){
event.preventDefault();
var $btn = $(event.target),
id = $btn.closest('.card').data('id'),
item = srvModel.items[id];
// Animate adding and then add then save
$(this).addAnimate(function(){
cart.addItem({
id: id,
title: item.title,
price: item.price,
image: typeof item.image != 'underfined' ? item.image : null
});
});
});
// Destroy the cart when the "pay button is clicked"
$('#js-cart-pay').click(function(){
cart.destroy();
});
// Repopulate cart items in the modal when it opens
$('#cartModal').on('show.bs.modal', function () {
cart.listItems();
});
});

View File

@ -0,0 +1,282 @@
function Cart() {
this.items = 0;
this.totalAmount = 0;
this.content = [];
this.tip = 0;
this.loadLocalStorage();
this.itemsCount();
this.listItems();
this.updateAmount();
}
Cart.prototype.addItem = function(item) {
// Increment the existing item count
var result = this.content.filter(function(obj){
if (obj.id === item.id){
obj.count++;
}
return obj.id === item.id
});
// Add new item because it doesn't exist yet
if (!result.length) {
this.content.push({id: item.id, title: item.title, price: item.price, count: 1, image: item.image})
}
this.items++;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
}
Cart.prototype.decrementItem = function(id) {
var self = this;
// Decrement the existing item count
this.content.filter(function(obj, index, arr){
if (obj.id === id)
{
obj.count--;
// It's the last item with the same ID, remove it
if (obj.count === 0) {
self.removeItem(id, index, arr);
}
}
});
this.items--;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
if (this.items === 0) {
this.emptyList();
}
}
Cart.prototype.removeItemAll = function(id) {
var self = this;
this.content.filter(function(obj, index, arr){
if (obj.id === id)
{
self.removeItem(id, index, arr);
for (var i = 0; i < obj.count; i++) {
self.items--;
}
}
});
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
if (this.items === 0) {
this.emptyList();
}
}
Cart.prototype.removeItem = function(id, index, arr) {
// Remove from the array
arr.splice(index, 1);
// Remove from the DOM
$('#js-cart-list').find('tr').eq(index+1).remove();
}
Cart.prototype.setTip = function(tip) {
return this.tip = tip;
}
Cart.prototype.itemsCount = function() {
$('#js-cart-items').text(this.items);
}
Cart.prototype.getTotal = function(plain) {
this.totalAmount = 0;
// Always calculate the total amount based on the cart content
for (var key in this.content) {
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined') {
var price = this.toCents(this.content[key].price.value);
this.totalAmount += (this.content[key].count * price);
}
}
this.totalAmount += this.toCents(this.tip);
return this.fromCents(this.totalAmount);
}
Cart.prototype.updateTotal = function() {
$('#js-cart-total').text(this.formatCurrency(this.getTotal()));
}
Cart.prototype.updateAmount = function() {
$('#js-cart-amount').val(this.getTotal());
}
Cart.prototype.escape = function(input) {
return ('' + input) /* Forces the conversion to string. */
.replace(/&/g, '&amp;') /* This MUST be the 1st replacement. */
.replace(/'/g, '&apos;') /* The 4 other predefined entities, required. */
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
;
}
Cart.prototype.listItems = function() {
var $table = $('#js-cart-list').find('tbody'),
self = this,
list = []
tableTemplate = '';
if (this.content.length > 0) {
// Prepare the list of items in the cart
for (var key in this.content) {
var item = this.content[key],
id = this.escape(item.id),
title = this.escape(item.title),
image = this.escape(item.image),
count = this.escape(item.count),
price = this.escape(item.price.formatted),
currencySymbol = this.escape(srvModel.currencySymbol),
step = this.escape(srvModel.step),
customTipText = this.escape(srvModel.customTipText);
tableTemplate = '<tr data-id="' + id + '">' +
(image !== null ? '<td class="align-middle pr-0" width="60"><img src="' + image + '" width="100%"></td>' : '') +
'<td class="align-middle pr-0"><b>' + title + '</b></td>' +
'<td class="align-middle pr-0" align="right"><div class="input-group">' +
' <input class="js-cart-item-count form-control form-control-sm pull-left" type="number" min="0" step="1" name="count" placeholder="Qty" value="' + count + '" data-prev="' + count + '">' +
' <div class="input-group-append"><a class="js-cart-item-remove btn btn-danger btn-sm" href="#"><i class="fa fa-remove"></i></a></div>' +
'</div></td>' +
'<td class="align-middle" align="right">' + price + '</td>' +
'</tr>';
list.push($(tableTemplate));
}
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-8 py-2">' + customTipText + '</div><div class="col-sm-4">' +
'<div class="input-group">' +
'<div class="input-group-prepend">' +
'<span class="input-group-text">' + currencySymbol + '</span>' +
'</div>' +
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" name="tip" placeholder="Amount">' +
'</div>' +
'</div></div></td></tr>';
list.push($(tableTemplate));
tableTemplate = '<tr class="bg-light h4"><td colspan="2">Total</td><td colspan="2" align="right"><span id="js-cart-total">' + this.formatCurrency(this.getTotal()) + '</span></td></tr>';
list.push($(tableTemplate));
// Add the list to DOM
$table.html(list);
// Update the cart when number of items is changed
$('.js-cart-item-count').off().on('input', function(event){
var _this = this,
id = $(this).closest('tr').data('id'),
count = parseInt($(this).val()),
prevCount = parseInt($(this).data('prev')),
increased = count > prevCount;
// User hasn't inputed any number so stop here
if (isNaN(count)) {
return false;
}
$(this).data('prev', count);
var item = self.content.filter(function(obj){
return obj.id === id
});
// Must be in the loop because user may change the count manually by more than 1
for (var i = 0; i < Math.abs(count - prevCount); i++) {
if (increased) {
self.addItem({
id: id,
title: item.title,
price: item.price,
image: typeof item.image != 'underfined' ? item.image : null
});
} else {
self.decrementItem(id);
}
}
});
// Remove item from the cart
$('.js-cart-item-remove').off().on('click', function(event){
event.preventDefault();
var id = $(this).closest('tr').data('id');
self.removeItemAll(id);
});
// Change total when tip is changed
$('.js-cart-tip').off().on('input', function(event){
self.setTip($(this).val());
self.updateTotal();
self.updateAmount();
});
} else { // No item in the cart
self.emptyList();
}
}
Cart.prototype.emptyList = function() {
var $table = $('#js-cart-list').find('tbody');
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
}
/* Get the currency symbol from an existing amount and use it with the new amount*/
Cart.prototype.formatCurrency = function(amount, example) {
var regex = /([0-9.]+)/gm;
// Get the first item's formated price
if (typeof example == 'undefined' && typeof srvModel != 'undefined') {
example = srvModel.items[0].price.formatted;
}
return example.replace(regex, amount.toFixed(2));
}
Cart.prototype.toCents = function(num) {
return num * 100;
}
Cart.prototype.fromCents = function(num) {
return num / 100;
}
Cart.prototype.saveLocalStorage = function() {
localStorage.setItem('cart', JSON.stringify(this.content));
}
Cart.prototype.loadLocalStorage = function() {
this.content = $.parseJSON(localStorage.getItem('cart')) || [];
// Get number of cart items
for (var key in this.content) {
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
this.items += this.content[key].count;
}
}
}
Cart.prototype.destroy = function() {
localStorage.removeItem('cart');
this.content = [];
this.items = 0;
this.totalAmount = 0;
}

View File

@ -6,10 +6,6 @@ html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
article,
aside,
details,
@ -333,8 +329,9 @@ body {
font-family: "Roboto", "Helvetica", sans-serif;
font-size: 1rem;
line-height: 1.5;
color: #373a3c;
background-color: #fff;
margin: 0;
background: #E4E4E4;
}
[tabindex="-1"]:focus {
@ -1296,10 +1293,6 @@ html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
article,
aside,
details,
@ -1619,14 +1612,6 @@ html {
-webkit-tap-highlight-color: transparent;
}
body {
font-family: "Roboto", "Helvetica", sans-serif;
font-size: 1rem;
line-height: 1.5;
color: #373a3c;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
@ -9084,11 +9069,12 @@ strong {
.close-icon {
flex-grow: 1;
display: flex;
display: none;
justify-content: flex-end;
padding: 13px;
transition: opacity 250ms ease;
opacity: 1;
cursor: pointer;
}
.close-icon img {
@ -10373,7 +10359,7 @@ All mobile class names should be prefixed by m- */
}
.paid:not(.paid-over) .status-icon {
padding-top: 54px;
padding-top: 45px;
}
.paid:not(.paid-over) .button-wrapper {
@ -11509,4 +11495,12 @@ low-fee-timeline {
}
#prettydropdown-DefaultLang{
min-width:200px;
}
#prettydropdown-DefaultLang ul{
width:100%;
}
#prettydropdown-DefaultLang ul li{
width:100% !important;
}

View File

@ -32,7 +32,6 @@ function changeCurrency(currency) {
}
function onDataCallback(jsonData) {
var newStatus = jsonData.status;
if (newStatus === "complete" ||
@ -123,20 +122,6 @@ $(document).ready(function () {
// initialize
onDataCallback(srvModel);
/* TAF
- Version mobile
- Réparer le décallage par timer
- Preparer les variables de l'API
- Gestion des differents evenements en fonction du status de l'invoice
- sécuriser les CDN
*/
// check if the Document expired
if (srvModel.expirationSeconds > 0) {
progressStart(srvModel.maxTimeSeconds); // Progress bar
@ -147,7 +132,19 @@ $(document).ready(function () {
hideEmailForm();
}
$(".close-action").on("click", function () {
$("invoice").fadeOut(300, function () {
window.parent.postMessage("close", "*");
});
});
window.parent.postMessage("loaded", "*");
jQuery("invoice").fadeOut(0);
jQuery("invoice").fadeIn(300);
// eof initialize
// FUNCTIONS
function hideEmailForm() {
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);

View File

@ -1,54 +0,0 @@
const locales_cs = {
nested: {
lang: 'Jazyk'
},
"Awaiting Payment...": "Očekávám platbu...",
"Pay with": "Zaplatit pomocí",
"Contact and Refund Email": "Kontaktní email",
"Contact_Body": "Prosímte poskytněte vaši emailovou adresu. Kontaktujeme vás v případě, že se objeví problému s vaší platbou.",
"Your email": "Váš email",
"Continue": "Pokračovat",
"Please enter a valid email address": "Prosíme vložte platnou emailovou adresu",
"Order Amount": "Cena objednávky",
"Network Cost": "Síťové náklady",
"Already Paid": "Již zaplaceno",
"Due": "Zbývá",
// Tabs
"Scan": "Skenovat",
"Copy": "Kopírovat",
"Conversion": "Konverze",
// Scan tab
"Open in wallet": "Otevřít v peněžence",
// Copy tab
"CompletePay_Body": "K dokončení platby, prosíme pošlete {{btcDue}} {{cryptoCode}} na adresu níže.",
"Amount": "Částka",
"Address": "Adresa",
"Copied": "Zkopírováno",
// Conversion tab
"ConversionTab_BodyTop": "Můžete zaplatit {{btcDue}} {{cryptoCode}} i pomocí altcoinů které přímo nepodporuje obchodník.",
"ConversionTab_BodyDesc": "Tato služba je poskytována třetí stranou. Prosíme mějte na paměti, že nemáme žádnou kontrolu nad tím, jak poskytovatelé budou nakládat s vašimi prostředky. Faktura bude označena jako zaplacena, pouze když jsou prostředky obdrženy v {{cryptoCode}} Blockchainu.",
"Shapeshift_Button_Text": "Zaplatit pomocí Altcoinů",
"ConversionTab_Lightning": "Pro platby Lightning Network nejsou dostupní žádní poskytovatelé konverzí.",
// Invoice expired
"Invoice expiring soon...": "Faktura brzy vyprší...",
"Invoice expired": "Faktura vypršela",
"What happened?": "Co se stalo?",
"InvoiceExpired_Body_1": "Tato faktura již vypršela. Faktura je platná pouze {{maxTimeMinutes}} minut. \
Můžete se vrátit do {{storeName}}, pokud chcete svojí objednávku založit znovu.",
"InvoiceExpired_Body_2": "Pokud jste se pokoušeli poslat platbu, nebyla zatím zaznamenána v Bitcoinové síti. Zatím jsme neobdrželi vaše prostředky.",
"InvoiceExpired_Body_3": "Pokud nebude transakce přijata Bitcoinovou sítí, vaše prostředky bude opět použitelné ve vaší peněžence. V závislosti na vaší peněžence toto může trvat 48-72 hodin.",
"Invoice ID": "ID Faktury",
"Order ID": "ID Objednávky",
"Return to StoreName": "Vrátit se na {{storeName}}",
// Invoice paid
"This invoice has been paid": "Faktura byla zaplacena",
// Invoice archived
"This invoice has been archived": "Tato faktura byla archivována",
"Archived_Body": "Prosíme kontaktujte prodejce pro informace o objednávce a případnou pomoc",
// Lightning
"BOLT 11 Invoice": "BOLT 11 Faktura",
"Node Info": "Info o uzlu",
//
"txCount": "{{count}} transakce",
"txCount_plural": "{{count}} transakcí"
};

View File

@ -1,54 +0,0 @@
const locales_de = {
nested: {
lang: 'Sprache'
},
"Awaiting Payment...": "Warten auf Zahlung...",
"Pay with": "Bezahlen mit",
"Contact and Refund Email": "Kontakt und Rückerstattungs Email",
"Contact_Body": "Bitte geben Sie unten eine E-Mail-Adresse an. Wir werden Sie unter dieser Adresse kontaktieren, falls ein Problem mit Ihrer Zahlung vorliegt.",
"Your email": "Ihre Email-Adresse",
"Continue": "Fortsetzen",
"Please enter a valid email address": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"Order Amount": "Bestellbetrag",
"Network Cost": "Netzwerkkosten",
"Already Paid": "Bereits bezahlt",
"Due": "Fällig",
// Tabs
"Scan": "Scan",
"Copy": "Kopieren",
"Conversion": "Umrechnung",
// Scan tab
"Open in wallet": "In der Wallet öffnen",
// Copy tab
"CompletePay_Body": "Um Ihre Zahlung abzuschließen, senden Sie bitte {{btcDue}} {{cryptoCode}} an die unten angegebene Adresse.",
"Amount": "Menge",
"Address": "Adresse",
"Copied": "Kopiert",
// Conversion tab
"ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit Altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.",
"ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst als bezahlt markiert, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.",
"Shapeshift_Button_Text": "Bezahlen mit Altcoins",
"ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Umrechnungsanbieter verfügbar.",
// Invoice expired
"Invoice expiring soon...": "Die Rechnung läuft bald ab...",
"Invoice expired": "Die Rechnung ist abgelaufen",
"What happened?": "Was ist passiert?",
"InvoiceExpired_Body_1": "Diese Rechnung ist abgelaufen. Eine Rechnung ist nur für {{maxTimeMinutes}} Minuten gültig. \
Sie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden möchten.",
"InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.",
"InvoiceExpired_Body_3": "Wenn die Transaktion vom Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Wallet verfügbar. Abhängig von Ihrer Wallet, kann dies 48-72 Stunden dauern.",
"Invoice ID": "Rechnungs ID",
"Order ID": "Auftrag ID",
"Return to StoreName": "Zurück zu {{storeName}}",
// Invoice paid
"This invoice has been paid": "Diese Rechnung wurde bezahlt",
// Invoice archived
"This invoice has been archived": "Diese Rechnung wurde archiviert",
"Archived_Body": "Bitte kontaktieren Sie den Shop für Bestellinformationen oder Hilfe",
// Lightning
"BOLT 11 Invoice": "BOLT 11 Rechnung",
"Node Info": "Netzwerkknoten Info",
//
"txCount": "{{count}} transaktion",
"txCount_plural": "{{count}} transaktionen"
};

View File

@ -1,55 +0,0 @@
const locales_en = {
nested: {
lang: 'Language'
},
"Awaiting Payment...": "Awaiting Payment...",
"Pay with": "Pay with",
"Contact and Refund Email": "Contact & Refund Email",
"Contact_Body": "Please provide an email address below. Well contact you at this address if there is an issue with your payment.",
"Your email": "Your email",
"Continue": "Continue",
"Please enter a valid email address": "Please enter a valid email address",
"Order Amount": "Order Amount",
"Network Cost": "Network Cost",
"Already Paid": "Already Paid",
"Due": "Due",
// Tabs
"Scan": "Scan",
"Copy": "Copy",
"Conversion": "Conversion",
// Scan tab
"Open in wallet": "Open in wallet",
// Copy tab
"CompletePay_Body": "To complete your payment, please send {{btcDue}} {{cryptoCode}} to the address below.",
"Amount": "Amount",
"Address": "Address",
"Copied": "Copied",
// Conversion tab
"ConversionTab_BodyTop": "You can pay {{btcDue}} {{cryptoCode}} using altcoins other than the ones merchant directly supports.",
"ConversionTab_BodyDesc": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_Lightning": "No conversion providers available for Lightning Network payments.",
// Invoice expired
"Invoice expiring soon...": "Invoice expiring soon...",
"Invoice expired": "Invoice expired",
"What happened?": "What happened?",
"InvoiceExpired_Body_1": "This invoice has expired. An invoice is only valid for {{maxTimeMinutes}} minutes. \
You can return to {{storeName}} if you would like to submit your payment again.",
"InvoiceExpired_Body_2": "If you tried to send a payment, it has not yet been accepted by the network. We have not yet received your funds.",
"InvoiceExpired_Body_3": "If the transaction is not accepted by the network, the funds will be spendable again in your wallet. Depending on your wallet, this may take 48-72 hours.",
"Invoice ID": "Invoice ID",
"Order ID": "Order ID",
"Return to StoreName": "Return to {{storeName}}",
// Invoice paid
"This invoice has been paid": "This invoice has been paid",
// Invoice archived
"This invoice has been archived": "This invoice has been archived",
"Archived_Body": "Please contact the store for order information or assistance",
// Lightning
"BOLT 11 Invoice": "BOLT 11 Invoice",
"Node Info": "Node Info",
//
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"
};

View File

@ -1,53 +0,0 @@
const locales_es = {
nested: {
lang: 'Lenguaje'
},
"Awaiting Payment...": "En espera de pago...",
"Pay with": "Pagar con",
"Contact and Refund Email": "Contacto y correo electrónico de reembolso",
"Contact_Body": "Por favor indica una dirección de correo electrónico a continuación. Nos pondremos en contacto contigo en esta dirección si hay algún problema con tu pago.",
"Your email": "Tu correo electrónico",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor ingresa un correo electrónico válido",
"Order Amount": "Total del pedido",
"Network Cost": "Costo de la red",
"Already Paid": "Ya has pagado",
"Due": "Aún debes",
// Tabs
"Scan": "Escanear",
"Copy": "Copiar",
"Conversion": "Conversión",
// Scan tab
"Open in wallet": "Abrir en la billetera",
// Copy tab
"CompletePay_Body": "Para completar tu pago, envía {{btcDue}} {{cryptoCode}} a la siguiente dirección:",
"Amount": "Cantidad",
"Address": "Dirección",
"Copied": "Copiado",
// Conversion tab
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando Altcoins que este comercio no soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros envían los fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
"Shapeshift_Button_Text": "Pagar con Altcoins",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La factura expira pronto...",
"Invoice expired": "La factura expiró",
"What happened?": "¿Qué sucedió?",
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. Puedes regresar a {{storeName}} si deseas volver a enviar tu pago.",
"InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.",
"InvoiceExpired_Body_3": "Si la transacción no es aceptada por la red de Bitcoin, los fondos se podrán gastar nuevamente en tu billetera. Dependiendo de tu billetera, esto puede tomar 48-72 horas.",
"Invoice ID": "ID de la factura",
"Order ID": "ID del pedido",
"Return to StoreName": "Regresar a {{storeName}}",
// Invoice paid
"This invoice has been paid": "Esta factura ha sido pagada",
// Invoice archived
"This invoice has been archived": "Esta factura ha sido archivada",
"Archived_Body": "Por favor, comunícate con la tienda para obtener información de tu pedido o asistencia",
// Lightning
"BOLT 11 Invoice": "Factura BOLT 11",
"Node Info": "Información del nodo",
//
"txCount": "{{count}} transacción",
"txCount_plural": "{{count}} transacciones"
};

View File

@ -1,54 +0,0 @@
const locales_fr = {
nested: {
lang: 'Langue'
},
"Awaiting Payment...": "En attente du paiement...",
"Pay with": "Payer avec",
"Contact and Refund Email": "Adresse de contact et de remboursement",
"Contact_Body": "Merci de renseigner l'adresse email ci-dessous. Nous vous contacterons à cette adresse si il y a un problème avec votre paiement.",
"Your email": "Votre email",
"Continue": "Continuer",
"Please enter a valid email address": "Merci de saisir une addrese email valide",
"Order Amount": "Montant de la commande",
"Network Cost": "Coût réseau",
"Already Paid": "Déjà payé",
"Due": "Reste à payer",
// Tabs
"Scan": "Scanner",
"Copy": "Copier",
"Conversion": "Convertir",
// Scan tab
"Open in wallet": "Ouvrir le portefeuille",
// Copy tab
"CompletePay_Body": "Pour terminer le paiement, merci d'envoyer {{btcDue}} {{cryptoCode}} à l'adresse ci-dessous.",
"Amount": "Montant",
"Address": "Adresse",
"Copied": "Copié",
// Conversion tab
"ConversionTab_BodyTop": "Vous pouvez payer {{btcDue}} {{cryptoCode}} en utilisant d'autres crypto-monnaies alternatives non supportées directement par le marchand.",
"ConversionTab_BodyDesc": "Ce service est fourni par un tiers. Nous n'avons aucun contrôle sur la façon dont seront traités vos fonds. La facture sera considérée payée seulement quand les fonds seront reçus sur la blockchain {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Payer en altcoins",
"ConversionTab_Lightning": "Le service de conversion n'est pas disponible pour les paiements sur le Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La facture va bientôt expirer...",
"Invoice expired": "Facture expirée",
"What happened?": "Que s'est-il passé ?",
"InvoiceExpired_Body_1": "La facture a expiré. Une facture est valide seulement {{maxTimeMinutes}} minutes. \
Si vous voulez soumettre à nouveau votre paiement, vous pouvez revenir sur {{storeName}} .",
"InvoiceExpired_Body_2": "Si vous avez envoyé un paiement, ce dernier n'a pas encore été inscrit dans la blockchain. Nous n'avons pas reçu vos fonds.",
"InvoiceExpired_Body_3": "Si votre transaction n'est pas inscrite dans la blockchain, vos fonds reviendront dans votre portefeuille. Selon votre portefeuille, cela peut prendre entre 48 et 72 heures.",
"Invoice ID": "Numéro de facture",
"Order ID": "Numéro de commande",
"Return to StoreName": "Retourner sur {{storeName}}",
// Invoice paid
"This invoice has been paid": "Cette facture a été payée",
// Invoice archived
"This invoice has been archived": "Cette facture a été archivée",
"Archived_Body": "Merci de contacter le marchand pour obtenir de l'aide ou des informations sur cette commande.",
// Lightning
"BOLT 11 Invoice": "Facture BOLT 11",
"Node Info": "Informations sur le nœud",
//
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"
};

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