Compare commits

...

47 Commits

Author SHA1 Message Date
dafd958f69 bump 2018-10-28 23:07:58 +09:00
f51af6c61c fix issue with changelly rates and cover with UTs (#368) 2018-10-28 23:07:36 +09:00
254db22063 Change test trait name 2018-10-28 22:51:02 +09:00
8be4256278 Fix unreliable tests 2018-10-28 22:46:03 +09:00
8e8669d63f Warning as errors 2018-10-28 22:15:32 +09:00
4625ff92f1 Run unreliable tests, attempt to make them a bit more reliable 2018-10-28 22:10:37 +09:00
6aa84326af Make sure tests run sequentially 2018-10-28 21:46:12 +09:00
9a384d81fe Run only dev time containers 2018-10-28 21:25:42 +09:00
0cbe36c048 Run reliable tests, remove the docker build 2018-10-28 21:19:18 +09:00
7f16aa8c7e Run only fast tests on CI 2018-10-28 20:59:59 +09:00
872f8a6229 Add circleCI badge 2018-10-28 20:28:16 +09:00
9b261daa6d Add circleci file 2018-10-28 20:06:04 +09:00
c46c15c258 Fix changelly tests 2018-10-28 01:10:07 +09:00
a8ba1ed1ed Removing Kukks changelly credential from the source code 2018-10-28 01:02:24 +09:00
ff4056d4f3 bump 2018-10-27 23:32:04 +09:00
ae152c3ffa bump NBXplorer 2018-10-27 23:30:57 +09:00
e2ff33d7db Document how to test mysql 2018-10-27 23:20:50 +09:00
ce94c05fd3 MySQL Support (#345)
* MySQL EF support added using Pomelo MySQL provider.

* MySQL EF support added using Pomelo MySQL provider.
2018-10-27 23:15:21 +09:00
9cde4dc7e2 Restart containers if crash 2018-10-27 23:14:26 +09:00
ca571cd756 Add authorization on WalletRescan 2018-10-27 22:52:09 +09:00
e5eb0c79c0 Exposing LND Rest, providing info in Server/Services (#363)
* Displaying LND Rest connection info in Services

* Code cleanup

* Tweaking UI

* Fix typo
2018-10-27 22:49:39 +09:00
43bd6587d3 re-enable changelly 2018-10-27 22:41:37 +09:00
3bb059ab74 refactor changelly & improve tests (#366) 2018-10-27 22:41:07 +09:00
4c963d6edf bump 2018-10-26 23:10:45 +09:00
396bc7f7b4 Commenting Changelly 2018-10-26 23:10:29 +09:00
2896a9b26f Add ScanUTXOSet support 2018-10-26 23:07:39 +09:00
9267a45449 Remove useless test 2018-10-26 19:07:19 +09:00
c430d470c4 Fix warnings and bump nbxplorer 2018-10-26 19:06:06 +09:00
3921a3ca22 Fix warnings, update libs 2018-10-26 18:36:58 +09:00
1ff0a98d30 Adding Ukrainian Translation (#352) 2018-10-24 15:18:31 +09:00
f0efd52cb7 Adding Kazakh Language (#350) 2018-10-24 15:17:09 +09:00
bb8fa88688 Adding Vietnamese (#351) 2018-10-24 15:16:30 +09:00
4b976c13c1 Changelly v2 (#343)
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes

* WIP: getting rid of changelly dependency

* client caching, compiling code, cleaner code

* Cleaner changelly

* fiat!

* updat i18n, css and error styler

* default keys

* pr changes part 1

* part2

* fix tests

* fix loader alignment and retry button responsiveness

* final pr change
2018-10-24 14:52:19 +09:00
f68d4efcdd update to 0.17.0 2018-10-19 19:05:12 +09:00
fea247b218 Fixing broken link in Wallets/WalletSend.cshtml (#342)
Removing the earlier Yubico link, since it's broken and the article no longer exists.
Furthermore, I tested this integration with other U2F supporting browsers (Firefox Nightly, Firefox) and it only works in Google Chrome, so I suggest we only suggest what works, and for now, that's Chrome only.
2018-10-18 12:43:41 +09:00
f419c56a3c Revert "Changelly Support (#267)"
This reverts commit a5fca7a1c43c4d23ad1a825253fa1fe3ab26677c.
2018-10-18 12:27:46 +09:00
a5fca7a1c4 Changelly Support (#267)
* Disable shapeshift and use changelly

* UI to manage changelly payment method

* wip on changelly api

* Add in Vue component for changelly and remove target currency from payment method

* add changelly merhcant id

* Small fixes to get Conversion to load

* wip fixing the component

* fix merge conflict

* fixes to UI

* remove debug, fix fee calc and move changelly to own partials

* Update ChangellyController.cs

* move original vue setup back to checkout

* Update core.js

* Extracting Changelly component to js file

* Proposal for loading spinner

* remove zone

* imrpove changelly ui

* add in changelly config checks

* try new method to calculate amount + remove to currency from list

* abstract changelly lofgic to provider and reduce dependency on js component

* Add UTs for Changelly

* refactor changelly backend

* fix failing UT

* add shitcoin tax

* pr changes

* pr changes
2018-10-18 12:13:39 +09:00
e18d0b5d51 Updating Yaml (#336) 2018-10-17 13:30:43 +09:00
9952cdca7f bump 2018-10-17 12:06:37 +09:00
6278145374 Removing old QR update code (#337) 2018-10-17 11:55:49 +09:00
84018a5caa Bugfixing race condition for QR code switch (#335)
Ref: #334
2018-10-17 11:49:30 +09:00
d7785fe2d2 Added Serilog file logger for debug file support. (#323) 2018-10-16 00:37:42 +09:00
e1751c4d91 [Fix] Querying rate with authenticated request should be successfull 2018-10-15 18:11:20 +09:00
913da79ff4 Remove dups lang 2018-10-14 21:35:21 +09:00
a4fbb2de7e creating italian localization file (#310) 2018-10-14 21:34:15 +09:00
b5601ed5e6 fix typo (#304) 2018-10-14 21:28:09 +09:00
42c4f15f22 fixed typos (#331) 2018-10-14 21:26:47 +09:00
73 changed files with 2198 additions and 287 deletions

29
.circleci/config.yml Normal file
View File

@ -0,0 +1,29 @@
version: 2
jobs:
build:
machine:
docker_layer_caching: true
steps:
- checkout
test:
machine: true
steps:
- checkout
- run:
command: |
lsb_release -a
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1
dotnet build /p:TreatWarningsAsErrors=true
cd BTCPayServer.Tests
dotnet test --filter Fast=Fast
docker-compose up -d dev
dotnet test --filter Integration=Integration
workflows:
version: 2
build_and_test:
jobs:
- test

View File

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<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">
<PrivateAssets>all</PrivateAssets>
@ -17,10 +17,6 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<None Update=".dockerignore">
<DependentUpon>Dockerfile</DependentUpon>
@ -28,6 +24,12 @@
<None Update="docker-compose.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -35,6 +36,12 @@ using Xunit;
namespace BTCPayServer.Tests
{
public enum TestDatabases
{
Postgres,
MySQL,
}
public class BTCPayServerTester : IDisposable
{
private string _Directory;
@ -57,6 +64,11 @@ namespace BTCPayServer.Tests
set;
}
public string MySQL
{
get; set;
}
public string Postgres
{
get; set;
@ -68,6 +80,10 @@ namespace BTCPayServer.Tests
get; set;
}
public TestDatabases TestDatabase
{
get; set;
}
public bool MockRates { get; set; } = true;
@ -94,7 +110,9 @@ namespace BTCPayServer.Tests
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
config.AppendLine($"mysql=" + MySQL);
else if (!String.IsNullOrEmpty(Postgres))
config.AppendLine($"postgres=" + Postgres);
var confPath = Path.Combine(chainDirectory, "settings.config");
File.WriteAllText(confPath, config.ToString());
@ -198,7 +216,7 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>();
}
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1", Port);
@ -206,7 +224,11 @@ namespace BTCPayServer.Tests
context.Request.Protocol = "http";
if (userId != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (additionalClaims != null)
claims.AddRange(additionalClaims);
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
}
if (storeId != null)
{

View File

@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class ChangellyTests
{
public ChangellyTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanSetChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.StoreData.GetStoreBlob();
Assert.Null(storeBlob.ChangellySettings);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.StoreData.GetStoreBlob();
Assert.NotNull(storeBlob.ChangellySettings);
Assert.NotNull(storeBlob.ChangellySettings);
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
updateModel.ApiSecret);
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
updateModel.ChangellyMerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanToggleChangellyPaymentMethod()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateChangellySettingsViewModel()
{
ApiSecret = "secret",
ApiKey = "key",
ApiUrl = "http://gozo.com",
ChangellyMerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var changellyController =
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
changellyController.IsTest = true;
//test non existing payment method
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
var updateModel = CreateDefaultChangellyParams(false);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//set payment method but disabled
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsType<BitpayErrorModel>(Assert
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
updateModel.Enabled = true;
//test with enabled method
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
Assert.IsNotType<BitpayErrorModel>(Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value);
}
}
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
{
return new UpdateChangellySettingsViewModel()
{
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
ChangellyMerchantId = "804298eb5753",
Enabled = enabled
};
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanGetCurrencyListFromChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
//save changelly settings
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
//confirm saved
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
var result = Assert
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
.Value as IEnumerable<CurrencyFull>;
Assert.True(result.Any());
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanCalculateToAmountForChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var updateModel = CreateDefaultChangellyParams(true);
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
.Value);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeBaseAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCorrectAmount()
{
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

View File

@ -35,6 +35,8 @@ 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
### Using the test bitcoin-cli

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Tests
public class RateRulesTest
{
[Fact]
[Trait("Fast", "Fast")]
public void SecondDuplicatedRuleIsIgnored()
{
StringBuilder builder = new StringBuilder();
@ -24,6 +25,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseRateRules()
{
// Check happy path

View File

@ -60,7 +60,9 @@ namespace BTCPayServer.Tests
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
IntegratedLightning = MerchantCharge.Client.Uri
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
@ -82,7 +84,7 @@ namespace BTCPayServer.Tests
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public Task EnsureConnectedToDestinations()
public Task EnsureChannelsSetup()
{
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
}

View File

@ -41,6 +41,9 @@ using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
using BTCPayServer.Models.WalletViewModels;
using System.Security.Claims;
using BTCPayServer.Security;
namespace BTCPayServer.Tests
{
@ -53,6 +56,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanHandleUriValidation()
{
var attribute = new UriAttribute();
@ -74,6 +78,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
@ -131,6 +136,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
@ -255,6 +261,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanAcceptInvoiceWithTolerance()
{
var entity = new InvoiceEntity();
@ -282,6 +289,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanAcceptInvoiceWithTolerance2()
{
using (var tester = ServerTester.Create())
@ -322,6 +330,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void RoundupCurrenciesCorrectly()
{
foreach (var test in new[]
@ -338,6 +347,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanPayUsingBIP70()
{
using (var tester = ServerTester.Create())
@ -388,11 +398,13 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanSetLightningServer()
[Trait("Integration", "Integration")]
public async Task CanSetLightningServer()
{
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
var storeController = user.GetController<StoresController>();
@ -424,18 +436,21 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCLightning()
{
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCharge()
{
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentLnd()
{
await ProcessLightningPayment(LightningConnectionType.LndREST);
@ -449,13 +464,12 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
await tester.EnsureConnectedToDestinations();
await CanSendLightningPaymentCore(tester, user);
await Task.WhenAll(Enumerable.Range(0, 5)
@ -488,6 +502,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUseServerInitiatedPairingCode()
{
using (var tester = ServerTester.Create())
@ -513,6 +528,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanSendIPN()
{
using (var callbackServer = new CustomServer())
@ -549,6 +565,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CantPairTwiceWithSamePubkey()
{
using (var tester = ServerTester.Create())
@ -570,6 +587,74 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanRescanWallet()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
var btcDerivationScheme = acc.DerivationScheme;
acc.RegisterDerivationScheme("LTC");
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.False(rescan.IsSupportedByCurrency);
Assert.False(rescan.IsServerAdmin);
walletId = new WalletId(acc.StoreId, "BTC");
var serverAdminClaim = new[] { new Claim(Policies.CanModifyServerSettings.Key, "true") };
walletController = tester.PayTester.GetController<WalletsController>(acc.UserId, additionalClaims: serverAdminClaim);
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.True(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSupportedByCurrency);
Assert.True(rescan.IsServerAdmin);
rescan.GapLimit = 100;
// Sending a coin
var txId = tester.ExplorerNode.SendToAddress(btcDerivationScheme.Derive(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
tester.ExplorerNode.Generate(1);
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
Assert.Empty(transactions.Transactions);
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
while(true)
{
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
if(rescan.Progress == null && rescan.LastSuccess != null)
{
if (rescan.LastSuccess.Found == 0)
continue;
// Scan over
break;
}
else
{
Assert.Null(rescan.TimeOfScan);
Assert.NotNull(rescan.RemainingTime);
Assert.NotNull(rescan.Progress);
Thread.Sleep(100);
}
}
Assert.Null(rescan.PreviousError);
Assert.NotNull(rescan.TimeOfScan);
Assert.Equal(1, rescan.LastSuccess.Found);
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
var tx = Assert.Single(transactions.Transactions);
Assert.Equal(tx.Id, txId.ToString());
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanListInvoices()
{
using (var tester = ServerTester.Create())
@ -611,6 +696,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanGetRates()
{
using (var tester = ServerTester.Create())
@ -641,6 +727,13 @@ namespace BTCPayServer.Tests
Assert.NotNull(GetCurrencyPairRateResult);
Assert.NotNull(GetCurrencyPairRateResult.Data);
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
// Should be OK because the request is signed, so we can know the store
var rates = acc.BitPay.GetRates();
HttpClient client = new HttpClient();
// Unauthentified requests should also be ok
var response = client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}").GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
}
}
@ -651,6 +744,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanRBFPayment()
{
using (var tester = ServerTester.Create())
@ -707,6 +801,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseFilter()
{
var filter = "storeid:abc status:abed blabhbalh ";
@ -728,6 +823,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseFingerprint()
{
Assert.True(SSH.SSHFingerprint.TryParse("4e343c6fc6cfbf9339c02d06a151e1dd", out var unused));
@ -744,6 +840,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void TestAccessBitpayAPI()
{
using (var tester = ServerTester.Create())
@ -810,6 +907,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUseExchangeSpecificRate()
{
using (var tester = ServerTester.Create())
@ -852,6 +950,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
@ -896,6 +995,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
@ -960,6 +1060,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanModifyRates()
{
using (var tester = ServerTester.Create())
@ -1020,6 +1121,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanPayWithTwoCurrencies()
{
using (var tester = ServerTester.Create())
@ -1131,6 +1233,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseCurrencyValue()
{
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
@ -1151,6 +1254,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet);
@ -1191,6 +1295,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanDisablePaymentMethods()
{
using (var tester = ServerTester.Create())
@ -1249,11 +1354,13 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanSetPaymentMethodLimits()
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
@ -1292,6 +1399,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanUsePoSApp()
{
using (var tester = ServerTester.Create())
@ -1336,6 +1444,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteApps()
{
using (var tester = ServerTester.Create())
@ -1372,6 +1481,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using (var tester = ServerTester.Create())
@ -1528,6 +1638,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider();
@ -1541,6 +1652,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanQueryDirectProviders()
{
var factory = CreateBTCPayRateFactory();
@ -1571,6 +1683,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
public void CanGetRateCryptoCurrenciesByDefault()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
@ -1590,7 +1703,7 @@ namespace BTCPayServer.Tests
}
}
private static RateProviderFactory CreateBTCPayRateFactory()
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
@ -1624,6 +1737,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
{
var spy = new SpyRateProvider();

View File

@ -1,48 +0,0 @@
using System;
using NBitcoin;
using Xunit;
namespace BTCPayServer.Tests
{
// Helper class for testing functionality and generating data needed during coding/debuging
public class UnitTestPeusa
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]
public void GeneratePubkey()
{
var network = Network.RegTest;
ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(network));
ExtPubKey masterPubKey = masterKey.Neuter();
ExtPubKey pubkey = masterPubKey.Derive(0);
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
}
}
}

View File

@ -14,7 +14,9 @@ services:
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
@ -34,14 +36,16 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.16.3
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
regtest=1
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lightningd
- merchant_lightningd
- lightning-charged
@ -49,21 +53,24 @@ services:
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.16.3
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
regtest=1
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- mysql
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.3.3
image: nicolasdorier/nbxplorer:1.1.0.4
restart: unless-stopped
ports:
- "32838:32838"
expose:
@ -87,13 +94,13 @@ services:
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.16.3
image: nicolasdorier/docker-bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
@ -111,7 +118,8 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6.1-dev
image: nicolasdorier/clightning:v0.6.1-1-dev
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -136,6 +144,7 @@ services:
lightning-charged:
image: shesek/lightning-charge:0.4.3
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -154,7 +163,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6.1-dev
image: nicolasdorier/clightning:v0.6.1-1-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -199,9 +208,19 @@ services:
- "39372:5432"
expose:
- "5432"
mysql:
image: mysql:8.0.12
expose:
- "3306"
ports:
- "33036:3306"
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:0.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -228,6 +247,7 @@ services:
customer_lnd:
image: btcpayserver/lnd:0.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"

View File

@ -0,0 +1,3 @@
{
"parallelizeTestCollections": false
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.111</Version>
<Version>1.0.3.2</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -44,15 +44,19 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<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.48" />
<PackageReference Include="NBitcoin" Version="4.1.1.66" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.3" />
<PackageReference Include="NBXplorer.Client" Version="1.0.3.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<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" />
@ -60,7 +64,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
<ItemGroup>
@ -124,6 +128,9 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndRestServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -136,7 +143,7 @@
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">
<Content Update="Views\Server\LndGrpcServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">
@ -148,6 +155,9 @@
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@ -15,6 +15,7 @@ using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
namespace BTCPayServer.Configuration
{
@ -78,6 +79,7 @@ namespace BTCPayServer.Configuration
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
{
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if (lightning.Length != 0)
@ -99,25 +101,31 @@ namespace BTCPayServer.Configuration
}
}
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.external.lnd.grpc", string.Empty);
var lightning = conf.GetOrDefault<string>(code, string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.lnd.grpc, " + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalLNDGRPC(connectionString));
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
@ -127,12 +135,12 @@ namespace BTCPayServer.Configuration
int waitTime = 0;
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
{
if(waitTime++ < 5)
if (waitTime++ < 5)
System.Threading.Thread.Sleep(1000);
else
throw new ConfigException($"sshkeyfile does not exist");
}
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
@ -224,6 +232,11 @@ namespace BTCPayServer.Configuration
get;
set;
}
public string MySQLConnectionString
{
get;
set;
}
public Uri ExternalUrl
{
get;
@ -250,29 +263,4 @@ namespace BTCPayServer.Configuration
return builder.ToString();
}
}
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
public class ExternalLNDGRPC : ExternalService
{
public ExternalLNDGRPC(LightningConnectionString connectionString)
{
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; set; }
}
}

View File

@ -31,6 +31,7 @@ namespace BTCPayServer.Configuration
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
@ -39,6 +40,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
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);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -107,6 +109,7 @@ namespace BTCPayServer.Configuration
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public abstract class ExternalLnd : ExternalService
{
public ExternalLnd(LightningConnectionString connectionString, LndTypes type)
{
ConnectionString = connectionString;
Type = type;
}
public LndTypes Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public enum LndTypes
{
gRPC, Rest
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
}

View File

@ -0,0 +1,119 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/{storeId}")]
public class ChangellyController : Controller
{
private readonly ChangellyClientProvider _changellyClientProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly RateFetcher _RateProviderFactory;
public ChangellyController(ChangellyClientProvider changellyClientProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
RateFetcher rateProviderFactory)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_changellyClientProvider = changellyClientProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[HttpGet]
[Route("currencies")]
public async Task<IActionResult> GetCurrencyList(string storeId)
{
try
{
var client = await TryGetChangellyClient(storeId);
return Ok(await client.GetCurrenciesFull());
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
{
try
{
var client = await TryGetChangellyClient(storeId);
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
}
var callCounter = 0;
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
while (true)
{
if (callCounter > 10)
{
BadRequest();
}
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
callCounter++;
if (computedAmount < toCurrencyAmount)
{
currentAmount =
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
toCurrencyAmount);
}
else
{
return Ok(currentAmount);
}
}
}
catch (Exception e)
{
return BadRequest(new BitpayErrorModel()
{
Error = e.Message
});
}
}
private async Task<Changelly> TryGetChangellyClient(string storeId)
{
var store = IsTest? null: HttpContext.GetStoreData();
storeId = storeId ?? store?.Id;
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
}
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
decimal toCurrencyAmount)
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);
}
public bool IsTest { get; set; } = false;
}
}

View File

@ -10,6 +10,7 @@ using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
@ -213,7 +214,6 @@ namespace BTCPayServer.Controllers
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
@ -245,6 +245,18 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
var changellyAmountDue = changelly != null
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
(1m + (changelly.AmountMarkupPercentage / 100m)))
: (decimal?)null;
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
@ -284,7 +296,10 @@ namespace BTCPayServer.Controllers
Status = invoice.Status,
NetworkFee = paymentMethodDetails.GetTxFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
AllowCoinConversion = storeBlob.AllowCoinConversion,
ChangellyEnabled = changelly != null,
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv => new PaymentModel.AvailableCrypto()

View File

@ -10,23 +10,32 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
public class RateController : Controller
{
RateFetcher _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public TokenRepository TokenRepository { get; }
public RateController(
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
TokenRepository tokenRepository,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
TokenRepository = tokenRepository;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
@ -36,7 +45,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var store = this.HttpContext.GetStoreData();
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
@ -66,7 +75,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -79,7 +88,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -87,11 +96,29 @@ namespace BTCPayServer.Controllers
return Json(new DataWrapper<Rate[]>(rates));
}
private async Task<string> GetStoreId(string storeId)
{
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
return storeId;
if(storeId == null)
{
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
}
if (storeId == null)
return null;
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return null;
this.HttpContext.SetStoreData(store);
return storeId;
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
storeId = await GetStoreId(storeId);
if (storeId == null)
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });

View File

@ -25,6 +25,7 @@ using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
namespace BTCPayServer.Controllers
{
@ -277,7 +278,7 @@ namespace BTCPayServer.Controllers
else
{
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
if(!e.CanTrust)
if (!e.CanTrust)
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
@ -421,17 +422,15 @@ namespace BTCPayServer.Controllers
var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "gRPC",
Index = i++,
});
}
Crypto = cryptoCode,
Type = grpcService.Type,
Index = i++,
});
}
}
result.HasSSH = _Options.SSHSettings != null;
@ -439,17 +438,17 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLNDConnectionString(cryptoCode, index);
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LNDGRPCServicesViewModel();
var model = new LndGrpcServicesViewModel();
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
@ -493,9 +492,9 @@ namespace BTCPayServer.Controllers
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
[HttpPost]
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
public IActionResult LndGrpcServicesPost(string cryptoCode, int index)
{
var external = GetExternalLNDConnectionString(cryptoCode, index);
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
LightningConfigurations confs = new LightningConfigurations();
@ -513,12 +512,12 @@ namespace BTCPayServer.Controllers
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
@ -531,13 +530,35 @@ namespace BTCPayServer.Controllers
}
catch
{
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
return connectionString;
}
[Route("server/services/lnd-rest/{cryptoCode}/{index}")]
public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndRestServicesViewModel();
model.BaseApiUrl = external.BaseUri.ToString();
if (external.CertificateThumbprint != null)
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
if (external.Macaroon != null)
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
return View(model);
}
[Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false)
{

View File

@ -0,0 +1,96 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/changelly")]
public IActionResult UpdateChangellySettings(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
{
var existing = store.GetStoreBlob().ChangellySettings;
if (existing == null) return;
vm.ApiKey = existing.ApiKey;
vm.ApiSecret = existing.ApiSecret;
vm.ApiUrl = existing.ApiUrl;
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
vm.Enabled = existing.Enabled;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
vm.ShowFiat = existing.ShowFiat;
}
[HttpPost]
[Route("{storeId}/changelly")]
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
var changellySettings = new ChangellySettings()
{
ApiKey = vm.ApiKey,
ApiSecret = vm.ApiSecret,
ApiUrl = vm.ApiUrl,
ChangellyMerchantId = vm.ChangellyMerchantId,
Enabled = vm.Enabled,
AmountMarkupPercentage = vm.AmountMarkupPercentage,
ShowFiat = vm.ShowFiat
};
switch (command)
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.ChangellySettings = changellySettings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Changelly settings modified";
_changellyClientProvider.InvalidateClient(storeId);
return RedirectToAction(nameof(UpdateStore), new {
storeId});
case "test":
try
{
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";
return View(vm);
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
default:
return View(vm);
}
}
}
}

View File

@ -114,7 +114,7 @@ namespace BTCPayServer.Controllers
}
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
return View(vm);
}
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
@ -9,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -48,16 +50,19 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
ChangellyClientProvider changellyClientProvider,
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
{
_RateFactory = rateFactory;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_changellyClientProvider = changellyClientProvider;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
_httpClientFactory = httpClientFactory;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_FeeRateProvider = feeRateProvider;
@ -77,7 +82,9 @@ namespace BTCPayServer.Controllers
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
private readonly ChangellyClientProvider _changellyClientProvider;
IHostingEnvironment _Env;
private IHttpClientFactory _httpClientFactory;
[TempData]
public string StatusMessage
@ -318,7 +325,6 @@ namespace BTCPayServer.Controllers
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
@ -362,7 +368,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
@ -447,6 +452,15 @@ namespace BTCPayServer.Controllers
Enabled = !excludeFilters.Match(paymentId)
});
}
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
Enabled = changellyEnabled,
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
}
[HttpPost]

View File

@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
@ -184,6 +185,74 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
[Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
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 vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched();
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)
{
vm.PreviousError = scanProgress.Error;
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
{
if (scanProgress.Progress == null)
{
vm.Progress = 0;
}
else
{
vm.Progress = scanProgress.Progress.OverallProgress;
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
}
}
if (scanProgress.Status == ScanUTXOStatus.Complete)
{
vm.LastSuccess = scanProgress.Progress;
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
}
}
return View(vm);
}
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel 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 explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
}
return RedirectToAction();
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
@ -367,9 +436,8 @@ namespace BTCPayServer.Controllers
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
@ -392,7 +460,6 @@ namespace BTCPayServer.Controllers
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();

View File

@ -17,7 +17,8 @@ namespace BTCPayServer.Data
public enum DatabaseType
{
Sqlite,
Postgres
Postgres,
MySQL,
}
public class ApplicationDbContextFactory
{
@ -95,6 +96,8 @@ namespace BTCPayServer.Data
builder
.UseNpgsql(_ConnectionString)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)

View File

@ -17,6 +17,7 @@ using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Security;
using BTCPayServer.Rating;
@ -261,11 +262,6 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; }
@ -307,6 +303,8 @@ namespace BTCPayServer.Data
public string RateScript { get; set; }
public bool AnyoneCanInvoice { get; set; }
public ChangellySettings ChangellySettings { get; set; }
string _LightningDescriptionTemplate;

View File

@ -47,7 +47,11 @@ namespace BTCPayServer.HostedServices
summary.Status != null &&
summary.Status.IsFullySynched;
}
public NBXplorerSummary Get(string cryptoCode)
{
_Summaries.TryGetValue(cryptoCode, out var summary);
return summary;
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;

View File

@ -38,10 +38,12 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
namespace BTCPayServer.Hosting
{
@ -75,17 +77,23 @@ namespace BTCPayServer.Hosting
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
if (!String.IsNullOrEmpty(opts.PostgresConnectionString))
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
else if(!String.IsNullOrEmpty(opts.MySQLConnectionString))
{
Logs.Configuration.LogInformation($"MySQL DB used ({opts.MySQLConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString);
}
else
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
return dbContext;
});
@ -125,6 +133,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<ChangellyClientProvider>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
@ -191,7 +201,7 @@ namespace BTCPayServer.Hosting
static void Retry(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(1000);
while (true)
{
try
@ -199,7 +209,9 @@ namespace BTCPayServer.Hosting
act();
return;
}
catch when(!cts.IsCancellationRequested)
// Starting up
catch (PostgresException ex) when (ex.SqlState == "57P03") { Thread.Sleep(1000); }
catch when (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
}

View File

@ -55,7 +55,10 @@ namespace BTCPayServer.Models.InvoicingModels
public string PaymentMethodName { get; set; }
public string CryptoImage { get; set; }
public bool AllowCoinConversion { get; set; }
public bool ChangellyEnabled { get; set; }
public string StoreId { get; set; }
public string PeerInfo { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal? ChangellyAmountDue { get; set; }
}
}

View File

@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LNDGRPCServicesViewModel
public class LndGrpcServicesViewModel
{
public string Host { get; set; }
public bool SSL { get; set; }

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LndRestServicesViewModel
{
public string BaseApiUrl { get; set; }
public string Macaroon { get; set; }
public string CertificateThumbprint { get; set; }
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration.External;
namespace BTCPayServer.Models.ServerViewModels
{
@ -10,7 +11,7 @@ namespace BTCPayServer.Models.ServerViewModels
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public string Type { get; set; }
public LndTypes Type { get; set; }
public int Index { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();

View File

@ -23,11 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
public bool AllowCoinConversion
{
get; set;
}
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }

View File

@ -21,7 +21,13 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletId WalletId { get; set; }
public bool Enabled { get; set; }
}
public class ThirdPartyPaymentMethod
{
public string Provider { get; set; }
public bool Enabled { get; set; }
public string Action { get; set; }
}
public StoreViewModel()
{
@ -52,6 +58,9 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
public List<ThirdPartyPaymentMethod> ThirdPartyPaymentMethods { get; set; } =
new List<ThirdPartyPaymentMethod>();
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class UpdateChangellySettingsViewModel
{
[Required] public string ApiKey { get; set; }
[Required] public string ApiSecret { get; set; }
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
[Display(Name = "Optional, Changelly Merchant Id")]
public string ChangellyMerchantId { get; set; }
[Display(Name = "Show Fiat Currencies as option in conversion")]
public bool ShowFiat { get; set; } = true;
[Required]
[Range(0, 100)]
[Display(Name =
"Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")]
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
public bool Enabled { get; set; }
public string StatusMessage { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using NBXplorer.Models;
namespace BTCPayServer.Models.WalletViewModels
{
public class RescanWalletModel
{
public bool IsServerAdmin { get; set; }
public bool IsSupportedByCurrency { get; set; }
public bool IsFullySync { get; set; }
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync;
[Range(1000, 10_000)]
public int BatchSize { get; set; } = 3000;
[Range(0, 10_000_000)]
public int StartingIndex { get; set; } = 0;
[Range(100, 100000)]
public int GapLimit { get; set; } = 10000;
public int? Progress { get; set; }
public string PreviousError { get; set; }
public ScanUTXOProgress LastSuccess { get; internal set; }
public string TimeOfScan { get; internal set; }
public string RemainingTime { get; internal set; }
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SshNet.Security.Cryptography;
namespace BTCPayServer.Payments.Changelly
{
public class Changelly
{
private readonly string _apisecret;
private readonly bool _showFiat;
private readonly HttpClient _httpClient;
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
{
_apisecret = apiSecret;
_showFiat = showFiat;
_httpClient = httpClientFactory.CreateClient();
_httpClient.BaseAddress = new Uri(apiUrl);
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
}
private static string ToHexString(byte[] array)
{
var hex = new StringBuilder(array.Length * 2);
foreach (var b in array)
{
hex.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", b);
}
return hex.ToString();
}
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
{
var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
var sign = ToHexString(hashMessage);
var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Headers.Add("sign", sign);
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
var result = await _httpClient.SendAsync(request);
if (!result.IsSuccessStatusCode)
throw new ChangellyException(result.ReasonPhrase);
var content =
await result.Content.ReadAsStringAsync();
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
}
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
{
const string message = @"{
""jsonrpc"": ""2.0"",
""id"": 1,
""method"": ""getCurrenciesFull"",
""params"": []
}";
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
var appendedResult = _showFiat
? result.Result.Concat(new[]
{
new CurrencyFull()
{
Enable = true,
Name = "EUR",
FullName = "Euro",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/eur.png"
},
new CurrencyFull()
{
Enable = true,
Name = "USD",
FullName = "US Dollar",
PayInConfirmations = 0,
ImageLink = "https://changelly.com/api/coins/usd.png"
}
})
: result.Result;
return appendedResult;
}
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
string toCurrency,
decimal amount)
{
var message =
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
var result = await PostToApi<string>(message);
return Convert.ToDecimal(result.Result, CultureInfo.InvariantCulture);
}
}
}

View File

@ -0,0 +1,16 @@
namespace BTCPayServer.Payments.Changelly
{
public static class ChangellyCalculationHelper
{
public static decimal ComputeBaseAmount(decimal baseRate, decimal toAmount)
{
return (1m / baseRate) * toAmount;
}
public static decimal ComputeCorrectAmount(decimal currentFromAmount, decimal currentAmount,
decimal expectedAmount)
{
return (currentFromAmount / currentAmount) * expectedAmount;
}
}
}

View File

@ -0,0 +1,70 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using NBitcoin;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyClientProvider
{
private readonly StoreRepository _storeRepository;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
new ConcurrentDictionary<string, Changelly>();
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
{
_storeRepository = storeRepository;
_httpClientFactory = httpClientFactory;
}
public void InvalidateClient(string storeId)
{
if (_clientCache.ContainsKey(storeId))
{
_clientCache.Remove(storeId, out var value);
}
}
public virtual async Task<Changelly> TryGetChangellyClient(string storeId, StoreData storeData = null)
{
if (_clientCache.ContainsKey(storeId))
{
return _clientCache[storeId];
}
if (storeData == null)
{
storeData = await _storeRepository.FindStore(storeId);
if (storeData == null)
{
throw new ChangellyException("Store not found");
}
}
var blob = storeData.GetStoreBlob();
var changellySettings = blob.ChangellySettings;
if (changellySettings == null || !changellySettings.IsConfigured())
{
throw new ChangellyException("Changelly not configured for this store");
}
if (!changellySettings.Enabled)
{
throw new ChangellyException("Changelly not enabled for this store");
}
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl, changellySettings.ShowFiat);
_clientCache.AddOrReplace(storeId, changelly);
return changelly;
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyException : Exception
{
public ChangellyException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,21 @@
namespace BTCPayServer.Payments.Changelly
{
public class ChangellySettings
{
public string ApiKey { get; set; }
public string ApiSecret { get; set; }
public string ApiUrl { get; set; }
public bool Enabled { get; set; }
public string ChangellyMerchantId { get; set; }
public decimal AmountMarkupPercentage { get; set; }
public bool ShowFiat { get; set; }
public bool IsConfigured()
{
return
!string.IsNullOrEmpty(ApiKey) ||
!string.IsNullOrEmpty(ApiSecret) ||
!string.IsNullOrEmpty(ApiUrl);
}
}
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class ChangellyResponse<T>
{
[JsonProperty("jsonrpc")]
public string JsonRPC { get; set; }
[JsonProperty("id")]
public object Id { get; set; }
[JsonProperty("result")]
public T Result { get; set; }
[JsonProperty("error")]
public Error Error { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class CurrencyFull
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("fullName")]
public string FullName { get; set; }
[JsonProperty("enabled")]
public bool Enable { get; set; }
[JsonProperty("payinConfirmations")]
public int PayInConfirmations { get; set; }
[JsonProperty("image")]
public string ImageLink { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Changelly.Models
{
public class Error
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
}

View File

@ -90,10 +90,10 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
}
return info.NodeInfo;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

View File

@ -15,11 +15,14 @@ using System.Collections.Generic;
using System.Collections;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading;
using Serilog;
namespace BTCPayServer
{
class Program
{
private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB.
static void Main(string[] args)
{
ServicePointManager.DefaultConnectionLimit = 100;
@ -31,7 +34,7 @@ namespace BTCPayServer
var logger = loggerFactory.CreateLogger("Configuration");
try
{
// This is the only way toat LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
// This is the only way that LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
if (conf == null)
return;
@ -50,6 +53,20 @@ namespace BTCPayServer
l.AddFilter("Microsoft", LogLevel.Error);
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
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()
.Enrich.FromLogContext()
.MinimumLevel.Debug()
.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}.");
}
})
.UseStartup<Startup>()
.Build();
@ -73,6 +90,7 @@ namespace BTCPayServer
Logs.Configuration.LogError("Configuration error");
if (host != null)
host.Dispose();
Serilog.Log.CloseAndFlush();
loggerProvider.Dispose();
}
}

View File

@ -2,18 +2,20 @@
"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_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"
},
"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/"
}
}

View File

@ -39,8 +39,6 @@ namespace BTCPayServer.Services.Fees
ExplorerClient _ExplorerClient;
public async Task<FeeRate> GetFeeRateAsync()
{
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
return _Factory.Fallback;
try
{
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;

View File

@ -40,7 +40,13 @@ namespace BTCPayServer.Services.Invoices
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
{
_Engine = new DBreezeEngine(dbreezePath);
int retryCount = 0;
retry:
try
{
_Engine = new DBreezeEngine(dbreezePath);
}
catch when (retryCount++ < 5) { goto retry; }
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
_ContextFactory = contextFactory;
}

View File

@ -33,7 +33,11 @@ namespace BTCPayServer.Services
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", "中文(简体)"),
};
}

View File

@ -148,7 +148,7 @@
<div class="payment-tabs__tab" id="copy-tab">
<span>{{$t("Copy")}}</span>
</div>
@if (Model.AllowCoinConversion)
@if (Model.ChangellyEnabled)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span>{{$t("Conversion")}}</span>
@ -253,7 +253,7 @@
</div>
</nav>
</div>
@if (Model.AllowCoinConversion)
@if (Model.ChangellyEnabled)
{
<div id="altcoins" class="bp-view payment manual-flow">
<nav v-if="srvModel.isLightning">
@ -271,17 +271,42 @@
{{$t("ConversionTab_BodyDesc", srvModel)}}
</span>
</div>
<center>
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
</a>
@*Changelly doesn't have TO_AMOUNT support so we can't include it
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
</a>*@
<center>
<changelly inline-template
:merchant-id="srvModel.changellyMerchantId"
:store-id="srvModel.storeId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.changellyAmountDue"
:to-currency-address="srvModel.btcAddress">
<div class="changelly-component">
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
<select
v-model="selectedFromCurrency"
:disabled="isLoading"
v-on:change="onCurrencyChange($event)"
ref="changellyCurrenciesDropdown">
<option value="">Select a currency to convert from</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">
{{currency.fullName}}
</option>
</select>
</div>
<a v-on:click="openDialog($event)" :href="url" class="changelly-component-button">
<img src="https://changelly.com/pay_button.png" alt="Changelly" v-show="url"/>
</a>
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
</changelly>
</center>
</nav>
</div>

View File

@ -53,49 +53,53 @@
</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" />
</div>
<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 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 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);
}
</select>
<script>
$(function () {
var storeDefaultLang = '@Model.DefaultLang';
if (urlParams.lang) {
$(".cmblang").val(urlParams.lang);
} else if (storeDefaultLang) {
$(".cmblang").val(storeDefaultLang);
}
$('select').prettyDropdown({
classic: false,
height: 32,
reverse: true,
hoverIntent: 5000
});
// 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
});
</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>
}
</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>
@ -120,45 +124,50 @@
'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 }
},
});
function changeLanguage(lang) {
i18next.changeLanguage(lang);
}
function changeLanguage(lang) {
i18next.changeLanguage(lang);
}
if (urlParams.lang) {
changeLanguage(urlParams.lang);
}
else if (storeDefaultLang) {
changeLanguage(storeDefaultLang);
}
if (urlParams.lang) {
changeLanguage(urlParams.lang);
} else if (storeDefaultLang) {
changeLanguage(storeDefaultLang);
}
const i18n = new VueI18next(i18next);
const i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [
'line-items',
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
i18n: i18n,
el: '#checkoutCtrl',
components: {
qrcode: VueQr
},
data: {
// TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [
'line-items',
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
i18n: i18n,
el: '#checkoutCtrl',
components: {
qrcode: VueQr,
changelly: ChangellyComponent
},
data: {
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false
}
});
</script>
}
});
</script>
</body>
</html>

View File

@ -1,10 +1,10 @@
@model BTCPayServer.Models.ServerViewModels.LNDGRPCServicesViewModel
@model LndGrpcServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>GRPC settings</h4>
<h4>LND GRPC</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">

View File

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

View File

@ -16,7 +16,7 @@
<div class="col-md-8">
<div class="form-group">
<span>You can get access here to LND-gRPC or SSH services exposed by your server</span>
<span>You can get access here to LND (gRPC, Rest) or SSH services exposed by your server</span>
</div>
<div class="form-group">
@ -29,17 +29,24 @@
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
@foreach (var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td>LND @lnd.Type.ToString()</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
@if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC)
{
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest)
{
<a asp-action="LndRestServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
</td>
</tr>
}
@if(Model.HasSSH)
@if (Model.HasSSH)
{
<tr>
<td>None</td>

View File

@ -38,10 +38,6 @@
<label asp-for="DefaultLang"></label>
<select asp-for="DefaultLang" asp-items="Model.Languages" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="AllowCoinConversion"></label>
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="RequiresRefundEmail"></label>
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check" />

View File

@ -0,0 +1,62 @@
@using Microsoft.AspNetCore.Mvc.Rendering
@model UpdateChangellySettingsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Changelly Settings");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">
<div class="col-md-10">
<form method="post">
<p>
You can obtain API keys at
<a href="https://changelly.com/?ref_id=804298eb5753" target="_blank">
Changelly.com
</a>
</p>
<div class="form-group">
<label asp-for="ApiUrl"></label>
<input asp-for="ApiUrl" class="form-control"/>
<span asp-validation-for="ApiUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ApiKey"></label>
<input asp-for="ApiKey" class="form-control"/>
<span asp-validation-for="ApiKey" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ApiSecret"></label>
<input asp-for="ApiSecret" class="form-control"/>
<span asp-validation-for="ApiSecret" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ChangellyMerchantId"></label>
<input asp-for="ChangellyMerchantId" class="form-control"/>
<span asp-validation-for="ChangellyMerchantId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AmountMarkupPercentage"></label>
<input asp-for="AmountMarkupPercentage" class="form-control"/>
<span asp-validation-for="AmountMarkupPercentage" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ShowFiat"></label>
<input asp-for="ShowFiat" class="form-check"/>
<span asp-validation-for="ShowFiat" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-primary">Test Settings</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -168,6 +168,43 @@
Available placeholders are: {StoreName}, {ItemDescription} and {OrderId}
</p>
</div>
<div class="form-group">
<div class="form-group">
<h5>Third party Payment methods</h5>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Provider</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.ThirdPartyPaymentMethods)
{
<tr>
<td>@scheme.Provider</td>
<td style="text-align:center;">
@if(scheme.Enabled)
{
<span class="fa fa-check"></span>
}
else
{
<span class="fa fa-times"></span>
}
</td>
<td style="text-align:right"><a asp-action="@scheme.Action" >Modify</a></td>
</tr>
}
</tbody>
</table>
</div>
</div>
@if(Model.CanDelete)
{
<div class="form-group">

View File

@ -0,0 +1,104 @@
@model RescanWalletModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Rescan wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Rescan);
}
<h4>@ViewData["Title"]</h4>
@if (!Model.Ok)
{
<div class="row">
<div class="col-md-10">
<p>This feature is disabled</p>
@if (Model.IsFullySync)
{
<p><span class="fa fa-check-circle" style="color:green;"></span> <span>The full node is synched</span></p>
}
else
{
<p><span class="fa fa-times-circle" style="color:red;"></span> <span>The full node is not synched</span></p>
}
@if (Model.IsServerAdmin)
{
<p><span class="fa fa-check-circle" style="color:green;"></span> <span>You are server administrator</span></p>
}
else
{
<p><span class="fa fa-times-circle" style="color:red;"></span> <span>You are not server administrator</span></p>
}
@if (Model.IsSupportedByCurrency)
{
<p><span class="fa fa-check-circle" style="color:green;"></span> <span>This full node support rescan of the UTXO set</span></p>
}
else
{
<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>
}
</div>
</div>
}
else if (!Model.Progress.HasValue)
{
<div class="row">
@if (Model.PreviousError != null)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>The previous scan stopped with an error: @Model.PreviousError</span>
</div>
}
else if (Model.LastSuccess != null)
{
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>The previous scan completed and found <b>@Model.LastSuccess.Found</b> UTXOs in <b>@Model.TimeOfScan</b> (The total UTXO set size is @Model.LastSuccess.TotalSizeOfUTXOSet.Value)</span>
</div>
}
<div class="col-md-10">
<p>
Scanning the UTXO set allow you to restore the balance of your wallet, but not all the transaction history.
</p>
<p>
This operation will scan the HD Path <b>0/*</b>, <b>1/*</b> and <b>*</b> from a starting index, until no UTXO are found in a whole gap limit.
</p>
<p>The batch size make sure the scan do not consume too much RAM at once by rescanning several time with smaller subset of addresses.</p>
<p>If you do not understand above, just keep the default values and click on <b>Start Scan</b></p>
</div>
</div>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<label asp-for="StartingIndex"></label>
<input asp-for="StartingIndex" class="form-control" type="number" />
</div>
<div class="form-group">
<label asp-for="GapLimit"></label>
<input asp-for="GapLimit" class="form-control" type="number" />
</div>
<div class="form-group">
<label asp-for="BatchSize"></label>
<input asp-for="BatchSize" class="form-control" type="number" />
</div>
<button type="submit" class="btn btn-primary">Start scan</button>
</form>
</div>
</div>
}
else
{
<div class="row">
<div class="col-md-10">
<p>Scanning in progress, refresh the page to see the progress...</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)%;">
@(Model.Progress.Value)% (estimated time: @Model.RemainingTime)
</div>
</div>
</div>
</div>
}

View File

@ -19,7 +19,7 @@
</p>
<ul>
<li>Make sure you are running the Ledger app with version superior or equal to 1.2.4</li>
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></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>

View File

@ -18,6 +18,11 @@
</style>
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
If this wallet got restored, should have received money but nothing is showing up, please <a asp-action="WalletRescan">Rescan it</a>.
</div>
</div>
<div class="row">
<div class="col-md-10">
<table class="table table-sm table-responsive-lg">
@ -29,7 +34,7 @@
</tr>
</thead>
<tbody>
@foreach(var transaction in Model.Transactions)
@foreach (var transaction in Model.Transactions)
{
<tr>
<td>@transaction.Timestamp.ToBrowserDate()</td>
@ -38,7 +43,7 @@
@transaction.Id
</a>
</td>
@if(transaction.Positive)
@if (transaction.Positive)
{
<td style="text-align:right; color:green;">@transaction.Balance</td>
}

View File

@ -8,6 +8,7 @@ namespace BTCPayServer.Views.Wallets
public enum WalletsNavPages
{
Send,
Transactions
Transactions,
Rescan
}
}

View File

@ -3,5 +3,6 @@
<div class="nav flex-column nav-pills">
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions">Transactions</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan">Rescan</a>
</div>

View File

@ -9390,7 +9390,7 @@ strong {
background-color: #fff;
color: #000;
font-size: 0px;
width:100%;
width: 100%;
}
.btnGroupLnd button:first-child {
@ -10995,6 +10995,15 @@ bp-spinner {
opacity: .85;
}
.general__spinner > bp-spinner > svg {
margin: auto 0px 0px auto;
height: 32px;
width: 32px;
fill: gray;
animation: spin 0.55s linear infinite;
opacity: .85;
}
bp-refund-address.ng-valid .bitcoin-logo {
opacity: 1;
margin-left: 0;
@ -11459,3 +11468,45 @@ low-fee-timeline {
opacity: 1;
transition: opacity 1s ease;
}
.changelly-component {
position: relative;
}
.changelly-component-dropdown-holder {
height: 32px;
margin-bottom: 10px;
}
.changelly-component .general__spinner bp-spinner {
width: 50px;
height: 50px;
}
.changelly-component .general__spinner bp-spinner svg {
margin: 0;
}
.changelly-component .retry-button {
border-radius: 5px;
border: 1px solid #a9a9a9;
background: #fff;
color: #000;
min-width:100px;
padding-left: 0.8rem;
padding-right: 0.8rem;
min-height: 30px;
word-break: break-all;
}
.changelly-component .retry-button :hover{
border-color: #7f7f7f;
}
.changelly-component .prettydropdown li img {
width:20px;
margin-right: 5px;
}

View File

@ -0,0 +1,131 @@
var ChangellyComponent =
{
props: ["storeId", "toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId"],
data: function () {
return {
currencies: [],
isLoading: true,
calculatedAmount: 0,
selectedFromCurrency: "",
prettyDropdownInstance: null,
calculateError: false,
currenciesError: false
};
},
computed: {
url: function () {
if (this.calculatedAmount && this.selectedFromCurrency && !this.isLoading) {
return "https://changelly.com/widget/v1?auth=email" +
"&from=" +
this.selectedFromCurrency +
"&to=" +
this.toCurrency +
"&address=" +
this.toCurrencyAddress +
"&amount=" +
this.calculatedAmount +
(this.merchantId ? "&merchant_id=" + this.merchantId + "&ref_id=" + this.merchantId : "");
}
return null;
}
},
watch: {
selectedFromCurrency: function (val) {
if (val) {
this.calculateAmount();
} else {
this.calculateAmount = 0;
}
}
},
mounted: function () {
this.prettyDropdownInstance = initDropdown(this.$refs.changellyCurrenciesDropdown);
this.loadCurrencies();
},
methods: {
getUrl: function () {
return window.location.origin + "/changelly/" + this.storeId;
},
loadCurrencies: function () {
this.isLoading = true;
this.currenciesError = false;
$.ajax(
{
context: this,
url: this.getUrl() + "/currencies",
dataType: "json",
success: function (result) {
for (i = 0; i < result.length; i++) {
if (result[i].enabled &&
result[i].name.toLowerCase() !== this.toCurrency.toLowerCase()) {
this.currencies.push(result[i]);
}
}
var self = this;
Vue.nextTick(function () {
self.prettyDropdownInstance
.refresh()
.on("change",
function (event) {
self.onCurrencyChange(self.$refs.changellyCurrenciesDropdown.value);
});
});
},
error: function(){
this.currenciesError = true;
},
complete: function () {
this.isLoading = false;
}
});
},
calculateAmount: function () {
this.isLoading = true;
this.calculateError = false;
$.ajax(
{
url: this.getUrl() + "/calculate",
dataType: "json",
data: {
fromCurrency: this.selectedFromCurrency,
toCurrency: this.toCurrency,
toCurrencyAmount: this.toCurrencyDue
},
context: this,
success: function (result) {
this.calculatedAmount = result;
},
error: function(){
this.calculateError = true;
},
complete: function () {
this.isLoading = false;
}
});
},
retry: function(type){
if(type=="loadCurrencies"){
this.loadCurrencies();
}else if(type=="calculateAmount"){
this.calculateAmount();
}
},
onCurrencyChange: function (value) {
this.selectedFromCurrency = value;
this.calculatedAmount = 0;
},
openDialog: function (e) {
if (e && e.preventDefault) {
e.preventDefault();
}
var changellyWindow = window.open(
this.url,
'Changelly',
'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0');
changellyWindow.focus();
}
}
};

View File

@ -32,9 +32,6 @@ function changeCurrency(currency) {
}
function onDataCallback(jsonData) {
// extender properties used
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
//
var newStatus = jsonData.status;
@ -72,15 +69,16 @@ function onDataCallback(jsonData) {
}
// restoring qr code view only when currency is switched
if (jsonData.paymentMethodId === srvModel.paymentMethodId &&
checkoutCtrl.scanDisplayQr === "") {
checkoutCtrl.scanDisplayQr = jsonData.invoiceBitcoinUrlQR;
}
if (jsonData.paymentMethodId === srvModel.paymentMethodId) {
$(".payment__currencies").show();
$(".payment__spinner").hide();
}
if (checkoutCtrl.scanDisplayQr === "") {
checkoutCtrl.scanDisplayQr = jsonData.invoiceBitcoinUrlQR;
}
if (jsonData.isLightning && checkoutCtrl.lndModel === null) {
var lndModel = {
toggle: 0

View File

@ -27,7 +27,8 @@ const locales_en = {
// 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.",
"Shapeshift_Button_Text": "Pay with Altcoins",
"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...",

View File

@ -0,0 +1,54 @@
const locales_it = {
nested: {
lang: 'Lingua'
},
"Awaiting Payment...": "In attesa del Pagamento...",
"Pay with": "Paga con",
"Contact and Refund Email": "Email di Contatto e Rimborso",
"Contact_Body": "Inserisci un indirizzo email qui sotto. Ti contatteremo a questo indirizzo in caso di problemi con il pagamento.",
"Your email": "La tua email",
"Continue": "Continua",
"Please enter a valid email address": "Inserisci un indirizzo email valido",
"Order Amount": "Importo dell'Ordine",
"Network Cost": "Costi di Rete",
"Already Paid": "Già pagato",
"Due": "Dovuto",
// Tabs
"Scan": "Scansiona",
"Copy": "Copia",
"Conversion": "Conversione",
// Scan tab
"Open in wallet": "Apri nel portafoglio",
// Copy tab
"CompletePay_Body": "Per completare il pagamento, inviare {{btcDue}} {{cryptoCode}} all'indirizzo riportato di seguito.",
"Amount": "Importo",
"Address": "Indirizzo",
"Copied": "Copiato",
// Conversion tab
"ConversionTab_BodyTop": "Puoi pagare {{btcDue}} {{cryptoCode}} usando altcoin diverse da quelle che il commerciante supporta direttamente.",
"ConversionTab_BodyDesc": "Questo servizio è fornito da terze parti. Si prega di tenere presente che non abbiamo alcun controllo su come i fornitori inoltreranno i fondi. La fattura verrà contrassegnata solo dopo aver ricevuto i fondi su {{cryptoCode}} Blockchain.",
"Shapeshift_Button_Text": "Paga con Altcoin",
"ConversionTab_Lightning": "Nessun fornitore di conversione disponibile per i pagamenti Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "Fattura in scadenza a breve...",
"Invoice expired": "Fattura scaduta",
"What happened?": "Cosa è successo?",
"InvoiceExpired_Body_1": "Questa fattura è scaduta. Una fattura è valida solo per {{maxTime minuti}} minuti. \
Puoi tornare a {{store name}} se desideri inviare nuovamente il pagamento.",
"InvoiceExpired_Body_2": "Se hai provato a inviare un pagamento, non è ancora stato accettato dalla rete. Non abbiamo ancora ricevuto i tuoi fondi.",
"InvoiceExpired_Body_3": "Se la transazione non viene accettata dalla rete, i fondi saranno nuovamente spendibili nel tuo portafoglio. A seconda del portafoglio, potrebbero essere necessarie 48-72 ore.",
"Invoice ID": "Numero della Fattura",
"Order ID": "Numero dell'Ordine",
"Return to StoreName": "Ritorna a {{storeName}}",
// Invoice paid
"This invoice has been paid": "La fattura è stata pagata",
// Invoice archived
"This invoice has been archived": "TQuesta fattura è stata pagata",
"Archived_Body": "Contatta il negozio per informazioni sull'ordine o per assistenza",
// Lightning
"BOLT 11 Invoice": "Fattura BOLT 11",
"Node Info": "Informazioni sul Nodo",
//
"txCount": "{{count}} transazione",
"txCount_plural": "{{count}} transazioni"
};

View File

@ -0,0 +1,54 @@
const locales_kk = {
nested: {
lang: 'Тіл'
},
"Awaiting Payment...": "Күтіп тұрған төлем…",
"Pay with": "Төлеу",
"Contact and Refund Email": "Байланыс және ақша қайтару үшін арналған электрондық пошта",
"Contact_Body": "Электрондық пошта мекенжайыңызды төменде көрсетуіңізді сұраймыз. Төлеміңіз туралы мәселе болса, біз сізге осы мекенжайыңыз арқылы хабарласамыз.",
"Your email": "Сіздің электрондық пошта мекенжайыңыз",
"Continue": "Жалғастыру",
"Please enter a valid email address": "Қолданыстағы электрондық пошта мекенжайыңызды еңгізуін сұраймыз",
"Order Amount": "Тапсырыс саны",
"Network Cost": "Желі бағасы",
"Already Paid": "Төленген",
"Due": "Жалпы сома",
// Tabs
"Scan": "Сканерлеу",
"Copy": "Көшіру",
"Conversion": "Айырбастау",
// Scan tab
"Open in wallet": "Әмиянда ашу",
// Copy tab
"CompletePay_Body": "Төлеміңізді аяқтау үшін төмендегі мекенжайға {{btcDue}} {{cryptoCode}} жіберуіңізді сұраймыз",
"Amount": "Сан",
"Address": "Мекенжай",
"Copied": "Көшірілді",
// Conversion tab
"ConversionTab_BodyTop": "Сатушы тікелей қолдау көрсетуден тыс кезде сіз altcoins көмегімен {{btcDue}} {{cryptoCode}} төлеуіңізге болады.",
"ConversionTab_BodyDesc": "Бұл қызмет үшінші тараптан қамтамасыз етіледі. Сіздің ақшаңызды провайдерлер сізге қалай жеткізетінін біз бақылауға алмайтынымызды есте сақтауыңызды сұраймыз. Шот тек қана {{cryptoCode}} Blockchain жүйесі қаражаттырылған соң көрсетіледі.",
"Shapeshift_Button_Text": "Төлеу Altcoins",
"ConversionTab_Lightning": "Lightning Төлемдерді айырбастау жеткізушілер байланыстан тыс жерде",
// Invoice expired
"Invoice expiring soon...": "Шот-фактура жақын арада аяқталады…",
"Invoice expired": "Шот-фактураның мерзімі аяқталды",
"What happened?": "Не жағдай болды?",
"InvoiceExpired_Body_1": "Бұл шот-фактураның мерзімі аяқталды. Шот-фактура {{maxTimeMinutes}} минуттарға жарамды. \
Төлеміңізді қайтадан жібергіңіз келсе {{storeName}} ға оралуға болады.",
"InvoiceExpired_Body_2": "Егер сіз төлемді жіберуге тырыссқан болсаңыз, ол әлі желімен қабылданған жоқ. Біз сіздің қаражатыңызды әлі алған жоқпыз.",
"InvoiceExpired_Body_3": "Егер транзакция желі арқылы қабылданбаса, қаражат әмияныңызға қайтадан қайтарылады. Бұл сіздің әмияныңызға байланысты 48-72 сағат алуы мүмкін.",
"Invoice ID": "Шот анықтамасы",
"Order ID": "Тапсырыс нөмірі",
"Return to StoreName": "{{storeName}} оралу",
// Invoice paid
"This invoice has been paid": "Бұл шот төленген",
// Invoice archived
"This invoice has been archived": "Бұл шот мұрағатталған",
"Archived_Body": "Тапсырыс туралы ақпарат немесе көмек үшін дүкенге хабарласуыңызды сұраймыз",
// Lightning
"BOLT 11 Invoice": "BOLT 11 Шот-фактура",
"Node Info": "Node анықтамасы",
//
"txCount": "{{count}} Транзакция",
"txCount_plural": "{{count}} Транзакциялар"
};

View File

@ -0,0 +1,54 @@
const locales_uk = {
nested: {
lang: 'Мова'
},
"Awaiting Payment...": "Очікування платежу…",
"Pay with": "Оплатити",
"Contact and Refund Email": "Електронна адреса для зв’язку та повернення коштів",
"Contact_Body": "Будь ласка, надайте електронну адресу нижче. Ми зв’яжемось з вами за цією адресою, якщо з вашим платежем виникнуть проблеми.",
"Your email": "Ваша адреса",
"Continue": "Продовжити",
"Please enter a valid email address": "Будь ласка, введіть дійсну електронну адресу",
"Order Amount": "Обсяг замовлення",
"Network Cost": "Мережева вартість",
"Already Paid": "Вже сплачено",
"Due": "До сплати",
// Tabs
"Scan": "Сканувати",
"Copy": "Копіювати",
"Conversion": "Конверсія",
// Scan tab
"Open in wallet": "Відкрити гаманець",
// Copy tab
"CompletePay_Body": "Для завершення платежу, будь ласка, надішліть {{btcDue}} {{cryptoCode}} на адресу нижче.",
"Amount": "Сума",
"Address": "Адреса",
"Copied": "Скопійовано",
// Conversion tab
"ConversionTab_BodyTop": "Ви можете сплатити {{btcDue}} {{cryptoCode}} використовуючи альткоїни, що відрізняються від тих, що підтримує продавець.",
"ConversionTab_BodyDesc": "Послуга надається третьою особою. Будь ласка, майте на увазі, що ми не контролюємо те, як провайдери пересилатимуть ваші кошти. Рахунок буде позначений як сплачений тільки після надходження коштів на блокчейн {{cryptoCode}}.",
"Shapeshift_Button_Text": "Заплатити альткоїнами",
"ConversionTab_Lightning": "Для платежів з використанням Lightning Network немає провайдерів конверсії.",
// Invoice expired
"Invoice expiring soon...": "Строк дії рахунку скоро закінчується...",
"Invoice expired": "Строк дії рахунку закінчився",
"What happened?": "Що сталось?",
"InvoiceExpired_Body_1": "Строк дії цього рахунку закінчився. Рахунок дійсний лише {{maxTimeMinutes}} хвилин. \
Ви можете повернутись до {{storeName}} якщо ви хочете ще раз надіслати ваш платіж.",
"InvoiceExpired_Body_2": "Якщо ви пробували надіслати платіж, він ще не був прийнятий мережею. Ми ще не отримали ваші кошти.",
"InvoiceExpired_Body_3": "Якщо транзакція не прийнята мережею, кошти будуть знову доступними у вашому гаманці. Залежно від вашого гаманця, це може зайняти приблизно 48-72 годин.",
"Invoice ID": "ID рахунку",
"Order ID": "ID замовлення",
"Return to StoreName": "Повернутись до {{storeName}}",
// Invoice paid
"This invoice has been paid": "Цей рахунок вже оплачений",
// Invoice archived
"This invoice has been archived": "Цей рахунок заархівований",
"Archived_Body": "Будь ласка, зв’яжіться з магазином для отримання інформації щодо замовлення або допомоги",
// Lightning
"BOLT 11 Invoice": "Рахунок BOLT 11",
"Node Info": "Інформація щодо вузла",
//
"txCount": "транзакція {{count}}",
"txCount_plural": "транзакції {{count}}"
};

View File

@ -0,0 +1,54 @@
const locales_vi = {
nested: {
lang: 'Ngôn ngữ'
},
"Awaiting Payment...": "Đang chờ Thanh toán...",
"Pay with": "Thanh toán bằng",
"Contact and Refund Email": "Email để Liên lạc và Hoàn Tiền",
"Contact_Body": "Hãy cung cấp một địa chỉ email dưới đây. Chúng tôi sẽ liên lạc với bạn tại địa chỉ email này nếu có trục trặc trong thanh toán.",
"Your email": "Email của bạn",
"Continue": "Tiếp tục",
"Please enter a valid email address": "Hãy nhập một địa chỉ email hợp lệ",
"Order Amount": "Tổng giá trị Đơn hàng",
"Network Cost": "Chi phí Mạng lưới",
"Already Paid": "Số Tiền Đã Thanh toán",
"Due": "Số tiền chưa thanh toán",
// Tabs
"Scan": "Quét mã",
"Copy": "Sao chép",
"Conversion": "Chuyển đổi",
// Scan tab
"Open in wallet": "Mở ví",
// Copy tab
"CompletePay_Body": "Để hoàn tất việc thanh toán của bạn, hãy gửi {{btcDue}} {{cryptoCode}}tới địa chỉ sau.",
"Amount": "Số tiền",
"Address": "Địa chỉ",
"Copied": "Đã sao chép",
// Conversion tab
"ConversionTab_BodyTop": "Bạn có thể dùng Altcoin để trả cho {{btcDue}} {{cryptoCode}} chứ không phải dùng những loại tiền người bán chấp nhận trực tiếp.",
"ConversionTab_BodyDesc": "Dịch vụ này được cung cấp bởi một bên thứ ba. Xin vui lòng ghi nhớ, chúng tôi không kiểm soát phương thức các nhà cung cấp chuyển tiếp các nguồn tiền của bạn. Hóa đơn sẽ chỉ được đánh dấu đã thanh toán khi nào các nguồn tiền đã được nhận bởi Chuỗi khối Blockchain {{cryptoCode}}.",
"Shapeshift_Button_Text": "Thanh toán bằng Altcoins",
"ConversionTab_Lightning": "Không có nhà cung cấp nào có dịch vụ chuyển đổi cho các phương thức thanh toán Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "Hóa đơn sắp quá hạn...",
"Invoice expired": "Hóa đơn đã quá hạn",
"What happened?": "Đã xảy ra chuyện gì?",
"InvoiceExpired_Body_1": "Hóa đơn này đã quá hạn. Một hóa đơn chỉ có giá trị trong {{maxTimeMinutes}} phút. \
Nếu muốn đệ trình việc thanh toán một lần nữa, bạn có thể quay lại {{storeName}}.",
"InvoiceExpired_Body_2": "Nếu bạn vừa định gửi thanh toán, thì hệ thống vẫn chưa chấp nhận thanh toán của bạn. Chúng tôi vẫn chưa nhận được tiền của bạn.",
"InvoiceExpired_Body_3": "Nếu giao dịch không được hệ thống chấp nhận, các khoản tiền của bạn sẽ được phân bổ trở lại ví của bạn để bạn tiêu dùng. Tuỳ thuộc vào ví của bạn, việc này có thể mất 48-72 giờ.",
"Invoice ID": "Số Hóa đơn",
"Order ID": "Số Đơn hàng",
"Return to StoreName": "Quay lại {{storeName}}",
// Invoice paid
"This invoice has been paid": "Hóa đơn này đã được thanh toán",
// Invoice archived
"This invoice has been archived": "Hóa đơn này đã được lưu trữ",
"Archived_Body": "Hãy liên lạc với cửa hàng để biết thông tin về đơn hàng hoặc nếu cần trợ giúp",
// Lightning
"BOLT 11 Invoice": "Hóa đơn BOLT 11",
"Node Info": "Nút Thông tin",
//
"txCount": "{{count}} giao dịch",
"txCount_plural": "{{count}} các giao dịch"
};

View File

@ -3,6 +3,7 @@
[![Docker Automated build](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/nicolasdorier/btcpayserver/)
[![Deploy to Azure](https://azuredeploy.net/deploybutton.svg)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fbtcpayserver%2Fbtcpayserver-azure%2Fmaster%2Fazuredeploy.json)
[![CircleCI](https://circleci.com/gh/btcpayserver/btcpayserver.svg?style=svg)](https://circleci.com/gh/btcpayserver/btcpayserver)
# BTCPay Server
@ -10,7 +11,7 @@
BTCPay Server is a free and open-source cryptocurrency payment processor which allows you to receive payments in Bitcoin and altcoins directly, with no fees, transaction cost or a middleman.
BTCPay is a non-custodial invoicing system which eliminates the involvement of a third-party. Payments with BTCPay go directly to your wallet, which increases the privacy and security. Your private keys are never uploaded to the server. There is no address re-use, since each invoice generates a new address deriving from your xpubkey.
BTCPay is a non-custodial invoicing system which eliminates the involvement of a third-party. Payments with BTCPay go directly to your wallet, which increases the privacy and security. Your private keys are never uploaded to the server. There is no address re-use since each invoice generates a new address deriving from your xpubkey.
The software is built in C# and conforms to the invoice [API of BitPay](https://bitpay.com/api). It allows for your website to be easily migrated from BitPay and configured as a self-hosted payment processor.
@ -26,8 +27,8 @@ Thanks to the apps built on top of it, you can use BTCPay to receive donations o
* Lightning Network support (LND and c-lightning)
* Altcoin support
* Complete control over private keys
* Fully compatability with BitPay API (easy migration)
* Enchanced privacy
* Full compatibility with BitPay API (easy migration)
* Enhanced privacy
* SegWit support
* Process payments for others
* Payment buttons
@ -60,7 +61,7 @@ You can also read the [BTCPay Merchants Guide](https://www.reddit.com/r/Bitcoin/
## How to build
While the documentation advise using docker-compose, you may want to build yourself outside of development purpose.
While the documentation advises to use docker-compose, you may want to build BTCPay yourself.
First install .NET Core SDK v2.1.4 (with patch version >= 402) as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
@ -76,7 +77,7 @@ On linux:
## How to run
Use the `run` scripts to run BTCPayServer, this example show how to print the available command line arguments of BTCPayServer.
Use the `run` scripts to run BTCPayServer, this example shows how to print the available command line arguments of BTCPayServer.
On Powershell:
```
@ -90,4 +91,4 @@ On linux:
## Other dependencies
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/#deployment).
For more information, see the documentation: [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/#deployment).