Compare commits

...

68 Commits

Author SHA1 Message Date
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 ()
* 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 ()
* 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 () 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 () 2018-10-24 15:18:31 +09:00
f0efd52cb7 Adding Kazakh Language () 2018-10-24 15:17:09 +09:00
bb8fa88688 Adding Vietnamese () 2018-10-24 15:16:30 +09:00
4b976c13c1 Changelly v2 ()
* 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 ()
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 ()"
This reverts commit a5fca7a1c43c4d23ad1a825253fa1fe3ab26677c.
2018-10-18 12:27:46 +09:00
a5fca7a1c4 Changelly Support ()
* 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 () 2018-10-17 13:30:43 +09:00
9952cdca7f bump 2018-10-17 12:06:37 +09:00
6278145374 Removing old QR update code () 2018-10-17 11:55:49 +09:00
84018a5caa Bugfixing race condition for QR code switch ()
Ref: 
2018-10-17 11:49:30 +09:00
d7785fe2d2 Added Serilog file logger for debug file support. () 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 () 2018-10-14 21:34:15 +09:00
b5601ed5e6 fix typo () 2018-10-14 21:28:09 +09:00
42c4f15f22 fixed typos () 2018-10-14 21:26:47 +09:00
6fbd9b2628 bump 2018-10-12 14:02:27 +09:00
d04bfb58a2 Resolving issue with long translations breaking layout () 2018-10-12 14:01:44 +09:00
cded2548f5 bump 2018-10-12 13:32:04 +09:00
dcc859a86a Disable export to JSON 2018-10-12 13:17:38 +09:00
9bec38559f Nepali Translation ()
* Nepali Language

* Delete Checkout.cshtml

* Delete LanguageService.cs

* add np.js

* Match Language File Style
2018-10-12 10:11:03 +09:00
2856454d41 [langs-fr] Correction and some minor improvement () 2018-10-12 10:10:42 +09:00
b3c4fc4003 Added Russian () 2018-10-12 10:10:20 +09:00
c2bbc04c4c Various bugfixes ()
* NotifyEmail field on Invoice, sending email when triggered

* Styling invoices page

* Exporting Invoices in JSON

* Recoding based on feedback

* Fixing image breaking responsive layout on mobile

* Reducing amount of data sent in email notification

* Turning bundling on by default
2018-10-12 10:09:13 +09:00
db40c7bc32 Solving the new version of btcpayserver caused btcpay-python not to create an order problem () 2018-10-11 23:50:28 +09:00
60707fdbd1 Add simplified chinese translation ()
* Add Chinese Simplified translation

* Add Chinese simplified translation
2018-10-11 14:25:16 +09:00
f05614f4da bump 2018-10-11 00:51:43 +09:00
a10c382bd4 [Tests] return WalletId when registering scheme 2018-10-10 00:13:37 +09:00
da2fb876cb Can pass pre filled amount and address to Send Wallet 2018-10-09 23:48:14 +09:00
3c58bff803 Comparable WalletId 2018-10-09 23:44:32 +09:00
a28814bc0e Fix RateRules crash if dups 2018-10-09 23:43:03 +09:00
3cff8261ae Set the id for apps only to 20 bytes, and output the Id in the controller so we can use it in tests 2018-10-09 23:38:56 +09:00
b16e8f7b76 Move GetAppDataIfOwner inside AppHelper 2018-10-09 23:34:51 +09:00
57ab001c0c [Tests] Pass Port to the fake http context 2018-10-09 23:32:47 +09:00
3d85dace38 Move currency display method into CurrencyNameTable 2018-10-09 23:30:26 +09:00
7a04c2974f Center QR Authenticator QR Code 2018-10-09 23:30:26 +09:00
d459839bf7 Displaying Node Info as QR code for Lightning payments ()
* Styling elements required for Node info

* Allowing switching QR between bolt11 and node info for lightning

* Equal width for Bolt11 and Node info buttons

* Certain languages were too verbose for display of "Pay with"

Fixes: 
2018-10-08 07:19:55 +09:00
657cfe1b23 bump 2018-10-06 23:21:33 +09:00
f4eaa0f01f Make sure X-Forwarded-Port does not override ExternalUrl 2018-10-06 23:20:32 +09:00
1e2ffcadf0 Add GRS lightning image () 2018-10-03 11:25:52 +09:00
dcc05af02e fix typo 2018-10-02 22:11:01 +09:00
4b7f78f38b document .net version update 2018-10-02 22:06:06 +09:00
f94ff4cc74 Update dotnet version + nbxplorer 2018-10-02 19:33:25 +09:00
b750663a1f update lnd/clightning 2018-09-28 17:08:51 +09:00
4c4b76e995 Remove Bitpay direct provider 2018-09-28 17:08:51 +09:00
da19d2c1a7 Bugfixing display of custom amount entry in POS ()
Ref: 
2018-09-28 13:31:59 +09:00
fb15c5b354 Increase timeout for wallet, update references 2018-09-28 10:15:35 +09:00
6ffe1cfcab bump LedgerWallet lib 2018-09-27 16:31:33 +09:00
87678c58ac Fix error message for ledger signing 2018-09-27 15:43:34 +09:00
feab4cc48a update ledgerwebsocket 2018-09-27 15:21:26 +09:00
104 changed files with 2686 additions and 481 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetworkProvider.Groestlcoin.csBTCPayServer.csproj
Configuration
Controllers
Data
HostedServices
Hosting
Models
Payments
Program.cs
Properties
Rating
Security
Services
Views
WalletId.cs
wwwroot
DockerfileREADME.mdglobal.json

@ -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>
@ -30,4 +26,7 @@
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

@ -35,6 +35,12 @@ using Xunit;
namespace BTCPayServer.Tests
{
public enum TestDatabases
{
Postgres,
MySQL,
}
public class BTCPayServerTester : IDisposable
{
private string _Directory;
@ -57,6 +63,11 @@ namespace BTCPayServer.Tests
set;
}
public string MySQL
{
get; set;
}
public string Postgres
{
get; set;
@ -68,6 +79,10 @@ namespace BTCPayServer.Tests
get; set;
}
public TestDatabases TestDatabase
{
get; set;
}
public bool MockRates { get; set; } = true;
@ -94,7 +109,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,15 +215,19 @@ 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");
context.Request.Host = new HostString("127.0.0.1", Port);
context.Request.Scheme = "http";
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)
{

@ -0,0 +1,227 @@
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]
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]
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",
};
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]
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 = new UpdateChangellySettingsViewModel
{
Enabled = 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);
}
}
[Fact]
public async void CanGetCurrencyListFromChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
//save changelly settings
var updateModel = new UpdateChangellySettingsViewModel()
{
Enabled = 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]
public async void CanCalculateToAmountForChangelly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var updateModel = new UpdateChangellySettingsViewModel()
{
Enabled = 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);
}
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

@ -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

@ -10,6 +10,19 @@ namespace BTCPayServer.Tests
{
public class RateRulesTest
{
[Fact]
public void SecondDuplicatedRuleIsIgnored()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("DOGE_X = 1.1");
builder.AppendLine("DOGE_X = 1.2");
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
rule.Reevaluate();
Assert.True(!rule.HasError);
Assert.Equal(1.1m, rule.BidAsk.Ask);
}
[Fact]
public void CanParseRateRules()
{

@ -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);

@ -73,11 +73,11 @@ namespace BTCPayServer.Tests
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
public WalletId RegisterDerivationScheme(string crytoCode)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
@ -92,6 +92,8 @@ namespace BTCPayServer.Tests
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }

@ -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
{
@ -332,7 +335,7 @@ namespace BTCPayServer.Tests
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
Assert.Equal(test.Item2, actual);
}
}
@ -569,6 +572,72 @@ namespace BTCPayServer.Tests
}
}
[Fact]
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]
public void CanListInvoices()
{
@ -641,6 +710,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();
}
}
@ -1359,6 +1435,7 @@ namespace BTCPayServer.Tests
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
@ -1589,7 +1666,7 @@ namespace BTCPayServer.Tests
}
}
private static RateProviderFactory CreateBTCPayRateFactory()
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}

@ -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));
}
}
}

@ -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.2.31
image: nicolasdorier/nbxplorer:1.1.0.4
restart: unless-stopped
ports:
- "32838:32838"
expose:
@ -87,31 +94,32 @@ 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
zmqpubrawtx=tcp://0.0.0.0:28332
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtxlock=tcp://0.0.0.0:28332
zmqpubhashblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
- "28332" # ZMQ
- "28333" # ZMQ
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:634f19a7b230edc686be56ab950b80784e56252c-dev
image: nicolasdorier/clightning:v0.6.1-1-dev
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -135,7 +143,8 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.15
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:634f19a7b230edc686be56ab950b80784e56252c-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.4.2.0
image: btcpayserver/lnd:0.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -209,11 +228,12 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53280:8080"
@ -226,7 +246,8 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.4.2.0
image: btcpayserver/lnd:0.5-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -234,11 +255,12 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53281:8080"

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

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.105</Version>
<Version>1.0.3.1</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,35 +33,38 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.0" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0.1" />
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<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.46" />
<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.2.18" />
<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.17" />
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
<ItemGroup>
@ -125,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>
@ -137,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">
@ -149,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>

@ -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; }
}
}

@ -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())

@ -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) { }
}
}

@ -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
{
}
}

@ -40,6 +40,7 @@ namespace BTCPayServer.Controllers
[TempData]
public string StatusMessage { get; set; }
public string CreatedAppId { get; set; }
public async Task<IActionResult> ListApps()
{
@ -104,7 +105,7 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
using (var ctx = _ContextFactory.CreateContext())
{
var appData = new AppData() { Id = id };
@ -115,7 +116,7 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
@ -136,21 +137,9 @@ namespace BTCPayServer.Controllers
});
}
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()

@ -181,5 +181,22 @@ namespace BTCPayServer.Controllers
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

@ -0,0 +1,117 @@
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 response1 = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
var currentAmount = response1;
while (true)
{
if (callCounter > 10)
{
BadRequest();
}
var response2 = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
callCounter++;
if (response2 < toCurrencyAmount)
{
var newCurrentAmount = ((toCurrencyAmount / response2) * 1m) * currentAmount;
currentAmount = newCurrentAmount;
}
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;
}
}

@ -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;
@ -57,7 +58,8 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -212,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)
@ -228,7 +229,7 @@ namespace BTCPayServer.Controllers
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c=> paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.FirstOrDefault();
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
@ -244,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,
@ -283,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()
@ -307,7 +323,7 @@ namespace BTCPayServer.Controllers
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (via Lightning)";
network.DisplayName : network.DisplayName + " (Lightning)";
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
@ -323,39 +339,12 @@ namespace BTCPayServer.Controllers
if (cryptoCode == productInformation.Currency)
return null;
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
}
[HttpGet]
@ -432,23 +421,17 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var model = new InvoicesModel
{
TextSearch = filterString.TextSearch,
Count = count,
SearchTerm = searchTerm,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
foreach (var invoice in list)
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
@ -460,12 +443,29 @@ namespace BTCPayServer.Controllers
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
});
return list;
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
@ -528,6 +528,7 @@ namespace BTCPayServer.Controllers
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationEmail = model.NotificationEmail,
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,

@ -83,6 +83,7 @@ namespace BTCPayServer.Controllers
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
//Another way of passing buyer info to support

@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
BuyerEmail = model.NotifyEmail,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true

@ -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)" });

@ -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)
{

@ -99,9 +99,17 @@ namespace BTCPayServer.Controllers
vm.Confirmation = false;
return View(vm);
}
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
if (!showAddress)
{
@ -110,9 +118,7 @@ namespace BTCPayServer.Controllers
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
store.SetStoreBlob(storeBlob);
}
catch

@ -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);
}
}
}
}

@ -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))

@ -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]

@ -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;
@ -32,17 +33,19 @@ namespace BTCPayServer.Controllers
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public class WalletsController : Controller
public partial class WalletsController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
public StoreRepository Repository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
RateFetcher _RateProvider;
public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -56,13 +59,13 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
_Repo = repo;
_RateProvider = rateProvider;
_NetworkProvider = networkProvider;
Repository = repo;
RateFetcher = rateProvider;
NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
_explorerProvider = explorerProvider;
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
@ -70,10 +73,10 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
@ -111,7 +114,7 @@ namespace BTCPayServer.Controllers
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
@ -120,7 +123,7 @@ namespace BTCPayServer.Controllers
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
@ -139,29 +142,33 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
model.CryptoCurrency = walletId.CryptoCode;
WalletModel model = new WalletModel()
{
DefaultAddress = defaultDestination,
DefaultAmount = defaultAmount,
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
CryptoCurrency = walletId.CryptoCode
};
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
@ -173,11 +180,79 @@ namespace BTCPayServer.Controllers
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch(Exception ex) { model.RateError = ex.Message; }
catch (Exception ex) { model.RateError = ex.Message; }
}
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)
@ -187,7 +262,7 @@ namespace BTCPayServer.Controllers
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch(ArgumentException) { }
catch (ArgumentException) { }
return null;
}
@ -197,7 +272,7 @@ namespace BTCPayServer.Controllers
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(_NetworkProvider)
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
@ -257,7 +332,7 @@ namespace BTCPayServer.Controllers
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
network = NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
@ -361,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)
@ -386,7 +460,6 @@ namespace BTCPayServer.Controllers
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
@ -403,7 +476,7 @@ namespace BTCPayServer.Controllers
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _explorerProvider.GetExplorerClient(network);
var explorer = ExplorerClientProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{

@ -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)

@ -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;

@ -20,6 +20,7 @@ using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.HostedServices
{
@ -52,24 +53,44 @@ namespace BTCPayServer.HostedServices
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger)
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
@ -203,7 +224,7 @@ namespace BTCPayServer.HostedServices
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
@ -264,15 +285,15 @@ namespace BTCPayServer.HostedServices
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
if (t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
if (t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
if (t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
@ -289,7 +310,7 @@ namespace BTCPayServer.HostedServices
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
if (executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);

@ -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;

@ -38,6 +38,7 @@ 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;
@ -75,17 +76,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 +132,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>();

@ -83,14 +83,14 @@ namespace BTCPayServer.Hosting
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
@ -105,8 +105,8 @@ namespace BTCPayServer.Hosting
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
@ -140,13 +140,9 @@ namespace BTCPayServer.Hosting
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
httpContext.Request.Scheme = reverseProxyScheme;
}
else
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
}
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else

@ -53,6 +53,12 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[EmailAddress]
public string NotificationEmail
{
get; set;
}
[Uri]
public string NotificationUrl
{

@ -142,5 +142,6 @@ namespace BTCPayServer.Models.InvoicingModels
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
}
}

@ -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; }
}
}

@ -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; }

@ -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; }
}
}

@ -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>();

@ -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; }

@ -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

@ -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; }
}
}

@ -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; }
}
}

@ -15,6 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
get;
set;
}
public string DefaultAddress { get; set; }
public string DefaultAmount { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

@ -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);
}
}
}

@ -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;
}
}
}

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

@ -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);
}
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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;

@ -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();
}
}

@ -2,19 +2,21 @@
"profiles": {
"Docker-Regtest": {
"commandName": "Project",
"commandLineArgs": "--debuglog debug.log",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_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/"
}
}
}
}

@ -77,7 +77,7 @@ namespace BTCPayServer.Rating
if (CurrencyPair.TryParse(id.Identifier.ValueText, out var currencyPair))
{
expression = expression.WithTriviaFrom(expression);
ExpressionsByPair.Add(currencyPair, (expression, id));
ExpressionsByPair.TryAdd(currencyPair, (expression, id));
}
}
base.VisitAssignmentExpression(node);

@ -210,14 +210,14 @@ namespace BTCPayServer.Security
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;

@ -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;

@ -283,6 +283,11 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public string NotificationEmail
{
get;
set;
}
public string NotificationURL
{
get;

@ -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;
}

@ -29,9 +29,16 @@ namespace BTCPayServer.Services
new Language("pt-PT", "Portuguese"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("np-NP", "नेपाली"),
new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"),
new Language("hr-HR", "Croatian"),
new Language("it-IT", "Italiano"),
new Language("kk-KZ", "Қазақша"),
new Language("ru-RU", "русский"),
new Language("uk-UA", "Українська"),
new Language("vi-VN", "Tiếng Việt"),
new Language("zh-SP", "中文(简体)"),
};
}
}

@ -101,6 +101,40 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
/// <summary>
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <param name="threeLetterSuffix">Add three letter suffix (like USD)</param>
/// <returns></returns>
public string DisplayFormatCurrency(decimal value, string currency, bool threeLetterSuffix = true)
{
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return value.ToString("C", provider);
else
return value.ToString("C", provider) + $" ({currency})";
}
Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()
@ -135,13 +169,16 @@ namespace BTCPayServer.Services.Rates
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
dico.TryAdd(network.CryptoCode, new CurrencyData()
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = 8,
Name = network.CryptoCode,
Crypto = true
});
}))
{
dico[network.CryptoCode].Crypto = true;
}
}
return dico.Values.ToArray();
@ -150,9 +187,9 @@ namespace BTCPayServer.Services.Rates
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
CurrencyData result;
if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
if (!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
{
if(useFallback)
if (useFallback)
{
var usd = GetCurrencyData("USD", false);
result = new CurrencyData()

@ -105,7 +105,6 @@ namespace BTCPayServer.Services.Rates
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });

@ -21,7 +21,7 @@
<h1 class="mb-4">@Model.Title</h1>
<form method="post" asp-antiforgery="false">
<div class="row">
@for(int i = 0; i < Model.Items.Length; i++)
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
var item = Model.Items[i];
@ -32,10 +32,11 @@
}
</div>
</form>
@if(Model.ShowCustomAmount)
@if (Model.ShowCustomAmount)
{
<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<div class="col-sm-3">&nbsp;</div>
<div class="col-sm-6">
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
@ -44,6 +45,7 @@
</div>
</form>
</div>
<div class="col-sm-3">&nbsp;</div>
</div>
}
</div>

@ -57,14 +57,14 @@
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-4 text-center">
<div class="col-md-2 text-center">
</div>
<div class="col-md-4 text-center">
<div class="col-md-8 text-center">
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank">
<img src="~/img/youtube.png" height="225" width="400" />
<img src="~/img/youtube.png" class="img-fluid" />
</a>
</div>
<div class="col-md-6 text-center">
<div class="col-md-2 text-center">
</div>
</div>
</div>

@ -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>
@ -188,9 +188,21 @@
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null }">
<div class="btnGroupLnd">
<button onclick="lndToggleBolt11()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 0 }"
v-bind:title="$t('BOLT 11 Invoice')">
{{$t("BOLT 11 Invoice")}}
</button>
<button onclick="lndToggleNode()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 1 }"
v-bind:title="$t('Node Info')">
{{$t("Node Info")}}
</button>
</div>
</div>
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" />
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
<qrcode v-bind:val="scanDisplayQr" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">
@ -241,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">
@ -259,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>

@ -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>
@ -117,43 +121,53 @@
'pt': { translation: locales_pt },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'np': { translation: locales_np },
'cs-CZ': { translation: locales_cs },
'is-IS': { translation: locales_is },
'hr-HR': { translation: locales_hr }
'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>

@ -44,6 +44,11 @@
<input asp-for="BuyerEmail" class="form-control" />
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="control-label"></label>
<input asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />

@ -87,6 +87,10 @@
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
@ -167,14 +171,14 @@
<th class="text-right">Rate</th>
<th class="text-right">Paid</th>
<th class="text-right">Due</th>
@if(Model.StatusException == "paidOver")
@if (Model.StatusException == "paidOver")
{
<th class="text-right">Overpaid</th>
}
</tr>
</thead>
<tbody>
@foreach(var payment in Model.CryptoPayments)
@foreach (var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.PaymentMethod</td>
@ -182,7 +186,7 @@
<td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td>
@if(Model.StatusException == "paidOver")
@if (Model.StatusException == "paidOver")
{
<td class="text-right">@payment.Overpaid</td>
}
@ -192,7 +196,7 @@
</table>
</div>
</div>
@if(Model.OnChainPayments.Count > 0)
@if (Model.OnChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
@ -207,7 +211,7 @@
</tr>
</thead>
<tbody>
@foreach(var payment in Model.OnChainPayments)
@foreach (var payment in Model.OnChainPayments)
{
var replaced = payment.Replaced ? "class='linethrough'" : "";
<tr @replaced>
@ -226,7 +230,7 @@
</div>
</div>
}
@if(Model.OffChainPayments.Count > 0)
@if (Model.OffChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
@ -239,7 +243,7 @@
</tr>
</thead>
<tbody>
@foreach(var payment in Model.OffChainPayments)
@foreach (var payment in Model.OffChainPayments)
{
<tr>
<td>@payment.Crypto</td>
@ -262,7 +266,7 @@
</tr>
</thead>
<tbody>
@foreach(var address in Model.Addresses)
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight-bold" : "";
<tr>
@ -286,7 +290,7 @@
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
@foreach (var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp.ToBrowserDate()</td>

@ -32,10 +32,19 @@
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed status:complete</code>.
</p>
</div>
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-4">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
</div>
<div class="col-lg-8">
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<form asp-action="SearchInvoice" method="post" style="float:right;">
<div class="input-group">
<input asp-for="SearchTerm" class="form-control" />
<input asp-for="SearchTerm" class="form-control" style="width:300px;" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
@ -50,7 +59,6 @@
</div>
<div class="row">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
@ -63,12 +71,12 @@
</tr>
</thead>
<tbody>
@foreach(var invoice in Model.Invoices)
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
@if(invoice.RedirectUrl != string.Empty)
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl">@invoice.OrderId</a>
}
@ -78,7 +86,7 @@
}
</td>
<td>@invoice.InvoiceId</td>
@if(invoice.Status == "paid")
@if (invoice.Status == "paid")
{
<td>
<div class="btn-group">
@ -97,7 +105,7 @@
}
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.ShowCheckout)
@if (invoice.ShowCheckout)
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
@ -107,7 +115,7 @@
</tbody>
</table>
<span>
@if(Model.Skip != 0)
@if (Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
{

@ -56,5 +56,6 @@
width: 150,
height: 150
});
$("#qrCode > img").css({ "margin": "auto" });
</script>
}

@ -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">

@ -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>

@ -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>

@ -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" />

@ -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")
}

@ -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">

@ -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>
}

@ -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>

@ -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>
}

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

@ -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>

@ -11,6 +11,8 @@ namespace BTCPayServer
static readonly Regex _WalletStoreRegex = new Regex("^S-([a-zA-Z0-9]{30,60})-([a-zA-Z]{2,5})$");
public static bool TryParse(string str, out WalletId walletId)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
walletId = null;
WalletId w = new WalletId();
var match = _WalletStoreRegex.Match(str);
@ -32,8 +34,37 @@ namespace BTCPayServer
}
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public override bool Equals(object obj)
{
WalletId item = obj as WalletId;
if (item == null)
return false;
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
}
public static bool operator ==(WalletId a, WalletId b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(WalletId a, WalletId b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode(StringComparison.Ordinal);
}
public override string ToString()
{
if (StoreId == null || CryptoCode == null)
return "";
return $"S-{StoreId}-{CryptoCode.ToUpperInvariant()}";
}
}

@ -9236,7 +9236,7 @@ strong {
}
.bp-view.scan {
padding-top: .9em !important;
padding-top: 0em !important;
}
.bp-view:not(.active) {
@ -9376,11 +9376,67 @@ strong {
border-bottom: 1px dotted;
}
.wrapBtnGroup {
text-align: center;
margin: -2px 0px 12px;
padding: 0 15px;
}
.btnGroupLnd {
margin: 0 auto;
text-align: center;
width: inherit;
display: table;
background-color: #fff;
color: #000;
font-size: 0px;
width: 100%;
}
.btnGroupLnd button:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-right: 0px;
}
.btnGroupLnd button:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.btnGroupLnd button {
display: table-cell;
border: solid 1px #24725B;
padding: 6px 9px;
margin: 0px;
font-size: 12px;
width: 50%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.btnGroupLnd button:hover {
background-color: #eeffee;
}
.btnGroupLnd button.active {
background-color: #329F80;
border-color: #329F80;
color: #fff;
}
.btnGroupLnd button.active:hover {
background-color: #24725B;
border-color: #24725B;
color: #fff;
}
.payment__details__instruction__open-wallet {
display: block;
margin-left: auto;
margin-right: auto;
margin: 15px 15px 18px;
margin: 0px 15px 18px;
text-align: center;
}
@ -10939,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;
@ -11403,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;
}

@ -54,6 +54,7 @@
.clickable_underline {
border-bottom: 1px dotted #ccc;
font-size: 13px;
}
.payment__currencies:hover .clickable_underline {

@ -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();
}
}
};

@ -20,10 +20,18 @@ function resetTabsSlider() {
closePaymentMethodDialog(null);
}
function changeCurrency(currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
checkoutCtrl.scanDisplayQr = "";
srvModel.paymentMethodId = currency;
fetchStatus();
}
return false;
}
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;
@ -61,25 +69,32 @@ 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 (jsonData.isLightning && checkoutCtrl.lndModel === null) {
var lndModel = {
toggle: 0
};
checkoutCtrl.lndModel = lndModel;
}
if (!jsonData.isLightning) {
checkoutCtrl.lndModel = null;
}
// updating ui
checkoutCtrl.srvModel = jsonData;
}
function changeCurrency(currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
srvModel.paymentMethodId = currency;
fetchStatus();
}
return false;
}
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.paymentMethodId + "/status";
$.ajax({
@ -93,6 +108,16 @@ function fetchStatus() {
});
}
function lndToggleBolt11() {
checkoutCtrl.lndModel.toggle = 0;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.invoiceBitcoinUrlQR;
}
function lndToggleNode() {
checkoutCtrl.lndModel.toggle = 1;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.peerInfo;
}
// private methods
$(document).ready(function () {
// initialize
@ -117,7 +142,7 @@ $(document).ready(function () {
progressStart(srvModel.maxTimeSeconds); // Progress bar
if (srvModel.requiresRefundEmail && !validateEmail(srvModel.customerEmail))
emailForm(); // Email form Display
showEmailForm();
else
hideEmailForm();
}
@ -133,7 +158,7 @@ $(document).ready(function () {
}
// Email Form
// Setup Email mode
function emailForm() {
function showEmailForm() {
$(".modal-dialog").addClass("enter-purchaser-email");
$("#emailAddressForm .action-button").click(function () {

@ -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...",

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

@ -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"
};

@ -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}} Транзакциялар"
};

@ -0,0 +1,55 @@
const locales_np = {
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 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": "चलानी आईडी",
"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": "नोड जानकारी",
//
"txCount": "{{count}} लेनदेन",
"txCount_plural": "{{count}} लेनदेन"
};

@ -0,0 +1,54 @@
const locales_ru = {
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}} используя Altcoins отличные от предпочитаемых продавцом.",
"ConversionTab_BodyDesc": "Эта услуга предоставляется третьим лицом. Пожалуйста, имейте в виду, что мы не контролируем, каким образом провайдеры переедут ваши фондовые средства. Счет будет закрыт только после {{cryptoCode}} Blockchain получения средств.",
"Shapeshift_Button_Text": "заплатить Altcoins",
"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": "Порядковый номер выставленного счета",
"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": "Информация об устройстве",
//
"txCount": "{{count}} Сделка",
"txCount_plural": "{{count}} операции"
};

@ -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}}"
};

@ -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"
};

@ -0,0 +1,54 @@
const locales_zh_sp = {
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}}的区块链接收之后,才会显示已支付。",
"Shapeshift_Button_Text": "Altcoins支付",
"ConversionTab_Lightning": "闪电网络不支持其他altcoins兑换支付。",
// 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 Info)",
//
"txCount": "{{count}}笔交易",
"txCount_plural": "{{count}}笔交易"
};

Binary file not shown.

After

(image error) Size: 93 KiB

@ -64,7 +64,7 @@
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason) {
if (reason.message === "Sign failed")
if (reason.name === "TransportError")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})

@ -15,7 +15,12 @@ $(function () {
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();
if (srvModel.defaultAddress !== null) {
$("#destination-textbox").val(srvModel.defaultAddress);
}
if (srvModel.defaultAmount !== null) {
$("#amount-textbox").val(srvModel.defaultAmount);
}
function WriteAlert(type, message) {
$("#walletAlert").removeClass("alert-danger");
$("#walletAlert").removeClass("alert-warning");
@ -31,9 +36,9 @@ $(function () {
$("#" + prefix + "-error").css("display", "none");
$("#" + prefix + "-success").css("display", "none");
$("#" + prefix+"-" + type).css("display", "block");
$("#" + prefix + "-" + type).css("display", "block");
$("." + prefix +"-label").text(message);
$("." + prefix + "-label").text(message);
}
$("#sendform").on("submit", function (elem) {
@ -61,7 +66,7 @@ $(function () {
confirmButton.prop("disabled", true);
confirmButton.addClass("disabled");
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
bridge.sendCommand('sendtoaddress', args, 60 * 10 /* timeout */)
.catch(function (reason) {
WriteAlert("danger", reason);
confirmButton.prop("disabled", false);
@ -133,9 +138,8 @@ $(function () {
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
if (reason.message === "Sign failed")
.catch(function (reason) {
if (reason.name === "TransportError")
reason = "Are you running the ledger app with version equals or above 1.2.4?";
Write('hw', 'error', reason);
})

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